Cómo compartir la red Thread con las APIs de Google Thread Credentials

1. Antes de comenzar

En nuestro codelab Router de borde de Thread (TBR), mostramos cómo compilar un router de borde de Thread basado en una Raspberry Pi. En ese codelab,

  • Establece la conectividad IP bidireccional entre Thread y las redes Wi-Fi/Ethernet.
  • Proporcionar detección bidireccional de servicios a través de mDNS (en un vínculo Wi-Fi/Ethernet) y SRP (en la red Thread).

Este codelab se basa en el anterior y explica cómo tu propio router de borde y tu app pueden interactuar con las APIs de Google para crear una sola red Thread. La convergencia de credenciales de Thread es importante, ya que aumenta la solidez de la red y simplifica las interacciones del usuario con las aplicaciones que dependen de Thread.

Requisitos previos

  • Completa el Codelab de OTBR.
  • Conocimientos básicos de redes de Linux, Android y Kotlin, y Thread

Qué aprenderás

  • Cómo usar las APIs de uso compartido de subprocesos para obtener y configurar conjuntos de credenciales
  • Cómo configurar tu propio router de borde OpenThread con las mismas credenciales que la red de Google

Requisitos

  • Placa Raspberry Pi 4 o alguna otra placa basada en Linux que ejecute el router de borde de Open Thread (OTBR)
  • Placa que proporciona conectividad IEEE 802.15.4 como coprocesador de radio (RCP). Consulta una lista de repositorios de diferentes proveedores de SoC y sus instrucciones en la página de GitHub de OpenThread.

2. Configura el servicio HTTP

El primer componente básico que necesitamos es una interfaz que nos permita leer Credenciales activas y escribir credenciales pendientes en tu OTBR. Cuando crees un TBR, usa tus propios mecanismos propios, como se muestra aquí con dos ejemplos. La primera opción muestra cómo interactuar con el agente de OTBR localmente a través de DBUS, mientras que la segunda aprovecha la API de REST que se puede compilar en la OTBR.

Ninguno de los métodos es seguro y no debe usarse tal como está en un entorno de producción. Sin embargo, un proveedor puede crear encriptación en torno a cualquiera de los métodos para usarlo en un entorno de producción, o puedes extender tu propio servicio de supervisión para emitir llamadas HTTP de bucle invertido o llamadas DBUS de forma inherente.

Opción 1: API de DBUS y HTTP en la secuencia de comandos de Python

91e5fdeed83e9354.png

En este paso, se crea un servicio HTTP básico que expone dos extremos para leer y establecer credenciales, y en última instancia llama a los comandos DBUS.

En la RPi que funcionará como tu OTBR, instala las dependencias de Python 3:

$ pip install dbus-python shlex json

Ejecuta la secuencia de comandos como:

$  sudo python credentials_server.py 8081
serving at port 8081

En el ejemplo, se configura un servidor HTTP en el puerto 8081 y se detecta en la ruta de acceso raíz una solicitud GET para recuperar credenciales de Thread o una solicitud POST para configurar credenciales de Thread. La carga útil siempre es una estructura JSON con el TLV.

La siguiente solicitud PUT establece nuevas credenciales de subproceso pendiente en la OTBR a través de la ruta /node/dataset/pending. En este caso, las credenciales pendientes se aplican en 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
}

Una solicitud GET a /node/dataset/active recupera las credenciales activas actuales.

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

La secuencia de comandos llama a los comandos R/W de DBUS a la ruta de acceso del bus io.openthread.BorderRouter.wpan0, a la ruta del 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)                               

DBUS permite la introspección de sus capacidades. Puedes hacer lo siguiente:

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

También puedes consultar las funciones compatibles documentadas aquí.

Opción 2: API de REST de HTTP nativa del agente de OTBR

c748ca5151b6cacb.png

El router de borde de OpenThread se compila de forma predeterminada con la marca REST_API=1, lo que habilita la API de REST. En caso de que la compilación de un codelab anterior no haya habilitado la API de REST, asegúrate de compilar OTBR en tu RPi con esa marca:

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

Un agente de OTBR se puede reiniciar mediante la ejecución del siguiente comando:

$ sudo systemctl restart otbr-agent.service

El agente inicia un servidor HTTP en el puerto 8081. Este servidor permite que un programa de usuario o de supervisión realice muchas tareas en la OTBR (que se documenta aquí). Puedes usar tu navegador, curl o wget para inspeccionar su contenido. Entre las muchas rutas de acceso admitidas, se encuentran los casos de uso descritos anteriormente, con el verbo GET en /node/dataset/active y el verbo PUT en /node/dataset/pending.

3. Cómo configurar el marco de trabajo de credenciales en Android

Credenciales preferidas

