Partager le réseau Thread avec les API Thread Credentials de Google

1. Avant de commencer

Dans notre atelier de programmation Routeur de bordure Thread (TBR), nous montrons comment créer un routeur de bordure Thread basé sur un Raspberry Pi. Dans cet atelier de programmation,

  • Établir une connectivité IP bidirectionnelle entre les réseaux Thread et les réseaux Wi-Fi/Ethernet.
  • fournir une détection de services bidirectionnelle via mDNS (sur un lien Wi-Fi/Ethernet) et SRP (sur le réseau Thread).

Cet atelier de programmation s'appuie sur le précédent et explique comment votre propre routeur de bordure et votre application peuvent interagir avec les API Google pour créer un réseau Thread unique. La convergence des identifiants Thread est importante, car elle renforce la robustesse du réseau et simplifie les interactions des utilisateurs avec les applications qui dépendent de Thread.

Prérequis

Points abordés

  • Utiliser les API de partage de threads pour obtenir et définir des ensembles d'identifiants
  • Comment configurer votre propre routeur de bordure OpenThread avec les mêmes identifiants que ceux du réseau Google

Prérequis

  • Carte Raspberry Pi 4 ou autre carte Linux exécutant le routeur de bordure à thread ouvert (OTBR, Open Thread Border Router)
  • Carte fournissant une connectivité IEEE 802.15.4 en tant que co-processeur radio (RCP). Consultez la liste des dépôts de différents fournisseurs de SoC et leurs instructions sur la page GitHub OpenThread

2. Configurer le service HTTP

Le premier élément de base dont nous avons besoin est une interface qui nous permet de lire les identifiants actifs et d'écrire les identifiants en attente dans votre OTBR. Lorsque vous créez un TBR, utilisez vos propres mécanismes propriétaires, comme illustré ici avec deux exemples. La première option montre comment interagir avec l'agent OTBR localement via DBUS, tandis que la seconde exploite l'API REST qui peut être créée sur l'OTBR.

Aucune de ces méthodes n'est sécurisée et ne doit pas être utilisée telle quelle dans un environnement de production. Toutefois, un fournisseur peut créer un chiffrement autour de l'une ou l'autre de ces méthodes pour l'utiliser dans un environnement de production, ou vous pouvez étendre votre propre service de surveillance pour émettre des appels DBUS locaux ou HTTP de rebouclage.

Option 1: API DBUS et HTTP sur un script Python

91e5fdeed83e9354.png

Cette étape crée un service HTTP simple qui expose deux points de terminaison pour lire et définir les identifiants, en appelant au final des commandes DBUS.

Sur le RPi qui servira d'OTBR, installez les dépendances Python 3:

$ pip install dbus-python shlex json

Exécutez le script en tant que:

$  sudo python credentials_server.py 8081
serving at port 8081

L'exemple configure un serveur HTTP sur le port 8081 et écoute sur le chemin racine, soit pour une requête GET pour récupérer les identifiants Thread, soit pour une requête POST pour définir les identifiants Thread. La charge utile est toujours une structure JSON avec TLV.

La requête PUT suivante définit de nouveaux identifiants de thread en attente sur l'OTBR via le chemin /node/dataset/pending. Dans ce cas, les identifiants en attente sont appliqués dans un délai de 10 secondes:

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
}

Une requête GET envoyée à /node/dataset/active récupère les identifiants actuellement actifs.

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

Le script appelle les commandes DBUS R/W vers le chemin de bus io.openthread.BorderRouter.wpan0, le chemin de l'objet /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 permet une introspection de ses fonctionnalités. Vous pouvez procéder comme suit:

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

Vous pouvez également consulter les fonctionnalités compatibles décrites sur cette page.

Option 2: API REST native HTTP de l'agent OTBR

c748ca5151b6cacb.png

Le routeur de bordure OpenThread se compile par défaut avec l'indicateur REST_API=1, ce qui active l'API REST. Si la compilation d'un atelier de programmation précédent n'a pas activé l'API REST, veillez à créer l'OTBR sur votre RPi avec cet indicateur:

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

Pour redémarrer un agent OTBR, exécutez la commande suivante:

$ sudo systemctl restart otbr-agent.service

L'agent démarre un serveur HTTP sur le port 8081. Ce serveur permet à un utilisateur ou à un programme de surveillance d'effectuer de nombreuses tâches dans l'OTBR (documenté ici). Vous pouvez inspecter son contenu à l'aide de votre navigateur curl ou wget. Les cas d'utilisation décrits ci-dessus figurent parmi les nombreux chemins acceptés, avec le verbe GET sur /node/dataset/active et le verbe PUT sur /node/dataset/pending

3. Configurer le framework d'identifiants sur Android

Identifiants souhaités

