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

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

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

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

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

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

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

מה תלמדו

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

הבקשה הבאה מסוג PUT מגדירה פרטי כניסה חדשים של Pending 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 Agent native HTTP Rest API

c748ca5151b6cacb.png

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

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

Google TBR Sync

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

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

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

cd8bc726f67b1fa1.png

4. יצירת עותקים כפולים של האפליקציה ל-Android ושינוי שלה

יצרנו אפליקציה ל-Android שמציגה את הקריאות העיקריות האפשריות ל-Thread API. אפשר להשתמש בדפוסים האלה באפליקציה שלכם. בסדנת הקוד הזו נכפיל (clone) את אפליקציית הדוגמה של Google Home ל-Matter מ-GitHub.

כל קוד המקור שמוצג כאן כבר מקודד באפליקציית הדוגמה. אתם מוזמנים לשנות אותו בהתאם לצרכים שלכם, אבל אתם יכולים פשוט לשכפל את האפליקציה או להריץ את קובצי ה-binary שנוצרו מראש כדי לבדוק את הפונקציונליות.

  1. משכפלים אותו באמצעות:
$ git clone https://github.com/google-home/sample-apps-for-matter-android.git
  1. מורידים את Android Studio ופותחים אותו.
  2. לוחצים על File (קובץ) > Open (פתיחה) ומצביעים על המאגר שהועתק.
  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, צריך ליצור ולהפעיל כוונה באמצעות התוצאה הזו מהשולח.

בקוד שלנו, למטרות ארגון או ארכיטקטורה, אנחנו משתמשים ב-MutableLiveData<IntentSender?> כי הקוד המקורי נמצא ב-ViewModel‏ (ThreadViewModel.kt) ומעקב ה-Intent נמצא ב-Activity Fragment‏ (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 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

החלק הזה הוא קנייני לכל ספק, ובסדנת הקוד הזו אנחנו מטמיעים אותו באמצעות DBUS+‏Python HTTP Rest Server או באמצעות HTTP Rest Server מקורי מ-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 לדוגמה

פיתוח גרסאות build וייבוא

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

חשוב לדעת שהאפליקציה לדוגמה שלנו מיועדת בעיקר להפעלת Matter. לכן, קובצי ה-Manifest ו-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. שאילתות כדי לבדוק אם קיימים פרטי כניסה מועדפים (כחול, שורה 1)
  2. בהתאם לתשובה
  3. קבלת פרטי הכניסה המועדפים ל-GPS (כחול, שורה 2)
  4. Set TBR credentials in GPS (כחול, שורה שלישית) -> Select your TBR -> Create Random -> Enter network name -> Ok
  5. עכשיו, כשיש לכם את פרטי הכניסה המועדפים, מגדירים אותם ב-OTBR באמצעות Set RPi OTBR credentials, כך שפרטי הכניסה האלה יחולו על הקבוצה בהמתנה.

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

7. סיכום

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

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

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

8. קובצי עזר

מעבד שותף של RCP

DBUS