פיתוח עם ממשקי 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

מה תלמדו

  • איך מתכנתים את הלחצנים ואת נוריות ה-LED בלוחות פיתוח של Nordic nRF52840
  • איך משתמשים בממשקי OpenThread API נפוצים ובמחלקת 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 Build a Thread Network with nRF52840 Boards and OpenThread (יצירת רשת Thread באמצעות לוחות nRF52840 ו-OpenThread), שכולל:

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

משכפלים את ה-repo של הדוגמאות של 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
  • ניהול של פרטי הכניסה לרשת, יחד עם התפקידים Commissioner ו-Joiner
  • ניהול נתבי גבולות
  • תכונות משופרות כמו פיקוח על ילדים וזיהוי שיבוש

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

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 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);
}

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

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

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

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

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

#include <string.h>

#include <openthread/message.h>
#include <openthread/udp.h>

#include "utils/code_utils.h"

הכותרת code_utils.h משמשת לפקודות המאקרו otEXPECT ו-otEXPECT_ACTION שמאמתות תנאים בזמן הריצה ומטפלות בשגיאות בצורה חלקה.

פעולה: הוספת הגדרות וקבועים:

בקובץ main.c, אחרי הקטע includes ולפני כל הצהרות #if, מוסיפים קבועים והגדרות ספציפיים ל-UDP:

#define UDP_PORT 1212

static const char UDP_DEST_ADDR[] = "ff03::1";
static const char UDP_PAYLOAD[]   = "Hello OpenThread World!";

ff03::1 היא כתובת מולטיקאסט מקומית ברשת. כל ההודעות שיישלחו לכתובת הזו יישלחו לכל המכשירים עם שרשור מלא ברשת. מידע נוסף על תמיכה בשידור מרובה משתתפים ב-OpenThread זמין במאמר בנושא שידור מרובה משתתפים ב-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 ID = 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. המערכת משתמשת בהם כדי לקמפל את האפליקציה ולקשר אותה.

./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 לקובץ CMakeLists.txt של NordicSemiconductor, כדי שהוא ייכלל ב-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

יוצרים את קובצי ה-binary של 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 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 תידלק בכל לוח. יכול להיות שאחרי ההבהוב תראו שהנורית דולקת במצב 2 במקום 3 (או במצב 1 במקום 2) (התכונה של שינוי תפקיד המכשיר).

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

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

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

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

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

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

UDP Multicast

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

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

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

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

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

איפוס הלוחות

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

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

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

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

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

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

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

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

f186a2618fdbe3fd.png

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

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

f5865ccb8ab7aa34.png

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

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

15. מעולה!

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

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

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

השלבים הבאים

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

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

קריאה נוספת

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

הפניה: