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

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

ב-Codelab של Thread Border Router (TBR), אנחנו מסבירים איך לפתח נתב Thread Border [נתב גבול של פרוטוקולי Thread] שמבוסס על Raspberry Pi. ב-Codelab הזה

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

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

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

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

מה תלמדו

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

מה צריך להכין

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

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

אבן הבניין הראשונה שאנחנו צריכים היא ממשק שמאפשר לנו לקרוא את ה-Active Credentials ולכתוב Pending Credentials (אישורים בהמתנה) ב-OTBR. כשיוצרים TBR, צריך להשתמש במנגנונים הקנייניים שלכם, כפי שמוצג כאן עם שתי דוגמאות. באפשרות הראשונה מוסבר איך להתממשק עם סוכן OTBR באופן מקומי דרך DBUS, והאפשרות השנייה משתמשת ב-Rest API שאפשר לבנות על בסיס OTBR.

אף אחת מהשיטות היא לא מאובטחת ואין להשתמש בה כפי שהיא בסביבת ייצור. עם זאת, ספק יכול לבנות הצפנה בכל אחת מהשיטות כדי להשתמש בה בסביבת ייצור, או להרחיב את שירות המעקב שלכם כדי להנפיק קריאות DBUS בלולאה חוזרת (loopback) או קריאות 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 ומבצעת האזנה בנתיב הבסיס כדי לקבל בקשת GET לאחזור פרטי כניסה של פרוטוקול Thread או בקשת POST להגדרת פרטי כניסה בפרוטוקול Thread. המטען הייעודי (Payload) תמיד הוא מבנה של JSON עם ה-TLV.

בקשת ה-PUT הבאה מגדירה פרטי כניסה חדשים בהמתנה בפרוטוקול Thread ל-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: ממשק OTBR (ראשי) של סוכן OTBR API

c748ca5151b6cacb.png

ה-OpenThread Border Router נוצר כברירת מחדל עם הדגל 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. הגדרה של Credential Framework ב-Android

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

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

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

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

סנכרון Google TBR

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

התהליך הזה יקרה גם כשמשתמש אחר של Google נמצא ב-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
  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) ותצפיתי הכוונה נמצאים במקטע הפעילות ( ThreadFragment.kt). לכן, אחרי הפרסום של ה-IntentSenderresults בנתונים בזמן אמת, נבצע את הפעולות של התוכן של הצופה הזה:

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 Services. האפליקציה שלך תהיה היחידה שיכולה לקרוא את פרטי הכניסה שמשויכים למזהה סוכן הגבולות של ה-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 הזה אנחנו מטמיעים אותו באמצעות שרת מנוחה ב-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 Verb כדי לקבל את פרטי הכניסה מה-TBR. ראו סקריפט Python לדוגמה.

פיתוח וייבוא

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

חשוב לשים לב שהאפליקציה לדוגמה שלנו מיועדת בעיקר להזמנת תקן 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 שלהם.

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

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

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

91979bf065e9673d.png

רצף אפשרי לתחילת העבודה עם TBR הוא:

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

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

7. סיכום

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

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

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

8. קובצי עזר

מעבד RCP

DBUS