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 vous montrons comment créer un routeur de bordure Thread basé sur un Raspberry Pi. Dans cet atelier de programmation, nous avons

  • Établissez une connectivité IP bidirectionnelle entre les réseaux Thread et Wi-Fi/Ethernet.
  • Fournit la détection de services bidirectionnelle via mDNS (sur une liaison Wi-Fi/Ethernet) et SRP (sur un 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 utilisateur avec les applications qui s'appuient sur Thread.

Conditions préalables

Points abordés

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

Ce dont vous aurez besoin

  • Carte Raspberry Pi 4 ou autre carte Linux exécutant un routeur de bordure Open Thread (OTBR)
  • Carte fournissant une connectivité IEEE 802.15.4 en tant que coprocesseur radio (RCP). Consultez la liste des dépôts de différents fournisseurs de SoC et leurs instructions sur la page GitHub consacrée à 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 sur 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 se connecter à l'agent OTBR en local via DBUS, tandis que la seconde exploite l'API REST qui peut être développée sur l'OTBR.

Aucune de ces deux méthodes n'est sûre et ne doit pas être utilisée telle quelle dans un environnement de production. Cependant, un fournisseur peut intégrer le chiffrement autour de l'une ou l'autre de ces méthodes afin de l'utiliser dans un environnement de production, ou vous pouvez étendre votre propre service de surveillance pour émettre des appels HTTP de rebouclage ou des appels DBUS locaux par nature.

Option 1: DBUS et API HTTP sur le script Python

91e5fdeed83e9354.png

Cette étape crée un service HTTP épuré qui expose deux points de terminaison afin qu'ils puissent lire et définir les identifiants, avant d'appeler des commandes DBUS.

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

$ pip install dbus-python shlex json

Exécutez le script comme suit:

$  sudo python credentials_server.py 8081
serving at port 8081

L'exemple configure un serveur HTTP sur le port 8081 et écoute 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 le 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 au bout 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 la ligne de bus io.openthread.BorderRouter.wpan0 et le chemin d'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 l'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 ici.

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

c748ca5151b6cacb.png

Le routeur de bordure OpenThread est créé par défaut avec l'indicateur REST_API=1, ce qui active l'API REST. Si votre build à partir d'un atelier de programmation précédent n'a pas activé l'API REST, assurez-vous de créer un OTBR sur votre RPi avec cet indicateur:

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

Un agent OTBR peut être redémarré en exécutant 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 le protocole OTBR (décrit ici). Vous pouvez utiliser votre navigateur curl ou wget pour inspecter son contenu. Parmi les nombreux chemins d'accès compatibles figurent les cas d'utilisation décrits ci-dessus, avec le verbe GET sur /node/dataset/active et le verbe PUT sur /node/dataset/pending.

3. Configurer le Credential Framework sur Android

Identifiants recommandés

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

L'application qui ajoute un ensemble d'identifiants du réseau Thread à son BAID devient le propriétaire des identifiants et dispose de toutes les autorisations d'accès. 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 toutes les applications avec le consentement de l'utilisateur. Nous vous recommandons de maintenir à jour les identifiants stockés dans les services Google Play lorsque le réseau de routeur de bordure Thread est mis à jour. Ces informations ne sont pas utilisées aujourd'hui, mais il est possible que nous vous proposions des parcours améliorés à l'avenir.

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

Synchronisation TBR de Google

Les appareils Android se synchronisent automatiquement avec les TBR Google. Si aucun identifiant n'existe sur Android, les appareils les extraient des TBR Google de votre réseau, et ces identifiants deviennent les identifiants préféré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 lorsqu'il 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, en l'absence d'identifiants préférés.

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 l'emportera sur le TBR.

Intégration de la fonctionnalité TBR tierce

Actuellement, l'espace de stockage des identifiants n'est pas limité par la maison connectée de l'utilisateur ( Structure). Chaque appareil Android dispose de son espace de stockage BAID. Cependant, dès qu'un TBR Google est configuré sur le réseau, les autres appareils Android et iOS qui exécutent l'application Google Home pour iOS se synchronisent avec ce service et tentent de définir des identifiants locaux sur l'espace de stockage du téléphone.

Avant qu'un nouveau TBR OOB crée 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. Ainsi, les appareils Thread sont connectés à un réseau Thread unique lorsque cela est possible.
  • S'il n'existe aucun réseau préféré, créez un ensemble d'identifiants et attribuez-le à votre service TBR dans les services Google Play. Android conservera ces identifiants comme identifiants standards définis pour 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 modèles dans votre application. Dans cet atelier de programmation, nous allons cloner l'application exemple Google Home pour Matter depuis GitHub.

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

  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 > Ouvrir, puis pointez sur le dépôt cloné.
  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édez à Wheel -> Developer Utilitaires -> Thread Network.
  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 privilégiés ?

La première question qu'un fabricant TBR doit poser à Google est de savoir si l'appareil dispose déjà d'un ensemble d'identifiants préféré. Cela doit être le point de départ de votre flux. Le code ci-dessous interroge le GPS pour vérifier l'existence d'identifiants. Le consentement de l'utilisateur n'est pas demandé, 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 vos identifiants GPS préférés

S'ils existent, vous devez lire les identifiants. 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 le résultat de l'expéditeur.

Dans notre code, nous utilisons un MutableLiveData<IntentSender?> à des fins d'organisation et d'architecture, car le code d'origine se trouve dans le ViewModel (ThreadViewModel.kt) et les observateurs d'intents 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()
 }
}

