פיתוח עם ממשקי API של OpenThread

1. מבוא

26b7f4f6b3ea0700.png

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

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

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

2a6db2e258c32237.png

מה תלמדו

  • איך לתכנת את הלחצנים ונורות ה-LED בלוחות פיתוח nRF52840 נורדיים
  • איך משתמשים בממשקי OpenThread API נפוצים ובמחלקה otInstance
  • איך לעקוב אחר שינויים במצב OpenThread ולהגיב עליהם
  • איך שולחים הודעות UDP לכל המכשירים ברשת בפרוטוקול Thread
  • איך משנים את קובצי ה-Makefile

למה תזדקק?

חומרה:

  • 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 באמצעות ה-nRF52840 Boards ו-OpenThread, שבעזרתם:

  • פירוט כל התוכנות שדרושות ליצירה ולפלאש
  • מלמד איך לפתח את OpenThread ולהריץ אותו בלוחות nRF52840 הנורדיים
  • הדגמה של היסודות של רשת פרוטוקול Thread

ב-Codelab הזה לא נדרשת הגדרה של הסביבה שנדרשת כדי לפתח את OpenThread ול-Flash את הלוחות, אלא רק הוראות בסיסיות להבהוב הלוחות. ההנחה היא שכבר השלמתם את התהליך Build a Thread Network Codelab.

מחשב Linux

ה-Codelab הזה נועד להשתמש במחשב Linux מבוסס-i386 או x86 כדי להריץ את כל לוחות הפיתוח של Thread. כל השלבים נבדקו ב-Ubuntu 14.04.5 LTS (Trusty Tahr).

לוחות nRF52840 של מוליכים למחצה נורדיים

ב-Codelab הזה נעשה שימוש בשלושה לוחות nRF52840 PDK.

a6693da3ce213856.png

התקנת התוכנה

כדי לבנות ולהריץ את OpenThread, עליכם להתקין את SEGGER J-Link, את כלי שורת הפקודה nRF5x, את ARM GNU Toolchain וחבילות Linux שונות. אם השלמתם את Build a Thread Network Codelab לפי הנדרש, כל מה שצריך כבר מותקן אצלכם. אם לא, יש להשלים את ביצוע ה-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
  • ניהול פרטי כניסה לרשתות, עם תפקידי 'נציב' ו'הצטרפות'
  • ניהול נתבים לגבול
  • תכונות משופרות כמו פיקוח על ילדים וזיהוי Jams

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

5. הטמעת הפשטה של פלטפורמת GPIO

בשלב הקודם, עברנו על ההצהרות על פונקציות ספציפיות לפלטפורמה ב-./openthread/examples/platforms/openthread-system.h שאפשר להשתמש בהן ל-GPIO. כדי לגשת ללחצנים ולנורות LED בלוחות הפיתוח nRF52840, צריך להטמיע את הפונקציות האלה בפלטפורמה nRF52840. בקוד הזה אתם תוסיפו פונקציות שמבצעות את הפעולות הבאות:

  • אתחול פינים ומצבים של GPIO
  • שליטה במתח דרך סיכה
  • הפעלת הפרעות GPIO ורישום קריאה חוזרת (callback)

בספרייה ./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 זמין במרכז המידע על מוליכים למחצה של 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 כשהמכשיר מקבל הודעת Multicast 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;
    }
}

פעולה: הוספת פונקציות לאתחול ולעיבוד של לחיצות על לחצנים.

הפונקציה הראשונה מפעילה את הלוח בלחיצה על לחצן, והפונקציה השנייה שולחת את הודעת ה-Multicast 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);

פעולה: הוספת רישום קריאה חוזרת לפונקציית המטפל בשינויי מצב.

בפונקציה 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: שימוש בריבוי שידורים כדי להפעיל נורית LED

באפליקציה שלנו, אנחנו רוצים גם לשלוח הודעות UDP לכל המכשירים האחרים ברשת כאשר לוחצים על כפתור1 בלוח אחד. כדי לאשר שההודעה התקבלה, נעביר את LED4 ללוחות האחרים בתגובה.

