Como compartilhar a rede Thread com as APIs Google Thread Credentials

1. Antes de começar

No codelab Roteador de borda do Thread (TBR, na sigla em inglês), mostramos como criar um roteador de borda do Thread com base em um Raspberry Pi. Nesse codelab,

  • Estabeleça a conectividade IP bidirecional entre redes Thread e Wi-Fi/Ethernet.
  • Ofereça descoberta de serviço bidirecional por mDNS (no link Wi-Fi/Ethernet) e SRP (na rede Thread).

Este codelab baseia-se no anterior e aborda como seu roteador de borda e seu app podem interagir com as APIs do Google para criar uma única rede Thread. Convergar 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 rede Thread

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 executa o roteador de borda Open Thread (OTBR)
  • Placa que fornece conectividade IEEE 802.15.4 como um processador de rádio (RCP). Veja uma lista de repositórios de diferentes fornecedores de SoC e as instruções deles na página do GitHub sobre o OpenThread (em inglês).

2. Como configurar o serviço HTTP

O primeiro elemento básico de que precisamos é uma interface que nos permita ler as Credenciais ativas e gravar as Credenciais pendentes no seu OTBR. Ao criar uma TBR, use seus mecanismos reservados, conforme mostrado aqui com dois exemplos. A primeira opção mostra como interagir localmente com o agente OTBR 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 um dos métodos 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 chamadas DBUS locais inerentemente.

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

91e5fdeed83e9354.png

Esta etapa cria um serviço HTTP básico que expõe dois endpoints para ler e definir credenciais e, por fim, chamar comandos DBUS.

Na 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 credenciais do Thread ou uma solicitação POST para definir 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 por meio do 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 R/W do DBUS para o caminho do 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 dos próprios recursos. Você pode fazer isso da seguinte maneira:

$ 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 os recursos compatíveis documentados aqui.

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

c748ca5151b6cacb.png

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 na sua 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). Use o navegador, curl ou wget, para inspecionar o conteúdo. Entre os muitos caminhos aceitos 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. Configurar o Credential Framework 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 de 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 da rede Thread à BAID se torna o proprietário das credenciais e tem permissões totais para acessá-las. Se você tentar acessar as 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 atualmente, poderemos oferecer jornadas aprimoradas no futuro.

Mesmo que o primeiro TBR seja excluído posteriormente, as credenciais preferenciais continuarão no dispositivo Android. Depois disso, outros apps que gerenciam as credenciais do Thread podem receber as credenciais de uma chamada getPreferredCredentials().

Google TBR Sync (em inglês)

Os dispositivos Android são sincronizados automaticamente com as TBRs do Google. Se não houver credenciais no Android, os dispositivos as extraem das TBRs do Google na sua rede, e essas credenciais se tornam as credenciais preferenciais. A sincronização entre TBRs e o dispositivo Android acontece somente se a TBR estiver pareada 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 no GHA para iOS quando o usuário estiver na mesma estrutura. No caso do GHA para iOS, as credenciais preferenciais são definidas no armazenamento do iOS, se não houver nenhuma.

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

Integração da TBR de terceiros

No momento, o armazenamento da credencial não está no escopo da casa inteligente do usuário ( Estrutura). Cada dispositivo Android terá o próprio armazenamento BAID, mas assim que houver uma TBR do Google na rede, outros dispositivos Android e iOS que executam o app Google Home para iOS serão sincronizados com essa TBR e tentarão definir credenciais locais no armazenamento do smartphone.

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

  • Se existir uma rede preferencial, 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 TBR no Google Play Services. O Android vai usar essas credenciais como padrão definidas em todas as TBRs baseadas no Google, e outros fornecedores poderão aumentar o alcance e a robustez da sua malha com outros dispositivos.

cd8bc726f67b1fa1.png

4. Como clonar e modificar o 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 fazer modificações de acordo com suas necessidades, mas é possível simplesmente 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 File > Open (Arquivo > Abrir) e selecione seu 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. Navegue até a Roda -> Utilitários do desenvolvedor -> 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 já existe um conjunto preferencial de credenciais no dispositivo. Esse deve ser o ponto de partida do seu fluxo. O código abaixo consulta o GPS sobre a existência das 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)
 }
}

Receber credenciais preferenciais de GPS

Se elas existirem, você vai precisar ler as credenciais. A única diferença do 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, depois que o intentSenderResult for postado nos dados ativos, 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 usando:

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 a seguir uma descrição para postar as credenciais em MutableLiveData<ThreadNetworkCredentials?>.

Configurar credenciais de GPS

Independentemente de existir ou não, você deve registrar 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. No entanto, se o TBR for o primeiro a ser registrado, essas credenciais serão copiadas para o conjunto de credenciais preferenciais. Essas informações podem ser acessadas por qualquer aplicativo no telefone, desde que o usuário a 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 o produto TBR

Essa parte é reservada para cada fornecedor e, neste codelab, ela será implementada usando o servidor HTTP REST DBUS+Python ou o servidor 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 conseguir as credenciais do produto TBR

Como mostrado anteriormente, use o verbo HTTP GET para receber as credenciais da sua TBR. Consulte o exemplo de script do Python.

Criação e importação

Ao criar seu app Android, você vai precisar fazer mudanças no manifesto, build e 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 Manifest e Gradle são mais complexos do que as adições necessárias para usar apenas credenciais de linha de execução.

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, assim como os respectivos BAIDs.

Isso é muito útil ao inserir as informações do seu TBR no armazenamento da credencial de GPS. No entanto, o uso deles 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, será possível integrar totalmente o OTBR de RPi. Nosso app de exemplo expõe oito botões:

91979bf065e9673d.png

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

  1. Consultar se há credenciais preferenciais (azul, primeira linha)
  2. Dependendo da resposta
  3. Solicitar credenciais preferenciais de GPS (azul, 2a linha)
  4. Definir credenciais TBR no GPS (azul, 3a linha) -> Selecionar a TBR -> Criar aleatória -> Inserir o nome da rede -> Ok
  5. Agora que você tem credenciais preferenciais, defina-as como seu OTBR usando Definir credenciais RPi OTBR, que vão 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 TBR da RPi (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 Thread Storage do Google Play Services. Usamos essas APIs para ter um conjunto de dados comum que podemos integrar em um TBR da RPi, que mostra a 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 falhas do usuário 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 o roteador de borda Thread.

8. Referências

Coprocessador de RCP

DBUS