与 Google Thread Credentials API 共享 Thread 网络

1. 准备工作

在我们的 Thread Border Router (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 sharing API 获取和设置凭据集
  • 如何使用与 Google 网络相同的凭据设置您自己的 OpenThread 边界路由器

所需条件

  • Raspberry Pi 4 开发板或运行 Open Thread Border Router (OTBR) 的其他基于 Linux 的开发板
  • 作为无线电协处理器 (RCP) 提供 IEEE 802.15.4 连接的开发板。在 OpenThread GitHub 页面上查看不同 SoC 供应商的代码库列表及其说明

2. 设置 HTTP 服务

我们需要的第一个构建块是一个接口,可让我们读取有效凭据并将待处理凭据写入您的 OTBR。构建 TBR 时,请使用您自己的专有机制,如此处的两个示例所示。第一个选项展示了如何通过 DBUS 在本地与 OTBR 代理进行交互,而第二个选项则利用可以在 OTBR 上构建的 REST API。

这两种方法都不太安全,也不应在生产环境中按原样使用。但是,供应商可以围绕在生产环境中使用的任何一种方法构建加密,或者您可以扩展自己的监控服务以发出环回 HTTP 或本身的本地 DBUS 调用。

方法 1:在 Python 脚本上使用 DBUS 和 HTTP API

91e5fdeed83e9354

此步骤将创建一个基本的 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 请求(用于检索 Thread 凭据)或 POST 请求(用于设置 Thread 凭据)。载荷始终是包含 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/activeGET 请求会提取当前有效凭据。

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 R/W 命令

# 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

c748ca5151b6cacb.png

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 中执行多项任务(详见此处)。您可以使用浏览器 curlwget 检查其内容。在众多支持的路径中,有上述用例,GET 动词在 /node/dataset/active 上使用,PUT 动词在 /node/dataset/pending 上使用

3. 在 Android 上设置凭据框架

首选凭据

Android 上的 Google Play 服务允许并期望为您的网络中的所有 TBR 注册凭据。每个虚拟机都通过其边界路由器代理 ID (BAID) 进行标识。您将使用 ThreadNetworkClient 接口的 addCredentials() 方法执行此任务。添加到 Google Play 服务存储空间的第一个 TBR 决定了此移动设备的首选凭据。

将一组 Thread 网络凭据添加到其 BAID 的应用将成为这些凭据的所有者,并拥有访问这些凭据的完整权限。如果您尝试访问其他应用添加的凭据,则会收到 PERMISSION_DENIED 错误。不过,用户同意后,任何应用始终都可以使用首选凭据。我们建议您在 Thread 边界路由器网络更新时,及时更新 Google Play 服务中存储的凭据。虽然我们目前无法使用这些信息,但将来可能会提供更优质的旅程。

即使稍后排除了第一个 TBR,首选凭据也将保留在 Android 设备上。设置后,管理 Thread 凭据的其他应用可以从 getPreferredCredentials() 调用获取凭据。

Google TBR 同步

Android 设备会自动与 Google TBR 同步。如果 Android 上不存在凭据,设备会从您网络中的 Google TBR 中提取凭据,这些凭据会成为首选凭据。仅当 TBR 与单个用户配对或者与同一个智能家居中的两个用户配对时(结构),TBR 与 Android 设备之间的同步才会发生。

当其他 Google 用户使用 Android 版 GHA 或 iOS 版 GHA 时,同一个结构也会发生这个过程。对于 iOS 版 GHA,如果不存在首选凭据,系统会在 iOS 存储空间上设置首选凭据。

如果同一网络中两台 Android 设备(或 Android + iGHA)具有不同的首选凭据组,则最初配置 TBR 的设备将优先采用 TBR。

第三方 TBR 新手入门

凭据的存储当前不在用户的智能家居范围(结构)的作用域内。每台 Android 设备都有自己的 BAID 存储空间,但是一旦该网络中有 Google TBR,运行 iOS 版 Google Home 应用的其他 Android 设备和 iOS 设备就会与该 TBR 同步,并尝试为手机存储空间设置本地凭据。

在新的 OOB TBR 创建网络之前,请务必检查 Android 存储空间中是否已存在首选网络。

  • 如果存在首选网络,供应商应使用该网络。这样可以确保 Thread 设备尽可能连接到单个 Thread 网络。
  • 如果首选网络不存在,请创建一个新的凭据集,并将其分配给 Google Play 服务中的 TBR。Android 会将这些凭据视为在所有基于 Google 的 TBR 上设置的标准凭据,其他供应商将能够通过其他设备增强您的网状网覆盖范围和稳健性

cd8bc726f67b1fa1.png

4. 克隆和修改 Android 应用

我们创建了一个 Android 应用,其中展示了对 Thread API 的主要可能调用。您可以在应用中使用这些模式。在本 Codelab 中,我们将从 GitHub 克隆适用于 Matter 的 Google Home 示例应用。

此处显示的所有源代码均已在示例应用中编码。我们诚邀您根据自己的需求对其进行修改,但您也可以直接克隆应用或运行预构建的二进制文件,以检查功能。

  1. 使用以下命令克隆:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. 下载并打开 Android Studio
  2. 依次点击“文件”>“打开”,然后指向克隆的代码库。
  3. 在 Android 手机上启用开发者模式
  4. 通过 USB 线将其连接到您的计算机。
  5. 使用 <Cmd+R> (OS X) 或 <Ctrl+R>(Win、Linux)在 Android Studio 中运行应用
  6. 依次前往“Wheel”->“Developer Utilities”->“Thread Network”
  7. 与提供的不同选项互动。在下面的部分中,我们将解压缩在每个按钮上执行的代码。

是否存在首选凭据?

TBR 制造商应问的第一个问题是,设备中是否已存在一组首选凭据。这应该是流程的起点。以下代码向 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 凭据

无论该实体是否存在,您都应在 Google Play 服务中注册您的 TBR。您的应用将是唯一能够读取与您的 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 Server 或 OTBR 的原生 HTTP Rest Server 来实现。

/**
* 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 服务线程模块。以下三个代码段总结了大部分新增内容。

请注意,我们的示例应用主要是为 Matter 调试而构建。因此,其 Manifest 和 Gradle 文件比仅使用 Thread Credentials 所需的额外内容更复杂。

清单更改

<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 发现功能创建网络中可用线程边界路由器的列表及其各自的 BAID。

将 TBR 的信息输入 GPS 凭据存储时,此功能非常有用。不过,其用法不在此 Codelab 的讨论范围内。我们使用 Android 服务发现库 NSDManager,完整的源代码可在示例应用的 ServiceDiscovery.kt 中找到。

6. 综合应用

实现这些调用或使用示例应用后,您就可以全面完成 RPi OTBR。我们的示例应用公开了 8 个按钮:

91979bf065e9673d.png

您可以采用以下步骤来引导 TBR 完成初始配置:

  1. 查询是否存在优先凭据(蓝色,第 1 行)
  2. 取决于答案
  3. 获取 GPS 首选凭据(蓝色,第二行)
  4. 在 GPS 中设置 TBR 凭据(蓝色,第 3 行)-> 选择您的 TBR -> 随机创建 -> 输入网络名称 -> 确定
  5. 现在您已拥有首选凭据,请使用设置 RPi OTBR 凭据将它们设置为您的 OTBR,以便将这些凭据应用于待处理的凭据。

示例应用的默认设置为使用 10 秒延迟。因此,此时间段过后,RPi TBR 的凭据(以及其网络上可能存在的其他节点)将迁移到新数据集。

7. 总结

在此 Codelab 中,我们克隆了一个示例 Android 应用,并分析了多个利用 Google Play 服务的 Thread Storage API 的代码段。我们使用这些 API 创建了一个通用数据集,以便在 RPi TBR 上加入,其中展示了供应商的 TBR。

将用户的所有 TBR 置于同一网络中可以提高 Thread 网络的弹性和覆盖范围。还可以防止应用因 Thread 设备无法访问凭据而无法对 Thread 设备进行初始配置。

我们希望此 Codelab 和示例应用可帮助您设计和开发自己的应用和 Thread Border Router 产品。

8. 参考文献

RCP 协处理器

DBUS