Les services Google Play sur Android autorisent et attendent l'enregistrement d'identifiants pour tous les TBR de votre réseau. Chacun d'eux est identifié par son ID d'agent de routeur bordure (BAID). Pour effectuer cette tâche, vous allez utiliser la méthode addCredentials() de l'interface ThreadNetworkClient. Le premier TBR ajouté à l'espace de stockage des services Google Play détermine les identifiants recommandés pour cet appareil mobile.

L'application qui ajoute un ensemble d'identifiants de réseau Thread à son BAID devient propriétaire des identifiants et dispose de toutes les autorisations nécessaires pour y accéder. Si vous essayez d'accéder à des identifiants ajoutés par d'autres applications, une erreur PERMISSION_DENIED s'affiche. Toutefois, les identifiants préférés sont toujours disponibles pour n'importe quelle application avec le consentement de l'utilisateur. Nous vous recommandons de mettre à jour les identifiants stockés dans les services Google Play lors de la mise à jour du réseau du routeur de bordure Thread. Bien que ces informations ne soient pas utilisées aujourd'hui, nous proposerons peut-être des parcours améliorés à l'avenir.

Même si le premier TBR est exclu par la suite, les identifiants préférés seront conservés sur l'appareil Android. Une fois ces identifiants définis, les autres applications qui gèrent les identifiants Thread peuvent les obtenir via un appel getPreferredCredentials().

Google TBR Sync

Les appareils Android se synchronisent automatiquement avec les TBR de Google. S'il n'existe aucun identifiant sur Android, les appareils les extraient des TBR Google de votre réseau. Ces identifiants deviennent les identifiants privilégiés. La synchronisation entre les TBR et l'appareil Android ne se produit que si le TBR est associé à un seul utilisateur ou à deux utilisateurs qui se trouvent dans la même maison connectée ( Structure).

Ce processus se produit également lorsqu'un autre utilisateur Google utilise GHA pour Android ou GHA pour iOS, et que l'utilisateur se trouve dans la même structure. Dans le cas de GHA pour iOS, les identifiants préférés sont définis sur l'espace de stockage iOS s'il n'existe aucun identifiant préféré.

Si deux appareils Android (ou Android + iGHA) existent sur le même réseau avec des ensembles d'identifiants préférés différents, l'appareil qui a initialement configuré le TBR prévaut sur le TBR.

Intégration des modes TBR tiers

L'espace de stockage associé à l'identifiant n'est actuellement pas limité par la maison connectée de l'utilisateur ( Structure). Chaque appareil Android dispose d'un espace de stockage BAID, mais une fois qu'un TBR Google est disponible sur le réseau, les autres appareils Android et iOS exécutant l'application Google Home pour iOS se synchronisent avec ce TBR et tentent de définir des identifiants locaux sur l'espace de stockage du téléphone.

Avant de créer un réseau, il est important de vérifier si un réseau préféré existe déjà dans l'espace de stockage Android.

  • Si un réseau préféré existe, le fournisseur doit l'utiliser. Cela permet de s'assurer que les appareils Thread sont connectés à un seul réseau Thread lorsque cela est possible.
  • S'il n'existe aucun réseau préféré, créez un ensemble d'identifiants et attribuez-le à votre TBR dans les services Google Play. Android utilisera ces identifiants comme étant les identifiants standards définis sur tous les TBR basés sur Google. Les autres fournisseurs pourront améliorer la portée et la robustesse de votre réseau maillé avec des appareils supplémentaires.

cd8bc726f67b1fa1.png

4. Cloner et modifier votre application Android

Nous avons créé une application Android qui présente les principaux appels possibles à l'API Thread. Vous pouvez utiliser ces schémas dans votre application. Dans cet atelier de programmation, vous allez cloner l'application exemple Google Home pour Matter à partir de GitHub.

Tout le code source présenté ici est déjà codé dans l'exemple d'application. Vous êtes invité à la modifier selon vos besoins, mais vous pouvez simplement cloner l'application ou exécuter les binaires prédéfinis pour inspecter la fonctionnalité.

  1. Clonez-le à l'aide de la commande suivante:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. Téléchargez et ouvrez Android Studio.
  2. Cliquez sur Fichier > Ouvrez le dépôt cloné et pointez vers celui-ci.
  3. Activez le mode développeur sur votre téléphone Android.
  4. Connectez-la à votre ordinateur à l'aide d'un câble USB.
  5. Exécutez l'application depuis Android Studio via <Cmd+R>. (OS X) ou <Ctrl+R> (Windows, Linux)
  6. Accéder à la roue -> Utilitaires pour les développeurs -> Réseau Thread
  7. Interagissez avec les différentes options disponibles. Dans les sections ci-dessous, nous allons décompresser le code exécuté sur chaque bouton.

Existe-t-il des identifiants préférés ?

La première question qu'un fabricant de TBR doit demander à Google est de savoir s'il existe déjà un ensemble d'identifiants préféré sur l'appareil. Cela doit être le point de départ de votre flux. Le code ci-dessous interroge le GPS sur l'existence d'identifiants. Elle ne demande pas le consentement de l'utilisateur, car aucun identifiant n'est partagé.

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

