1. מבוא
Openthread שהושק על ידי Nest הוא הטמעת קוד פתוח של פרוטוקול הרשת Thread®. Nest השיקה את OpenThread כדי להפוך את הטכנולוגיה שבה נעשה שימוש במוצרי Nest לזמינה באופן נרחב למפתחים, כדי להאיץ את פיתוח המוצרים לבית החכם.
המפרט של Thread מגדיר פרוטוקול תקשורת אמין, מאובטח וחזק עם חיבור Bluetooth למכשירים שונים, לשימוש באפליקציות ביתיות. Openthread מיישם את כל שכבות הרשת של Thread, כולל IPv6, 6LoWPAN, IEEE 802.15.4 עם אבטחת MAC, תשתית לקישור רשת וניתוב רשת.
ב-Codelab הזה תשתמשו בממשקי API של OpenThread כדי להפעיל רשת של Thread, לעקוב אחרי השינויים בתפקידי המכשיר ולהגיב עליהם, לשלוח הודעות UDP וגם לקשר את הפעולות האלה ללחצנים ולנורות LED בחומרה אמיתית.
מה תלמדו
- איך לתכנת את הלחצנים ואת נורות ה-LED בלוחות הפיתוח למפתחים הנורדיים nRF52840
- איך משתמשים בממשקי API נפוצים של OpenThread ובמחלקה
otInstance
- איך לעקוב אחרי השינויים במצב OpenThread ולהגיב להם
- איך לשלוח הודעות UDP לכל המכשירים ברשת Thread
- איך לשנות קובצי Files
מה הדרישות כדי להצטרף לתוכנית?
חומרה:
- 3 לוחות נורדיים למחצה nRF52840 פיתוח
- 3 כבלי USB למיקרו USB כדי לחבר את הלוחות
- מכונת Linux עם לפחות 3 יציאות USB
תוכנה:
- ארגז כלים של GNU
- כלי שורת הפקודה הנורדיים של nRF5x
- תוכנת Segger J-Link
- OpenThread
- Git
אלא אם צוין אחרת, השימוש בתוכן של Codelab זה נעשה בכפוף לרישיון Creative Commons Attribution 3.0 ודוגמאות הקוד הן ברישיון Apache 2.0.
2. איך מתחילים
השלמת ה-Codelab של החומרה
לפני שמתחילים את ה-Codelab הזה, צריך להשלים את ה-Build a Thread Network with nRF52840 Tables ו-OpenThread, שבמסגרתו:
- התוכנה מספקת את כל התוכנות הנחוצות ליצירה ולפלאש
- מלמד איך לבנות את OpenThread ומהבהב אותו על לוחות nRF52840 נורדיים
- מדגים את היסודות של רשת Thread
אף אחת מהסביבות שלא נדרשת כדי לבנות את OpenThread והבזק את הלוחות מפורטת ב-Codelab הזה – רק הוראות בסיסיות להבהוב של הלוחות. ההנחה היא שכבר השלמת את ה-Codelab של Build a Thread Network.
מכונת Linux
שיעור ה-Codelab הזה מתוכנן לשימוש במחשב Linux מבוסס i386 או x86 כדי להבהב את כל לוחות הפיתוח של Thread. כל השלבים נבדקו ב-Ubuntu 14.04.5 LTS (Trusty Tahr).
לוחות nRF52840 נורדיים
Codelab זה משתמש בשלושה nRF52840 PDK לוחות.
התקנת התוכנה
כדי ליצור ולהפעיל את OpenThread, צריך להתקין את SEGGER J-Link, את כלי ה-nRF5x Command Line, את ARM GNU Toolchain וחבילות Linux שונות. אם השלמתם את ה-Codelab של Thread a Thread לפי הצורך, כל מה שצריך כבר מותקן אצלכם. אם לא, יש להשלים את ה-Codelab לפני שממשיכים ליצור ולבנות את OpenThread עבור לוחות פיתוח של nRF52840.
3. שכפול המאגר
Openthread מגיע עם קוד אפליקציה לדוגמה שאפשר להשתמש בו כנקודת התחלה ל-Codelab הזה.
שכפול הדוגמאות של Openthread Nordic nRF528xx ולבנות את OpenThread:
$ git clone --recursive https://github.com/openthread/ot-nrf528xx $ cd ot-nrf528xx $ ./script/bootstrap
4. עקרונות הבסיס של OpenThread API
ממשקי ה-API הציבוריים של OpenThread ממוקמים ב-./openthread/include/openthread
במאגר של OpenThread. ממשקי ה-API האלה מספקים גישה למגוון של תכונות ופונקציות של OpenThread, ברמת ה-thread ורמת הפלטפורמה, לשימוש באפליקציות:
- מידע ושליטה במופע של OpenThread
- שירותי אפליקציות כמו IPv6, UDP ו-CoAP
- ניהול פרטי כניסה של רשת, יחד עם תפקידים של נציבים ומצטרפים
- ניהול נתבי גבול
- תכונות משופרות כמו פיקוח על ילדים וזיהוי Jam
מידע על קובצי עזר בכל ממשקי ה-API של OpenThread זמין בכתובת openthread.io/reference.
שימוש ב-API
כדי להשתמש ב-API, יש לכלול את קובץ הכותרת באחד מקובצי האפליקציה. לאחר מכן מפעילים את הפונקציה הרצויה.
לדוגמה, האפליקציה לדוגמה של CLI שמופיעה ב-OpenThread משתמשת בכותרות ה-API הבאות:
./openthread/examples/apps/cli/main.c
#include <openthread/config.h> #include <openthread/cli.h> #include <openthread/diag.h> #include <openthread/tasklet.h> #include <openthread/platform/logging.h>
מופע ה-OpenThread
המבנה של otInstance
ישמש אתכם כשעובדים עם ממשקי API של OpenThread. לאחר האתחול, המבנה הזה מייצג מופע סטטי של ספריית OpenThread, ומאפשר למשתמשים לבצע קריאות ל-OpenThread API.
לדוגמה, המכונה של OpenThread מופעלת באמצעות הפונקציה main()
באפליקציה לדוגמה CLI:
./openthread/examples/apps/cli/main.c
int main(int argc, char *argv[]) { otInstance *instance ... #if OPENTHREAD_ENABLE_MULTIPLE_INSTANCES // Call to query the buffer size (void)otInstanceInit(NULL, &otInstanceBufferLength); // Call to allocate the buffer otInstanceBuffer = (uint8_t *)malloc(otInstanceBufferLength); assert(otInstanceBuffer); // Initialize OpenThread with the buffer instance = otInstanceInit(otInstanceBuffer, &otInstanceBufferLength); #else instance = otInstanceInitSingle(); #endif ... return 0; }
פונקציות ספציפיות לפלטפורמה
אם רוצים להוסיף פונקציות ספציפיות לפלטפורמה לאחת מהאפליקציות לדוגמה שנכללות ב-OpenThread, קודם צריך להצהיר על כך בכותרת ./openthread/examples/platforms/openthread-system.h
ולהשתמש במרחב השמות otSys
לכל הפונקציות. לאחר מכן עליכם להטמיע אותם בקובץ מקור ספציפי לפלטפורמה. אבסטרקטי בצורה זו, אפשר להשתמש באותן כותרות של פונקציות עבור פלטפורמות לדוגמה אחרות.
לדוגמה, פונקציות GPIO שבהן נשתמש כדי לחבר את לחצני nRF52840 ונורות ה-LED יש להצהיר ב-openthread-system.h
.
פותחים את הקובץ ./openthread/examples/platforms/openthread-system.h
בעורך הטקסט המועדף עליכם.
./openthread/examples/platforms/openthread-system.h
פעולה: הוספת הצהרות לגבי פונקציות GPIO ספציפיות לפלטפורמה.
מוסיפים את הצהרות הפונקציה הבאות אחרי #include
עבור הכותרת openthread/instance.h
:
/** * Init LED module. * */ void otSysLedInit(void); void otSysLedSet(uint8_t aLed, bool aOn); void otSysLedToggle(uint8_t aLed); /** * A callback will be called when GPIO interrupts occur. * */ typedef void (*otSysButtonCallback)(otInstance *aInstance); void otSysButtonInit(otSysButtonCallback aCallback); void otSysButtonProcess(otInstance *aInstance);
נטמיע את הפרטים האלה בשלב הבא.
הצהרת הפונקציה otSysButtonProcess
משתמשת בotInstance
. כך האפליקציה יכולה לגשת למידע על המופע של OpenThread כשלוחצים על לחצן, במקרה הצורך. הכול תלוי בצרכים של הבקשה שלך. אם אין צורך בהטמעה של הפונקציה, ניתן להשתמש במאקרו OT_UNUSED_VARIABLE
מ-OpenThread API כדי למנוע שגיאות בשגיאות הקשורות למשתנים מסוימים שאינם נכללים במחרוזות מסוימות של כלים. בהמשך נציג דוגמאות לכך.
5. הטמעת הפשטה של פלטפורמת GPIO
בשלב הקודם, דיברנו על הצהרות הפונקציות הספציפיות לפלטפורמה ./openthread/examples/platforms/openthread-system.h
, שבהן אפשר להשתמש ב-GPIO. כדי לגשת ללחצנים ולמנורות LED בלוחות הפיתוח של nRF52840, צריך להטמיע את הפונקציות האלה בפלטפורמת nRF52840. בקוד הזה מוסיפים פונקציות:
- הפעלה של סיכות ומצבים ב-GPIO
- שליטה במתח של הסיכה
- הפעלת הפרעות מ-GPIO והתקשרות חזרה
בספרייה ./src/src
, יוצרים קובץ חדש בשם gpio.c
. בקובץ החדש, מוסיפים את התוכן הבא.
./src/src/gpio.c (קובץ חדש)
פעולה: הוספה של הגדרות.
ההגדרה הזו משמשת כתקצירים בין ערכים ספציפיים ל-nRF52840 ולמשתנים שמשמשים ברמת אפליקציית OpenThread.
/** * @file * This file implements the system abstraction for GPIO and GPIOTE. * */ #define BUTTON_GPIO_PORT 0x50000300UL #define BUTTON_PIN 11 // button #1 #define GPIO_LOGIC_HI 0 #define GPIO_LOGIC_LOW 1 #define LED_GPIO_PORT 0x50000300UL #define LED_1_PIN 13 // turn on to indicate leader role #define LED_2_PIN 14 // turn on to indicate router role #define LED_3_PIN 15 // turn on to indicate child role #define LED_4_PIN 16 // turn on to indicate UDP receive
לקבלת מידע נוסף על לחצנים ונורות LED של nRF52840, ניתן לעיין במרכז המידע של Semiconductor Nordic.
פעולה: הוספת כותרת כוללת.
בשלב הבא, מוסיפים את הכותרת כך שדרושה פונקציונליות של GPIO.
/* Header for the functions defined here */ #include "openthread-system.h" #include <string.h> /* Header to access an OpenThread instance */ #include <openthread/instance.h> /* Headers for lower-level nRF52840 functions */ #include "platform-nrf5.h" #include "hal/nrf_gpio.h" #include "hal/nrf_gpiote.h" #include "nrfx/drivers/include/nrfx_gpiote.h"
פעולה: הוספת פונקציות התקשרות חזרה והפרעה לתפקוד של לחצן 1.
בשלב הבא צריך להוסיף את הקוד. הפונקציה in_pin1_handler
היא פונקציית הקריאה החוזרת הרשומה בעת הפעלת הפונקציונליות של לחיצה על הלחצן (מאוחר יותר בקובץ זה).
חשוב לשים לב איך הקריאה החוזרת (callback) משתמשת במאקרו OT_UNUSED_VARIABLE
, כי המשתנים שמועברים אל in_pin1_handler
לא משמשים בפועל לפונקציה.
/* Declaring callback function for button 1. */ static otSysButtonCallback sButtonHandler; static bool sButtonPressed; /** * @brief Function to receive interrupt and call back function * set by the application for button 1. * */ static void in_pin1_handler(uint32_t pin, nrf_gpiote_polarity_t action) { OT_UNUSED_VARIABLE(pin); OT_UNUSED_VARIABLE(action); sButtonPressed = true; }
פעולה: הוספת פונקציה להגדרת נורות ה-LED.
צריך להוסיף את הקוד הזה כדי להגדיר את המצב ואת המצב של כל נורות ה-LED במהלך האתחול.
/** * @brief Function for configuring: PIN_IN pin for input, PIN_OUT pin for output, * and configures GPIOTE to give an interrupt on pin change. */ void otSysLedInit(void) { /* Configure GPIO mode: output */ nrf_gpio_cfg_output(LED_1_PIN); nrf_gpio_cfg_output(LED_2_PIN); nrf_gpio_cfg_output(LED_3_PIN); nrf_gpio_cfg_output(LED_4_PIN); /* Clear all output first */ nrf_gpio_pin_write(LED_1_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_2_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_3_PIN, GPIO_LOGIC_LOW); nrf_gpio_pin_write(LED_4_PIN, GPIO_LOGIC_LOW); /* Initialize gpiote for button(s) input. Button event handlers are set in the application (main.c) */ ret_code_t err_code; err_code = nrfx_gpiote_init(); APP_ERROR_CHECK(err_code); }
פעולה: הוספת פונקציה כדי להגדיר את המצב של נורת LED.
הפונקציה הזו תשמש כאשר תפקיד המכשיר ישתנה.
/** * @brief Function to set the mode of an LED. */ void otSysLedSet(uint8_t aLed, bool aOn) { switch (aLed) { case 1: nrf_gpio_pin_write(LED_1_PIN, (aOn == GPIO_LOGIC_HI)); break; case 2: nrf_gpio_pin_write(LED_2_PIN, (aOn == GPIO_LOGIC_HI)); break; case 3: nrf_gpio_pin_write(LED_3_PIN, (aOn == GPIO_LOGIC_HI)); break; case 4: nrf_gpio_pin_write(LED_4_PIN, (aOn == GPIO_LOGIC_HI)); break; } }
פעולה: הוספת פונקציה להחלפת מצב של נורת LED.
הפונקציה הזו תשמש להחלפת המצב של נורת ה-LED4 כשהמכשיר מקבל הודעת UDP מולטיקאסט.
/** * @brief Function to toggle the mode of an LED. */ void otSysLedToggle(uint8_t aLed) { switch (aLed) { case 1: nrf_gpio_pin_toggle(LED_1_PIN); break; case 2: nrf_gpio_pin_toggle(LED_2_PIN); break; case 3: nrf_gpio_pin_toggle(LED_3_PIN); break; case 4: nrf_gpio_pin_toggle(LED_4_PIN); break; } }
פעולה: מוסיפים פונקציות כדי להפעיל ולעבד לחיצות על לחצנים.
הפונקציה הראשונה מפעילה את הלוח לחיצה על לחצן, והפונקציה השנייה שולחת את הודעת ה-UDP מולטיקאסט כשלוחצים על לחצן 1.
/** * @brief Function to initialize the button. */ void otSysButtonInit(otSysButtonCallback aCallback) { nrfx_gpiote_in_config_t in_config = NRFX_GPIOTE_CONFIG_IN_SENSE_LOTOHI(true); in_config.pull = NRF_GPIO_PIN_PULLUP; ret_code_t err_code; err_code = nrfx_gpiote_in_init(BUTTON_PIN, &in_config, in_pin1_handler); APP_ERROR_CHECK(err_code); sButtonHandler = aCallback; sButtonPressed = false; nrfx_gpiote_in_event_enable(BUTTON_PIN, true); } void otSysButtonProcess(otInstance *aInstance) { if (sButtonPressed) { sButtonPressed = false; sButtonHandler(aInstance); } }
פעולה: שומרים וסוגרים את הקובץ gpio.c
.
6. API: תגובה לשינויים בתפקיד המכשיר
אנחנו רוצים להאיר נורות LED שונות באפליקציה בהתאם לתפקיד של המכשיר. נעקוב אחר התפקידים הבאים: 'מנהיג', 'נתב', 'מכשיר קצה'. אנחנו יכולים להקצות אותם לנורות LED באופן הבא:
- LED1 = מוביל
- LED2 = נתב
- LED3 = סוף המכשיר
כדי להפעיל את הפונקציונליות הזו, האפליקציה צריכה לדעת מתי תפקיד המכשיר השתנה ואיך להפעיל את נורת ה-LED הנכונה בתגובה. נשתמש במופע של OpenThread בחלק הראשון, ובפלטפורמה של GPIO השני.
פותחים את הקובץ ./openthread/examples/apps/cli/main.c
בעורך הטקסט המועדף עליכם.
./openthread/examples/apps/cli/main.c
פעולה: הוספת כותרת כוללת.
בקטע 'הכללה' בקובץ main.c
, מוסיפים את קובצי הכותרת של ה-API הנדרשים לצורך שינוי התפקיד.
#include <openthread/instance.h> #include <openthread/thread.h> #include <openthread/thread_ftd.h>
פעולה: הוספת הצהרה על פונקציית handler לשינוי המצב של מופע OpenThread
מוסיפים את ההצהרה הזו אל main.c
, אחרי שהכותרת כוללת ולפני כל הצהרה של #if
. הפונקציה הזו תוגדר אחרי האפליקציה הראשית.
void handleNetifStateChanged(uint32_t aFlags, void *aContext);
פעולה: הוספת רישום לקריאה חוזרת (callback) לפונקציית הטיפול בשינויי מצב.
ב-main.c
, מוסיפים את הפונקציה הזו לפונקציה main()
אחרי הקריאה ל-otAppCliInit
. רישום זה של קריאה חוזרת מורה ל-OpenThread להתקשר לפונקציה handleNetifStateChange
בכל פעם שמצב ה-OpenThread משתנה.
/* Register Thread state change handler */ otSetStateChangedCallback(instance, handleNetifStateChanged, instance);
פעולה: הוספת הטמעה של שינוי מדינה.
ב-main.c
, אחרי הפונקציה main()
, מטמיעים את הפונקציה handleNetifStateChanged
. הפונקציה בודקת את הסימון של OT_CHANGED_THREAD_ROLE
במופע של OpenThread, ואם הוא השתנה, מפעילה או מכבה את נורות ה-LED לפי הצורך.
void handleNetifStateChanged(uint32_t aFlags, void *aContext) { if ((aFlags & OT_CHANGED_THREAD_ROLE) != 0) { otDeviceRole changedRole = otThreadGetDeviceRole(aContext); switch (changedRole) { case OT_DEVICE_ROLE_LEADER: otSysLedSet(1, true); otSysLedSet(2, false); otSysLedSet(3, false); break; case OT_DEVICE_ROLE_ROUTER: otSysLedSet(1, false); otSysLedSet(2, true); otSysLedSet(3, false); break; case OT_DEVICE_ROLE_CHILD: otSysLedSet(1, false); otSysLedSet(2, false); otSysLedSet(3, true); break; case OT_DEVICE_ROLE_DETACHED: case OT_DEVICE_ROLE_DISABLED: /* Clear LED4 if Thread is not enabled. */ otSysLedSet(4, false); break; } } }
7. API: שימוש ב-Multicast כדי להפעיל LED
באפליקציה שלנו אנחנו רוצים גם לשלוח הודעות UDP לכל שאר המכשירים ברשת כשלוחצים על לחצן 1 בלוח. כדי לאשר את קבלת ההודעה, נוסיף את מצב LED4 ללוחות האחרים בתגובה.
כדי להפעיל את הפונקציונליות הזו, האפליקציה צריכה:
- אתחול חיבור UDP בעת ההפעלה
- אפשרות לשלוח הודעת UDP לכתובת מרובת-השידורים המקומית ברשת
- טיפול בהודעות נכנסות של UDP
- החלפת המצב של נורת LED בתגובה להודעות UDP נכנסות
פותחים את הקובץ ./openthread/examples/apps/cli/main.c
בעורך הטקסט המועדף עליכם.
./openthread/examples/apps/cli/main.c
פעולה: הוספת כותרת כוללת.
בקטע 'הכללה' בחלק העליון של הקובץ main.c
, מוסיפים את קובצי הכותרת של ה-API הנדרשים עבור תכונת ה-UDP של מולטיקאסט.
#include <string.h> #include <openthread/message.h> #include <openthread/udp.h> #include "utils/code_utils.h"
הכותרת code_utils.h
משמשת עבור רכיבי המאקרו otEXPECT
ו-otEXPECT_ACTION
שמאמתים תנאים בזמן הריצה ומטפלים בשגיאות בחינניות.
פעולה: הוספת הגדרות וקבועים:
בקובץ main.c
, אחרי הקטע 'Include' (הכללה) ולפני כל הצהרה ב-#if
, מוסיפים קבועות ספציפיות ל-UDP ומגדירים:
#define UDP_PORT 1212 static const char UDP_DEST_ADDR[] = "ff03::1"; static const char UDP_PAYLOAD[] = "Hello OpenThread World!";
ff03::1
היא הכתובת המקומית לשידורים מרובים ברשת. הודעות שיישלחו לכתובת הזו יישלחו לכל מכשירי השרשור המלא ברשת. מידע נוסף על תמיכה בריבוי שידורים ב-Openthread זמין במאמר Multicast ב-Openthread.io.
פעולה: הוספת הצהרות על פונקציות.
בקובץ main.c
, אחרי ההגדרה otTaskletsSignalPending
ולפני הפונקציה main()
, מוסיפים פונקציות ספציפיות ל-UDP וכן משתנה סטטי שמייצג שקע UDP:
static void initUdp(otInstance *aInstance); static void sendUdp(otInstance *aInstance); static void handleButtonInterrupt(otInstance *aInstance); void handleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo); static otUdpSocket sUdpSocket;
פעולה: הוספת שיחות להפעלת נורות ה-LED והלחצן של GPIO.
ב-main.c
, צריך להוסיף את הקריאות לפונקציה הזו לפונקציה main()
אחרי הקריאה ל-otSetStateChangedCallback
. הפונקציות האלה מפעילות את הסיכות של GPIO ו-GPIOTE ומגדירות handler של לחצן כדי לטפל באירועי לחצנים.
/* init GPIO LEDs and button */ otSysLedInit(); otSysButtonInit(handleButtonInterrupt);
פעולה: הוספת קריאה להפעלה של UDP.
בחודש main.c
, צריך להוסיף את הפונקציה הזו לפונקציה main()
אחרי הקריאה otSysButtonInit
שהוספת עכשיו:
initUdp(instance);
הקריאה הזו מבטיחה שקע UDP מופעל עם הפעלת האפליקציה. בלי זה, המכשיר לא יוכל לשלוח או לקבל הודעות UDP.
פעולה: הוספת קריאה לעיבוד האירוע של לחצן GPIO.
ב-main.c
, מוסיפים את הקריאה לפונקציה הזו לפונקציה main()
אחרי הקריאה otSysProcessDrivers
, בלולאה של while
. הפונקציה המוצהרת ב-gpio.c
בודקת אם הלחצן נלחץ, ואם כן, מתקשר ל-handler שהוגדר (handleButtonInterrupt
) שהוגדר בשלב הקודם.
otSysButtonProcess(instance);
פעולה: handler של הפרעה בלחצן.
ב-main.c
, מוסיפים את ההטמעה של הפונקציה handleButtonInterrupt
אחרי הפונקציה handleNetifStateChanged
שהוספתם בשלב הקודם:
/** * Function to handle button push event */ void handleButtonInterrupt(otInstance *aInstance) { sendUdp(aInstance); }
פעולה: הטמעה של אתחול UDP.
ב-main.c
, מוסיפים את ההטמעה של הפונקציה initUdp
אחרי הפונקציה handleButtonInterrupt
שהוספתם:
/** * Initialize UDP socket */ void initUdp(otInstance *aInstance) { otSockAddr listenSockAddr; memset(&sUdpSocket, 0, sizeof(sUdpSocket)); memset(&listenSockAddr, 0, sizeof(listenSockAddr)); listenSockAddr.mPort = UDP_PORT; otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, aInstance); otUdpBind(aInstance, &sUdpSocket, &listenSockAddr, OT_NETIF_THREAD); }
UDP_PORT
הוא היציאה שהגדרת קודם (1212). הפונקציה otUdpOpen
פותחת את השקע ומתעדת פונקציית קריאה חוזרת (handleUdpReceive
) כאשר מתקבלת הודעת UDP. otUdpBind
מחייבת את השקע לממשק רשת השרשור על ידי העברה של OT_NETIF_THREAD
. לאפשרויות אחרות של ממשק הרשת, יש לעיין בספירה של otNetifIdentifier
בחומר העזר של UDP API.
פעולה: הטמעת הודעות UDP.
ב-main.c
, מוסיפים את ההטמעה של הפונקציה sendUdp
אחרי הפונקציה initUdp
שהוספתם:
/** * Send a UDP datagram */ void sendUdp(otInstance *aInstance) { otError error = OT_ERROR_NONE; otMessage * message; otMessageInfo messageInfo; otIp6Address destinationAddr; memset(&messageInfo, 0, sizeof(messageInfo)); otIp6AddressFromString(UDP_DEST_ADDR, &destinationAddr); messageInfo.mPeerAddr = destinationAddr; messageInfo.mPeerPort = UDP_PORT; message = otUdpNewMessage(aInstance, NULL); otEXPECT_ACTION(message != NULL, error = OT_ERROR_NO_BUFS); error = otMessageAppend(message, UDP_PAYLOAD, sizeof(UDP_PAYLOAD)); otEXPECT(error == OT_ERROR_NONE); error = otUdpSend(aInstance, &sUdpSocket, message, &messageInfo); exit: if (error != OT_ERROR_NONE && message != NULL) { otMessageFree(message); } }
יש לשים לב לפקודות המאקרו otEXPECT
ו-otEXPECT_ACTION
. ההגדרות האלה מבטיחות שהודעת ה-UDP תקינה ומוקצית בצורה תקינה למאגר הנתונים הזמני, ואם לא, הפונקציה מטפלת בצדק בשגיאות על ידי דילוג לבלוק exit
, שם הוא מפנה את המאגר.
מידע נוסף על הפונקציות המשמשות להפעלה של UDP זמין בהפניות ל-IPv6 ול-UDP.
פעולה: הטמעת טיפול בהודעות UDP.
ב-main.c
, מוסיפים את ההטמעה של הפונקציה handleUdpReceive
אחרי הפונקציה sendUdp
שהוספתם. הפונקציה הזו פשוט תחליף את LED4.
/** * Function to handle UDP datagrams received on the listening socket */ void handleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo) { OT_UNUSED_VARIABLE(aContext); OT_UNUSED_VARIABLE(aMessage); OT_UNUSED_VARIABLE(aMessageInfo); otSysLedToggle(4); }
8. ממשק API: הגדרת הרשת של Thread
כדי להקל על ההדגמה, אנחנו רוצים שהמכשירים שלנו יתחילו מיד את Thread ויצטרפו לרשת כאשר הם פועלים. כדי לעשות זאת, נשתמש במבנה otOperationalDataset
. המבנה הזה מכיל את כל הפרמטרים הנדרשים כדי להעביר את פרטי הכניסה של פרוטוקול Thread למכשיר.
שימוש במבנה הזה יבטל את ברירות המחדל של הרשת המובנות ב-OpenThread, כדי להפוך את האפליקציה למאובטחת יותר ולהגביל את צומתי השרשור ברשת שלנו רק לאלו שמפעילים את האפליקציה.
שוב, פותחים את הקובץ ./openthread/examples/apps/cli/main.c
בעורך הטקסט המועדף.
./openthread/examples/apps/cli/main.c
פעולה: הוספת כותרת.
בקטע 'הכללה' בראש הקובץ main.c
, מוסיפים את קובץ הכותרת של ה-API שצריך להגדיר את פרוטוקול Thread:
#include <openthread/dataset_ftd.h>
פעולה: הוספת הצהרת פונקציה להגדרת התצורה של הרשת.
מוסיפים את ההצהרה הזו אל main.c
, אחרי שהכותרת כוללת ולפני כל הצהרה של #if
. הפונקציה הזו תוגדר אחרי הפונקציה הראשית באפליקציה.
static void setNetworkConfiguration(otInstance *aInstance);
פעולה: הוספת קריאה להגדרה של הרשת.
ב-main.c
, מוסיפים את הקריאה לפונקציה הזו לפונקציה main()
אחרי הקריאה otSetStateChangedCallback
. הפונקציה הזו מגדירה את מערך הנתונים של רשת Thread.
/* Override default network credentials */ setNetworkConfiguration(instance);
פעולה: הוספת קריאות להפעלת סטאק וממשק הרשת של ה-thread.
ב-main.c
, צריך להוסיף את הקריאות לפונקציה הזו לפונקציה main()
אחרי הקריאה ל-otSysButtonInit
.
/* Start the Thread network interface (CLI cmd > ifconfig up) */ otIp6SetEnabled(instance, true); /* Start the Thread stack (CLI cmd > thread start) */ otThreadSetEnabled(instance, true);
פעולה: הטמעת תצורת רשת של Thread
ב-main.c
, מוסיפים את ההטמעה של הפונקציה setNetworkConfiguration
אחרי הפונקציה main()
:
/** * Override default network settings, such as panid, so the devices can join a network */ void setNetworkConfiguration(otInstance *aInstance) { static char aNetworkName[] = "OTCodelab"; otOperationalDataset aDataset; memset(&aDataset, 0, sizeof(otOperationalDataset)); /* * Fields that can be configured in otOperationDataset to override defaults: * Network Name, Mesh Local Prefix, Extended PAN ID, PAN ID, Delay Timer, * Channel, Channel Mask Page 0, Network Key, PSKc, Security Policy */ aDataset.mActiveTimestamp.mSeconds = 1; aDataset.mActiveTimestamp.mTicks = 0; aDataset.mActiveTimestamp.mAuthoritative = false; aDataset.mComponents.mIsActiveTimestampPresent = true; /* Set Channel to 15 */ aDataset.mChannel = 15; aDataset.mComponents.mIsChannelPresent = true; /* Set Pan ID to 2222 */ aDataset.mPanId = (otPanId)0x2222; aDataset.mComponents.mIsPanIdPresent = true; /* Set Extended Pan ID to C0DE1AB5C0DE1AB5 */ uint8_t extPanId[OT_EXT_PAN_ID_SIZE] = {0xC0, 0xDE, 0x1A, 0xB5, 0xC0, 0xDE, 0x1A, 0xB5}; memcpy(aDataset.mExtendedPanId.m8, extPanId, sizeof(aDataset.mExtendedPanId)); aDataset.mComponents.mIsExtendedPanIdPresent = true; /* Set network key to 1234C0DE1AB51234C0DE1AB51234C0DE */ uint8_t key[OT_NETWORK_KEY_SIZE] = {0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE}; memcpy(aDataset.mNetworkKey.m8, key, sizeof(aDataset.mNetworkKey)); aDataset.mComponents.mIsNetworkKeyPresent = true; /* Set Network Name to OTCodelab */ size_t length = strlen(aNetworkName); assert(length <= OT_NETWORK_NAME_MAX_SIZE); memcpy(aDataset.mNetworkName.m8, aNetworkName, length); aDataset.mComponents.mIsNetworkNamePresent = true; otDatasetSetActive(aInstance, &aDataset); /* Set the router selection jitter to override the 2 minute default. CLI cmd > routerselectionjitter 20 Warning: For demo purposes only - not to be used in a real product */ uint8_t jitterValue = 20; otThreadSetRouterSelectionJitter(aInstance, jitterValue); }
כפי שמפורט בפונקציה, הפרמטרים של רשתות Thread שבהם אנחנו משתמשים באפליקציה הזו הם:
- ערוץ = 15
- מזהה PAN = 0x2222
- מזהה PAN מורחב = C0DE1AB5C0DE1AB5
- מפתח רשת = 1234C0DE1AB51234C0DE1AB51234C0DE
- שם הרשת = OTCodelab
בנוסף, כאן נקטין את הרעידות לבחירת הנתבים, כך שהמכשירים שלנו ישנו את התפקידים מהר יותר למטרות הדגמה. שימו לב לכך רק אם הצומת הוא FTD (מכשיר עם שרשור מלא). מידע נוסף בנושא הזה בשלב הבא.
9. API: פונקציות מוגבלות
חלק מממשקי ה-API של OpenThread משנים הגדרות שצריך לשנות רק למטרות הדגמה או בדיקה. אין להשתמש בממשקי ה-API האלה בפריסת ייצור של אפליקציה באמצעות OpenThread.
לדוגמה, הפונקציה otThreadSetRouterSelectionJitter
מתאימה את הזמן (בשניות) הנדרש למכשיר קצה כדי לקדם את עצמו לנתב. ברירת המחדל לערך זה היא 120, בהתאם למפרט השרשור. כדי שיהיה קל להשתמש ב-Codelab הזה, נשנה אותו ל-20, כך שאין צורך להמתין זמן רב עד שצומת של Thread ישנה את התפקידים.
הערה: מכשירי MTD לא הופכים לנתבים, ותמיכה בפונקציה כמו otThreadSetRouterSelectionJitter
לא כלולה ב-build של MTD. בהמשך נצטרך לציין את אפשרות CMake -DOT_MTD=OFF
, אחרת נתקלנו בכשל build.
כדי לבדוק זאת, צריך לבדוק את ההגדרה של הפונקציה otThreadSetRouterSelectionJitter
, שנכללת בהנחיה של מעבד המידע OPENTHREAD_FTD
:
./openthread/src/core/api/thread_ftd_api.cpp
#if OPENTHREAD_FTD #include <openthread/thread_ftd.h> ... void otThreadSetRouterSelectionJitter(otInstance *aInstance, uint8_t aRouterJitter) { Instance &instance = *static_cast<Instance *>(aInstance); instance.GetThreadNetif().GetMle().SetRouterSelectionJitter(aRouterJitter); } ... #endif // OPENTHREAD_FTD
10. עדכונים מ-CMake
לפני יצירת האפליקציה, נדרשים כמה עדכונים קלים לשלושה קובצי CMake. מערכות ה-build האלה משמשות לאיסוף ולקישור של האפליקציה שלך.
./third_party/NordicSemiconductor/CMakeLists.txt
עכשיו צריך להוסיף כמה סימונים ל-NoricSemiconductor CMakeLists.txt
, כדי לוודא שפונקציות GPIO מוגדרות באפליקציה.
פעולה: הוספת סימונים לקובץ CMakeLists.txt
.
יש לפתוח את ./third_party/NordicSemiconductor/CMakeLists.txt
בעורך הטקסט המועדף עליך ולהוסיף את השורות הבאות בקטע COMMON_FLAG
.
... set(COMMON_FLAG -DSPIS_ENABLED=1 -DSPIS0_ENABLED=1 -DNRFX_SPIS_ENABLED=1 -DNRFX_SPIS0_ENABLED=1 ... # Defined in ./third_party/NordicSemiconductor/nrfx/templates/nRF52840/nrfx_config.h -DGPIOTE_ENABLED=1 -DGPIOTE_CONFIG_IRQ_PRIORITY=7 -DGPIOTE_CONFIG_NUM_OF_LOW_POWER_EVENTS=1 ) ...
./src/CMakeLists.txt
עורכים את הקובץ ./src/CMakeLists.txt
כדי להוסיף את קובץ המקור החדש gpio.c
:
פעולה: הוספת מקור ה-gpio לקובץ ./src/CMakeLists.txt
.
פותחים את ./src/CMakeLists.txt
בעורך הטקסט המועדף ומוסיפים את הקובץ לקטע NRF_COMM_SOURCES
.
... set(NRF_COMM_SOURCES ... src/gpio.c ... ) ...
./third_party/NordicSemiconductor/CMakeLists.txt
לבסוף, מוסיפים את קובץ הנהג של nrfx_gpiote.c
לקובץ NordicSemiconductor CMakeLists.txt
, כך שהוא ייכלל ב-build של הספרייה של הנהגים הנורדיים.
פעולה: הוסיפו את נהג ה-gpio לקובץ NordicSemiconductor CMakeLists.txt
.
פותחים את ./third_party/NordicSemiconductor/CMakeLists.txt
בעורך הטקסט המועדף ומוסיפים את הקובץ לקטע COMMON_SOURCES
.
... set(COMMON_SOURCES ... nrfx/drivers/src/nrfx_gpiote.c ... ) ...
11. הגדרת המכשירים
לאחר שכל עדכוני הקוד יסתיימו, תהיה מוכן לבנות את האפליקציה ולהבהב אותה לכל שלושת לוחות ה-NRF52840 הנורדיים. כל מכשיר יתפקד כמכשיר שרשור מלא.
בניית שרשור פתוח
יצירת קבצים בינאריים של OpenThread FTD לפלטפורמת nRF52840.
$ cd ~/ot-nrf528xx $ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF
מנווטים לספרייה עם התוכנה הבינארית OpenThread FTD CLI וממירים אותה לפורמט הקסדצימלי עם ARM inline Toolin:
$ cd build/bin $ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex
הבהוב של לוחות העריכה
קובץ Flash ot-cli-ftd.hex
משתנה לכל לוח nRF52840.
מחברים את כבל ה-USB ליציאת ניפוי הבאגים של המיקרו USB לצד סיכת החשמל החיצונית בלוח nRF52840. לאחר מכן מחברים אותו למכונת ה-Linux. להגדיר בצורה נכונה את LED5.
כמו בעבר, יש לשים לב למספר הסידורי של לוח nRF52840:
מנווטים אל המיקום של כלי שורת הפקודה nRFx, והופכים את הקובץ ההקסדצימלי של OpenThread CLI FTD ללוח nRF52840, באמצעות המספר הסידורי של הלוח:
$ cd ~/nrfjprog $ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \ ~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset
נורת ה-LED תיכבה לזמן קצר במהלך הפלאש. הפלט הבא נוצר בהצלחה:
Parsing hex file. Erasing user available code and UICR flash areas. Applying system reset. Checking that the area to write is not protected. Programing device. Applying system reset. Run.
חוזרים על השלב 'פלאש בלוחות' בשני הלוחות האחרים. כל לוח צריך להיות מחובר אל מכונת Linux באותו אופן, והפקודה לפלאש זהה, מלבד המספר הסידורי של הלוח. חשוב להשתמש במספר הסידורי הייחודי של כל לוח
nrfjprog
פקודת Flash.
אם הפעולה מסתיימת בהצלחה, נורת LED1 , LED2 או LED3 דולקת בכל לוח. יכול להיות גם שתאורת ה-LED המוארת תעבור מ-3 ל-2 (או בין 2 ל-1) זמן קצר לאחר מכן שתהבהב (תכונת שינוי התפקיד של המכשיר).
12. פונקציונליות של אפליקציה
כל שלושת לוחות ה-nRF52840 אמורים להיות מופעלים ומופעלים באמצעות אפליקציית OpenThread. כפי שפירטנו קודם, לאפליקציה הזו יש שתי תכונות עיקריות.
אינדיקטורים של תפקידי מכשירים
נורת ה-LED המוארת בכל לוח משקפת את התפקיד הנוכחי של הצומת Thread:
- LED1 = מוביל
- LED2 = נתב
- LED3 = סוף המכשיר
כשהתפקיד משתנה, גם נורת ה-LED המוארת הייתם אמורים לראות את השינויים האלה על לוח או שניים ב-20 שניות מכל אחד מהמכשירים.
UDP מולטיקאסט
כשלוחצים על לחצן 1 בלוח, נשלחת הודעת UDP לכתובת מולטיקאסט של רשתות מקומיות, שכוללת את כל הצמתים האחרים ברשת השרשורים. בתגובה לקבלת ההודעה הזו, LED4 בכל הלוחות האחרים מופעל או מושבת. LED4 נשאר פעיל או כבוי בכל לוח עד שהוא מקבל הודעת UDP נוספת.
13. הדגמה: צפייה בשינויים בתפקיד המכשיר
המכשירים מהבהבים הם סוג ספציפי של מכשיר עם חוט מלא (FTD) שנקרא מכשיר קצה מתאים לנתב (REED). כלומר, הם יכולים לשמש כנתב או כמכשיר קצה ולקדם את עצמם ממכשיר קצה לנתב.
החוט יכול לתמוך בעד 32 נתבים, אבל הוא מנסה לשמור על מספר הנתבים בין 16 ל-23. אם REED מתחבר כמכשיר קצה ומספר הנתבים נמוך מ-16, הוא מקדם את עצמו באופן אוטומטי לנתב. השינוי הזה צריך להתבצע בזמן אקראי בתוך מספר השניות שהוגדר לערך otThreadSetRouterSelectionJitter
באפליקציה (20 שניות).
לכל רשת Thread יש גם Leader, נתב שאחראי על ניהול הנתבים ברשת Thread. כשכל המכשירים פועלים, אחרי 20 שניות אחד מהם צריך להיות מוביל (LED1 מופעל) ושני המכשירים האחרים צריכים להיות נתבים (LED2 פועלים).
הסרת המנהיג
אם מוביל יוסר מרשת השרשור, נתב אחר מקדם את עצמו ל'מנהל' כדי להבטיח שלרשת עדיין יהיה 'מנהיג'.
מכבים את לוח ה-Leader (הלוח המואר באמצעות LED1) באמצעות מתג ההפעלה. ממתינים כ-20 שניות. באחד משני הלוחות שנותרו, LED2 (Router) יכובה ו-LED1 (Leader) תופעל. המכשיר הזה מוגדר עכשיו כמוביל רשת הרשת.
הפעלה מחדש של לוח ה-Leader המקורי. הוא אמור להתחבר מחדש לרשת השרשורים כמכשיר קצה (LED3 מואר). תוך 20 שניות (הרעידות לבחירת נתב) הן מקודמות לנתב (LED2 מואר).
איפוס הלוחות
מכבים את כל שלושת הלוחות ואז מפעילים אותם מחדש ומעיינים בנורות ה-LED. הלוח הראשון שנוצר אמור להתחיל בתפקיד 'מנהיג' (LED1 מואר) — הנתב הראשון ברשת Thread הופך ל'מנהיג' באופן אוטומטי.
שני הלוחות האחרים מתחברים בתחילה לרשת כשמכשירי קצה (LED3 מוארים), אך אמורים לקדם את עצמם לנתבים (LED2 מוארת) בתוך 20 שניות.
מחיצות ברשת
אם הלוחות לא מקבלים מספיק חשמל, או שחיבור הרדיו ביניהם חלש, יכול להיות שרשת ה-Threads תפוצל למחיצות, ויכול להיות יותר ממכשיר אחד יופיע כ-Lead.
השרשור מרפא את עצמו, לכן בסופו של דבר מחיצות אמורות להתחלק למחיצה אחת עם מוביל אחד.
14. הדגמה: שליחת UDP מולטיקאסט
אם ממשיכים מהתרגיל הקודם, אין להדליק את LED4 באף מכשיר.
בוחרים לוח כלשהו ולוחצים על 'לחצן1'. נורת LED בכל שאר הלוחות ברשת ה-Thread שמריצה את האפליקציה צריכה להחליף את המצב שלהם. אם ממשיכים מהתרגיל הקודם, עכשיו הם מופעלים.
יש ללחוץ שוב על לחצן 1 עבור אותו לוח. נורת ה-LED4 בכל שאר הלוחות אמורה לחזור לפעול.
יש ללחוץ על לחצן 1 בלוח אחר ולראות איך מתבצעת הפעלה של LED4 בלוחים האחרים. יש ללחוץ על לחצן 1 באחד מהלוחות שבהם מופעל LED4. נורת ה-LED עדיין פועלת בלוח העריכה, אבל המתגים האחרים מופעלים.
מחיצות ברשת
אם הלוחות שלכם חולקו למחלק מסוים ויש יותר ממנהיג אחד ביניהם, התוצאה של ההודעה עם כמה העברות תשתנה בין לוחות. אם לוחצים על לחצן1 בלוח שחולק למחיצות (ולכן הוא היחיד ברשת השרשורים המחולקים למחיצות), LED4 בלוחות האחרים לא יואר בתגובה. במקרה כזה, מומלץ לאפס את הלוחות – באופן אידיאלי, הם יערכו מחדש פרוטוקול Thread אחד והודעת ה-UDP אמורה לפעול בצורה תקינה.
15. מזל טוב!
יצרת אפליקציה שמשתמשת ב-OpenThread API!
עכשיו אתם יודעים:
- איך לתכנת את הלחצנים ואת נורות ה-LED בלוחות הפיתוח למפתחים הנורדיים nRF52840
- איך משתמשים בממשקי API נפוצים של OpenThread ובמחלקה
otInstance
- איך לעקוב אחרי השינויים במצב OpenThread ולהגיב להם
- איך לשלוח הודעות UDP לכל המכשירים ברשת Thread
- איך לשנות קובצי Files
השלבים הבאים
על סמך Codelab הזה, נסו את התרגילים הבאים:
- צריך לשנות את המודול של ה-GPIO כדי להשתמש בסיכות GPIO במקום בנורות LED שמחוברות לחשמל, ולחבר נורות LED חיצוניות של RGB המשנות את הצבע על סמך תפקיד הנתב
- הוספת תמיכה ב-GPIO לפלטפורמה לדוגמה אחרת
- במקום להשתמש בניתוב מולטיקאסט כדי לשלוח פינג לכל המכשירים מקליק על לחצן, יש להשתמש ב-Router/Leader API כדי לאתר מכשיר ספציפי ולענות לו
- יש לחבר את רשת האריג שלך לאינטרנט באמצעות נתב גבולות פתוח של OpenThread והעברה שלהם (cast) מחוץ לרשת ה-Thread כדי להדליק את נורות ה-LED
קריאה נוספת
ב-openthread.io וב-GitHub יש מגוון משאבים של OpenThread, כולל:
- פלטפורמות נתמכות – כל הפלטפורמות התומכות ב-OpenThread
- Open OpenThread — פרטים נוספים על יצירה והגדרה של OpenThread
- Thread Primer – חומר עזר נהדר על קונספטים של Thread
עיון: