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
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
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.
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.
- Clone-a 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 Arquivo > Abra e aponte para o repositório clonado.
- Ative o modo de desenvolvedor no seu smartphone Android.
- Conecte o dispositivo ao computador com um cabo USB.
- Execute o app no Android Studio usando <Cmd+R>. (OS X) ou <Ctrl+R> (Win, Linux)
- Navegue até a roda -> Utilitários 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.
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:
Uma possível sequência para integrar seu TBR é:
- Consultar se há credenciais preferenciais (azul, 1a linha)
- Dependendo da resposta
- Receber credenciais preferenciais do GPS (azul, segunda linha)
- Defina as credenciais de TBR no GPS (azul, terceira linha) -> Selecione seu TBR -> Criar aleatório -> Insira o nome da rede -> OK
- 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