Los Servicios de Google Play en Android permiten y espera el registro de credenciales para todos los TBR de tu red. Cada uno se identifica por su ID de agente de router de borde (BAID). Usarás el método addCredentials() de la interfaz ThreadNetworkClient para realizar esta tarea. El primer TBR que se agrega al almacenamiento de los Servicios de Google Play determina las credenciales preferidas para este dispositivo móvil.

La app que agrega un conjunto de credenciales de red de Thread a su BAID se convierte en la propietaria de las credenciales y tiene permisos completos para acceder a ellas. Si intentas acceder a credenciales que hayan agregado otras apps, recibirás el error PERMISSION_DENIED. Sin embargo, las credenciales preferidas siempre están disponibles para cualquier app con el consentimiento del usuario. Te recomendamos que mantengas actualizadas las credenciales almacenadas en los Servicios de Google Play cuando se actualice la red del router de borde de Thread. Si bien esta información no se utiliza actualmente, es posible que proporcionemos recorridos mejorados en el futuro.

Incluso si se excluye el primer TBR más adelante, las credenciales preferidas se conservarán en el dispositivo Android. Una vez establecida, otras apps que administran credenciales de Thread pueden obtener las credenciales de una llamada a getPreferredCredentials().

Sincronización TBR de Google

Los dispositivos Android se sincronizan automáticamente con los TBR de Google. Si no existen credenciales en Android, los dispositivos las extraen de los TBR de Google en tu red, y esas credenciales se convierten en credenciales preferidas. La sincronización entre los TBR y el dispositivo Android se produce solo si el TBR se sincroniza con un solo usuario o con dos usuarios que se encuentran en la misma casa inteligente ( estructura).

Este proceso también se llevará a cabo cuando otro usuario de Google utilice GHA para Android o GHA para iOS cuando se encuentre en la misma estructura. En el caso de GHA para iOS, las credenciales preferidas se establecen en el almacenamiento de iOS, si no existen credenciales preferidas.

Si existen dos dispositivos Android (o Android + iGHA) en la misma red con diferentes conjuntos de credenciales preferidas, el dispositivo que configuró originalmente la TBR prevalecerá en la TBR.

Integración de TBR de terceros

Actualmente, el almacenamiento de la credencial no se limita al alcance de la casa inteligente ( estructura) del usuario. Cada dispositivo Android tendrá su almacenamiento BAID, pero una vez que haya un TBR de Google en la red, los demás dispositivos Android y iOS que ejecuten la app de Google Home para iOS se sincronizarán con ese TBR e intentarán establecer credenciales locales en el almacenamiento del teléfono.

Antes de que un nuevo OOB TBR cree una red, es importante verificar si ya existe una red preferida en el almacenamiento de Android.

  • Si existe una red preferida, el proveedor debe usarla. De esta manera, se garantiza que los dispositivos Thread estén conectados a una sola red Thread siempre que sea posible.
  • Cuando no exista una red preferida, crea un nuevo conjunto de credenciales y asígnalo a tu TBR en los Servicios de Google Play. Android respetará esas credenciales como las credenciales estándar establecidas en todos los TBR basados en Google, y otros proveedores podrán mejorar el alcance y la solidez de la malla con dispositivos adicionales.

cd8bc726f67b1fa1.png

4. Cómo clonar y modificar tu app para Android

Creamos una app para Android que muestra las principales llamadas posibles a la API de Thread. Puedes usar estos patrones en tu app. En este codelab, clonaremos la app de ejemplo de Google Home para Matter desde GitHub.

Todo el código fuente que se muestra aquí ya está codificado en la app de ejemplo. Te invitamos a modificarlo según tus propias necesidades, pero puedes simplemente clonar la app o ejecutar los objetos binarios compilados previamente para inspeccionar la funcionalidad.

  1. Clónala con el siguiente comando:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. Descarga y abre Android Studio.
  2. Haz clic en Archivo > Abrir y selecciona tu repositorio clonado.
  3. Habilita el modo de desarrollador en tu teléfono Android.
  4. Conéctalo a la computadora con un cable USB.
  5. Ejecuta la app desde Android Studio con <Cmd+R> (OS X) o <Ctrl+R> (Windows, Linux).
  6. Ve a Wheel -> Developer Utilities -> Thread Network.
  7. Interactúa con las diferentes opciones disponibles. En las siguientes secciones, desempaquetaremos el código que se ejecuta en cada botón.

¿Existen credenciales preferidas?

La primera pregunta que un fabricante de TBR debe hacerle a Google es si ya existe un conjunto preferido de credenciales en el dispositivo. Este debería ser el punto de partida de tu flujo. El siguiente código consulta la existencia de credenciales con el GPS. No se solicita el consentimiento del usuario porque no se comparte ninguna credencial.

