1. 准备工作
在 Thread 边界路由器 (TBR) Codelab 中,我们展示了如何基于 Raspberry Pi 构建 Thread 边界路由器。在该 Codelab 中,我们
- 在 Thread 和 Wi-Fi/以太网网络之间建立双向 IP 连接。
- 通过 mDNS(开启 Wi-Fi/以太网链路)和 SRP(开启 Thread 网络)提供双向服务发现。
此 Codelab 基于上一个 Codelab,介绍了您自己的边界路由器和应用如何与 Google API 互动以创建单个 Thread 网络。合并 Thread 凭据非常重要,因为这有助于提高网络稳健性,并简化用户与依赖 Thread 的应用的互动。
前提条件
- 完成 OTBR Codelab
- 具备 Linux、Android/Kotlin 和 Thread 网络方面的基础知识
学习内容
- 如何使用 Thread 共享 API 获取和设置凭据集
- 如何设置自己的 OpenThread 边界路由器,并使用与 Google 网络相同的凭据
所需条件
- Raspberry Pi 4 板或其他运行开放 Thread 边界路由器 (OTBR) 的基于 Linux 的板
- 以无线电协处理器 (RCP) 形式提供 IEEE 802.15.4 连接的板。如需查看不同 SoC 供应商的代码库列表及其说明,请访问 OpenThread GitHub 页面
2. 设置 HTTP 服务
我们需要的第一项构建块是一个接口,用于读取有效凭据并将待处理凭据写入 OTBR。构建 TBR 时,请使用您自己的专有机制,如下所示的两个示例。第一个选项展示了如何通过 DBUS 在本地与 OTBR 代理进行交互,而第二个选项则利用了可在 OTBR 上构建的 REST API。
这两种方法都不安全,不应在生产环境中直接使用。不过,供应商可以围绕任一方法构建加密功能,以便在生产环境中使用该方法;或者,您可以扩展自己的监控服务,以发出环回 HTTP 或固有的本地 DBUS 调用。
选项 1:Python 脚本上的 DBUS 和 HTTP API

此步骤会创建一个最基本的 HTTP 服务,该服务会公开两个用于读取和设置凭据的端点,最终调用 DBUS 命令。
在将用作 OTBR 的 RPi 上,安装 Python 3 依赖项:
$ pip install dbus-python shlex json
以以下方式运行脚本:
$ sudo python credentials_server.py 8081 serving at port 8081
该示例在端口 8081 上设置 HTTP 服务器,并侦听根路径,以接收用于检索 Thread 凭据的 GET 请求或用于设置 Thread 凭据的 POST 请求。载荷始终是包含 TLV 的 JSON 结构。
以下 PUT 请求使用 /node/dataset/pending 路径为 OTBR 设置新的待处理线程凭据。在这种情况下,系统会在 10 秒后应用待处理的凭据:
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
}
向 /node/dataset/active 发出的 GET 请求会提取当前有效的凭据。
GET /node/dataset/active Host: <IP>:8081 ContentType = "application/json" acceptMimeType = "text/plain" ... <TLV encoded Thread Dataset>
该脚本会调用 DBUS R/W 命令,以访问总线路径 io.openthread.BorderRouter.wpan0 和对象路径 /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 允许自省其功能。您可以按如下方式操作:
$ sudo dbus-send --system --dest=io.openthread.BorderRouter.wpan0 \
--type=method_call --print-reply /io/openthread/BorderRouter/wpan0 \
org.freedesktop.DBus.Introspectable.Introspect
您还可以查看此处记录的支持的功能。
选项 2:OTBR 代理原生 HTTP Rest API

