Como compartilhar a rede Thread com as APIs Google Thread Credentials

1. Antes de começar

No codelab Thread Border Router (TBR), mostramos como criar um roteador de borda do Thread com base em um Raspberry Pi. Nesse codelab,

  • Estabeleça a conectividade IP bidirecional entre as redes Thread e Wi-Fi/Ethernet.
  • Fornece descoberta de serviços bidirecional via mDNS (em link Wi-Fi/Ethernet) e SRP (em 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 convergência das 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 Linux, Android/Kotlin e rede de threads

O que você vai aprender

  • Como usar as APIs de compartilhamento do Thread para receber e definir conjuntos de credenciais
  • Como configurar seu próprio roteador de borda 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 Open Thread (OTBR)
  • Placa que oferece conectividade IEEE 802.15.4 como um coprocessador de rádio (RCP, na sigla em inglês). 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 elemento básico necessário é uma interface que permite ler credenciais ativas e gravar credenciais pendentes no OTBR. Ao criar uma TBR, use seus próprios mecanismos proprietários, como mostrado aqui com dois exemplos. A primeira opção mostra como interagir com o agente OTBR localmente usando o 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: API DBUS e HTTP em script Python

91e5fdeed83e9354.png

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 o caminho raiz para uma solicitação GET para recuperar credenciais de linha de execução ou uma solicitação POST para definir credenciais de linha de execução. O payload é sempre uma estrutura JSON com o TLV.

A solicitação PUT a seguir define novas credenciais de thread pendentes 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 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 comandos de leitura/gravação do DBUS para o caminho do barramento io.openthread.BorderRouter.wpan0 e 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 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

Você também pode conferir as funcionalidades compatíveis documentadas aqui.

Opção 2: API REST HTTP nativa do agente OTBR

c748ca5151b6cacb.png

O roteador de borda 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 na RPi com esta 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 realize muitas tarefas no OTBR (documentado aqui). Você pode usar 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 o verbo PUT em /node/dataset/pending.

3. Como configurar o Credential Framework no Android

Credenciais preferenciais

O Google Play Services no Android permite e espera o registro de credenciais para todas as TBRs na sua rede. Cada um é identificado pelo 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 esse dispositivo móvel.

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

Mesmo que a primeira TBR seja excluída depois, as credenciais preferenciais vão permanecer no dispositivo Android. Depois de definidas, outros apps que gerenciam credenciais do Thread podem obtê-las com uma chamada getPreferredCredentials().

Sincronização do TBR do Google

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 elas se tornarão as credenciais preferenciais. A sincronização entre os TBRs e o dispositivo Android só acontece se o TBR estiver pareado com um único usuário ou 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 na mesma estrutura. No caso do GHA para iOS, as credenciais preferenciais são definidas no armazenamento do iOS, se não houver credenciais preferenciais.

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

Integração de TBR de terceiros

No momento, o armazenamento da credencial não é definido pelo usuário da casa inteligente (Estrutura). Cada dispositivo Android tem seu próprio armazenamento de BAID, mas, quando há um TBR do Google na rede, outros dispositivos Android e iOS que executam o app Google Home para iOS são sincronizados com esse TBR e tentam definir credenciais locais no armazenamento do smartphone.

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

  • Se houver uma rede preferida, o fornecedor deverá usá-la. Isso garante que os dispositivos Thread estejam conectados a uma única rede Thread, quando 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 honrar 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 sua malha com dispositivos adicionais.

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 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 também pode clonar o app ou executar os binários pré-criados para inspecionar a funcionalidade.

  1. Clone 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" > "Abrir" e aponte para o repositório clonado.
  3. Ative o modo de desenvolvedor no smartphone Android.
  4. Conecte-o ao computador com um cabo USB.
  5. Execute o app no Android Studio usando <Cmd+R> (OS X) ou <Ctrl+R> (Win, Linux).
  6. Acesse a roda -> Utilitários do desenvolvedor -> Rede Thread
  7. Interaja com as diferentes opções disponíveis. Nas seções abaixo, vamos analisar o código executado em cada botão.

As credenciais preferenciais existem?

A primeira pergunta que um fabricante de TBR precisa fazer ao Google é se um conjunto de credenciais preferenciais 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. Não pede 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

Se elas existirem, leia as credenciais. A única diferença do código anterior é que, depois de receber o intentSenderResult, você quer criar e iniciar um 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 postado nos dados ativos, vamos executar o conteúdo deste 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 para compartilhar credenciais 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, "")
   }
 }

A postagem das credenciais em MutableLiveData<ThreadNetworkCredentials?> é descrita abaixo.

Definir credenciais de GPS

Seja qual for o caso, 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 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 ficam acessíveis a 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)
     }
 }
}

Definir as credenciais para seu produto TBR

Essa parte é exclusiva de cada fornecedor. Neste codelab, implementamos usando o servidor REST HTTP DBUS+Python ou o servidor REST HTTP 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 seu produto TBR

Conforme mostrado anteriormente, use o verbo GET HTTP para receber as credenciais do seu TBR. Confira o exemplo de script Python.

Criação e importações

Ao criar seu app Android, você precisará fazer mudanças no manifesto, na build e nas importações para oferecer suporte ao módulo de linhas 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 o provisionamento do Matter. Portanto, os arquivos de manifesto e Gradle são mais complexos do que as adições necessárias apenas para usar 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, bem como os BAIDs respectivos.

Isso é muito útil ao inserir as informações da TBR no armazenamento de credenciais do GPS. No entanto, o uso dele 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 o OTBR do RPi. Nosso app de exemplo expõe oito botões:

91979bf065e9673d.png

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

  1. Consultar se existem credenciais preferenciais (azul, primeira linha)
  2. Dependendo da resposta
  3. Receber credenciais preferenciais de 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 as credenciais preferenciais, defina-as no OTBR usando Definir credenciais do OTBR do RPi, que vai aplicar essas credenciais ao conjunto pendente.

O app de exemplo usa um atraso de 10 segundos por padrão. Assim, após esse período, as credenciais do TBR do RPi (e de outros nós que possam existir na rede dele) 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 do Google Play Services. Usamos essas APIs para ter um conjunto de dados comum que pode ser integrado a uma TBR RPi, que mostra uma 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ário com falhas em que os apps não podem integrar dispositivos Thread porque não têm acesso a credenciais.

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

8. Referências

Coprocessador RCP

DBUS