1. Antes de comenzar
En nuestro codelab de router de borde de Thread (TBR), mostramos cómo crear un router de borde de Thread basado en una Raspberry Pi. En ese codelab, hicimos lo siguiente:
- Establece conectividad de IP bidireccional entre Thread y redes Wi-Fi o Ethernet.
- Proporciona el descubrimiento de servicios bidireccional a través de mDNS (en el vínculo Wi-Fi/Ethernet) y SRP (en la red Thread).
Este codelab se basa en el anterior y aborda 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 Linux, Android/Kotlin y redes de 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 que 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 un 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. Cómo configurar el servicio HTTP
El primer elemento básico que necesitamos es una interfaz que nos permita leer las credenciales activas y escribir las credenciales pendientes en tu 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 de forma local a través de DBUS, mientras que la segunda aprovecha la API de Rest que se puede compilar en el 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 compilar la encriptación en cualquiera de los métodos para usarla en un entorno de producción, o bien puedes extender tu propio servicio de supervisión para emitir llamadas HTTP de bucle invertido o 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 establecer credenciales y, en última instancia, llamar a los comandos de DBUS.
En la RPi que funcionará como 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 la muestra, se configura un servidor HTTP en el puerto 8081 y se escucha en la ruta de acceso raíz una solicitud GET para recuperar las credenciales de Thread o una solicitud POST para establecerlas. La carga útil siempre es una estructura JSON con el TLV.
La siguiente solicitud PUT establece nuevas credenciales de subproceso pendiente en el OTBR con la ruta de acceso /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 actualmente.
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 de lectura y escritura de DBUS a la ruta de acceso del bus io.openthread.BorderRouter.wpan0
, la ruta de acceso 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 documentadas aquí.
Opción 2: API de REST HTTP nativa del agente de OTBR
El router de borde OpenThread se compila de forma predeterminada con la marca REST_API=1
, 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
Para reiniciar un agente de OTBR, ejecuta 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 realice muchas tareas en el OTBR (documentado 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 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 frontera (BAID). Para realizar esta tarea, usarás el método addCredentials()
de la interfaz ThreadNetworkClient
. La primera 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 Thread a su BAID se convierte en el propietario de las credenciales y tiene permisos completos para acceder a ellas. Si intentas acceder a las credenciales que agregaron otras apps, recibirás un 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 esa información no se usa en la actualidad, es posible que proporcionemos recorridos mejorados en el futuro.
Incluso si se excluye el primer TBR más adelante, las credenciales preferidas persistirán en el dispositivo Android. Una vez configuradas, otras apps que administren credenciales de Thread pueden obtenerlas de una llamada getPreferredCredentials()
.
Google TBR Sync
Los dispositivos Android se sincronizan automáticamente con las TBR de Google. Si no hay credenciales en Android, los dispositivos las extraen de los TBR de Google en tu red, y esas credenciales se convierten en las credenciales preferidas. La sincronización entre los TBR y el dispositivo Android solo se produce si el TBR está vinculado a un solo usuario o a dos usuarios que se encuentran en la misma casa inteligente (estructura).
Este proceso también se realizará cuando otro usuario de Google esté en GHA para Android o GHA para iOS y 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 hay dos dispositivos Android (o Android + iGHA) en la misma red con diferentes conjuntos de credenciales preferidas, prevalecerá el dispositivo que configuró originalmente la TBR.
Integración de TBR de terceros
Actualmente, la Smart Home del usuario (Structure) no abarca el almacenamiento de la credencial. Cada dispositivo Android tendrá su almacenamiento de 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 una nueva TBR fuera del alcance 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. Esto garantiza que los dispositivos Thread estén conectados a una sola red Thread siempre que sea posible.
- Cuando no haya una red preferida, crea un nuevo conjunto de credenciales y asígnale tu TBR en los Servicios de Google Play. Android respetará esas credenciales como las credenciales estándar configuradas 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. 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 necesidades, pero también puedes clonar la app o ejecutar los objetos binarios precompilados para inspeccionar la funcionalidad.
- Clonarlo con lo siguiente:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
- Descarga y abre Android Studio.
- Haz clic en File > Open y selecciona tu repositorio clonado.
- Habilita el modo de desarrollador en tu teléfono Android.
- Conecta el dispositivo a la computadora con un cable USB.
- Ejecuta la app desde Android Studio con <Cmd+R> (OS X) o <Ctrl+R> (Win, Linux).
- Navega a la rueda -> Utilidades para desarrolladores -> Red de Thread.
- Interactúa con las diferentes opciones disponibles. En las siguientes secciones, analizaremos 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 debe ser el punto de partida de tu flujo. El siguiente código consulta el 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 preferidas de GPS
En caso de que existan, debes leer las credenciales. La única diferencia con el código anterior es que, después de recibir el intentSenderResult
, quieres compilar y lanzar un intent con ese resultado del remitente.
En nuestro código, con fines de organización o arquitectura, usamos un MutableLiveData<IntentSender?>
porque el código original está en el ViewModel (ThreadViewModel.kt) y los observadores de intents están en el fragmento de actividad (ThreadFragment.kt). Por lo tanto, una vez que se publique el intentSenderResult
en los datos en vivo, 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 el uso compartido de credenciales y, si se aprueba, mostrará el contenido a través de las siguientes opciones:
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 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 perímetro de tu TBR, pero si tu TBR es el primero en registrarse, esas credenciales se copiarán en el conjunto de credenciales preferidas. Cualquier app del teléfono puede acceder a esa información, siempre y cuando el usuario lo 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 con el servidor HTTP REST de DBUS+Python o el servidor HTTP 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 de TBR
Como se mostró antes, usa el verbo HTTP GET para obtener las credenciales de tu TBR. Consulta la secuencia de comandos de Python de ejemplo.
Compilación e importaciones
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. En los siguientes tres fragmentos, se resumen la mayoría de las incorporaciones.
Ten en cuenta que nuestra app de ejemplo se creó principalmente para la comisión de Matter. Por lo tanto, sus archivos de manifiesto y Gradle son más complejos que las incorporaciones necesarias para usar solo las credenciales de subproceso.
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 el descubrimiento 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 de GPS. Sin embargo, su uso está fuera del alcance de este codelab. Usamos la biblioteca de descubrimiento de servicios de Android 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 uses la app de ejemplo, podrás integrar por completo tu OTBR de RPi. Nuestra app de ejemplo expone 8 botones:
Una posible secuencia para integrar tu TBR es la siguiente:
- Consulta si existen credenciales preferenciales (en azul, 1ª fila).
- Según la respuesta
- Obtener credenciales preferidas de GPS (en azul, 2ª fila)
- Establece credenciales de TBR en GPS (azul, 3ª fila) -> Selecciona tu TBR -> Crea una aleatoria -> Ingresa el nombre de la red -> Aceptar
- Ahora que tienes las credenciales preferidas, configúralas en tu OTBR con Set RPi OTBR credentials, que aplicará esas credenciales al conjunto pendiente.
La configuración predeterminada de la app de ejemplo es usar una demora de 10 segundos. Por lo tanto, después de este período, las credenciales de tu TBR de RPi (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 usan 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 una TBR de RPi, que muestra la 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 RCP
DBUS