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

1. מבוא

26b7f4f6b3ea0700.png

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

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

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

2a6db2e258c32237.png

מה תלמדו

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

הדרישות

חומרה:

  • ‫3 לוחות פיתוח Nordic Semiconductor nRF52840
  • ‫3 כבלים מ-USB למיקרו USB לחיבור הלוחות
  • מחשב Linux עם לפחות 3 יציאות USB

תוכנה:

  • GNU Toolchain
  • כלי שורת הפקודה Nordic nRF5x
  • תוכנת Segger J-Link
  • OpenThread
  • Git

למעט מקרים שבהם צוין אחרת, התוכן של ה-Codelab הזה מורשה תחת רישיון Creative Commons שמותנה בייחוס 3.0, ודוגמאות הקוד מורשות תחת רישיון Apache 2.0.

2. תחילת העבודה

משלימים את ה-Codelab בנושא חומרה

לפני שמתחילים את שיעור ה-Codelab הזה, כדאי להשלים את שיעור ה-Codelab בנושא יצירת רשת Thread באמצעות לוחות nRF52840 ו-OpenThread, שכולל:

  • פירוט כל התוכנות שצריך כדי לבצע build ו-flashing
  • איך ליצור OpenThread ולהפעיל אותו בלוחות Nordic nRF52840
  • הדגמה של היסודות של רשת Thread

ב-Codelab הזה לא מפורטות ההגדרות הנדרשות בסביבה כדי ליצור OpenThread ולהפעיל את הלוחות – רק הוראות בסיסיות להפעלת הלוחות. אנחנו מניחים שכבר השלמתם את Codelab בנושא יצירת רשת Thread.

מחשב Linux

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

לוחות Nordic Semiconductor nRF52840

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

a6693da3ce213856.png

התקנת תוכנה

כדי ליצור ולהפעיל את OpenThread, צריך להתקין את SEGGER J-Link, את כלי שורת הפקודה nRF5x, את ARM GNU Toolchain וחבילות שונות של Linux. אם השלמתם את Codelab בנושא בניית רשת 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 Basics

ממשקי ה-API הציבוריים של OpenThread נמצאים בכתובת ./openthread/include/openthread במאגר OpenThread. ממשקי ה-API האלה מספקים גישה למגוון תכונות ופונקציות של OpenThread ברמת Thread וברמת הפלטפורמה, לשימוש באפליקציות שלכם:

  • מידע על מופע 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 הוא מבנה שתשתמשו בו לעיתים קרובות בעבודה עם ממשקי 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 שבהן נשתמש כדי להתחבר ללחצנים ולנוריות ה-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 (קובץ חדש)

ACTION: Add defines.

ההגדרות האלה משמשות כהפשטות בין ערכים ספציפיים ל-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.

פעולה: הוספת קבצים לכותרת העליונה

בשלב הבא, מוסיפים את קובצי הכותרות שצריך לפונקציונליות של 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 היא הקריאה החוזרת שנרשמת כשפונקציונליות הלחיצה על הכפתור מאותחלת (בהמשך הקובץ הזה).

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

ACTION: Add a function to set the mode of an 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 של ה-multicast כשלוחצים על לחצן 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 שונות יידלקו בהתאם לתפקיד המכשיר. נבדוק את התפקידים הבאים: Leader, ‏ Router, ‏ End Device. אפשר להקצות אותם לנוריות ה-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>

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

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

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

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

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

פעולה: הוספת קבצים לכותרת העליונה

בקטע includes בחלק העליון של הקובץ 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, אחרי הקטע 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 היא כתובת מולטיקאסט מקומית ברשת. כל ההודעות שיישלחו לכתובת הזו יישלחו לכל המכשירים עם Thread מלא ברשת. מידע נוסף על תמיכה ב-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;

פעולה: מוסיפים קריאות לאתחול של נוריות ה-LED וכפתור ה-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, בודקת אם נלחץ על הכפתור, ואם כן, קוראת ל-handler (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 enumeration במאמר UDP API Reference.

פעולה: הטמעה של העברת הודעות ב-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, אחרי הכללות של הכותרת ולפני כל הצהרות #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
  • Network Key = 1234C0DE1AB51234C0DE1AB51234C0DE
  • שם הרשת = OTCodelab

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

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

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

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

הערה: מכשירי MTD לא הופכים לנתבים, ותמיכה בפונקציה כמו otThreadSetRouterSelectionJitter לא נכללת בגרסת MTD. בהמשך נצטרך לציין את האפשרות -DOT_MTD=OFF של CMake, אחרת נתקל בכשל בבנייה.

אפשר לוודא זאת באמצעות הגדרת הפונקציה 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

עכשיו מוסיפים כמה דגלים ל-NordicSemiconductor 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 של ספריית הדרייברים של Nordic.

ACTION: Add the gpio driver to the NordicSemiconductor CMakeLists.txt file.

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

...

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

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

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

פיתוח OpenThread

יוצרים את קובצי ה-FTD הבינאריים של OpenThread לפלטפורמת nRF52840.

$ cd ~/ot-nrf528xx
$ ./script/build nrf52840 UART_trans -DOT_MTD=OFF -DOT_APP_RCP=OFF -DOT_RCP=OFF

עוברים לספרייה עם הקובץ הבינארי של OpenThread FTD CLI וממירים אותו לפורמט הקסדצימלי באמצעות ARM Embedded Toolchain:

$ 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 Command Line Tools ומפעילים את קובץ ה-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 באותה דרך, והפקודה לצריבת ROM‏ (flash) זהה, למעט המספר הסידורי של הלוח. חשוב להשתמש במספר הסידורי הייחודי של כל לוח ב

nrfjprog פקודה להבהוב.

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

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

שלושת לוחות ה-nRF52840 אמורים להיות מופעלים ולהריץ את אפליקציית OpenThread שלנו. כמו שצוין קודם, לאפליקציה הזו יש שתי תכונות עיקריות.

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

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

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

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

UDP Multicast

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

203dd094acca1f97.png9bbd96d9b1c63504.png

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

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

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

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

4e1e885861a66570.png

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

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

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

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

איפוס הלוחות

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

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

חלוקת משאבי רשת למחיצות

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

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

14. הדגמה: שליחת שידור מרובה משתתפים ב-UDP

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

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

f186a2618fdbe3fd.png

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

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

f5865ccb8ab7aa34.png

חלוקת משאבי רשת למחיצות

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

15. מעולה!

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

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

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

השלבים הבאים

אפשר לנסות את התרגילים הבאים על סמך ה-Codelab הזה:

  • משנים את מודול ה-GPIO כך שישתמש בפיני GPIO במקום בנוריות ה-LED המובנות, ומחברים נוריות LED חיצוניות מסוג RGB שמשנות את הצבע שלהן בהתאם לתפקיד הנתב
  • הוספת תמיכה ב-GPIO לפלטפורמה אחרת לדוגמה
  • במקום להשתמש ב-multicast כדי לבצע ping לכל המכשירים בלחיצה על לחצן, אפשר להשתמש ב-Router/Leader API כדי לאתר מכשיר ספציפי ולבצע לו ping.
  • כדי להדליק את נוריות ה-LED, צריך לחבר את רשת האריג לאינטרנט באמצעות נתב גבולות OpenThread ולשלוח את הנתונים בשיטת Multicast מחוץ לרשת Thread.

קריאה נוספת

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

הפניה: