1. 准备工作
在 Thread 边界路由器 (TBR) Codelab 中,我们将介绍如何基于 Raspberry Pi 构建 Thread 边界路由器。在该 Codelab 中,我们
- 在 Thread 网络和 Wi-Fi/以太网网络之间建立双向 IP 连接。
- 通过 mDNS(开启 Wi-Fi/以太网链路)和 SRP(开启 Thread 网络)提供双向服务发现。
本 Codelab 基于上一个 Codelab,介绍了如何让您自己的边界路由器和应用与 Google API 交互,以构建单个线程网络。合并 Thread 凭据非常重要,因为这有助于提高网络稳健性,并简化用户与依赖于 Thread 的应用的互动。
前提条件
- 完成 OTBR Codelab
- 具备 Linux、Android/Kotlin 和线程网络方面的基础知识
学习内容
- 如何使用线程共享 API 获取和设置凭据集
- 如何使用与 Google 网络相同的凭据设置自己的 OpenThread 边界路由器
所需条件
- Raspberry Pi 4 开发板或其他搭载 Open 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 服务器,并在根路径上监听用于检索线程凭据的 GET 请求或用于设置线程凭据的 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>
该脚本会向总线路径 io.openthread.BorderRouter.wpan0
、对象路径 /io/openthread/BorderRouter/wpan0
调用 DBUS 读写命令:
# 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 Agent 原生 HTTP Rest API
OpenThread 边界路由器默认使用标志 REST_API=1
进行构建,以启用 REST API。如果您在上一个 Codelab 中构建的项目未启用 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 上设置 Credential Framework
首选凭据
Android 上的 Google Play 服务允许并要求为网络中的所有 TBR 注册凭据。每个代理都通过其边界路由器代理 ID (BAID) 进行标识。您将使用 ThreadNetworkClient
接口的 addCredentials()
方法来执行此任务。添加到 Google Play 服务存储空间的第一个 TBR 决定了此移动设备的首选凭据。
向其 BAID 添加一组 Thread 网络凭据的应用会成为凭据的所有者,并拥有对这些凭据的完整访问权限。如果您尝试访问其他应用添加的凭据,则会收到 PERMISSION_DENIED 错误。不过,在用户同意的情况下,首选凭据始终可供任何应用使用。我们建议您在更新 Thread 边界路由器网络时,及时更新存储在 Google Play 服务中的凭据。虽然目前不会使用这些信息,但我们将来可能会提供增强型转化历程。
即使之后排除了第一个 TBR,首选凭据仍会保留在 Android 设备上。设置完成后,管理线程凭据的其他应用可以通过 getPreferredCredentials()
调用获取凭据。
Google TBR 同步
Android 设备会自动与 Google TBR 同步。如果 Android 设备上没有凭据,则设备会从您网络中的 Google TBR 中提取凭据,这些凭据会成为首选凭据。只有在 TBR 与单个用户配对,或者与同一智能住宅 (Structure) 中的两位用户配对的情况下,才会在 TBR 和 Android 设备之间进行同步。
如果其他 Google 用户使用的是 Android 版 GHA 或 iOS 版 GHA,并且位于同一结构中,也会发生此过程。对于适用于 iOS 的 GHA,如果不存在首选凭据,系统会在 iOS 存储空间上设置首选凭据。
如果同一网络中存在两部 Android 设备(或 Android 设备 + iGHA),且它们具有不同的首选凭据集,则最初配置 TBR 的设备在 TBR 上占优。
第三方 TBR 初始配置
凭据的存储空间目前不受用户的智能住宅(结构)的范围限制。每部 Android 设备都有自己的 BAID 存储空间,但一旦网络中存在 Google TBR,其他搭载 Google Home 应用(适用于 iOS)的 Android 设备和 iOS 设备将与该 TBR 同步,并尝试在手机存储空间中设置本地凭据。
在新的 OOB TBR 创建网络之前,请务必检查 Android 存储空间中是否已存在首选网络。
- 如果存在首选网络,供应商应使用该网络。这可确保 Thread 设备尽可能连接到单个 Thread 网络。
- 如果不存在首选网络,请创建新的凭据集,并在 Google Play 服务中将其分配给您的 TBR。Android 将这些凭据视为所有基于 Google 的 TBR 上设置的标准凭据,其他供应商将能够通过其他设备增强您的网状网络覆盖范围和稳健性
4. 克隆和修改 Android 应用
我们创建了一个 Android 应用,展示了对 Thread API 可能的的主要调用。您可以在应用中使用这些模式。在此 Codelab 中,我们将从 GitHub 克隆适用于 Matter 的 Google Home 示例应用。
此处显示的所有源代码都已在示例应用中编写。您可以根据自己的需求对其进行修改,但也可以直接克隆应用或运行预构建的二进制文件来检查功能。
- 使用以下命令克隆该项目:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
- 下载并打开 Android Studio。
- 依次点击“File”>“Open”,然后指向您克隆的代码库。
- 在 Android 手机上启用开发者模式。
- 使用 USB 线将其连接到计算机。
- 通过 <Cmd+R>(OS X)或 <Ctrl+R>(Win、Linux)从 Android Studio 运行应用
- 依次前往“轮”->“开发者实用程序”->“线程网络”
- 与可用的不同选项互动。在以下部分中,我们将解压缩在每个按钮上执行的代码。
是否存在首选凭据?
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 的边境代理 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 应用时,您需要更改清单、build 和导入内容,以支持 Google Play 服务线程模块。以下三个代码段总结了大部分新增内容。
请注意,我们的示例应用主要用于 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 -> 创建随机 -> 输入网络名称 -> 确定
- 现在,您已拥有首选凭据,请使用设置 RPi OTBR 凭据将其设置为 OTBR,这会将这些凭据应用于待处理的集合。
示例应用的默认延迟时间为 10 秒。因此,在此期限结束后,RPi TBR(以及其网络上可能存在的其他节点)的凭据将迁移到新的数据集。
7. 总结
在此 Codelab 中,我们克隆了一个 Android 示例应用,并分析了几段使用 Google Play 服务的线程存储 API 的代码。我们使用这些 API 创建了一个通用数据集,可在 RPi TBR 上进行初始配置,以展示供应商的 TBR。
将用户的所有 TBR 放置在同一网络中,可以提高线程网络的弹性和覆盖范围。它还可防止出现用户体验缺陷的情况,即应用因无权访问凭据而无法引导用户设置 Thread 设备。
我们希望此 Codelab 和示例应用能帮助您设计和开发自己的应用和 Thread 边界路由器产品。
8. 参考文献
RCP 协处理器
DBUS