Como compartilhar a rede Thread com as APIs Google Thread Credentials

1. Antes de começar

No codelab Roteador de borda do Thread (TBR), mostramos como criar um roteador de borda do Thread baseado em um Raspberry Pi. Neste codelab,

  • Estabelecer uma conectividade de IP bidirecional entre as redes Thread e Wi-Fi/Ethernet.
  • Fornece descoberta de serviços bidirecionais via mDNS (no link Wi-Fi/Ethernet) e SRP (na rede Thread).

Este codelab se baseia no anterior, abordando como seu próprio roteador de borda e seu app podem interagir com as APIs do Google para criar uma única rede Thread. A conversão de credenciais do Thread é importante porque aumenta a robustez da rede e simplifica as interações do usuário com os aplicativos que dependem do Thread.

Pré-requisitos

  • Conclua o Codelab do OTBR
  • Conhecimento básico de redes Linux, Android/Kotlin e Thread

O que você vai aprender

  • Como usar as APIs Thread Share para receber e definir conjuntos de credenciais
  • Como configurar seu próprio roteador de borda do OpenThread com as mesmas credenciais da rede do Google

O que é necessário

  • Placa Raspberry Pi 4 ou outra placa baseada em Linux que executa o roteador de borda Open Thread (OTBR)
  • Placa que fornece conectividade IEEE 802.15.4 como um coprocessador de rádio (RCP). Confira uma lista de repositórios de diferentes fornecedores de SoC e as instruções deles na página do GitHub do OpenThread

2. Como configurar o serviço HTTP

O primeiro elemento básico de que precisamos é uma interface que nos permita ler credenciais ativas e gravar credenciais pendentes no seu OTBR. Ao criar um TBR, use seus próprios mecanismos, como mostrado aqui com dois exemplos. A primeira opção mostra como interagir com o agente do OTBR localmente via DBUS, enquanto a segunda aproveita a API REST, que pode ser criada no OTBR.

Nenhum dos métodos é seguro e não deve ser usado no estado em que se encontra em um ambiente de produção. No entanto, um fornecedor pode criar criptografia em torno de qualquer método para usá-lo em um ambiente de produção, ou você pode estender seu próprio serviço de monitoramento para emitir chamadas HTTP de loopback ou DBUS inerentemente locais.

Opção 1: DBUS e API HTTP no script Python

91e5fdeed83e9354.png

Essa etapa cria um serviço HTTP básico que expõe dois endpoints para ler e definir credenciais, chamando comandos do DBUS.

No RPi que servirá como seu OTBR, instale as dependências do Python 3:

$ pip install dbus-python shlex json

Execute o script como:

$  sudo python credentials_server.py 8081
serving at port 8081

O exemplo configura um servidor HTTP na porta 8081 e detecta no caminho raiz uma solicitação GET para recuperar as credenciais do Thread ou uma solicitação POST para definir as credenciais do Thread. O payload é sempre uma estrutura JSON com o TLV.

A solicitação PUT a seguir define novas credenciais de linha de execução pendentes para o OTBR pelo caminho /node/dataset/pending. Neste caso, as credenciais pendentes são aplicadas em 10 segundos:

PUT /node/dataset/pending
Host: <IP>:8081
ContentType: "application/json"
acceptMimeType: "application/json"
...
{
        "ActiveDataset": "<TLV encoded new Thread Dataset>"
"PendingTimestamp": {
        "Seconds": <Unix timestamp in seconds>,
        "Ticks": 0,
        "Authoritative": false
},
"Delay": 10000 // in milliseconds
}

Uma solicitação GET para /node/dataset/active busca as credenciais ativas no momento.

GET /node/dataset/active
Host: <IP>:8081
ContentType = "application/json"
acceptMimeType = "text/plain"
...
<TLV encoded Thread Dataset>

O script chama os comandos R/W do DBUS para o caminho de barramento io.openthread.BorderRouter.wpan0, o caminho do objeto /io/openthread/BorderRouter/wpan0

# D-BUS interface
def call_dbus_method(interface, method_name, *arguments):
    bus = dbus.SystemBus()
    obj = bus.get_object('io.openthread.BorderRouter.wpan0', '/io/openthread/BorderRouter/wpan0')
    iface = dbus.Interface(obj, interface)
    method = getattr(iface, method_name)
    res = method(*arguments)
    return res

def get_dbus_property(property_name):
    return call_dbus_method('org.freedesktop.DBus.Properties', 'Get', 'io.openthread.BorderRouter',
                                 property_name)

def set_dbus_property(property_name, property_value):
    return call_dbus_method('org.freedesktop.DBus.Properties', 'Set', 'io.openthread.BorderRouter',
                                 property_name, property_value)                               

O DBUS permite introspecção de seus recursos. Você pode fazer isso das seguintes formas:

$ sudo dbus-send --system --dest=io.openthread.BorderRouter.wpan0 \
        --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 \
        org.freedesktop.DBus.Introspectable.Introspect

Confira também os recursos compatíveis documentados aqui.

Opção 2: API OTBR Agent native HTTP Rest

c748ca5151b6cacb.png

O roteador de borda do OpenThread cria por padrão com a flag REST_API=1, ativando a API REST. Caso seu build de um codelab anterior não tenha ativado a API REST, crie o OTBR na sua RPi com essa flag:

$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup

Para reiniciar um agente OTBR, execute:

$ sudo systemctl restart otbr-agent.service

O agente inicia um servidor HTTP na porta 8081. Esse servidor permite que um usuário ou programa de monitoramento execute várias tarefas no OTBR (documentado aqui). Use seu navegador, curl ou wget, para inspecionar o conteúdo. Entre os muitos caminhos compatíveis estão os casos de uso descritos acima, com o verbo GET em /node/dataset/active e PUT em /node/dataset/pending

3. Como configurar o framework de credenciais no Android

Credenciais preferenciais

O Google Play Services no Android permite e espera o registro de credenciais para todos os TBRs na sua rede. Cada um é identificado pelo seu ID do agente do roteador de borda (BAID, na sigla em inglês). Você vai usar o método addCredentials() da interface ThreadNetworkClient para realizar essa tarefa. O primeiro TBR adicionado ao armazenamento do Google Play Services determina as credenciais preferenciais para o dispositivo móvel.

O app que adiciona um conjunto de credenciais de rede Thread ao BAID se torna o proprietário das credenciais e tem permissão total para acessá-las. Se tentar acessar credenciais adicionadas por outros apps, você vai receber um erro PERMISSION_DENIED. No entanto, as credenciais preferenciais estão sempre disponíveis para qualquer app mediante o consentimento do usuário. Recomendamos que você mantenha atualizadas as credenciais armazenadas no Google Play Services quando a rede do roteador de borda do Thread for atualizada. Embora essas informações não sejam usadas no momento, podemos oferecer jornadas melhores no futuro.

Mesmo que o primeiro TBR seja excluído posteriormente, as credenciais preferenciais permanecerão no dispositivo Android. Depois disso, outros apps que gerenciam credenciais do Thread poderão recebê-las de uma chamada getPreferredCredentials().

Google TBR Sync (em inglês)

Os dispositivos Android sincronizam com os TBRs do Google automaticamente. Se não houver credenciais no Android, os dispositivos vão extraí-las dos TBRs do Google na sua rede, e essas credenciais se tornarão as credenciais preferenciais. A sincronização entre TBRs e o dispositivo Android acontece somente se o TBR estiver pareado com um único usuário ou com dois usuários na mesma casa inteligente ( Estrutura).

Esse processo também vai acontecer quando outro usuário do Google estiver no GHA para Android ou GHA para iOS quando o usuário estiver na mesma estrutura. No caso da GHA para iOS, as credenciais preferenciais são definidas no armazenamento do iOS, se não houver credenciais preferenciais.

Se dois dispositivos Android (ou Android + iGHA) existirem na mesma rede com diferentes conjuntos de credenciais preferenciais, o dispositivo que originalmente configurou o TBR prevalecerá no TBR.

Integração de TBR de terceiros

No momento, o armazenamento da credencial não é definido pela casa inteligente do usuário ( Estrutura). Cada dispositivo Android terá seu armazenamento BAID, mas, quando houver um TBR do Google na rede, outros dispositivos Android e iOS que executam o app Google Home para iOS serão sincronizados com esse TBR e tentarão definir credenciais locais no armazenamento do smartphone.

Antes que um novo TBR OOB crie uma rede, é importante verificar se já existe uma rede preferencial no armazenamento do Android.

  • Se uma rede preferencial existir, o fornecedor deve usá-la. Isso garante que os dispositivos Thread sejam conectados a uma única rede Thread sempre que possível.
  • Quando não houver uma rede preferencial, crie um novo conjunto de credenciais e atribua-o ao seu TBR no Google Play Services. O Android vai respeitar essas credenciais como as credenciais padrão definidas em todos os TBRs baseados no Google, e outros fornecedores poderão aumentar o alcance e a robustez da malha com mais dispositivos.

cd8bc726f67b1fa1.png

4. Como clonar e modificar seu app Android

Criamos um app Android que mostra as principais chamadas possíveis para a API Thread. Você pode usar esses padrões no seu app. Neste codelab, vamos clonar o app de exemplo do Google Home para o Matter do GitHub.

Todo o código-fonte mostrado aqui já está codificado no app de exemplo. Você pode modificá-lo de acordo com suas necessidades, mas pode simplesmente clonar o app ou executar os binários pré-criados para inspecionar a funcionalidade.

  1. Clone-a usando:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. Faça o download e abra o Android Studio.
  2. Clique em Arquivo > Abra e aponte para o repositório clonado.
  3. Ative o modo de desenvolvedor no seu smartphone Android.
  4. Conecte o dispositivo ao computador com um cabo USB.
  5. Execute o app no Android Studio usando <Cmd+R>. (OS X) ou <Ctrl+R> (Win, Linux)
  6. Navegue até a roda -> Utilitários para desenvolvedores -> Rede Thread
  7. Interaja com as diferentes opções disponíveis. Nas seções abaixo, vamos descompactar o código que é executado em cada botão.

Existem credenciais preferenciais?

A primeira pergunta que um fabricante de TBR deve fazer ao Google é se um conjunto preferido de credenciais já existe no dispositivo. Esse precisa ser o ponto de partida do seu fluxo. O código abaixo consulta o GPS sobre a existência de credenciais. Ele não solicita o consentimento do usuário porque nenhuma credencial é compartilhada.

/**
* Prompts whether credentials exist in storage or not. Consent from user is not necessary
*/

fun doGPSPreferredCredsExist(activity: FragmentActivity) {
 try {
   // Uses the ThreadNetwork interface for the preferred credentials, adding
   // a listener that will receive an intentSenderResult. If that is NULL, 
   // preferred credentials don't exist. If that isn't NULL, they exist.
   // In this case we'll not use it.

   ThreadNetwork.getClient(activity).preferredCredentials.addOnSuccessListener { intentSenderResult ->
     intentSenderResult.intentSender?.let { intentSender ->
       ToastTimber.d("threadClient: preferred credentials exist", activity)
       // don't post the intent on `threadClientIntentSender` as we do when
       // we really want to know which are the credentials. That will prompt a
       // user consent. In this case we just want to know whether they exist
     } ?: ToastTimber.d(
       "threadClient: no preferred credentials found, or no thread module found", activity
     )
   }.addOnFailureListener { e: Exception ->
     Timber.d("ERROR: [${e}]")
   }
 } catch (e: Exception) {
   ToastTimber.e("Error $e", activity)
 }
}

Como receber credenciais preferenciais de GPS

Caso elas existam, leia as credenciais. A única diferença em relação ao código anterior é que, depois de receber a intentSenderResult, você quer criar e iniciar uma intent usando esse resultado do remetente.

No nosso código, para fins de organização/arquitetura, usamos um MutableLiveData<IntentSender?> porque o código original está no ViewModel (ThreadViewModel.kt) e os observadores de intent estão no fragmento de atividade ( ThreadFragment.kt). Assim, assim que o intentSenderResult for publicado nos dados ativos, executaremos o conteúdo desse observador:

viewModel.threadClientIntentSender.observe(viewLifecycleOwner) { sender ->
 Timber.d(
   "threadClient: intent observe is called with [${intentSenderToString(sender)}]"
 )
 if (sender != null) {
   Timber.d("threadClient: Launch GPS activity to get ThreadClient")
   threadClientLauncher.launch(IntentSenderRequest.Builder(sender).build())
   viewModel.consumeThreadClientIntentSender()
 }
}

Isso acionará o consentimento do usuário com credenciais de compartilhamento e, se aprovado, retornará o conteúdo por meio de:

threadClientLauncher =
 registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
   if (result.resultCode == RESULT_OK) {
     val threadNetworkCredentials =
       ThreadNetworkCredentials.fromIntentSenderResultData(result.data!!)
     viewModel.threadPreferredCredentialsOperationalDataset.postValue(
       threadNetworkCredentials
     )
   } else {
     val error = "User denied request."
     Timber.d(error)
     updateThreadInfo(null, "")
   }
 }

Veja abaixo a descrição de como postar as credenciais em MutableLiveData<ThreadNetworkCredentials?>.

Como definir credenciais de GPS

Independentemente de ela existir ou não, registre seu TBR no Google Play Services. Seu app será o único capaz de ler as credenciais associadas ao ID do agente de borda do seu TBR, mas se ele for o primeiro a ser registrado, essas credenciais serão copiadas para as credenciais preferenciais definidas. Essas informações podem ser acessadas por qualquer app do smartphone, contanto que o usuário as autorize.

/**
* Last step in setting the GPS thread credentials of a TBR
*/
private fun associateGPSThreadCredentialsToThreadBorderRouterAgent(
 credentials: ThreadNetworkCredentials?,
 activity: FragmentActivity,
 threadBorderAgent: ThreadBorderAgent,
) {
 credentials?.let {
   ThreadNetwork.getClient(activity).addCredentials(threadBorderAgent, credentials)
     .addOnSuccessListener {
       ToastTimber.d("threadClient: Credentials added", activity)
     }.addOnFailureListener { e: Exception ->
       ToastTimber.e("threadClient: Error adding the new credentials: $e", activity)
     }
 }
}

Como configurar as credenciais para seu produto TBR

Essa parte é de propriedade de cada fornecedor e, neste codelab, a implementamos pelo DBUS+Python HTTP Rest Server ou pelo HTTP Rest Server nativo do OTBR.

/**
* Creates credentials in the format used by the OTBR HTTP server. See its documentation in
* https://github.com/openthread/ot-br-posix/blob/main/src/rest/openapi.yaml#L215
*/
fun createJsonCredentialsObject(newCredentials: ThreadNetworkCredentials): JSONObject {
 val jsonTimestamp = JSONObject()
 jsonTimestamp.put("Seconds", System.currentTimeMillis() / 1000)
 jsonTimestamp.put("Ticks", 0)
 jsonTimestamp.put("Authoritative", false)

 val jsonQuery = JSONObject()
 jsonQuery.put(
   "ActiveDataset",
   BaseEncoding.base16().encode(newCredentials.activeOperationalDataset)
 )
 jsonQuery.put("PendingTimestamp", jsonTimestamp)
 // delay of committing the pending set into active set: 10000ms
 jsonQuery.put("Delay", 10000)

 Timber.d(jsonQuery.toString())

 return jsonQuery
}

//(...)

var response = OtbrHttpClient.createJsonHttpRequest(
 URL("http://$ipAddress:$otbrPort$otbrDatasetPendingEndpoint"),
 activity,
 OtbrHttpClient.Verbs.PUT,
 jsonQuery.toString()
)

Como conseguir as credenciais do seu produto TBR

Conforme mostrado anteriormente, use o Verb HTTP GET para obter as credenciais do TBR. Consulte o exemplo de script Python.

Criação e importações

Ao criar seu app Android, você precisará fazer mudanças no manifesto, no build e nas importações para oferecer suporte ao módulo Thread do Google Play Services. Os três snippets a seguir resumem a maioria das adições.