Le consentement de l'utilisateur sera déclenché avec des identifiants de partage et, en cas d'approbation, le contenu sera renvoyé 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 dans MutableLiveData<ThreadNetworkCredentials?> est décrite ci-dessous.

Définir des identifiants GPS

Qu'elles existent ou non, vous devez enregistrer votre TBR dans les services Google Play. Votre application sera la seule à pouvoir lire les identifiants associés à l'ID d'agent frontal de votre service TBR, mais si votre TBR est le premier à s'enregistrer, ces identifiants seront copiés dans l'ensemble d'identifiants préférés. Ces informations sont accessibles à n'importe quelle Application sur le téléphone, à condition que l'utilisateur l'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émenterons via le serveur REST HTTP DBUS + Python ou 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 de votre produit TBR

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

Compilation et importation

Lorsque vous créez votre application Android, vous devez modifier votre fichier manifeste, votre build et vos importations pour assurer la compatibilité avec le module Thread des services Google Play. Les trois extraits de code suivants résument la plupart de ces ajouts.

Notez que notre application exemple est principalement conçue pour la mise en service de Matter. Par conséquent, ses fichiers manifeste et Gradle sont plus complexes que les ajouts nécessaires uniquement à l'utilisation des identifiants 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 concernées

// 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 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.

Ceci est très utile lors de la saisie des 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 le code source complet est disponible dans l'application exemple, sous ServiceDiscovery.kt.

6. Synthèse

Une fois que vous avez implémenté ces appels ou utilisé l'application exemple, vous pouvez procéder à l'intégration complète de votre OTBR RPi. Notre application exemple présente huit boutons:

91979bf065e9673d.png

Voici comment procéder pour intégrer votre service TBR:

  1. Demander si des identifiants préférentiels existent (bleu, première ligne)
  2. En fonction de la réponse
  3. Obtenir vos identifiants GPS préférés (bleu, 2e ligne)
  4. Définissez les identifiants TBR dans le GPS (bleu, 3e ligne) -> Sélectionnez votre TBR -> Create Random -> Enter network name -> OK
  5. Maintenant que vous avez défini vos identifiants préférés, définissez-les sur votre OTBR à l'aide de l'option Set RPi OTBR credentials (Définir les identifiants OTBR RPi). Ces identifiants seront 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) seront donc migrés 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 API Thread Storage des services Google Play. 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.

Avoir tous les TBR d'un utilisateur sur le même réseau améliore la résilience et la portée du réseau Thread. Cela permet également d'éviter les parcours utilisateur erronés dans lesquels les applications ne peuvent pas intégrer les appareils Thread, car elles n'ont pas accès aux identifiants.

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

8. Références

Coprocesseur RCP

DBUS