שיתוף של Thread Network עם ממשקי Google Thread Credentials API

1. לפני שמתחילים

ב-Codelab שלנו בנושא Thread Border Router (TBR), אנחנו מסבירים איך ליצור Thread Border Router על סמך Raspberry Pi. באותו Codelab

  • יצירת קישוריות IP דו-כיוונית בין רשתות Thread לרשתות Wi-Fi/Ethernet.
  • לספק גילוי שירות דו-כיווני באמצעות mDNS (בקישור ל-Wi-Fi/Ethernet) ו-SRP (ברשת Thread).

ה-Codelab הזה מבוסס על הקודם, ומתייחס לאופן שבו נתב הגבולות שלך והאפליקציה שלך יכולים לקיים אינטראקציה עם ממשקי ה-API של Google כדי ליצור Thread Network אחד. השילוב של פרטי הכניסה ל-Thread חשוב כי הוא מגביר את החוסן של הרשת ומפשט את האינטראקציות של המשתמשים עם האפליקציות שמסתמכות על Thread.

דרישות מוקדמות

  • צריך להשלים את OTBR Codelab
  • ידע בסיסי של Linux, Android/Kotlin, ורשת Thread

מה תלמדו

  • איך משתמשים בממשקי ה-API של שיתוף בשרשור כדי לקבל ולהגדיר קבוצות פרטי כניסה
  • איך מגדירים נתב Border Border משלכם עם אותם פרטי כניסה שמוגדרים ברשת של Google

מה צריך להכין

  • לוח Raspberry Pi 4 או לוח אחר מבוסס Linux שמפעיל את נתב Thread Border (OTBR)
  • לוח שמספק קישוריות IEEE 802.15.4 כמעבד משותף של רדיו (RCP). בדף OpenThread GitHub יש רשימה של מאגרים של ספקי SoC שונים והוראות שלהם.

2. הגדרת שירות HTTP

אבן הבניין הראשונה שדרושה לנו היא ממשק שמאפשר לנו לקרוא פרטי כניסה פעילים ולכתוב 'פרטי כניסה בהמתנה' ל-OTBR. כשאתם בונים TBR, השתמשו במנגנונים הקנייניים שלכם, כפי שמוצג כאן עם שתי דוגמאות. האפשרות הראשונה מראה איך להתממשק עם סוכן OTBR באופן מקומי דרך DBUS, ואילו האפשרות השנייה משתמשת ב-API ל-REST שאפשר לבנות על OTBR.

אף אחת מהשיטות לא מאובטחת ואין להשתמש בה כפי שהיא בסביבת ייצור. עם זאת, ספק יכול לבנות הצפנה בכל אחת מהשיטות כדי להשתמש בה בסביבת ייצור, או שאתם יכולים להרחיב את שירות המעקב שלכם כדי להנפיק HTTP חזרה או קריאות DBUS מקומיות מטבען.

אפשרות 1: DBUS ו-HTTP API ב-Python Script

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 ומאזינה בנתיב הבסיסי (root) לבקשת GET לאחזור פרטי כניסה ל-Thread או לבקשת POST להגדרת פרטי כניסה ל-Thread. המטען הייעודי הוא תמיד מבנה 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: HTTPREST API של סוכן OTBR

c748ca5151b6cacb.png

נתב Border Border נבנה כברירת מחדל עם הדגל REST_API=1, ומפעיל את ה-API ל-REST. אם ה-build מ-codelab קודם לא הפעיל את ה-API ל-REST, חשוב ליצור 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. הגדרת מסגרת של פרטי הכניסה ב-Android

פרטי כניסה מועדפים

Google Play Services ב-Android מאפשרת ומצפה לרישום של פרטי הכניסה לכל ה-TBR ברשת. כל אחד מהם מזוהה באמצעות מזהה הסוכן של נתב הגבול (BAID) שלו. כדי לבצע את המשימה הזו, עליכם להשתמש בשיטה addCredentials() של הממשק ThreadNetworkClient. פרטי הכניסה המועדפים נקבעים על ידי ה-TBR הראשון שמתווסף לאחסון של שירותי Google Play.