Obtenir des identifiants GPS préférés

S'ils existent, vous devez les lire. La seule différence par rapport au code précédent est qu'après avoir reçu intentSenderResult, vous souhaitez créer et lancer un intent en utilisant ce résultat de l'expéditeur.

Dans notre code, à des fins d'organisation et d'architecture, nous utilisons un MutableLiveData<IntentSender?>, car le code d'origine se trouve dans le ViewModel (ThreadViewModel.kt) et les observateurs d'intent se trouvent dans le fragment d'activité ( ThreadFragment.kt). Ainsi, une fois que l'intentSenderResult est publié dans les données actives, nous exécutons le contenu de cet observateur:

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()
 }
}

Cela déclenchera le consentement de l'utilisateur avec les identifiants de partage et, en cas d'approbation, renverra le contenu via:

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, "")
   }
 }

La publication des identifiants sur MutableLiveData<ThreadNetworkCredentials?> est décrite ci-dessous.

Configurer des identifiants GPS

Qu'elles existent ou non, vous devez enregistrer votre TBR auprès des services Google Play. Votre application sera la seule à pouvoir lire les identifiants associés à l'ID de l'agent de frontière de votre TBR, mais si celui-ci est le premier à s'enregistrer, ces identifiants seront copiés dans l'ensemble d'identifiants privilégiés. Ces informations sont accessibles par toutes les applications installées sur le téléphone, à condition que l'utilisateur les y autorise.

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

Définir les identifiants de votre produit TBR

Cette partie est la propriété de chaque fournisseur. Dans cet atelier de programmation, nous l'implémentons via un serveur REST HTTP DBUS+Python, ou via le serveur REST HTTP natif d'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()
)

Obtenir les identifiants à partir de votre produit TBR

Comme indiqué précédemment, utilisez le verbe HTTP GET pour obtenir les identifiants de votre TBR. Consultez l'exemple de script Python.

Compilation et importations

Lors de la création de votre application Android, vous devez apporter des modifications à votre fichier manifeste, à votre build et à vos importations afin de prendre en charge le module Thread des services Google Play. Les trois extraits de code suivants résument la plupart des ajouts.

Notez que notre application exemple est principalement conçue pour la mise en service avec Matter. Par conséquent, ses fichiers manifeste et Gradle sont plus complexes que les ajouts nécessaires pour utiliser uniquement les identifiants de thread.

Modifications du fichier manifeste

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

Importations pertinentes

// 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. Découverte de mDNS/SD

Notre application exemple utilise la détection mDNS/SD pour créer une liste des routeurs de bordure Thread disponibles sur le réseau, ainsi que leurs BAID respectifs.

C'est très utile lorsque vous saisissez les informations de votre TBR dans l'espace de stockage des identifiants GPS. Cependant, son utilisation dépasse le cadre de cet atelier de programmation. Nous utilisons la bibliothèque Android Service Discovery NSDManager, et l'intégralité du code source est disponible dans l'application exemple, dans ServiceDiscovery.kt.

6. Synthèse

Une fois que vous avez implémenté ces appels ou utilisé l'application exemple, vous pouvez entièrement intégrer votre RPi OTBR. Notre application exemple présente huit boutons:

91979bf065e9673d.png

Voici un exemple de séquence d'intégration de votre taux de conversion:

  1. Demander si des identifiants préférentiels existent (bleu, 1re ligne)
  2. Selon la réponse
  3. Obtenir les identifiants GPS recommandés (bleu, 2e ligne)
  4. Définir les identifiants TBR sur le GPS (bleu, 3e ligne) -> Sélectionnez votre TBR -> Créer un ordre aléatoire -> Saisissez le nom du réseau -> OK
  5. Maintenant que vous disposez d'identifiants de prédilection, définissez-les sur votre OTBR à l'aide de l'option Définir les identifiants RPi OTBR. Ces identifiants seront alors appliqués à l'ensemble en attente.

Par défaut, l'application exemple utilise un délai de 10 secondes. Après cette période, les identifiants de votre TBR RPi (et des autres nœuds pouvant exister sur son réseau) migreront vers le nouvel ensemble de données.

7. Conclusion

Dans cet atelier de programmation, nous avons cloné un exemple d'application Android et analysé plusieurs extraits de code qui utilisent les services Google Play. API Thread Storage. Nous avons utilisé ces API pour disposer d'un ensemble de données commun que nous pouvons intégrer à un TBR RPi, qui présente le TBR d'un fournisseur.

Le fait d'avoir tout le TBR d'un utilisateur sur le même réseau améliore la résilience et la portée du réseau Thread. Il empêche également les parcours utilisateur défectueux où les applications ne peuvent pas intégrer des appareils Thread parce qu'elles n'ont pas accès aux identifiants.

Nous espérons que cet atelier de programmation et les applications exemples vous aideront à concevoir et à développer votre propre application et votre produit de routeur de bordure Thread.

8. Références

Coprocesseur RCP

DBUS