1. Прежде чем начать
В нашем практическом занятии по созданию маршрутизатора граничных потоков (Thread Border Router, TBR) мы покажем, как собрать маршрутизатор граничных потоков на базе Raspberry Pi. В этом практическом занятии мы
- Установите двустороннюю IP-связь между сетью Thread и сетями Wi-Fi/Ethernet.
- Обеспечьте двунаправленное обнаружение сервисов через mDNS (по Wi-Fi/Ethernet-соединению) и SRP (в сети Thread).
Этот практический пример основан на предыдущем и рассматривает, как ваш собственный пограничный маршрутизатор и ваше приложение могут взаимодействовать с API Google для создания единой сети Thread. Объединение учетных данных Thread важно, поскольку это повышает надежность сети и упрощает взаимодействие пользователей с приложениями, которые используют Thread.
Предварительные требования
- Выполните задание OTBR Codelab.
- Базовые знания Linux, Android/Kotlin и сетевых технологий Thread.
Что вы узнаете
- Как использовать API совместного использования потоков для получения и установки наборов учетных данных
- Как настроить собственный маршрутизатор OpenThread Border Router с теми же учетными данными, что и в сети Google.
Что вам понадобится
- Плата Raspberry Pi 4 или другая плата на базе Linux, работающая под управлением Open Thread Border Router (OTBR).
- Плата, обеспечивающая подключение по стандарту IEEE 802.15.4 в качестве радиопроцессора (RCP). Список репозиториев различных производителей SoC и инструкции к ним можно найти на странице OpenThread на Github.
2. Настройка HTTP-сервиса
Первым необходимым элементом является интерфейс, позволяющий считывать активные учетные данные и записывать ожидающие учетные данные в ваш OTBR. При создании TBR используйте собственные механизмы, как показано здесь на двух примерах. Первый вариант демонстрирует взаимодействие с агентом OTBR локально через DBUS, а второй использует REST API, который можно построить на основе OTBR.
Ни один из методов не является безопасным и не должен использоваться в неизменном виде в производственной среде. Однако поставщик может добавить шифрование к любому из этих методов для использования в производственной среде, или вы можете расширить свою собственную службу мониторинга для отправки HTTP-запросов через локальную сеть или локальных вызовов DBUS.
Вариант 1: DBUS и HTTP API в скрипте Python

На этом этапе создается базовый HTTP-сервис, предоставляющий две конечные точки для чтения и установки учетных данных, в конечном итоге вызывающий команды DBUS.
На Raspberry Pi, который будет использоваться в качестве OTBR, установите зависимости Python 3:
$ pip install dbus-python shlex json
Запустите скрипт следующим образом:
$ sudo python credentials_server.py 8081 serving at port 8081
В примере настраивается HTTP-сервер на порту 8081, который прослушивает корневой путь либо для GET-запроса на получение учетных данных потока, либо для POST-запроса на установку учетных данных потока. Полезная нагрузка всегда представляет собой структуру JSON с TLV.
Следующий PUT- запрос устанавливает новые ожидающие учетные данные потока для OTBR, используя путь /node/dataset/pending . В этом случае ожидающие учетные данные применяются через 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
}
Запрос GET к /node/dataset/active получает текущие активные учетные данные.
GET /node/dataset/active Host: <IP>:8081 ContentType = "application/json" acceptMimeType = "text/plain" ... <TLV encoded Thread Dataset>
Скрипт вызывает команды чтения/записи DBUS для пути шины io.openthread.BorderRouter.wpan0 , путь к объекту /io/openthread/BorderRouter/wpan0 :
# 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: Собственный HTTP REST API агента OTBR