האפליקציה שמוסיפה ל-BAID שלה קבוצה של פרטי כניסה של רשת Thread הופכת לבעלים של פרטי הכניסה, עם הרשאות מלאות כדי לגשת אליהם. אם תנסו לגשת לפרטי כניסה שנוספו על ידי אפליקציות אחרות, תקבלו את הודעת השגיאה permission_DENIED. עם זאת, פרטי הכניסה המועדפים זמינים תמיד לכל אפליקציה בהסכמת המשתמש. אנחנו ממליצים לעדכן את פרטי הכניסה שמאוחסנים ב-Google Play Services כשרשת Thread Border outer מתעדכנת. למרות שלא נעשה שימוש במידע הזה היום, יכול להיות שנספק מסלולים משופרים בעתיד.

גם אם ה-TBR הראשון יוחרג מאוחר יותר, פרטי הכניסה המועדפים יישמרו במכשיר Android. לאחר ההגדרה, אפליקציות אחרות שמנהלות פרטי כניסה ב-thread עשויות לקבל את פרטי הכניסה מקריאה ל-getPreferredCredentials().

סנכרון Google TBR

מכשירי Android מסתנכרנים באופן אוטומטי עם מכשירי TBR של Google. אם לא קיימים פרטי כניסה ב-Android, המכשירים מחלצים אותם מ-TBR של Google ברשת שלכם, ופרטי הכניסה האלה הופכים לפרטי הכניסה המועדפים. הסנכרון בין מכשירי TBR למכשיר Android מתרחש רק אם ה-TBR הותאם למשתמש יחיד, או אם הוא הותאם לשני משתמשים שנמצאים באותו בית חכם ( מבנה).

התהליך הזה יתרחש גם כשמשתמש אחר של Google משתמש ב-GHA (ל-GHA) ל-Android או ב-GHA (ל-iOS) כשהמשתמש נמצא באותו מבנה. במקרה של GHA ל-iOS, פרטי הכניסה המועדפים מוגדרים באחסון iOS, אם לא קיימים פרטי כניסה מועדפים.

אם שני מכשירי Android (או Android + iGHA) קיימים באותה רשת עם קבוצות שונות של פרטי כניסה מועדפים, המכשיר שמגדיר במקור את TBR הוא הקובע של TBR.

הצטרפות באמצעות TBR של צד שלישי

נפח האחסון של פרטי הכניסה לא בהיקף האחסון של הבית החכם של המשתמש ( מבנה). לכל מכשיר Android יהיה אחסון מסוג BAID, אבל ברגע שיהיה TBR של Google ברשת, מכשירי Android ומכשירי iOS אחרים עם אפליקציית Google Home ל-iOS יסונכרנו עם ה-TBR הזה וינסו להגדיר פרטי כניסה מקומיים באחסון הטלפון.

לפני ש-OOB TBR חדש יוצר רשת, חשוב לבדוק אם רשת מועדפת כבר קיימת באחסון של Android.

  • אם קיימת רשת מועדפת, הספק צריך להשתמש בה. כך אפשר לוודא שמכשירי Thread יהיו מחוברים לרשת Thread אחת כשהדבר אפשרי.
  • אם לא קיימת רשת מועדפת, יש ליצור קבוצה חדשה של פרטי כניסה ולהקצות אותה ל-TBR ב-Google Play Services. מערכת Android תכבד את פרטי הכניסה האלה בתור פרטי הכניסה הרגילים שמוגדרים בכל מכשירי ה-TBR שמבוססים על Google. ספקים אחרים יוכלו לשפר את פוטנציאל החשיפה והתקינות של הרשת באמצעות מכשירים נוספים.

cd8bc726f67b1fa1.png

4. שכפול ושינוי של אפליקציית Android

יצרנו אפליקציה ל-Android שמציגה את הקריאות העיקריות האפשריות ל-Thread API. אפשר להשתמש בדפוסים האלה באפליקציה שלך. בשיעור ה-codelab הזה נשכפל את אפליקציית 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. עוברים לגלגל 'גלגל' -> 'כלי ניהול למפתחים' -> '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 רוצים ליצור ולהפעיל Intent על סמך התוצאה הזו מהשולח.

