透過 Google Thread Credentials API 分享 Thread 網路

1. 事前準備

Thread Border Router (TBR) 程式碼研究室中,我們展示瞭如何根據 Raspberry Pi 建構 Thread 邊界路由器。在本程式碼研究室中,我們

  • 在 Thread 和 Wi-Fi/乙太網路之間建立雙向 IP 連線。
  • 透過 mDNS (使用 Wi-Fi/乙太網路連結) 和 SRP (支援 Thread 網路) 提供雙向服務探索功能。

本程式碼研究室以先前的版本為基礎,可協助您確定您自己的邊界路由器和應用程式與 Google API 如何互動,以建立單一 Thread 網路。Thread 憑證非常重要,因為這類憑證能提升網路穩定性,並簡化使用者與 Thread 應用程式之間的互動。

必要條件

課程內容

  • 如何使用 Thread Share API 取得及設定憑證集
  • 如何使用與 Google 網路相同的憑證設定您自己的 OpenThread 邊界路由器

軟硬體需求

  • Raspberry Pi 4 電路板或其他執行 Open Thread 邊界路由器 (OTBR) 的 Linux 電路板
  • 這個電路板提供 IEEE 802.15.4 連線能力做為無線電共同處理者 (RCP)。如要查看不同 SoC 廠商的存放區清單和相關操作說明,請前往 OpenThread GitHub 頁面

2. 設定 HTTP 服務

我們需要的第一個建構模塊是一個介面,可讓我們讀取有效憑證,並將待處理憑證寫入您的 OTBR。建立 TBR 時,請使用您自己的專屬機制,如下方兩個範例所示。第一個選項會說明如何透過 DBUS 在本機與 OTBR 代理程式連接,第二個選項則使用可在 OTBR 上建構的 Rest API。

這兩種方法都不安全,請勿在正式環境中依原樣使用。然而,廠商可以根據這兩種方法建立加密,以便在實際工作環境中使用,或者您也可以擴充自己的監控服務,以發出回送 HTTP 或本機 DBUS 呼叫。

選項 1:Python Script 上的 DBUS 和 HTTP API

91e5fdeed83e9354.png

這個步驟會建立基本 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/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 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。如果使用先前程式碼研究室中的建構作業未啟用 REST API,請務必使用該旗標在 RPi 上建構 OTBR:

$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup

如要重新啟動 OTBR 代理程式,請執行以下指令:

$ sudo systemctl restart otbr-agent.service

代理程式會啟動通訊埠 8081 的 HTTP 伺服器。這個伺服器可讓使用者或監控程式在 OTBR 中執行多項工作 (說明文件請見此處)。您可以使用瀏覽器 curlwget 檢查內容。在許多支援的路徑中,用途包括上述用途,在 /node/dataset/active 上使用 GET 動詞、/node/dataset/pending 上的 PUT 動詞

3. 在 Android 上設定憑證架構

偏好憑證

Android 版 Google Play 服務允許並預期註冊您網路中所有 TBR 的憑證。每個 ID 都是透過邊界路由器代理程式 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 與 Android 裝置同步只會在 TBR 與單一使用者配對的情形下,或與兩個屬於同一智慧型住宅 ( 結構) 的使用者配對時。

如果使用者都位於相同結構中,另一位 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 呼叫。您可以在應用程式中使用這些模式。在本程式碼研究室中,我們會從 GitHub 複製 Matter 適用的 Google Home 範例應用程式。

這裡顯示的所有原始碼都已在範例應用程式中完成編碼。您已受邀根據自己的需求修改程式碼,但您可以直接複製應用程式,或執行預先建構的二進位檔檢查功能。

  1. 使用以下方式複製該工具:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. 下載並開啟 Android Studio
  2. 按一下「File」>「Open」,然後指向複製的存放區。
  3. 在 Android 手機上啟用開發人員模式
  4. 使用 USB 傳輸線將傳輸線連接至電腦。
  5. 透過 <Cmd+R> (OS X) 或 <Ctrl+R> (Win、Linux) 從 Android Studio 執行應用程式
  6. 依序前往「Wheel」->「Developer Utilities」->「Thread Network」
  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 後,您會想使用傳送端的結果建構並啟動意圖。

在我們的程式碼中,基於組織/架構目的,我們使用 MutableLiveData<IntentSender?>,因為原始程式碼位於 ViewModel (ThreadViewModel.kt),而意圖觀察器位於活動片段 ( 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。您的應用程式是唯一可以讀取與 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 產品的憑證

這部分專屬於每個廠商,在本程式碼研究室中,我們會透過 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 Verb 從 TBR 取得憑證。請參閱 Python 指令碼範例

建構與匯入

建立 Android 應用程式時,您需要變更資訊清單、建構和匯入項目,支援 Google Play 服務執行緒模組。以下三個程式碼片段會摘要列出大多數新增項目。

請注意,我們的範例應用程式主要是以 Matter 佣金為基礎建構而成。因此,其資訊清單和 Gradle 檔案比僅使用 Thread 憑證所需的新增項目還要複雜。

資訊清單異動

<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。

在 GPS 憑證儲存體中輸入您的 TBR 資訊時,這項資訊非常有用。不過,它的用途不在本程式碼研究室的涵蓋範圍內。我們使用 Android 服務探索程式庫 NSDManager,您可以在 ServiceDiscovery.kt 的範例應用程式中找到完整原始碼。

6. 總結

導入這些呼叫或使用範例應用程式後,您就可以完全開始使用 RPi OTBR。我們的範例應用程式顯示 8 個按鈕:

91979bf065e9673d.png

加入 TBR 的可能順序如下:

  1. 查詢是否具有偏好憑證 (藍色、第 1 列)
  2. 根據答案
  3. 取得 GPS 偏好憑證 (藍色,第 2 列)
  4. 在 GPS 中設定 TBR 憑證 (藍色,第 3 列) -> 選取您的 TBR ->「建立隨機」->「輸入網路名稱」->「確定」
  5. 現在您已取得偏好憑證,請使用設定 RPi OTBR 憑證將憑證設為 OTBR,這麼做會將這些憑證套用至待處理集合。

範例應用程式的預設延遲時間為 10 秒。在這段期間內,您的 RPi TBR (以及當中可能存在的其他節點) 的憑證會遷移至新的資料集。

7. 結論

在本程式碼研究室中,我們複製了一個 Android 應用程式範例,並分析了幾個運用 Google Play 服務 Thread Storage API 的程式碼。我們透過這些 API 提供一個通用資料集,可以用來整合 RPi TBR,顯示供應商的 TBR。

將使用者的所有 TBR 放在同一個網路中,能提升 Thread Network 的彈性和連線能力。此外,還能防止應用程式因無法存取憑證,而無法在 Thread 裝置中新增的使用者歷程有瑕疵。

我們希望本程式碼研究室和範例應用程式可協助您設計並開發自己的應用程式和 Thread 邊界路由器產品。

8. 參考資料

RCP 輔助處理器

DBUS