1. Before you begin
In our Thread Border Router (TBR) codelab, we show how to build a Thread Border Router based on a Raspberry Pi. In that codelab we
- Establish Bidirectional IP connectivity between Thread and Wi-Fi/Ethernet networks.
- Provide Bidirectional service discovery via mDNS (on Wi-Fi/Ethernet link) and SRP (on Thread network).
This codelab builds on the previous one, addressing how your own border router and your app can interact with Google APIs to make a single Thread Network. Converging Thread credentials is important as it adds to the network robustness and simplifies user interactions with the applications that rely on Thread.
Prerequisites
- Complete the OTBR Codelab
- Basic knowledge of Linux, Android/Kotlin, and Thread networking
What you'll learn
- How to use the Thread Sharing APIs to Get and Set credential sets
- How to set up your own OpenThread Border Router with the same credentials as Google's network
What you'll need
- Raspberry Pi 4 board or another Linux-based board running the Open Thread Border Router (OTBR)
- Board that provides IEEE 802.15.4 connectivity as a Radio Co-Processor (RCP). See a list of repositories of different SoC vendors and their instructions on the OpenThread Github page
2. Setting up HTTP service
The first building block we need is an interface that allows us to read Active Credentials and write Pending Credentials to your OTBR. When building a TBR, use your own proprietary mechanisms, as shown here with two examples. The first option showcases how to interface with the OTBR agent locally via DBUS, while the second leverages the Rest API that can be built on the OTBR.
Neither method is secure, and should not be used as-is in a production environment. However, a vendor can build encryption around either method to use it in a production environment, or you can extend your own monitor service to issue loopback HTTP or inherently local DBUS calls.
Option 1: DBUS and HTTP API on Python Script
This step creates a bare-bones HTTP service that exposes two endpoints to read and set credentials, ultimately calling DBUS commands.
On the RPi that will serve as your OTBR, install the Python 3 dependencies:
$ pip install dbus-python shlex json
Run the script as:
$ sudo python credentials_server.py 8081 serving at port 8081
The sample sets up an HTTP server on port 8081 and listens on the root path either for a GET request to retrieve Thread credentials, or a POST request to set Thread credentials. The payload is always a JSON structure with the TLV.
The following PUT request sets new Pending Thread Credentials to the OTBR via the /node/dataset/pending
path. In this case, the pending credentials are applied in 10 seconds:
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 }
A GET request to /node/dataset/active
fetches the currently Active Credentials.
GET /node/dataset/active Host: <IP>:8081 ContentType = "application/json" acceptMimeType = "text/plain" ... <TLV encoded Thread Dataset>
The script calls DBUS R/W commands to the bus path io.openthread.BorderRouter.wpan0
, object path /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 allows introspection of its capabilities. You may do this as:
$ sudo dbus-send --system --dest=io.openthread.BorderRouter.wpan0 \ --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 \ org.freedesktop.DBus.Introspectable.Introspect
You can also check the supported capabilities documented here.
Option 2: OTBR Agent native HTTP Rest API
The OpenThread Border Router builds by default with the flag REST_API=1
, enabling the REST API. In case your build from a previous codelab did not enable the REST API, make sure to build OTBR on your RPi with that flag:
$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup
An OTBR agent can be restarted by running:
$ sudo systemctl restart otbr-agent.service
The agent starts an HTTP server on port 8081. This server allows a user or monitor program to perform many tasks in the OTBR (documented here). You can use your browser, curl
or wget
to inspect its contents. Among the many supported paths are the use cases described above, with the GET
verb on /node/dataset/active
and the PUT
verb on /node/dataset/pending
3. Setting up Credential Framework on Android
Preferred Credentials
Google Play Services on Android allows and expects the registration of credentials for all the TBRs in your network. Each is identified by its Border Router Agent ID (BAID). You'll use the addCredentials()
method of the ThreadNetworkClient
interface to perform this task. The first TBR that is added to the Google Play Services storage determines the Preferred Credentials for this mobile device.
The app that adds a set of Thread network credentials to its BAID becomes the owner of the credentials, and has full permissions to access them. If you try to access credentials added by other apps, you'll receive a PERMISSION_DENIED error. However, the preferred credentials are always available for any app upon user consent. We recommend that you keep credentials that are stored in Google Play services up-to-date when the Thread Border Router network is updated. While that information is not used today, we may provide enhanced journeys in the future.
Even if the first TBR is later excluded, the Preferred Credentials will persist on the Android device. Once set, other Apps that manage Thread credentials may obtain the credentials from a getPreferredCredentials()
call.
Google TBR Sync
Android devices sync with Google TBRs automatically. If no credentials exist on Android, the devices extract them from Google TBRs in your network, and those credentials become the Preferred Credentials. Syncing between TBRs and the Android device happens only if the TBR is paired with a single user, or if it's paired with two users that are in the same Smart Home ( Structure).
This process will also happen when another Google user is on GHA for Android or GHA for iOS when the user is in the same Structure. In the case of GHA for iOS, the preferred credentials are set on iOS storage, if no preferred credentials exist.
If two Android devices (or Android + iGHA) exist in the same network with different sets of preferred credentials, the device that originally configured the TBR will prevail on the TBR.
3rd-party TBR onboarding
The credential's storage is currently not scoped by the user's Smart Home ( Structure). Each Android device will have its BAID storage, but once there is a Google TBR in the network, other Android devices and iOS devices running Google Home App for iOS will sync with that TBR and try to set local credentials on phone storage.
Before a new OOB TBR creates a network, it is important to check whether a preferred network already exists in Android's storage.
- If a preferred network exists, the vendor should use it. This ensures that Thread devices are connected to a single Thread network when possible.
- When no preferred network exists, create a new credential set and assign it to your TBR in Google Play Services. Android will honor those credentials as the standard credentials set on all Google-based TBRs, and other vendors will be able to enhance your mesh reach and robustness with additional devices
4. Cloning and Modifying your Android App
We've created an Android App that showcases the main possible calls to the Thread API. You can use these patterns in your app. In this codelab we'll clone the Google Home Sample App for Matter from Github.
All the source code shown here is already coded in the sample app. You're invited to modify it to your own needs, but you can simply clone the app or run the pre-built binaries to inspected the functionality.
- Clone it using:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
- Download and open Android Studio.
- Click on File > Open and point to your cloned repository.
- Enable the developer mode on your Android phone.
- Connect it to your computer via an USB cable.
- Run the App from Android Studio via <Cmd+R> (OS X) or <Ctrl+R> (Win, Linux)
- Navigate to the Wheel -> Developer Utilities -> Thread Network
- Interact with the different options available. In the sections below we'll unpack the code that is executed on every button.
Do preferred credentials exist?
The first question a TBR manufacturer should ask Google is whether a preferred set of credentials already exists in the device. This should be the starting point of your flow. The code below queries GPS on the existence of credentials. It does not prompt for user consent because no credential is shared.
/** * 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) } }
Getting GPS preferred credentials
In case they exist, you want to read the credentials. The only difference from the previous code is that after receiving the intentSenderResult
, you want to build and launch an intent using that result from the sender.
In our code, for organization/architecture purposes we use a MutableLiveData<IntentSender?>
because the original code is in the ViewModel (ThreadViewModel.kt) and the intent observers are in the Activity Fragment ( ThreadFragment.kt). Thus, once the intentSenderResult is posted to the live data, we will execute the contents of this observer:
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() } }
This will trigger user consent with sharing credentials, and, if approved, will return contents 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, "") } }
Posting the credentials to MutableLiveData<ThreadNetworkCredentials?>
is described below.
Setting GPS credentials
Whether they exist or not, you should register your TBR into Google Play Services. Your app will be the only one able to read the credentials associated with the Border Agent ID of your TBR, but if your TBR is the first to register, those credentials will be copied to the Preferred Credentials set. That information is accessible to any App on the phone, as long as the user authorizes it.
/** * 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) } } }
Setting the credentials to your TBR Product
This part is proprietary to each vendor, and in this codelab we implement it via either DBUS+Python HTTP Rest Server or the native HTTP Rest Server from 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() )
Getting the credentials from your TBR Product
As shown earlier, use the GET HTTP Verb for obtaining the credentials from your TBR. See the sample Python script.
Build and Imports
When creating your Android App, you'll need to make changes to your manifest, build, and imports to support the Google Play Services Thread Module. The following three snippets summarize most of the additions.
Note that our sample app primarily is built for Matter commissioning. Therefore, its Manifest and Gradle files are more complex than the additions necessary for just using Thread Credentials.
Manifest changes
<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'
Relevant Imports
// 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. mDNS/SD Discovery
Our sample app uses mDNS/SD discovery to create a list of available Thread Border Routers in the network, as well as their respective BAIDs.
This is very helpful when entering the information of your TBR into GPS credential's storage. However, its usage is beyond the scope of this codelab. We use Android Service Discovery library NSDManager, and the full source code is available in the Sample App, in ServiceDiscovery.kt
.
6. Putting everything together
Once you've implemented these calls or use the Sample App, you can fully onboard your RPi OTBR. Our Sample App exposes 8 buttons:
A possible sequence for onboarding your TBR is:
- Query whether preferential credentials exist (blue, 1st row)
- Depending on the answer
- Get GPS preferred credentials (blue, 2nd row)
- Set TBR credentials in GPS (blue, 3rd row) -> Select your TBR -> Create Random -> Enter network name -> Ok
- Now that you have preferred credentials, set them to your OTBR using Set RPi OTBR credentials, which will apply those credentials to the pending set.
The default for the sample app is to use 10 seconds delay. Thus after this period, the credentials of your RPi TBR (and other nodes that might exist on its network) will migrate to the new dataset.
7. Conclusion
In this codelab, we cloned a sample Android App and analyzed several snippets of code that make use of the Google Play Services' Thread Storage APIs. We used those APIs to have a common dataset we can onboard on a RPi TBR, which showcases a vendor's TBR.
Having all of a user's TBR in the same network improves the resilience and reach of the Thread Network. It also prevents flawed user journeys where apps cannot onboard Thread Devices because they don't have access to credentials.
We hope this codelab and Sample Apps help you design and develop your own App and your Thread Border Router product.
8. References
RCP co-processor
DBUS