1. Antes de comenzar
En nuestro codelab Router de borde de Thread (TBR), mostramos cómo crear un router de borde de Thread basado en una placa Raspberry Pi. En ese codelab,
- Establece la conectividad de IP bidireccional entre las redes Thread y las redes Wi-Fi/Ethernet.
- Proporciona detección de servicios bidireccional a través de mDNS (en vínculos Wi-Fi/Ethernet) y SRP (en la red Thread).
Este codelab se basa en el anterior y aborda la forma en que tu router de borde y tu app pueden interactuar con las APIs de Google para crear una sola red de 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 Linux, Android/Kotlin y las redes Thread
Qué aprenderás
- Cómo usar las APIs de Thread sharing para obtener y configurar conjuntos de credenciales
- Cómo configurar tu propio router de borde OpenThread con las mismas credenciales de la red de Google
Requisitos
- Placa Raspberry Pi 4 o alguna otra placa basada en Linux que ejecute el router de borde 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 su OTBR. Cuando crees un TBR, usa tus propios mecanismos de propiedad, 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 cualquier método para usarla en un entorno de producción, o puedes extender tu propio servicio de supervisión para emitir llamadas HTTP de bucle invertido o llamadas a DBUS inherentemente locales.
Opción 1: API de DBUS y HTTP en la secuencia de comandos de Python
En este paso, se crea un servicio HTTP básico que expone dos extremos para leer y configurar credenciales y, en última instancia, llamar a los comandos de 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 de la siguiente manera:
$ sudo python credentials_server.py 8081 serving at port 8081
En el ejemplo, se configura un servidor HTTP en el puerto 8081 y se escucha en la ruta raíz en busca de 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 configura credenciales nuevas de subprocesos pendientes 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 del bus io.openthread.BorderRouter.wpan0
y 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 hacerlo de las siguientes maneras:
$ 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 que se documentan aquí.
Opción 2: API de REST de HTTP nativa del agente de OTBR
El router de borde de OpenThread compila de forma predeterminada con la marca REST_API=1
, lo que habilita la API de REST. En caso de que tu 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 OTBR se puede reiniciar ejecutando lo siguiente:
$ sudo systemctl restart otbr-agent.service
El agente inicia un servidor HTTP en el puerto 8081. Este servidor permite que un usuario o un programa de supervisión realicen muchas tareas en la OTBR (se documentan aquí). Puedes usar tu navegador, curl
o wget
, para inspeccionar su contenido. Entre las muchas rutas 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. Configura Credential Framework en Android
Credenciales preferidas
Los Servicios de Google Play en Android permiten y esperan 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 agregaron otras apps, recibirás un error PERMISSION_DENIED. Sin embargo, las credenciales preferidas siempre están disponibles para cualquier aplicación con el consentimiento del usuario. Te recomendamos que mantengas actualizadas las credenciales almacenadas en Servicios de Google Play cuando se actualice la red del router de borde de Thread. Si bien esa información no se usa actualmente, es posible que ofrezcamos recorridos mejorados en el futuro.
Incluso si luego se excluye el primer TBR, las credenciales preferidas se conservarán en el dispositivo Android. Una vez configuradas, otras apps que administran credenciales de Thread pueden obtenerlas de una llamada a getPreferredCredentials()
.
Sincronización de 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 las preferidas. La sincronización entre los TBR y el dispositivo Android ocurre solo si el TBR está vinculado con un solo usuario o si está vinculado con dos usuarios que están en la misma casa inteligente ( estructura).
Este proceso también ocurrirá cuando otro usuario de Google use 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, en caso de que no existan.
Si dos dispositivos Android (o Android + iGHA) existen en la misma red con diferentes conjuntos de credenciales preferidas, el dispositivo que configuró originalmente el TBR prevalecerá en el TBR.
Integración de TBR de terceros
Actualmente, la casa inteligente del usuario no limita el almacenamiento de la credencial ( Estructura). Cada dispositivo Android tendrá su almacenamiento BAID, pero una vez que haya un TBR de Google en la red, otros dispositivos Android y dispositivos iOS que ejecuten la app de Google Home para iOS se sincronizarán con ese TBR y tratarán de establecer credenciales locales en el almacenamiento del teléfono.
Antes de que un OOB TBR cree una red, es importante comprobar si ya existe una red preferida en el almacenamiento de Android.
- Si existe una red preferida, el proveedor debe utilizarla. De esta manera, se garantiza que los dispositivos Thread se conecten a una sola red de Thread cuando 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.
4. Clona y modifica 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 necesidades, pero puedes simplemente clonar la app o ejecutar los objetos binarios compilados previamente para inspeccionar la funcionalidad.
- Para clonarla, usa el siguiente comando:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
- Descarga y abre Android Studio.
- Haz clic en Archivo > Ábrelo y selecciona el repositorio clonado.
- Habilita el modo de desarrollador en tu teléfono Android.
- Conéctalo a tu computadora a través de un cable USB.
- Ejecuta la app desde Android Studio con <Cmd+R>. (OS X) o <Ctrl+R> (Windows y Linux)
- Navega hasta la rueda -> Utilidades para desarrolladores -> Red de Thread
- Interactúa con las diferentes opciones disponibles. En las siguientes secciones, descomprimiremos 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 de credenciales preferido en el dispositivo. Este debería ser el punto de partida de tu flujo. El siguiente código realiza una consulta al GPS sobre la existencia de credenciales. No 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 de preferencia de GPS
Si existen, te recomendamos leerlas. La única diferencia con el código anterior es que, después de recibir el intentSenderResult
, debes compilar e iniciar un intent con el resultado del remitente.
En nuestro código, para fines de organización y arquitectura, usamos un MutableLiveData<IntentSender?>
porque el código original está en ViewModel (ThreadViewModel.kt) y los observadores de intents están en el fragmento de actividad ( ThreadFragment.kt). Por lo tanto, una vez que intentSenderResult se publique en los datos en tiempo real, ejecutaremos el contenido 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() } }
Esto activará el consentimiento del usuario con las credenciales de uso compartido y, si se aprueba, mostrará el 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 la publicación de las credenciales en MutableLiveData<ThreadNetworkCredentials?>
.
Configuración de las 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 del agente de frontera de tu TBR, pero si tu TBR es la primera persona en registrarse, esas credenciales se copiarán en el conjunto Credenciales preferidas. Cualquier app del 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) } } }
Configura las credenciales para tu producto TBR
Esta parte es propiedad de cada proveedor y, en este codelab, la implementamos a través del servidor REST HTTP de DBUS+Python o el servidor REST 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 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 de subprocesos de los Servicios de Google Play. Los siguientes tres fragmentos resumen la mayoría de las adiciones.
Ten en cuenta que nuestra app de ejemplo se diseñó principalmente para la puesta en marcha 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. Detección de mDNS/SD
En nuestra app de ejemplo, se usa 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 resulta muy útil cuando se ingresa 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 NSDManager de Android Service Discovery y el código fuente completo está disponible en la app de ejemplo, en ServiceDiscovery.kt
.
6. Resumen
Una vez que implementes estas llamadas o uses la app de ejemplo, puedes integrar por completo tu OTBR de RPi. Nuestra app de ejemplo expone 8 botones:
Esta es una posible secuencia para integrar la TBR:
- Consulta si existen credenciales preferenciales (azul, 1a fila)
- Según la respuesta
- Obtener credenciales preferidas del GPS (azul, 2a fila)
- Configurar las credenciales de TBR en el GPS (azul, 3a fila) -> Selecciona tu TBR -> Crear de forma aleatoria -> Ingresa el nombre de la red -> Aceptable
- Ahora que tienes credenciales preferidas, configúralas para tu OTBR con Set RPi OTBR credentials, que aplicará esas credenciales al conjunto pendiente.
La configuración predeterminada para la app de ejemplo es usar un retraso de 10 segundos. Entonces, después de este período, las credenciales de tu TBR de RPi (y otros nodos que podrían 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 hacen uso de los Servicios de Google Play APIs de Thread Storage. Usamos esas APIs para tener un conjunto de datos común que pudiéramos integrar en un TBR de RPi, que muestra el TBR de un proveedor.
Tener todos los TBR de un usuario en la misma red mejora la resiliencia y el alcance de la red Thread. También evita recorridos del usuario defectuosos en los que las apps no pueden incorporar 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 Thread.
8. Referencias
Coprocesador de RCP
DBUS