/**
* 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)
 }
}

Cómo obtener credenciales preferidas de GPS

En caso de que existan, te recomendamos leer las credenciales. La única diferencia con el código anterior es que, después de recibir el intentSenderResult, debes compilar e iniciar un intent usando ese resultado del remitente.

En nuestro código, con fines de organización y arquitectura, usamos MutableLiveData<IntentSender?> porque el código original se encuentra en ViewModel (ThreadViewModel.kt) y los observadores de intents están en el fragmento de actividad ( ThreadFragment.kt). Por lo tanto, una vez que el intentSenderResult se publica en los datos en tiempo real, ejecutaremos los contenidos de este 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()
 }
}

Esta acción activará el consentimiento del usuario con credenciales de uso compartido y, si se aprueba, mostrará contenido a través de lo siguiente:

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 continuación, se describe cómo publicar las credenciales en MutableLiveData<ThreadNetworkCredentials?>.

Cómo configurar credenciales de GPS

Independientemente de si existen o no, debes registrar tu TBR en los Servicios de Google Play. Tu app será la única que podrá leer las credenciales asociadas con el ID de agente de frontera de tu TBR, pero si tu TBR es la primera en registrarse, esas credenciales se copiarán en el conjunto de credenciales preferidas. Cualquier aplicación en el teléfono puede acceder a esa información, siempre que el usuario la autorice.

/**
* 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)
     }
 }
}

Cómo configurar las credenciales de tu producto de TBR

Esta parte es propiedad de cada proveedor y, en este codelab, la implementamos a través del servidor de REST HTTP DBUS+Python o el servidor de REST HTTP nativo de 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()
)

Cómo obtener las credenciales de tu producto de TBR

Como se mostró anteriormente, usa el verbo HTTP GET para obtener las credenciales de tu TBR. Consulta la secuencia de comandos de Python de muestra.

Importación y compilación

Cuando crees tu app para Android, deberás realizar cambios en el manifiesto, la compilación y las importaciones para admitir el módulo Thread de Servicios de Google Play. Los siguientes tres fragmentos resumen la mayoría de las adiciones.

Ten en cuenta que nuestra app de ejemplo se compiló principalmente para la asignación de Matter. Por lo tanto, sus archivos de manifiesto y Gradle son más complejos que las adiciones necesarias para usar solo las credenciales de Thread.

Cambios en el manifiesto

<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'

Importaciones 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. Descubrimiento de mDNS/SD

Nuestra app de ejemplo usa la detección de mDNS/SD para crear una lista de routers de borde de Thread disponibles en la red, así como sus respectivos BAID.

Esto es muy útil cuando ingresas la información de tu TBR en el almacenamiento de la credencial GPS. Sin embargo, su uso está fuera del alcance de este codelab. Usamos la biblioteca de Android Service Discovery NSDManager, y el código fuente completo está disponible en la app de ejemplo, en ServiceDiscovery.kt.

6. Resumen

Una vez que hayas implementado estas llamadas o utilices la app de ejemplo, puedes integrar por completo tu OTBR RPi. Nuestra app de ejemplo expone 8 botones:

91979bf065e9673d.png

Una posible secuencia para integrar tu TBR es:

  1. Consulta si existen credenciales preferenciales (azul, primera fila)
  2. Según la respuesta,
  3. Obtén las credenciales preferidas de GPS (azul, 2a fila)
  4. Establecer credenciales de TBR en GPS (azul, tercera fila) -> Seleccionar tu TBR -> Crear aleatoriamente -> Ingresar nombre de la red -> Aceptar
  5. Ahora que tienes credenciales preferidas, configúralas en tu OTBR con la opción Set RPi OTBR credentials, que aplicará esas credenciales al conjunto pendiente.

El valor predeterminado para la app de ejemplo es usar un retraso de 10 segundos. Por lo tanto, después de este período, las credenciales de tu RPi TBR (y otros nodos que puedan existir en su red) migrarán al nuevo conjunto de datos.

7. Conclusión

En este codelab, clonamos una app para Android de ejemplo y analizamos varios fragmentos de código que utilizan las APIs de Thread Storage de los Servicios de Google Play. Usamos esas APIs para tener un conjunto de datos común que podamos incorporar en un TBR del RPi, que muestra el TBR de un proveedor.

Tener todo el TBR de un usuario en la misma red mejora la resiliencia y el alcance de la red de Thread. También evita recorridos del usuario defectuosos en los que las apps no pueden integrar dispositivos Thread porque no tienen acceso a las credenciales.

Esperamos que este codelab y las apps de ejemplo te ayuden a diseñar y desarrollar tu propia app y tu producto de router de borde de Thread.

8. Referencias

Coprocesador de RCP

BUSCARV (DBUS)