与 Google Thread Credentials API 共享 Thread 网络

1. 准备工作

在我们的线程边界路由器 (TBR) Codelab 中,我们展示了如何基于 Raspberry Pi 构建线程边界路由器。在该 Codelab 中,

  • 在 Thread 和 Wi-Fi/以太网网络之间建立双向 IP 连接。
  • 通过 mDNS(通过 Wi-Fi/以太网链接)和 SRP(在 Thread 网络上)提供双向服务发现。

此 Codelab 以上一个 Codelab 为基础,探讨了您自己的边界路由器和应用如何与 Google API 交互以构建单 Thread 网络。收敛 Thread 凭据很重要,因为它可以提高网络的稳健性,并简化用户与依赖于 Thread 的应用之间的交互。

前提条件

  • 完成 OTBR Codelab
  • 具备 Linux、Android/Kotlin 和线程网络方面的基础知识

学习内容

  • 如何使用 Thread Shared API 获取和设置凭据集
  • 如何使用与 Google 网络相同的凭据设置您自己的 OpenThread 边界路由器

所需条件

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

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 服务器,并监听根路径,以监听用于检索 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/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/activePUT 动词用于 /node/dataset/pending

3. 在 Android 上设置凭据框架

首选凭据

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

将一组 Thread 网络凭据添加到其 BAID 的应用会成为这些凭据的所有者,并拥有访问这些凭据的完整权限。如果您尝试访问其他应用添加的凭据,则会收到 PERMISSION_DENIED 错误。不过,在征得用户同意后,任何应用始终可以使用首选凭据。我们建议您在线程边界路由器网络更新时及时更新 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,其他 Android 设备和运行 iOS 版 Google Home 应用的 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> 从 Android Studio 运行应用(OS X) 或 <Ctrl+R>(Win、Linux)
  6. 导航至方向盘 ->开发者工具 ->Thread 网络
  7. 使用不同的可用选项进行互动。在以下部分中,我们将解压对每个按钮执行的代码。

是否存在首选凭据?

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 凭据

无论它们是否存在,您都应在 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 调试而构建。因此,其清单和 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 发现功能来创建网络中可用的线程边界路由器列表以及它们各自的 BAID。

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

6. 整合所有内容

实现这些调用或使用示例应用后,您就可以全面完成 RPP OTBR 的初始配置。我们的示例应用提供 8 个按钮:

91979bf065e9673d

初始配置 TBR 的可能顺序是:

  1. 查询是否存在首选凭据(蓝色,第 1 行)
  2. 根据回答
  3. 获取首选 GPS 凭据(蓝色,第 2 行)
  4. 在 GPS 中设置 TBR 凭据(蓝色,第 3 行)->选择您的 TBR ->创建随机 ->输入网络名称 ->好的
  5. 现在您已经拥有首选凭据,接下来请使用设置 RPI OTBR 凭据将它们设置为您的 OTBR,此操作会将这些凭据应用于待处理凭据。

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

7. 总结

在此 Codelab 中,我们克隆了一个示例 Android 应用,并分析了几个利用 Google Play 服务Thread Storage API。我们使用这些 API 构建了一个通用数据集,该数据集可用于 Ri TBR 上,展示了供应商的 TBR。

将用户的所有 TBR 置于同一网络中,从而提高 Thread 网络的弹性和覆盖面。此外,这还可以防止用户体验历程出现缺陷,即应用因没有凭据而无法进行 Thread 设备的初始配置。

我们希望此 Codelab 和示例应用可以帮助您设计和开发自己的应用和线程边界路由器产品。

8. 参考文献

RCP 协处理器

DBUS