Совместное использование сети потоков с помощью API учетных данных потоков Google

1. Прежде чем начать

В нашей кодовой лаборатории Thread Border Router (TBR) мы покажем, как создать Thread Border Router на базе Raspberry Pi. В этой кодовой лаборатории мы

  • Установите двунаправленное IP-соединение между сетями Thread и Wi-Fi/Ethernet.
  • Обеспечьте обнаружение двунаправленной службы через mDNS (по каналу Wi-Fi/Ethernet) и SRP (в сети Thread).

Эта лаборатория кода основана на предыдущей и описывает, как ваш собственный пограничный маршрутизатор и ваше приложение могут взаимодействовать с API Google для создания однопоточной сети. Объединение учетных данных Thread важно, поскольку оно повышает надежность сети и упрощает взаимодействие пользователей с приложениями, использующими Thread.

Предварительные условия

Что вы узнаете

  • Как использовать API-интерфейсы совместного использования потоков для получения и установки наборов учетных данных
  • Как настроить собственный пограничный маршрутизатор OpenThread с теми же учетными данными, что и в сети 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

91e5fdeed83e9354.png

На этом этапе создается простая служба HTTP, которая предоставляет две конечные точки для чтения и установки учетных данных, в конечном итоге вызывая команды DBUS.

На RPi, который будет служить вашим 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 R/W для пути шины 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.

c748ca5151b6cacb.png

Пограничный маршрутизатор OpenThread по умолчанию строится с флагом REST_API=1 , который включает REST API. Если ваша сборка из предыдущей кодовой лаборатории не включала REST API, обязательно создайте OTBR на своем RPi с этим флагом:

$ 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 Синхронизация

Устройства Android автоматически синхронизируются с Google TBR. Если на Android нет учетных данных, устройства извлекают их из Google TBR в вашей сети, и эти учетные данные становятся предпочтительными учетными данными. Синхронизация между TBR и устройством Android происходит только в том случае, если TBR сопряжен с одним пользователем или с двумя пользователями, которые находятся в одном Умном доме ( Структура ).

Этот процесс также произойдет, когда другой пользователь Google находится в GHA для Android или GHA для iOS, когда пользователь находится в той же структуре. В случае GHA для iOS предпочтительные учетные данные устанавливаются в хранилище iOS, если предпочтительные учетные данные не существуют.

Если в одной сети существуют два устройства Android (или Android + iGHA) с разными наборами предпочтительных учетных данных, то устройство, которое первоначально настроило TBR, будет преобладать в TBR.

Подключение сторонней TBR

Хранилище учетных данных в настоящее время не ограничено Умным домом пользователя ( Структура ). Каждое устройство Android будет иметь свое хранилище BAID, но как только в сети появится Google TBR, другие устройства Android и iOS, на которых работает приложение Google Home для iOS, будут синхронизироваться с этим TBR и пытаться установить локальные учетные данные в памяти телефона.

Прежде чем новый OOB TBR создаст сеть, важно проверить, существует ли уже предпочтительная сеть в хранилище Android.

  • Если предпочтительная сеть существует, поставщик должен ее использовать. Это гарантирует, что устройства Thread будут подключены к одной сети Thread, когда это возможно.
  • Если предпочитаемой сети не существует, создайте новый набор учетных данных и назначьте его своему TBR в Сервисах Google Play. Android будет учитывать эти учетные данные как стандартные учетные данные, установленные на всех TBR на базе Google, а другие поставщики смогут расширить охват и надежность вашей сети с помощью дополнительных устройств.

cd8bc726f67b1fa1.png

4. Клонирование и изменение вашего приложения для Android

Мы создали приложение для Android, которое демонстрирует основные возможные вызовы API Thread. Вы можете использовать эти шаблоны в своем приложении. В этой лаборатории кода мы клонируем пример приложения Google Home для Matter с Github.

Весь показанный здесь исходный код уже закодирован в примере приложения. Вам предлагается изменить его в соответствии со своими потребностями, но вы можете просто клонировать приложение или запустить предварительно созданные двоичные файлы, чтобы проверить функциональность.

  1. Клонируйте его, используя:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. Загрузите и откройте Android Studio .
  2. Нажмите «Файл» > «Открыть» и укажите свой клонированный репозиторий.
  3. Включите режим разработчика на своем телефоне Android.
  4. Подключите его к компьютеру через USB-кабель.
  5. Запустите приложение из Android Studio через <Cmd+R> (OS X) или <Ctrl+R> (Win, Linux).
  6. Перейдите в Колесо -> Утилиты разработчика -> Сеть потоков.
  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 зарегистрируется первым, эти учетные данные будут скопированы в набор предпочтительных учетных данных. Эта информация доступна любому приложению на телефоне, если пользователь разрешает это.

/**
* 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-сервер DBUS+Python, либо через собственный HTTP-сервер отдыха от 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

Как было показано ранее, используйте команду GET HTTP для получения учетных данных из вашего TBR. См. пример сценария Python .

Сборка и импорт

При создании приложения для Android вам потребуется внести изменения в манифест, сборку и импорт для поддержки модуля потоков сервисов Google Play. Следующие три фрагмента суммируют большинство дополнений.

Обратите внимание, что наш пример приложения в первую очередь создан для ввода в эксплуатацию 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>

Сборка.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. Однако его использование выходит за рамки данной лаборатории. Мы используем библиотеку Android Service Discovery NSDManager , а полный исходный код доступен в примере приложения в ServiceDiscovery.kt .

6. Собираем все вместе

После того как вы реализовали эти вызовы или использовали образец приложения, вы можете полностью подключить свой RPi OTBR. В нашем примере приложения имеется 8 кнопок:

91979bf065e9673d.png

Возможная последовательность подключения вашего TBR:

  1. Запросить , существуют ли льготные учетные данные (синий, 1-я строка)
  2. В зависимости от ответа
  3. Получите предпочтительные учетные данные GPS (синий, 2-й ряд)
  4. Установите учетные данные TBR в GPS (синий, 3-й ряд) -> Выберите свой TBR -> Создать случайный -> Введите имя сети -> Ок.
  5. Теперь, когда у вас есть предпочтительные учетные данные, установите их в свой OTBR с помощью команды Set RPi OTBR Credentials , которая применит эти учетные данные к ожидающему набору.

По умолчанию для примера приложения используется задержка в 10 секунд. Таким образом, по истечении этого периода учетные данные вашего RPi TBR (и других узлов, которые могут существовать в его сети) будут перенесены в новый набор данных.

7. Заключение

В этой лаборатории кода мы клонировали пример приложения Android и проанализировали несколько фрагментов кода, в которых используются API-интерфейсы хранилища потоков сервисов Google Play. Мы использовали эти API, чтобы иметь общий набор данных, который мы можем внедрить в TBR RPi, который демонстрирует TBR поставщика.

Наличие всех TBR пользователя в одной сети повышает устойчивость и охват сети потоков. Это также предотвращает ошибочные действия пользователя, когда приложения не могут подключиться к устройствам Thread, поскольку у них нет доступа к учетным данным.

Мы надеемся, что эта кодовая лаборатория и примеры приложений помогут вам спроектировать и разработать собственное приложение и продукт Thread Border Router.

8. Ссылки

RCP-сопроцессор

ДБУС