В OpenThread Border Router по умолчанию используется флаг REST_API=1 , который включает REST API. Если в вашей сборке из предыдущего практического задания REST API не был включен, убедитесь, что вы собрали OTBR на вашем Raspberry Pi с этим флагом:
$ REST_API=1 INFRA_IF_NAME=wlan0 ./script/setup
Агент OTBR можно перезапустить, выполнив следующую команду:
$ sudo systemctl restart otbr-agent.service
Агент запускает HTTP-сервер на порту 8081. Этот сервер позволяет пользователю или программе мониторинга выполнять множество задач в OTBR (описано здесь ). Вы можете использовать свой браузер, curl или wget для просмотра его содержимого. Среди множества поддерживаемых путей есть описанные выше варианты использования, с глаголом GET для /node/dataset/active и глаголом PUT для /node/dataset/pending .
3. Настройка Credential Framework на Android
Предпочтительные квалификации
Сервисы Google Play на Android позволяют и ожидают регистрации учетных данных для всех TBR в вашей сети. Каждый из них идентифицируется по своему идентификатору агента пограничного маршрутизатора (BAID). Для выполнения этой задачи вы будете использовать метод addCredentials() интерфейса ThreadNetworkClient . Первый TBR, добавленный в хранилище сервисов Google Play, определяет предпочтительные учетные данные для этого мобильного устройства.
Приложение, добавляющее набор учетных данных сети Thread в свой BAID, становится владельцем этих учетных данных и получает полные права доступа к ним. Если вы попытаетесь получить доступ к учетным данным, добавленным другими приложениями, вы получите ошибку PERMISSION_DENIED. Однако предпочтительные учетные данные всегда доступны для любого приложения с согласия пользователя. Мы рекомендуем обновлять учетные данные, хранящиеся в сервисах Google Play, при обновлении сети Thread Border Router. Хотя эта информация в настоящее время не используется, в будущем мы можем предоставить улучшенные сценарии взаимодействия.
Даже если первый поток TBR будет впоследствии исключен, предпочтительные учетные данные сохранятся на устройстве Android. После установки другие приложения, управляющие учетными данными потоков, могут получить эти учетные данные с помощью вызова метода getPreferredCredentials() .
Google TBR Sync
Устройства Android автоматически синхронизируются с устройствами Google TBR. Если на Android отсутствуют учетные данные, устройства извлекают их из устройств Google TBR в вашей сети, и эти учетные данные становятся предпочтительными. Синхронизация между устройствами TBR и устройством Android происходит только в том случае, если устройство TBR сопряжено с одним пользователем или с двумя пользователями, находящимися в одной системе «Умный дом» ( структура ).
Этот процесс также будет происходить, когда другой пользователь Google находится в GHA для Android или GHA для iOS, если он находится в той же структуре. В случае GHA для iOS предпочтительные учетные данные устанавливаются в хранилище iOS, если таковые отсутствуют.
Если в одной сети находятся два устройства Android (или Android + iGHA) с разными наборами предпочтительных учетных данных, то приоритет будет иметь устройство, которое изначально настроило TBR.
сторонний процесс регистрации TBR
В настоящее время хранилище учетных данных не ограничено возможностями «умного дома» пользователя ( структура ). Каждое устройство Android будет иметь собственное хранилище BAID, но как только в сети появится устройство Google TBR, другие устройства Android и устройства iOS с запущенным приложением Google Home для iOS синхронизируются с этим устройством TBR и попытаются установить локальные учетные данные в память телефона.
Прежде чем новый OOB TBR создаст сеть, важно проверить, существует ли уже предпочтительная сеть в памяти Android.
- Если существует предпочтительная сеть, поставщик должен использовать её. Это гарантирует, что устройства Thread будут по возможности подключены к одной сети Thread.
- Если предпочтительная сеть отсутствует, создайте новый набор учетных данных и назначьте его своему TBR в Google Play Services. Android будет использовать эти учетные данные в качестве стандартных для всех TBR на базе Google, а другие производители смогут расширить зону покрытия и повысить надежность вашей mesh-сети за счет добавления новых устройств.

4. Клонирование и модификация вашего Android-приложения
Мы создали Android-приложение, демонстрирующее основные возможные вызовы API потоков. Вы можете использовать эти шаблоны в своем приложении. В этом практическом занятии мы клонируем пример приложения Google Home для Matter с Github.
Весь представленный здесь исходный код уже встроен в пример приложения. Вы можете изменить его в соответствии со своими потребностями, но также можете просто клонировать приложение или запустить предварительно скомпилированные бинарные файлы для проверки функциональности.
- Клонируйте его, используя:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
- Скачайте и откройте Android Studio .
- Нажмите «Файл» > «Открыть» и укажите путь к клонированному репозиторию.
- Включите режим разработчика на своем телефоне Android.
- Подключите его к компьютеру с помощью USB-кабеля.
- Запустите приложение из Android Studio с помощью <Cmd+R> (OS X) или <Ctrl+R> (Windows, Linux).
- Перейдите в меню «Колесо» -> «Утилиты разработчика» -> «Сеть потоков».
- Взаимодействуйте с различными доступными опциями. В разделах ниже мы подробно рассмотрим код, который выполняется при нажатии каждой кнопки.
Существуют ли предпочтительные учетные данные?
Первый вопрос, который производитель 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), а наблюдатели намерений — в Activity Fragment ( ThreadFragment.kt ). Таким образом, как только intentSenderResult будет отправлен в LiveData, мы выполним содержимое этого наблюдателя:
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 зарегистрируется первым, эти учетные данные будут скопированы в набор предпочтительных учетных данных. Эта информация будет доступна любому приложению на телефоне, если пользователь даст на это разрешение.
/**
* 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
Эта часть является собственностью каждого поставщика, и в этом практическом занятии мы реализуем её либо с помощью HTTP REST-сервера DBUS+Python, либо с помощью встроенного HTTP REST-сервера от OTBR.
/**
* 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
Как показано ранее, используйте HTTP-запрос GET для получения учетных данных из вашего TBR. См. пример скрипта на Python .
Сборка и импорт
При создании Android-приложения вам потребуется внести изменения в манифест, сборку и импорт, чтобы обеспечить поддержку модуля потоков Google Play Services. Следующие три фрагмента кода суммируют большинство дополнений.
Обратите внимание, что наше демонстрационное приложение в первую очередь предназначено для ввода в эксплуатацию Matter. Поэтому его файлы Manifest и 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 Discovery
В нашем демонстрационном приложении используется обнаружение mDNS/SD для создания списка доступных маршрутизаторов Thread Border Router в сети, а также их соответствующих идентификаторов BAID.
Это очень полезно при вводе информации о вашем TBR в память GPS-навигатора. Однако его использование выходит за рамки данного практического занятия. Мы используем библиотеку обнаружения служб Android NSDManager , а полный исходный код доступен в примере приложения в файле ServiceDiscovery.kt .
6. Собираем всё воедино
После того, как вы реализуете эти вызовы или используете пример приложения, вы сможете полностью подключить свой RPi OTBR. Наш пример приложения предоставляет доступ к 8 кнопкам:

Возможная последовательность действий для подключения вашего TBR выглядит следующим образом:
- Проверьте наличие приоритетных учетных данных (синий цвет, 1-я строка).
- В зависимости от ответа
- Получите предпочтительные учетные данные GPS (синие, 2-й ряд).
- Настройте учетные данные TBR в GPS (синий, 3-й ряд) -> Выберите свой TBR -> Создать случайный -> Введите имя сети -> ОК
- Теперь, когда у вас есть предпочтительные учетные данные, установите их для вашего OTBR с помощью команды Set RPi OTBR credentials , которая применит эти учетные данные к ожидающему набору.
В тестовом приложении по умолчанию используется задержка в 10 секунд. Таким образом, по истечении этого периода учетные данные вашего Raspberry Pi TBR (и других узлов, которые могут находиться в его сети) будут перенесены в новый набор данных.
7. Заключение
В этом практическом занятии мы клонировали пример Android-приложения и проанализировали несколько фрагментов кода, использующих API хранилища потоков (Thread Storage) сервисов Google Play. Мы использовали эти API для создания общего набора данных, который можно загрузить в модуль TBR на Raspberry Pi, демонстрирующий модуль TBR от конкретного производителя.
Размещение всех устройств Thread Network (TBR) пользователя в одной сети повышает отказоустойчивость и охват сети Thread Network. Это также предотвращает ошибки в пользовательских сценариях, когда приложения не могут подключить устройства Thread Network, поскольку у них нет доступа к учетным данным.
Мы надеемся, что этот практический урок и примеры приложений помогут вам разработать собственное приложение и продукт Thread Border Router.
8. Список литературы
RCP сопроцессор
DBUS