OpenThread 边界路由器默认使用标志 REST_API=1 构建,从而启用 REST API。如果您之前 Codelab 中的 build 未启用 REST API,请务必使用以下标志在 RPi 上构建 OTBR:
$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup
可通过运行以下命令重启 OTBR 代理:
$ sudo systemctl restart otbr-agent.service
代理在端口 8081 上启动 HTTP 服务器。此服务器允许用户或监控程序在 OTBR 中执行许多任务(相关文档请参阅此处)。您可以使用浏览器、curl 或 wget 来检查其内容。在许多支持的路径中,包括上述用例,其中 /node/dataset/active 上有 GET 动词,/node/dataset/pending 上有 PUT 动词。
3. 在 Android 上设置凭据框架
首选凭据
Android 上的 Google Play 服务允许并期望注册网络中所有 TBR 的凭据。每个设备都通过其边界路由器代理 ID (BAID) 进行标识。您将使用 ThreadNetworkClient 接口的 addCredentials() 方法来执行此任务。添加到 Google Play 服务存储空间中的第一个 TBR 决定了相应移动设备的偏好凭据。
向其 BAID 添加一组 Thread 网络凭据的应用将成为相应凭据的所有者,并拥有访问这些凭据的完整权限。如果您尝试访问其他应用添加的凭据,则会收到 PERMISSION_DENIED 错误。不过,在征得用户同意后,任何应用都可以随时使用首选凭据。我们建议您在 Thread 边框路由器网络更新时,及时更新存储在 Google Play 服务中的凭据。虽然目前我们不会使用这些信息,但将来可能会提供更完善的体验。
即使稍后排除第一个 TBR,首选凭据仍会保留在 Android 设备上。设置后,管理 Thread 凭据的其他应用可以通过 getPreferredCredentials() 调用获取凭据。
Google TBR 同步
Android 设备会自动与 Google TBR 同步。如果 Android 上不存在任何凭据,设备会从您网络中的 Google TBR 中提取凭据,这些凭据会成为首选凭据。只有在 TBR 与单个用户配对,或者与同一智能住宅(结构)中的两个用户配对时,TBR 与 Android 设备之间才会进行同步。
当其他 Google 用户在 GHA for Android 或 GHA for iOS 上时,如果该用户与您位于同一结构中,系统也会执行此流程。对于适用于 iOS 的 GHA,如果没有首选凭据,则会在 iOS 存储空间中设置首选凭据。
如果同一网络中存在两部 Android 设备(或 Android + iGHA),且这两部设备具有不同的首选凭据集,则最初配置 TBR 的设备将优先使用 TBR。
第三方 TBR 入门
凭据的存储空间目前不受用户智能家居(结构)的限制。每部 Android 设备都有自己的 BAID 存储空间,但一旦网络中存在 Google TBR,其他 Android 设备和运行 Google Home 应用(适用于 iOS)的 iOS 设备就会与该 TBR 同步,并尝试在手机存储空间中设置本地凭据。
在新的 OOB TBR 创建网络之前,务必要检查 Android 的存储空间中是否已存在首选网络。
- 如果存在首选网络,供应商应使用该网络。这可确保 Thread 设备尽可能连接到单个 Thread 网络。
- 如果不存在首选网络,请在 Google Play 服务中创建新的凭据集并将其分配给您的 TBR。Android 会将这些凭据视为所有基于 Google 的 TBR 上设置的标准凭据,其他供应商将能够通过其他设备来增强网状网络的覆盖范围和稳健性

4. 克隆和修改 Android 应用
我们创建了一个 Android 应用,其中展示了对 Thread API 的主要可能调用。您可以在自己的应用中使用这些模式。在此 Codelab 中,我们将从 GitHub 克隆 Google Home Sample App for Matter。
此处显示的所有源代码均已在示例应用中编码。您可以根据自己的需求修改这些代码,也可以直接克隆应用或运行预构建的二进制文件来检查相应功能。
- 使用以下命令克隆该代码库:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
- 下载并打开 Android Studio。
- 依次点击“文件”>“打开”,然后指向您克隆的代码库。
- 在 Android 手机上启用开发者模式。
- 通过 USB 线将其连接到计算机。
- 通过 <Cmd+R> (OS X) 或 <Ctrl+R> (Win, Linux) 从 Android Studio 运行应用
- 依次前往“轮子”图标 ->“开发者实用程序”图标 ->“Thread 网络”图标
- 与可用的不同选项互动。在下面的部分中,我们将详细介绍在每个按钮上执行的代码。
是否存在首选凭据?
TBR 制造商应向 Google 提出的第一个问题是,设备中是否已存在首选的一组凭据。这应该是您流程的起点。以下代码用于查询 GPS 上是否存在凭据。由于未共享任何凭据,因此系统不会提示用户同意。
/**
* 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)
}
}
获取 GPS 首选凭据
如果存在,您需要读取凭据。与之前的代码相比,唯一的区别在于,在收到 intentSenderResult 后,您希望使用发件人的相应结果来构建和启动 intent。
在我们的代码中,出于组织/架构目的,我们使用了 MutableLiveData<IntentSender?>,因为原始代码位于 ViewModel (ThreadViewModel.kt) 中,而 intent 观察者位于 Activity Fragment (ThreadFragment.kt) 中。因此,一旦将 intentSenderResult 发布到实时数据,我们将执行此观察者的内容:
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()
}
}
此操作将触发用户同意共享凭据,如果获得批准,将通过以下方式返回内容:
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, "")
}
}
将凭据发布到 MutableLiveData<ThreadNetworkCredentials?> 的方法如下所述。
设置 GPS 凭据
无论它们是否存在,您都应将 TBR 注册到 Google Play 服务中。您的应用将是唯一能够读取与 TBR 的 Border Agent ID 相关联的凭据的应用,但如果您的 TBR 是第一个注册的,则这些凭据将被复制到首选凭据集中。只要用户授权,手机上的任何应用都可以访问该信息。
/**
* 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)
}
}
}
为 TBR 产品设置凭据
此部分是各供应商的专有部分,在此 Codelab 中,我们通过 DBUS+Python HTTP Rest 服务器或 OTBR 的原生 HTTP Rest 服务器来实现它。
/**
* 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()
)
从 TBR 产品获取凭据
如前所示,使用 GET HTTP 动词从 TBR 获取凭据。请参阅示例 Python 脚本。
构建和导入
创建 Android 应用时,您需要对清单、构建和导入进行更改,以支持 Google Play 服务 Thread Module。以下三个代码段总结了大部分新增内容。
请注意,我们的示例应用主要是为 Matter 调试而构建的。因此,其清单文件和 Gradle 文件比仅使用线程凭据所需的添加项更复杂。
清单变更
<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'
相关导入
// 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 发现
我们的示例应用使用 mDNS/SD 发现功能来创建网络中可用的 Thread 边界路由器列表,以及它们各自的 BAID。
在将 TBR 的信息输入到 GPS 凭据的存储空间时,此功能非常有用。不过,其用法超出了此 Codelab 的范围。我们使用 Android 服务发现库 NSDManager,完整源代码可在示例应用中的 ServiceDiscovery.kt 中找到。
6. 小结
实现这些调用或使用示例应用后,您就可以完全配置 RPi OTBR。我们的示例应用公开了 8 个按钮:

以下是为 TBR 设置初始配置的可能顺序:
- 查询是否存在优先凭据(蓝色,第 1 行)
- 取决于回答
- 获取 GPS 首选凭据(蓝色,第 2 行)
- 在 GPS 中设置 TBR 凭据(蓝色,第 3 行)-> 选择您的 TBR -> 创建随机 -> 输入网络名称 -> 确定
- 现在您已拥有首选凭据,请使用 Set RPi OTBR credentials 将其设置为 OTBR,这会将这些凭据应用于待处理的设置。
示例应用的默认延迟时间为 10 秒。因此,在此时间段之后,您的 RPi TBR(以及可能存在于其网络中的其他节点)的凭据将迁移到新数据集。
7. 总结
在此 Codelab 中,我们克隆了一个示例 Android 应用,并分析了使用 Google Play 服务的线程存储 API 的多个代码段。我们使用这些 API 来获得一个可在 RPi TBR 上载入的通用数据集,该数据集展示了供应商的 TBR。
将用户的所有 TBR 放在同一网络中可提高 Thread 网络的弹性和覆盖范围。它还可以防止出现有缺陷的用户历程,即应用无法载入 Thread 设备,因为它们无法访问凭据。
我们希望本 Codelab 和示例应用能帮助您设计和开发自己的应用和 Thread 边界路由器产品。
8. 参考编号
RCP 协处理器
DBUS