1. מבוא
OpenThread, שפורסם על ידי Nest, הוא הטמעה בקוד פתוח של פרוטוקול הרשת Thread®. Nest השיקה את OpenThread כדי שהטכנולוגיה שמשמשת במוצרי Nest תהיה זמינה למפתחים באופן נרחב, וכך לזרז את הפיתוח של מוצרים לבית החכם.
מפרט Thread מגדיר פרוטוקול תקשורת אלחוטית מהימן, מאובטח וחסכוני מבוסס-IPv6 בין מכשירים לאפליקציות ביתיות. OpenThread מטמיע את כל שכבות הרשת של Thread, כולל IPv6, 6LoWPAN, IEEE 802.15.4 עם אבטחת MAC, יצירת קישורי רשתות וניתוב רשתות.
ב-Codelab הזה תלמדו להשתמש בממשקי ה-API של OpenThread כדי להפעיל רשת Thread, לעקוב אחרי שינויים בתפקידי המכשירים ולגיבוש תגובה לשינויים האלה, לשלוח הודעות UDP ולקשר את הפעולות האלה ללחצנים ולנורות LED בחומרה אמיתית.
מה תלמדו
- איך לתכנות את הלחצנים והנורות הלד בלוחות הפיתוח של Nordic nRF52840
- איך משתמשים בממשקי API נפוצים של OpenThread ובכיתה
otInstance
- איך לעקוב אחרי שינויים במצב של OpenThread ולהגיב להם
- איך שולחים הודעות UDP לכל המכשירים ברשת Thread
- איך משנים קובצי Makefile
מה נדרש
חומרה:
- 3 לוחות פיתוח של Nordic Semiconductor nRF52840
- 3 כבלים מסוג USB ל-Micro-USB לחיבור הלוחות
- מחשב Linux עם לפחות 3 יציאות USB
תוכנה:
- GNU Toolchain
- כלי שורת הפקודה של Nordic nRF5x
- תוכנת Segger J-Link
- OpenThread
- Git
למעט במקרים שבהם צוין אחרת, התוכן של Codelab הזה מוצג ברישיון Creative Commons Attribution 3.0, והשימוש בדוגמאות הקוד נעשה על פי רישיון Apache 2.0.
2. תחילת העבודה
השלמת ה-Codelab בנושא חומרה
לפני שמתחילים את הקודלהב הזה, צריך להשלים את הקודלהב יצירת רשת Thread באמצעות לוחות nRF52840 ו-OpenThread, שבו:
- פרטים על כל התוכנות הנדרשות ליצירה ולחיבור (flashing)
- איך יוצרים את OpenThread ומעבירים אותו ל-flash בלוחות Nordic nRF52840
- הדגמה של העקרונות הבסיסיים של רשת Thread
לא מפורט ב-Codelab הזה אף אחד מההגדרות של הסביבה הנדרשות כדי ליצור את OpenThread ולבצע את ה-flash של הלוחות – רק הוראות בסיסיות לביצוע ה-flash של הלוחות. נקודת המוצא היא שכבר השלמת את Codelab בנושא יצירת רשת Thread.
מכונה עם Linux
קורס Codelab הזה נועד לשימוש במכונה מבוססת-Linux מסוג i386 או x86 כדי לבצע פלאש לכל לוחות הפיתוח של Thread. כל השלבים נבדקו ב-Ubuntu 14.04.5 LTS (Trusty Tahr).
לוחות Nordic Semiconductor nRF52840
ב-Codelab הזה נעשה שימוש בשלוש לוחות nRF52840 PDK.
התקנת תוכנה
כדי ליצור ולבצע אימג' של OpenThread, צריך להתקין את SEGGER J-Link, את הכלים של שורת הפקודה של nRF5x, את ARM GNU Toolchain וחבילות Linux שונות. אם השלמתם את 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 ברמת השרשור וברמת הפלטפורמה, לשימוש באפליקציות שלכם:
- מידע על מכונות OpenThread ובקרה עליהן
- שירותי אפליקציות כמו IPv6, UDP ו-CoAP
- ניהול פרטי הכניסה לרשת, יחד עם התפקידים 'מפקח' ו'מצטרף'
- ניהול של נתב גבולות
- תכונות משופרות כמו פיקוח על ילדים וזיהוי הפרעות
מידע על כל ממשקי ה-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 שבהן נשתמש כדי להתחבר ללחצנים ולנורות ה-LED של nRF52840 בקובץ 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 כדי לדכא שגיאות build שקשורות למשתנים שלא בשימוש בכלי פיתוח מסוימים. בהמשך נראה דוגמאות לכך.
5. הטמעת הפשטה של פלטפורמת GPIO
בשלב הקודם, העברנו על הצהרות הפונקציות הספציפיות לפלטפורמה ב-./openthread/examples/platforms/openthread-system.h
שאפשר להשתמש בהן ל-GPIO. כדי לגשת ללחצנים ולנורות LED בלוחות הפיתוח של nRF52840, צריך להטמיע את הפונקציות האלה בפלטפורמת nRF52840. בקוד הזה תוסיפו פונקציות שמבצעות את הפעולות הבאות:
- איך מאתחלים את הפינים והמצבים של GPIO
- שליטה במתח בפין
- הפעלת הפסקות GPIO ורישום קריאה חוזרת (callback)
בספרייה ./src/src
, יוצרים קובץ חדש בשם gpio.c
. בקובץ החדש, מוסיפים את התוכן הבא.
./src/src/gpio.c (new file)
פעולה: הוספת הגדרות.
ההגדרות האלה משמשות כאבסוקציות בין ערכים ספציפיים ל-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 זמין במרכז המידע של Nordic Semiconductor.
פעולה: מוסיפים רכיבים של כותרת עליונה.
בשלב הבא, מוסיפים את ה-include של הכותרות הנדרשות לפונקציונליות של 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) שמירתמת כשפונקציית לחיצת הלחצן מופעלת (בהמשך הקובץ).
שימו לב שהקריאה החוזרת (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 של Multicast.
/** * @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
פעולה: מוסיפים רכיבים לכותרת.
בקטע ה-includes בקובץ main.c
, מוסיפים את קובצי הכותרות של ה-API הנדרשים לתכונה של שינוי התפקיד.
#include <openthread/instance.h> #include <openthread/thread.h> #include <openthread/thread_ftd.h>
פעולה: מוסיפים הצהרה על פונקציית טיפול באירועים לשינוי המצב של מופע OpenThread.
מוסיפים את ההצהרה הזו ל-main.c
, אחרי שכותרת ה-include וקודם להצהרות #if
. הפונקציה הזו תוגדר אחרי האפליקציה הראשית.
void handleNetifStateChanged(uint32_t aFlags, void *aContext);
פעולה: מוסיפים רישום של קריאה חוזרת לפונקציה לטיפול בשינוי המצב.
ב-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 לכתובת ה-multicast המקומית של הרשת
- טיפול בהודעות UDP נכנסות
- הפעלה/השבתה של LED4 בתגובה להודעות UDP נכנסות
פותחים את הקובץ ./openthread/examples/apps/cli/main.c
בכלי לעריכת טקסט.
./openthread/examples/apps/cli/main.c
פעולה: מוסיפים רכיבים של כותרת עליונה.
בקטע ה-includes בחלק העליון של הקובץ 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
, אחרי הקטע includes ולפני כל משפט #if
, מוסיפים קבועים והגדרות ספציפיים ל-UDP:
#define UDP_PORT 1212 static const char UDP_DEST_ADDR[] = "ff03::1"; static const char UDP_PAYLOAD[] = "Hello OpenThread World!";
ff03::1
היא כתובת ה-multicast המקומית ברשת התאמה. כל ההודעות שנשלחות לכתובת הזו יישלחו לכל המכשירים ברשת עם תמיכה בשרשור מלא. מידע נוסף על תמיכה ב-multicast ב-OpenThread זמין במאמר Multicast on 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;
פעולה: מוסיפים קריאות לאינטוליזיה של הלחצן והנורות של GPIO.
ב-main.c
, מוסיפים את קריאות הפונקציות האלה לפונקציה main()
אחרי הקריאה ל-otSetStateChangedCallback
. הפונקציות האלה מאתחלות את הפינים GPIO ו-GPIOTE ומגדירות טיפול בכפתור כדי לטפל באירועי לחיצה על כפתור.
/* 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
, בודקת אם הכפתור נלחץ. אם כן, היא קוראת למטפל (handleButtonInterrupt
) שהוגדר בשלב שלמעלה.
otSysButtonProcess(instance);
פעולה: מטמיעים את Button Interrupt 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
מקשר את השקע לממשק הרשת של Thread על ידי העברת 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 באתר openthread.io.
פעולה: מטמיעים טיפול בהודעות 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, כדי לשפר את האבטחה של האפליקציה שלנו ולהגביל את צמתים של Thread ברשת שלנו רק לאלה שמפעילים את האפליקציה.
שוב, פותחים את הקובץ ./openthread/examples/apps/cli/main.c
בכלי לעריכת טקסט.
./openthread/examples/apps/cli/main.c
פעולה: הוספת הכלל של הכותרת
בקטע ה-includes בחלק העליון של הקובץ main.c
, מוסיפים את קובץ הכותרת של ה-API שדרוש כדי להגדיר את רשת Thread:
#include <openthread/dataset_ftd.h>
פעולה: מוסיפים הצהרת פונקציה להגדרת תצורת הרשת.
מוסיפים את ההצהרה הזו ל-main.c
, אחרי שכותרת ה-include וקודם להצהרות #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 שבהם אנחנו משתמשים באפליקציה הזו הם:
- Channel = 15
- מזהה PAN = 0x2222
- מזהה PAN מורחב = C0DE1AB5C0DE1AB5
- מפתח הרשת = 1234C0DE1AB51234C0DE1AB51234C0DE
- שם הרשת = OTCodelab
בנוסף, כאן אנחנו מפחיתים את התנודות בבחירת הנתב, כדי שהמכשירים שלנו ישנו תפקידים מהר יותר למטרות הדגמה. חשוב לדעת שהפעולה הזו מתבצעת רק אם הצומת הוא FTD (מכשיר עם שרשור מלא). מידע נוסף על כך זמין בשלב הבא.
9. API: פונקציות מוגבלות
חלק מממשקי ה-API של OpenThread משנים הגדרות שצריך לשנות רק למטרות הדגמה או בדיקה. אין להשתמש בממשקי ה-API האלה בפריסת ייצור של אפליקציה באמצעות OpenThread.
לדוגמה, הפונקציה otThreadSetRouterSelectionJitter
מתאימה את הזמן (בשניות) שנדרש למכשיר קצה כדי לקדם את עצמו לנתב. ערך ברירת המחדל של הערך הזה הוא 120, בהתאם למפרט של השרשור. כדי להקל על השימוש ב-Codelab הזה, נשנה את הערך ל-20, כדי שלא תצטרכו להמתין הרבה זמן עד שתפנו את התפקידים של צומת בשרשור.
הערה: מכשירי MTD לא הופכים לנתב, ותמיכה בפונקציה כמו otThreadSetRouterSelectionJitter
לא כלולה ב-build של MTD. בהמשך נצטרך לציין את האפשרות -DOT_MTD=OFF
ב-CMake, אחרת נתקלת בכשלים ב-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
עכשיו מוסיפים כמה דגלים ל-CMakeLists.txt
של NordicSemiconductor, כדי לוודא שפונקציות ה-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
לבסוף, מוסיפים את קובץ ה-driver nrfx_gpiote.c
לקובץ CMakeLists.txt
של NordicSemiconductor, כדי שהוא ייכלל ב-build של הספרייה של מנהלי ההתקנים של Nordic.
פעולה: מוסיפים את מנהל ה-gpio לקובץ CMakeLists.txt
של NordicSemiconductor.
פותחים את ./third_party/NordicSemiconductor/CMakeLists.txt
בכלי לעריכת טקסט מועדף, ומוסיפים את הקובץ לקטע COMMON_SOURCES
.
... set(COMMON_SOURCES ... nrfx/drivers/src/nrfx_gpiote.c ... ) ...
11. הגדרת המכשירים
אחרי שכל עדכוני הקוד בוצעו, אתם מוכנים ליצור את האפליקציה ולבצע לה פלאש בכל שלוש לוחות הפיתוח של Nordic nRF52840. כל מכשיר יפעל כמכשיר עם תמיכה בשרשור מלא (FTD).
פיתוח OpenThread
פיתוח קובצי הבינארי של OpenThread FTD לפלטפורמת nRF52840.
$ cd ~/ot-nrf528xx $ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF
עוברים לספרייה עם קובץ ה-CLI הבינארי של OpenThread FTD וממירים אותו לפורמט הקסדצימלי באמצעות ARM Embedded Toolchain:
$ cd build/bin $ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex
איך מאפסים את הלוחות
מעבירים את הקובץ ot-cli-ftd.hex
לכל לוח nRF52840.
מחברים את כבל ה-USB ליציאת ניפוי הבאגים של מיקרו-USB ליד סיכת החשמל החיצונית בלוח nRF52840, ואז מחברים אותו למכונת Linux. מגדירים את האות בצורה נכונה, LED5 דולקת.
כמו קודם, מציינים את המספר הסידורי של לוח ה-nRF52840:
עוברים למיקום של כלי שורת הפקודה של nRFx, ומעבירים את קובץ ה-hex של OpenThread CLI FTD ללוח nRF52840 באמצעות מספר הלוח הסידורי:
$ cd ~/nrfjprog $ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \ ~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset
נורית LED5 תכבה לזמן קצר במהלך ההבהוב. אם הפעולה תושלם, יופיע הפלט הבא:
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
פקודת הבהוב.
אם הפעולה תתבצע בהצלחה, נורית LED1, LED2 או LED3 תידלק בכל לוח. יכול להיות שגם תראו שהנורית הדולקת עוברת מ-3 ל-2 (או מ-2 ל-1) זמן קצר אחרי שהיא מהבהבת (התכונה 'שינוי תפקיד המכשיר').
12. פונקציונליות האפליקציה
עכשיו כל שלוש הלוחות של nRF52840 צריכים לפעול ולהריץ את אפליקציית OpenThread שלנו. כפי שצוין קודם, לאפליקציה הזו יש שתי תכונות עיקריות.
אינדיקטורים של תפקידי המכשיר
נורית ה-LED דולקת בכל לוח משקפת את התפקיד הנוכחי של צומת השרשור:
- LED1 = מוביל/ה
- LED2 = נתב
- LED3 = מכשיר קצה
כשהתפקיד משתנה, גם ה-LED נדלק. השינויים האלה אמורים להופיע בלוח אחד או שניים תוך 20 שניות ממועד ההפעלה של כל מכשיר.
UDP Multicast
כשמקישים על לחצן 1 בלוח, נשלחת הודעה ב-UDP לכתובת ה-multicast המקומית של רשת הצמתים, שכוללת את כל הצמתים האחרים ברשת Thread. בתגובה לקבלת ההודעה הזו, הנורית LED4 בכל הלוחות האחרים נדלקת או כבויה. הנורית LED4 נשארת דלוקה או כבויה בכל לוח עד שהיא מקבלת הודעת UDP נוספת.
13. הדגמה: מעקב אחרי שינויים בתפקידים של מכשירים
המכשירים שעדכנתם הם סוג ספציפי של מכשיר Thread מלא (FTD) שנקרא מכשיר קצה מתאים לנתב (REED). המשמעות היא שהם יכולים לפעול כנתב או כמכשיר קצה, ויכולים לשדרג את עצמם ממכשיר קצה לנתב.
השרשור יכול לתמוך ב-32 נתב, אבל הוא מנסה לשמור על מספר הנתב בין 16 ל-23. אם מכשיר REED מצורף כמכשיר קצה ומספר הנתב הוא פחות מ-16, הוא יתקדם באופן אוטומטי לנתב. השינוי אמור להתרחש בזמן אקראי במהלך מספר השניות שהגדרתם לערך otThreadSetRouterSelectionJitter
באפליקציה (20 שניות).
לכל רשת Thread יש גם 'מנהיג', שהוא נתב שאחראי לניהול קבוצת הנתבנים ברשת Thread. אחרי 20 שניות, כשכל המכשירים מופעלים, אחד מהם אמור להיות 'מנהיג' (נורית LED1 דולקת) ושני המכשירים האחרים אמורים להיות 'נתבים' (נורית LED2 דולקת).
הסרת הבכיר בארגון
אם המכשיר המוביל יוסר מרשת Thread, נתב אחר יוכל להפוך למכשיר מוביל כדי להבטיח שלרשת עדיין יהיה מכשיר מוביל.
משביתים את לוח הבקרה (זה שבו נורית LED1 דולקת) באמצעות המתג Power. ממתינים כ-20 שניות. באחת משתי הלוחות הנותרים, הנורה LED2 (נתב) תיכבה והנורה LED1 (מנהיג) תידלק. המכשיר הזה הוא עכשיו המכשיר המוביל ברשת Thread.
מפעילים מחדש את לוח הבקרה המקורי. הוא אמור להצטרף שוב לרשת Thread באופן אוטומטי כמכשיר קצה (נורית LED3 דולקת). תוך 20 שניות (התנודות בבחירת הנתב) הוא מוגדר לנתב (נורית LED2 דולקת).
איפוס הלוחות
מכבים את כל שלוש הלוחות, מפעילים אותם מחדש ומתבוננים בנורות ה-LED. הלוח הראשון שהופעל אמור להתחיל בתפקיד 'מנהיג' (נורית LED1 דולקת) – הנתב הראשון ברשת Thread הופך באופן אוטומטי למנהיג.
שתי הלוחות האחרים מתחברים לרשת בהתחלה כמכשירי קצה (נורית LED3 דולקת), אבל הם אמורים לשדרג את עצמם לנתב (נורית LED2 דולקת) תוך 20 שניות.
מחיצות רשת
אם הלוחות לא מקבלים מספיק חשמל או שהחיבור הרדיו ביניהם חלש, רשת Thread עשויה להתחלק למחיצות ויכול להיות שיופיע יותר ממכשיר אחד בתור 'מנהיג'.
השרשור מתקן את עצמו, כך שבסופו של דבר המחיצות אמורות להתמזג חזרה למחיצה אחת עם מנהיג אחד.
14. הדגמה: שליחת UDP multicast
אם ממשיכים מהתרגיל הקודם, נורית LED4 לא אמורה להאיר באף מכשיר.
בוחרים לוח כלשהו ולוחצים על לחצן 1. נורית LED4 בכל הלוחות האחרים ברשת Thread שבה פועלת האפליקציה אמורה להחליף את המצב שלה. אם ממשיכים מהתרגיל הקודם, הן אמורות לפעול עכשיו.
לוחצים שוב על לחצן 1 לאותה לוח. נורית LED4 בכל הלוחות האחרים אמורה להבהב שוב.
לוחצים על Button1 בלוח אחר ומתבוננים בהתנהגות של LED4 בלוחות האחרים. לוחצים על לחצן 1 באחת מהלוחות שבהם נורית LED4 דולקת כרגע. נורית LED4 נשארת דולקת בלוח הזה, אבל עוברת למצב כבוי/דלוק בלוחות האחרים.
מחיצות רשת
אם הלוחות שלכם מחולקים למחיצות ויש בהם יותר ממנהיג אחד, התוצאה של הודעת ה-multicast תהיה שונה בין הלוחות. אם לוחצים על Button1 בלוח שבו בוצע חלוקה למחיצות (ולכן הוא המכשיר היחיד ברשת Thread המחולקת), נורית LED4 בלוחות האחרים לא תידלק בתגובה. אם זה קורה, צריך לאפס את הלוחות. במקרה הטוב, הם ייצרו רשת Thread אחת והעברת הודעות ב-UDP אמורה לפעול כראוי.
15. מעולה!
יצרתם אפליקציה שמשתמשת בממשקי API של OpenThread!
עכשיו אתם יודעים:
- איך לתכנות את הלחצנים והנורות הלד בלוחות הפיתוח של Nordic nRF52840
- איך משתמשים בממשקי API נפוצים של OpenThread ובכיתה
otInstance
- איך לעקוב אחרי שינויים במצב של OpenThread ולהגיב להם
- איך שולחים הודעות UDP לכל המכשירים ברשת Thread
- איך משנים קובצי Makefile
השלבים הבאים
אחרי שנסיים את הקודלאב הזה, נסו את התרגילים הבאים:
- שינוי מודול ה-GPIO כך שישתמש בסיכות GPIO במקום בנורות ה-LED המובנות, וחיבור של נורות RGB חיצוניות שמשנות את הצבע בהתאם לתפקיד הנתב
- הוספת תמיכה ב-GPIO לפלטפורמה לדוגמה אחרת
- במקום להשתמש ב-multicast כדי לשלוח הודעות ping לכל המכשירים בלחיצה על לחצן, אפשר להשתמש ב-Router/Leader API כדי לאתר מכשיר ספציפי ולשלוח אליו הודעות ping.
- מחברים את רשת העיגול לאינטרנט באמצעות נתב גבול של OpenThread ומפעילים את ה-LED באמצעות שידור מרובה מחוץ לרשת Thread
קריאה נוספת
באתר openthread.io וב-GitHub תוכלו למצוא מגוון משאבים של OpenThread, כולל:
- פלטפורמות נתמכות – כאן תוכלו למצוא את כל הפלטפורמות שתומכות ב-OpenThread
- פיתוח OpenThread – פרטים נוספים על פיתוח והגדרה של OpenThread
- מבוא ל-Thread – מקור מידע מצוין על המושגים של Thread
מקור: