1. Antes de começar
No nosso codelab sobre roteador de borda Thread (TBR), mostramos como criar um roteador de borda Thread com base em um Raspberry Pi. Neste codelab, nós
- Estabeleça a conectividade IP bidirecional entre as redes Thread e Wi-Fi/Ethernet.
- Ofereça descoberta de serviço bidirecional por mDNS (em link Wi-Fi/Ethernet) e SRP (na rede Thread).
Este codelab é baseado no anterior e aborda como o roteador de borda e o app podem interagir com as APIs do Google para criar uma única rede Thread. A convergência 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
- Concluir o Codelab do OTBR
- Conhecimento básico de Linux, Android/Kotlin e redes de threads
O que você vai aprender
- Como usar as APIs de compartilhamento de Thread 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 execute o roteador de borda do Open Thread (OTBR, na sigla em inglês).
- Placa que oferece 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 OpenThread no GitHub.
2. Como configurar o serviço HTTP
O primeiro bloco de construção necessário é uma interface que permita ler as credenciais ativas e gravar as credenciais pendentes no OTBR. Ao criar uma TBR, use seus próprios mecanismos proprietários, conforme mostrado aqui com dois exemplos. A primeira opção mostra como interagir com o agente OTBR localmente pelo 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 a 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 HTTP de loopback ou chamadas DBUS inerentemente locais.
Opção 1: API DBUS e HTTP no script Python
Esta etapa cria um serviço HTTP básico que expõe dois endpoints para ler e definir credenciais, chamando comandos DBUS.
No RPi que vai servir como 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 da linha de execução ou uma solicitação POST para definir as credenciais da linha de execução. O payload é sempre uma estrutura JSON com o TLV.
A solicitação PUT a seguir define novas credenciais de linha de execução pendente para o OTBR usando o caminho /node/dataset/pending
. Nesse 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
recupera as credenciais ativas.
GET /node/dataset/active Host: <IP>:8081 ContentType = "application/json" acceptMimeType = "text/plain" ... <TLV encoded Thread Dataset>
O script chama comandos R/W DBUS para o caminho de ônibus io.openthread.BorderRouter.wpan0
, caminho de 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 a introspecção dos recursos. Você pode fazer isso da seguinte forma:
$ 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 REST HTTP nativa do OTBR Agent
O roteador de borda do OpenThread é criado por padrão com a flag REST_API=1
, ativando a API REST. Caso o build de um codelab anterior não tenha ativado a API REST, crie o OTBR no RPi com essa flag:
$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup
Um agente OTBR pode ser reiniciado executando:
$ 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 muitas tarefas no OTBR (documentado aqui). Você pode usar o 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 o verbo 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 ID do agente do roteador de fronteira (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 dos Serviços do Google Play determina as credenciais preferidas para esse dispositivo móvel.
O app que adiciona um conjunto de credenciais da rede Thread ao BAID se torna o proprietário das credenciais e tem permissões totais para acessá-las. Se você tentar acessar credenciais adicionadas por outros apps, vai receber um erro PERMISSION_DENIED. No entanto, as credenciais preferidas estão sempre disponíveis para qualquer app com o consentimento do usuário. Recomendamos que você mantenha as credenciais armazenadas nos Serviços do Google Play atualizadas quando a rede do roteador de borda Thread for atualizada. Embora essas informações não sejam usadas no momento, podemos oferecer jornadas aprimoradas no futuro.
Mesmo que a primeira TBR seja excluída mais tarde, as credenciais preferidas vão permanecer no dispositivo Android. Depois de definidas, outros apps que gerenciam credenciais do Thread podem receber as credenciais de uma chamada getPreferredCredentials()
.
Google TBR Sync
Os dispositivos Android são sincronizados automaticamente com as TBRs do Google. Se não houver credenciais no Android, os dispositivos vão extraí-las dos TBRs do Google na sua rede, e essas credenciais vão se tornar as credenciais preferidas. A sincronização entre os TBRs e o dispositivo Android só acontece se o TBR estiver pareado com um único usuário ou se estiver pareado com dois usuários que estão 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 e estiver na mesma estrutura. No caso do GHA para iOS, as credenciais preferidas são definidas no armazenamento do iOS, se não houver credenciais preferidas.
Se dois dispositivos Android (ou Android + iGHA) estiverem na mesma rede com conjuntos diferentes de credenciais preferenciais, o dispositivo que configurou originalmente o TBR vai prevalecer.
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 de BAID, mas, quando houver um TBR do Google na rede, outros dispositivos Android e iOS que executam o app Google Home para iOS vão sincronizar com esse TBR e tentar definir credenciais locais no armazenamento do smartphone.
Antes que uma nova TBR OOB crie uma rede, é importante verificar se uma rede preferencial já existe no armazenamento do Android.
- Se uma rede preferencial existir, o fornecedor vai precisar 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 TBR no Google Play Services. O Android vai reconhecer essas credenciais como as credenciais padrão definidas em todas as TBRs baseadas no Google, e outros fornecedores poderão melhorar o alcance e a robustez da rede mesh com outros dispositivos.
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 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 basta clonar o app ou executar os binários pré-criados para inspecionar a funcionalidade.
- Clone usando:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
- Faça o download e abra o Android Studio.
- Clique em "File" > "Open" e aponte para o repositório clonado.
- Ative o modo de desenvolvedor no smartphone Android.
- Conecte-o ao computador usando um cabo USB.
- Execute o app no Android Studio usando <Cmd+R> (OS X) ou <Ctrl+R> (Win, Linux).
- Navegue até a roda > Ferramentas para desenvolvedores > Rede Thread
- Interaja com as diferentes opções disponíveis. Nas seções abaixo, vamos descompactar o código que é executado em cada botão.
As credenciais preferidas existem?
A primeira pergunta que um fabricante de TBR precisa fazer ao Google é se um conjunto de credenciais preferido já existe no dispositivo. Esse deve 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 as credenciais preferidas do GPS
Caso elas existam, você vai querer ler as credenciais. A única diferença em relação ao código anterior é que, depois de receber o 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, quando o intentSenderResult
for enviado aos dados em tempo real, vamos executar 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 vai acionar o consentimento do usuário com credenciais de compartilhamento e, se aprovado, vai retornar o conteúdo por:
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, "") } }
Confira abaixo como postar as credenciais para MutableLiveData<ThreadNetworkCredentials?>
.
Como definir credenciais de GPS
Se eles existirem ou não, registre sua TBR nos Serviços do Google Play. Seu app será o único capaz de ler as credenciais associadas ao ID do agente de fronteira do seu TBR, mas se o TBR for o primeiro a se registrar, essas credenciais serão copiadas para o conjunto de credenciais preferenciais. Essas informações podem ser acessadas por qualquer app no smartphone, desde que o usuário 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 definir as credenciais do produto de TBR
Essa parte é reservada a cada fornecedor, e neste codelab, ela é implementada pelo servidor HTTP Rest DBUS + Python ou pelo servidor HTTP Rest 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 receber as credenciais do produto do TBR
Conforme mostrado anteriormente, use o verbo HTTP GET para conseguir as credenciais do TBR. Consulte o exemplo de script Python.
Build e importações
Ao criar seu app Android, você precisa fazer mudanças no manifesto, no build e nas importações para oferecer suporte ao módulo de linha de execução do Google Play Services. Os três snippets a seguir resumem a maioria das adições.
Nosso app de exemplo foi criado principalmente para a ativação do Matter. Portanto, os arquivos de manifesto e 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 mDNS/SD para criar uma lista de roteadores de borda Thread disponíveis na rede, além dos respectivos BAIDs.
Isso é muito útil ao inserir as informações do TBR no armazenamento da credencial do GPS. No entanto, o uso dele está fora do escopo deste codelab. Usamos a biblioteca de descoberta de serviço do Android 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ê pode integrar totalmente o RPi OTBR. Nosso app de exemplo expõe oito botões:
Uma possível sequência para a integração da TBR é:
- Consultar se existem credenciais preferenciais (azul, 1ª linha)
- Dependendo da resposta
- Receber as credenciais preferidas do GPS (azul, 2ª linha)
- Definir credenciais do TBR no GPS (azul, 3ª linha) -> Selecionar o TBR -> Criar aleatório -> Inserir o nome da rede -> Ok
- Agora que você tem as credenciais preferidas, defina-as para o OTBR usando Definir credenciais do OTBR do RPi, que vai aplicar essas credenciais ao conjunto pendente.
O padrão do app de exemplo é usar um atraso de 10 segundos. Assim, após esse período, as credenciais do RPi TBR (e outros nós que possam existir na rede) serão migradas para o novo conjunto de dados.
7. Conclusão
Neste codelab, clonamos um app Android de exemplo e analisamos vários snippets de código que usam as APIs de armazenamento de linhas de execução dos Serviços do Google Play. Usamos essas APIs para ter um conjunto de dados comum que pode ser integrado a uma TBR do RPi, que mostra a TBR de um fornecedor.
Ter todos os TBRs de um usuário na mesma rede melhora a resiliência e o alcance da rede Thread. Isso também evita jornadas de usuários 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 produto de roteador de borda Thread.
8. Referências
Coprocessador RCP
DBUS