פיתוח באמצעות ממשקי API של OpenThread

1. מבוא

26b7f4f6b3ea0700.png

Openthread שהושק על ידי Nest הוא הטמעת קוד פתוח של פרוטוקול הרשת Thread®. Nest השיקה את OpenThread כדי להפוך את הטכנולוגיה שבה נעשה שימוש במוצרי Nest לזמינה באופן נרחב למפתחים, כדי להאיץ את פיתוח המוצרים לבית החכם.

המפרט של Thread מגדיר פרוטוקול תקשורת אמין, מאובטח וחזק עם חיבור Bluetooth למכשירים שונים, לשימוש באפליקציות ביתיות. Openthread מיישם את כל שכבות הרשת של Thread, כולל IPv6, 6LoWPAN, IEEE 802.15.4 עם אבטחת MAC, תשתית לקישור רשת וניתוב רשת.

ב-Codelab הזה תשתמשו בממשקי API של OpenThread כדי להפעיל רשת של Thread, לעקוב אחרי השינויים בתפקידי המכשיר ולהגיב עליהם, לשלוח הודעות UDP וגם לקשר את הפעולות האלה ללחצנים ולנורות LED בחומרה אמיתית.

2a6db2e258c32237.png

מה תלמדו

  • איך לתכנת את הלחצנים ואת נורות ה-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 לוחות.

a6693da3ce213856.png

התקנת התוכנה

כדי ליצור ולהפעיל את 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.

20a3b4b480356447.png

כמו בעבר, יש לשים לב למספר הסידורי של לוח nRF52840:

c00d519ebec7e5f0.jpeg

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

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. הדגמה: צפייה בשינויים בתפקיד המכשיר

המכשירים מהבהבים הם סוג ספציפי של מכשיר עם חוט מלא (FTD) שנקרא מכשיר קצה מתאים לנתב (REED). כלומר, הם יכולים לשמש כנתב או כמכשיר קצה ולקדם את עצמם ממכשיר קצה לנתב.

החוט יכול לתמוך בעד 32 נתבים, אבל הוא מנסה לשמור על מספר הנתבים בין 16 ל-23. אם REED מתחבר כמכשיר קצה ומספר הנתבים נמוך מ-16, הוא מקדם את עצמו באופן אוטומטי לנתב. השינוי הזה צריך להתבצע בזמן אקראי בתוך מספר השניות שהוגדר לערך otThreadSetRouterSelectionJitter באפליקציה (20 שניות).

לכל רשת Thread יש גם Leader, נתב שאחראי על ניהול הנתבים ברשת Thread. כשכל המכשירים פועלים, אחרי 20 שניות אחד מהם צריך להיות מוביל (LED1 מופעל) ושני המכשירים האחרים צריכים להיות נתבים (LED2 פועלים).

4e1e885861a66570.png

הסרת המנהיג

אם מוביל יוסר מרשת השרשור, נתב אחר מקדם את עצמו ל'מנהל' כדי להבטיח שלרשת עדיין יהיה 'מנהיג'.

מכבים את לוח ה-Leader (הלוח המואר באמצעות LED1) באמצעות מתג ההפעלה. ממתינים כ-20 שניות. באחד משני הלוחות שנותרו, LED2 (Router) יכובה ו-LED1 (Leader) תופעל. המכשיר הזה מוגדר עכשיו כמוביל רשת הרשת.

4c57c87adb40e0e3.png

הפעלה מחדש של לוח ה-Leader המקורי. הוא אמור להתחבר מחדש לרשת השרשורים כמכשיר קצה (LED3 מואר). תוך 20 שניות (הרעידות לבחירת נתב) הן מקודמות לנתב (LED2 מואר).

5f40afca2dcc4b5b.png

איפוס הלוחות

מכבים את כל שלושת הלוחות ואז מפעילים אותם מחדש ומעיינים בנורות ה-LED. הלוח הראשון שנוצר אמור להתחיל בתפקיד 'מנהיג' (LED1 מואר) — הנתב הראשון ברשת Thread הופך ל'מנהיג' באופן אוטומטי.

שני הלוחות האחרים מתחברים בתחילה לרשת כשמכשירי קצה (LED3 מוארים), אך אמורים לקדם את עצמם לנתבים (LED2 מוארת) בתוך 20 שניות.

מחיצות ברשת

אם הלוחות לא מקבלים מספיק חשמל, או שחיבור הרדיו ביניהם חלש, יכול להיות שרשת ה-Threads תפוצל למחיצות, ויכול להיות יותר ממכשיר אחד יופיע כ-Lead.

השרשור מרפא את עצמו, לכן בסופו של דבר מחיצות אמורות להתחלק למחיצה אחת עם מוביל אחד.

14. הדגמה: שליחת UDP מולטיקאסט

אם ממשיכים מהתרגיל הקודם, אין להדליק את LED4 באף מכשיר.

בוחרים לוח כלשהו ולוחצים על 'לחצן1'. נורת LED בכל שאר הלוחות ברשת ה-Thread שמריצה את האפליקציה צריכה להחליף את המצב שלהם. אם ממשיכים מהתרגיל הקודם, עכשיו הם מופעלים.

f186a2618fdbe3fd.png

יש ללחוץ שוב על לחצן 1 עבור אותו לוח. נורת ה-LED4 בכל שאר הלוחות אמורה לחזור לפעול.

יש ללחוץ על לחצן 1 בלוח אחר ולראות איך מתבצעת הפעלה של LED4 בלוחים האחרים. יש ללחוץ על לחצן 1 באחד מהלוחות שבהם מופעל LED4. נורת ה-LED עדיין פועלת בלוח העריכה, אבל המתגים האחרים מופעלים.

f5865ccb8ab7aa34.png

מחיצות ברשת

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

עיון: