1. 始める前に
Thread Border Router(TBR)Codelab では、Raspberry Pi に基づいて Thread Border Router を構築する方法を紹介しています。この 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 ボーダー ルーターを設定する方法
必要なもの
- Open Thread Border Router(OTBR)を実行している Raspberry Pi 4 ボードまたは別の Linux ベースのボード
- 無線コプロセッサ(RCP)として IEEE 802.15.4 接続を提供するボード。さまざまな SoC ベンダーのリポジトリとその手順については、OpenThread Github ページをご覧ください。
2. HTTP サービスの設定
最初に必要なビルディング ブロックは、OTBR からアクティブな認証情報を読み取り、保留中の認証情報を書き込むことができるインターフェースです。TBR を構築する際は、次の 2 つの例に示すように、独自のメカニズムを使用します。1 つ目のオプションは、DBUS を介してローカルで OTBR エージェントとインターフェースする方法を示しています。2 つ目のオプションは、OTBR で構築できる Rest API を活用しています。
どちらの方法も安全ではなく、本番環境でそのまま使用しないでください。ただし、ベンダーはどちらかのメソッドを基に暗号化を構築して本番環境で使用できます。また、独自のモニター サービスを拡張して、ループバック HTTP 呼び出しや本質的にローカルな DBUS 呼び出しを発行することもできます。
オプション 1: Python スクリプトの DBUS と HTTP API

このステップでは、認証情報の読み取りと設定を行う 2 つのエンドポイントを公開し、最終的に DBUS コマンドを呼び出す、最小限の HTTP サービスを作成します。
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>
スクリプトは、バスパス 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

OpenThread Border Router は、デフォルトで 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 の認証情報の登録が許可され、登録が想定されています。各デバイスは Border Router Agent ID(BAID)で識別されます。このタスクを行うには、ThreadNetworkClient インターフェースの addCredentials() メソッドを使用します。Google Play 開発者サービスのストレージに追加された最初の TBR によって、このモバイル デバイスの優先認証情報が決定されます。
BAID に一連の Thread ネットワーク認証情報を追加するアプリは、認証情報のオーナーとなり、認証情報にアクセスするための完全な権限を持ちます。他のアプリによって追加された認証情報にアクセスしようとすると、PERMISSION_DENIED エラーが発生します。ただし、ユーザーの同意があれば、優先認証情報は常にどのアプリでも利用できます。Thread Border Router ネットワークが更新された場合は、Google Play サービスに保存されている認証情報を最新の状態に保つことをおすすめします。現時点ではこの情報は使用されていませんが、今後、ユーザー ジャーニーの強化版が提供される可能性があります。
最初の TBR が後で除外された場合でも、優先認証情報は Android デバイスに保持されます。設定されると、Thread 認証情報を管理する他のアプリは getPreferredCredentials() 呼び出しから認証情報を取得できます。
Google TBR Sync
Android デバイスは Google TBR と自動的に同期されます。Android に認証情報が存在しない場合、デバイスはネットワーク内の Google TBR から認証情報を抽出し、その認証情報が優先認証情報になります。TBR と Android デバイス間の同期は、TBR が 1 人のユーザーとペア設定されている場合、または同じスマートホーム(ストラクチャ)に属する 2 人のユーザーとペア設定されている場合にのみ行われます。
このプロセスは、ユーザーが同じストラクチャーに属しているときに、別の Google ユーザーが Android 版 Google Home アプリまたは iOS 版 Google Home アプリを使用している場合にも発生します。iOS 向け GHA の場合、優先認証情報が存在しない場合は、優先認証情報が iOS ストレージに設定されます。
2 台の 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 に設定された標準の認証情報として認識します。他のベンダーは、追加のデバイスを使用してメッシュの範囲と堅牢性を強化できます。

4. Android アプリのクローン作成と変更
Thread API への主な呼び出しを実演する Android アプリを作成しました。これらのパターンはアプリで使用できます。この Codelab では、Github から Google Home Sample App for Matter のクローンを作成します。
ここで示すソースコードはすべて、サンプルアプリにすでにコーディングされています。必要に応じて変更できますが、アプリを複製するか、ビルド済みのバイナリを実行して機能を検査することもできます。
- 次のコマンドを使用してクローンを作成します。
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
- Android Studio をダウンロードして開きます。
- [File] > [Open] をクリックして、クローンを作成したリポジトリを指定します。
- Android スマートフォンでデベロッパー モードを有効にします。
- USB ケーブルでパソコンに接続します。
- Android Studio から <Cmd+R>(OS X)または <Ctrl+R>(Win、Linux)でアプリを実行する
- ホイール -> [Developer Utilities] -> [Thread Network] に移動します。
- 利用可能なさまざまなオプションを操作します。以下のセクションでは、各ボタンで実行されるコードを詳しく見ていきます。
優先認証情報は存在するか
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 の Border Agent ID に関連付けられた認証情報を読み取ることができる唯一のアプリになりますが、TBR が最初に登録された場合、これらの認証情報は Preferred Credentials セットにコピーされます。ユーザーが承認すれば、スマートフォンのどのアプリでもその情報にアクセスできます。
/**
* 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 プロダクトから認証情報を取得する
前述のように、TBR から認証情報を取得するには GET HTTP 動詞を使用します。Python スクリプトのサンプルをご覧ください。
ビルドとインポート
Android アプリを作成する際は、Google Play 開発者サービス スレッド モジュールをサポートするために、マニフェスト、ビルド、インポートを変更する必要があります。次の 3 つのスニペットは、追加のほとんどをまとめたものです。
なお、このサンプルアプリは主に 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 を作成します。
これは、GPS 認証情報のストレージに TBR の情報を入力する際に非常に役立ちます。ただし、その使用方法はこの Codelab の範囲外です。Android Service Discovery ライブラリ NSDManager を使用します。完全なソースコードは、サンプルアプリの ServiceDiscovery.kt で入手できます。
6. すべてをまとめる
これらの呼び出しを実装するか、サンプルアプリを使用すると、RPi OTBR を完全にオンボーディングできます。サンプルアプリには 8 つのボタンがあります。

TBR のオンボーディングのシーケンスは次のようになります。
- 優先認証情報が存在するかどうかをクエリします(青色、1 行目)。
- 回答に応じて、
- GPS の優先認証情報を取得する(青色、2 行目)
- GPS で TBR 認証情報を設定する(青色、3 行目) -> TBR を選択 -> [Create Random](ランダムに作成) -> ネットワーク名を入力 -> [OK]
- 優先認証情報を取得したら、[Set RPi OTBR credentials] を使用して OTBR に設定します。これにより、保留中のセットに認証情報が適用されます。
サンプルアプリのデフォルトでは、10 秒の遅延を使用します。この期間が過ぎると、RPi TBR の認証情報(およびそのネットワーク上に存在する可能性のある他のノード)が新しいデータセットに移行されます。
7. まとめ
この Codelab では、サンプル Android アプリを複製し、Google Play 開発者サービスの Thread Storage API を使用するコード スニペットをいくつか分析しました。これらの API を使用して、ベンダーの TBR を示す RPi TBR にオンボーディングできる共通のデータセットを作成しました。
ユーザーの TBR をすべて同じネットワークに配置すると、Thread ネットワークの復元力と到達範囲が向上します。また、アプリが認証情報にアクセスできないために Thread デバイスをオンボーディングできないという、欠陥のあるユーザー ジャーニーも防ぎます。
この Codelab とサンプルアプリが、独自のアプリと Thread ボーダー ルーター プロダクトの設計と開発に役立つことを願っています。
8. リファレンス
RCP コプロセッサ
DBUS