Nosso app de exemplo foi criado principalmente para o comissionamento do Matter. Portanto, os arquivos de manifesto e do Gradle são mais complexos do que as adições necessárias para usar apenas as credenciais de thread.

Mudanças no manifesto

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    (...)
    <!-- usesCleartextTraffic needed for OTBR local unencrypted communication -->
    <!-- Not needed for Thread Module, only used for HTTP -->
    <uses-feature
    (...)
        android:usesCleartextTraffic="true">

    <application>
    (...)
    <!-- GPS automatically downloads scanner module when app is installed -->
    <!-- Not needed for Thread Module, only used for scanning QR Codes -->
    <meta-data
        android:name="com.google.mlkit.vision.DEPENDENCIES"
        android:value="barcode_ui"/>
    </application>
</manifest>

Build.gradle

// Thread Network
implementation 'com.google.android.gms:play-services-threadnetwork:16.0.0'
// Thread QR Code Scanning
implementation 'com.google.android.gms:play-services-code-scanner:16.0.0'
// Thread QR Code Generation
implementation 'com.journeyapps:zxing-android-embedded:4.1.0'
// Needed for using BaseEncoding class
implementation 'com.google.guava:guava:31.1-jre'

Importações relevantes

// Thread Network Module
import com.google.android.gms.threadnetwork.ThreadNetworkCredentials
import com.google.android.gms.threadnetwork.ThreadBorderAgent
import com.google.android.gms.threadnetwork.ThreadNetwork

// Conversion of credentials to/fro Base16 (hex)
import com.google.common.io.BaseEncoding

// HTTP
import java.io.BufferedInputStream
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets

// Co-routines for HTTP calls
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch


// JSON
import org.json.JSONObject

// Logs
import timber.log.Timber

// mDNS/SD
import android.net.nsd.NsdServiceInfo

// QR Code reader / writer
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder

5. Descoberta de mDNS/SD

Nosso app de exemplo usa a descoberta de mDNS/SD para criar uma lista de roteadores de borda do Thread disponíveis na rede, bem como os respectivos BAIDs.

Isso é muito útil ao inserir as informações do seu TBR no armazenamento de credenciais de GPS. No entanto, o uso dela está além do escopo deste codelab. Usamos a biblioteca Android Service Discovery NSDManager, e o código-fonte completo está disponível no app de exemplo, em ServiceDiscovery.kt.

6. Reunindo tudo

Depois de implementar essas chamadas ou usar o app de exemplo, você poderá integrar totalmente seu RPi OTBR. Nosso app de exemplo mostra oito botões:

91979bf065e9673d.png

Uma possível sequência para integrar seu TBR é:

  1. Consultar se há credenciais preferenciais (azul, 1a linha)
  2. Dependendo da resposta
  3. Receber credenciais preferenciais do GPS (azul, segunda linha)
  4. Defina as credenciais de TBR no GPS (azul, terceira linha) -> Selecione seu TBR -> Criar aleatório -> Insira o nome da rede -> OK
  5. Agora que você tem credenciais preferenciais, defina-as para seu OTBR usando a opção Definir credenciais RPi OTBR, que aplicará essas credenciais ao conjunto pendente.

O padrão para o app de exemplo é usar um atraso de 10 segundos. Assim, após esse período, as credenciais do seu RPi TBR (e outros nós que possam existir em sua rede) migrarão para o novo conjunto de dados.

7. Conclusão

Neste codelab, clonamos um exemplo de app Android e analisamos vários snippets de código que usam o Thread Storage. Usamos essas APIs para ter um conjunto de dados comum que podemos integrar em um RPi TBR, que mostra o TBR de um fornecedor.

Ter todo o TBR de um usuário na mesma rede melhora a resiliência e o alcance da rede Thread. Isso também evita jornadas do usuário com falhas em que os apps não podem integrar dispositivos Thread porque não têm acesso às credenciais.

Esperamos que este codelab e os apps de exemplo ajudem você a projetar e desenvolver seu próprio app e o roteador de borda do Thread.

8. Referências

Coprocessador de RCP

DBUS