בקוד שלנו, למטרות ארגון/ארכיטקטורה, אנחנו משתמשים ב-MutableLiveData<IntentSender?> כי הקוד המקורי נמצא ב-ViewModel (ThreadViewModel.kt) וצופים ב-Intent נמצאים במקטע הפעילות ( 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 קיים וגם אם לא, עליכם לרשום את ה-TBR ב-Google Play Services. האפליקציה שלך תהיה היחידה שיוכל לקרוא את פרטי הכניסה שמשויכים למזהה Border Agent של ה-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

החלק הזה הוא קנייני של כל ספק, וב-codelab הזה אנחנו מטמיעים אותו דרך DBUS+Python HTTPREST Server או דרך שרת HTTPREST המקורי מ-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 Verb כדי לקבל את פרטי הכניסה מה-TBR. דוגמה לסקריפט Python

פיתוח וייבוא

כשיוצרים אפליקציה ל-Android, צריך לבצע שינויים במניפסט, ב-build ובייבוא, כדי לתמוך במודול השרשורים של Google Play Services. שלושת קטעי הקוד הבאים מסכמים את רוב התוספות.

הערה: האפליקציה לדוגמה שלנו מיועדת בעיקר להזמנה בתקן Matter. לכן, קובצי המניפסט ו-Gradle מורכבים יותר מהתוספות שנדרשות לשימוש רק ב-Thread Credentials.

שינויים במניפסט

<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 המתאימים.

דבר זה שימושי מאוד כאשר מזינים את המידע של ה-TBR באחסון של אישור ה-GPS. עם זאת, השימוש בו חורג מהיקף ה-Codelab הזה. אנחנו משתמשים בספריית Android Service Discovery NSDManager, וקוד המקור המלא זמין באפליקציה לדוגמה בServiceDiscovery.kt.

6. אז מה זה אומר?

לאחר הטמעת השיחות או השימוש באפליקציה לדוגמה, ניתן יהיה לצרף ל-RPi OTBR באופן מלא. באפליקציה לדוגמה מוצגות 8 לחצנים:

91979bf065e9673d.png

רצף אפשרי לתחילת השימוש ב-TBR הוא:

  1. שאילתה אם קיימים פרטי כניסה מועדפים (כחול, שורה ראשונה)
  2. בהתאם לתשובה
  3. קבלת פרטי כניסה מועדפים ל-GPS (כחול, שורה שנייה)
  4. הגדרת פרטי כניסה של TBR ב-GPS (כחול, שורה שלישית) -> בחירת TBR -> יצירה אקראית -> הזנת שם רשת -> אישור
  5. עכשיו, אחרי שיש לכם פרטי כניסה מועדפים, תוכלו להגדיר אותם ל-OTBR באמצעות הגדרה של פרטי כניסה מסוג OTBR ב-RPi. פעולה זו תחיל את פרטי הכניסה האלה על הקבוצה שבהמתנה.

ברירת המחדל באפליקציה לדוגמה היא עיכוב של 10 שניות. כך, אחרי התקופה הזו, פרטי הכניסה של ה-TBR של ה-RPi (וצמתים אחרים שאולי קיימים ברשת שלו) יועברו למערך הנתונים החדש.

7. סיכום

בשיעור ה-codelab הזה שכפולנו אפליקציה לדוגמה ל-Android וניתחנו מספר קטעי קוד שעושים שימוש בממשקי ה-API של Thread Storage של Google Play Services. השתמשנו בממשקי ה-API האלה כדי ליצור מערך נתונים משותף שנוכל לצרף ל-TBR של RPi, שמציג TBR של ספק.

כל רשת TBR של המשתמש באותה רשת משפרת את העמידות ואת פוטנציאל החשיפה של רשת Thread Network. היא גם מונעת שגיאות בתהליכים של המשתמשים שבהם האפליקציות לא יכולות לצרף מכשירי Thread כי אין להם גישה לפרטי הכניסה.

אנחנו מקווים שה-codelab והאפליקציות לדוגמה האלה יעזרו לכם לעצב ולפתח אפליקציה משלכם ואת המוצר של Thread Border Router.

8. קובצי עזר

מעבד מידע משותף ב-RCP

DBUS