כדי להפעיל את הפונקציונליות הזו, האפליקציה צריכה:

  • אתחול חיבור UDP בזמן ההפעלה
  • יכולת לשלוח הודעת UDP לכתובת Multicast מקומית ברשת
  • טיפול בהודעות UDP נכנסות
  • החלפת מצב של LED4 בתגובה להודעות UDP נכנסות

פותחים את הקובץ ./openthread/examples/apps/cli/main.c בכלי לעריכת טקסט המועדף עליכם.

./openthread/examples/apps/cli/main.c

פעולה: הוספת כותרת כוללת.

בקטע 'כולל' בחלק העליון של הקובץ main.c, מוסיפים את קובצי הכותרות של ה-API שנחוצים לתכונת Multicast 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, אחרי קטע ההכללה ולפני כל הצהרת #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 המקומית. הודעות שיישלחו לכתובת הזו יישלחו לכל המכשירים עם פרוטוקול Thread מלא ברשת. למידע נוסף על תמיכה ב-multicast ב-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);

פעולה: הטמעה של הגורם המטפל בשיבושים בלחצן.

ב-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 בחומר העזר בנושא API של UDP.

פעולה: הטמעת הודעות 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

פעולה: הוספת כותרת עליונה.

בקטע 'הכללות' שבחלק העליון של הקובץ main.c, מוסיפים את קובץ הכותרת של ה-API שצריך להגדיר את רשת ה-Thread:

#include <openthread/dataset_ftd.h>

פעולה: הוספת הצהרת (declare) פונקציה להגדרת תצורת הרשת.

צריך להוסיף את ההצהרה הזו אל 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 (מכשיר פרוטוקול Thread מלא). מידע נוסף על כך בשלב הבא.

9. API: פונקציות מוגבלות

חלק מממשקי ה-API של OpenThread משנים הגדרות שצריך לשנות רק למטרות הדגמה או בדיקה. אין להשתמש בממשקי ה-API האלה בפריסת ייצור של אפליקציה באמצעות OpenThread

לדוגמה, הפונקציה otThreadSetRouterSelectionJitter מתאימה את הזמן (בשניות) שנדרש למכשיר קצה כדי לקדם את עצמו לנתב. ברירת המחדל לערך הזה היא 120, לפי מפרט פרוטוקול ה-Thread. כדי שיהיה לכם קל להשתמש ב-Codelab הזה, אנחנו נשנה אותו ל-20 כדי שלא יהיה צורך להמתין הרבה זמן עד שצומת של Thread ישנה את התפקיד.

הערה: מכשירי 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. Cביצוע עדכונים

לפני בניית האפליקציה, יש כמה עדכונים קלים שדרושים לשלושה קובצי CMake. מערכת ה-build משתמשת בהם כדי להדר ולקשר את האפליקציה.

./third_party/NordicSemiconductor/CMakeLists.txt

עכשיו צריך להוסיף כמה סימונים ל-NordicSemicondocor 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 לקובץ CMakeLists.txt של NordicSemicondoc, כדי שייכלל ב-build של הספרייה של מנהלי ההתקנים הנורדיים.

פעולה: מוסיפים את מנהל התקן ה-gpio לקובץ CMakeLists.txt NordicSemicondoc.

פותחים את ./third_party/NordicSemiconductor/CMakeLists.txt בכלי לעריכת טקסט מועדף ומוסיפים את הקובץ לקטע COMMON_SOURCES.

...

set(COMMON_SOURCES
  ...
  nrfx/drivers/src/nrfx_gpiote.c
  ...
)
...

11. הגדרת המכשירים

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

פיתוח פרוטוקול OpenThread

בניית הקבצים הבינאריים של 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:

$ 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 פועלת.

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

נורית ה-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.

חוזרים על ההודעה "Flash the boards" (הבהוב על הלוחות) בשביל שני הלוחות האחרים. כל לוח צריך להיות מחובר למחשב Linux באותו אופן, והפקודה להבהב זהה, למעט המספר הסידורי של הלוח. חשוב להשתמש במספר הסידורי הייחודי של כל לוח

nrfjprog פקודת מהבהבת.

אם הפעולה בוצעה ללא שגיאות, נורות LED1, LED2 או LED3 יידלקו בכל לוח. יכול להיות אפילו שתראו את מתג ה-LED המואר מ-3 ל-2 (או 2 ל-1) זמן קצר אחרי ההבהוב (התכונה של שינוי התפקיד במכשיר).

12. תכונות האפליקציה

כל שלושת הלוחות מסוג nRF52840 אמורים לפעול עכשיו ולהפעיל את אפליקציית OpenThread שלנו. כפי שפירטנו קודם, לאפליקציה הזו יש שתי תכונות עיקריות.

אינדיקטורים של תפקידי מכשיר

נורית ה-LED הדולקת בכל לוח משקפת את התפקיד הנוכחי של צומת ה-thread:

  • LED1 = מנהיג
  • LED2 = נתב
  • LED3 = מכשיר קצה

ככל שהתפקיד משתנה, כך גם נורית ה-LED הדולקת. השינויים האלה היו אמורים להופיע על לוח או שניים כבר 20 שניות אחרי כל הפעלה של כל מכשיר.

Multicast UDP

כשלוחצים על כפתור 1 על לוח, הודעת UDP נשלחת לכתובת Multicast מקומית ברשת, שכוללת את כל הצמתים האחרים ברשת פרוטוקול Thread. בתגובה לקבלת ההודעה, LED4 בכל שאר הלוחות עובר למצב מופעל או כבוי. נורית ה-LED4 נשארת מופעלת או כבויה עבור כל לוח עד שהיא מקבלת הודעת UDP נוספת.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. הדגמה: מעקב אחר שינויים בתפקיד של מכשיר

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

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

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

4e1e885861a66570.png

הסרת הבכיר בארגון

אם ה-Leader מוסר מרשת ה-thread, נתב אחר מציג את עצמו כמנהיג כדי לוודא שלרשת עדיין יהיה מנהיג.

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

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

איפוס הלוחות

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

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

מחיצות רשת

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

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

14. הדגמה: שליחת Multicast ב-UDP

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

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

f186a2618fdbe3fd.png

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

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

f5865ccb8ab7aa34.png

מחיצות רשת

אם הלוחות מחולקים למחיצות ויש יותר ממנהיג אחד ביניהם, התוצאה של הודעת ה-Multicast שונה בין הלוחות. אם לוחצים על לחצן1 בלוח שחולק למחיצות (ולכן הוא החבר היחיד ברשת פרוטוקול Thread המחולקת למחיצות), נורית LED4 בלוחות האחרים לא תידלק בתגובה. במקרה כזה, צריך לאפס את הלוחות. רצוי שהם יבצעו רפורמציה רק ברשת פרוטוקול Thread אחת, והודעות ה-UDP אמורות לפעול בצורה תקינה.

15. מעולה!

יצרת אפליקציה שמשתמשת בממשקי API של OpenThread!

עכשיו אתם יודעים:

  • איך לתכנת את הלחצנים ונורות ה-LED בלוחות פיתוח nRF52840 נורדיים
  • איך משתמשים בממשקי OpenThread API נפוצים ובמחלקה otInstance
  • איך לעקוב אחר שינויים במצב OpenThread ולהגיב עליהם
  • איך שולחים הודעות UDP לכל המכשירים ברשת בפרוטוקול Thread
  • איך משנים את קובצי ה-Makefile

השלבים הבאים

בהמשך ל-Codelab הזה, כדאי לנסות את התרגילים הבאים:

  • יש לשנות את מודול ה-GPIO כך שיעשה שימוש בסיכות GPIO במקום נורות LED שמובנות, ולחבר נורות LED חיצוניות של RGB שמשנות את הצבע בהתאם לתפקיד הנתב
  • הוספת תמיכה ב-GPIO לפלטפורמה אחרת לדוגמה
  • במקום לשלוח פינג לכל המכשירים בלחיצה על לחצן, כדאי להשתמש ב-Router/Leader API כדי לאתר מכשיר ספציפי ולבצע בו פינג
  • מחברים את רשת האריג לאינטרנט באמצעות נתב Border Border (נתב גבול פתוח)

קריאה נוספת

ב-openthread.io וב-GitHub יש מגוון משאבים ב-OpenThread, כולל:

הפניה: