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

1. מבוא

26b7f4f6b3ea0700.png

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

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

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

2a6db2e258c32237.png

מה תלמדו

  • איך לתכנת את הלחצנים ואת נורות ה-LED בלוחות פיתוח בסגנון נורדי nRF52840?
  • איך להשתמש בממשקי API נפוצים של Openthread ובכיתת otInstance
  • איך לעקוב אחר השינויים במצב OpenThread ולהגיב להם
  • איך שולחים הודעות UDP לכל המכשירים ברשת שרשור
  • איך משנים את MakeFiles

מה תצטרך להכין

חומרה:

  • 3 לוחות Semiconector nRF55840 נורדיים
  • 3 כבלי USB למיקרו USB לחיבור של הלוחות
  • מכונה Linux עם 3 יציאות USB לפחות

תוכנה:

  • ארגז כלים של GNU
  • כלי שורת פקודה נורדית מסוג nRF5x
  • תוכנת J-Ger של Digger
  • OpenThread
  • Git

אלא אם צוין אחרת, התוכן של Codelab זה מאושר בכפוף לרישיון Creative Commons Attribution 3.0 ודוגמאות הקוד הן ברישיון של רישיון Apache 2.0.

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

השלמת החומרה של החומרה

לפני תחילת השימוש ב-Codelab הזה, עליכם להשלים את ה-Build Network Thread Network עם nRF52840 Board and Openthread (קוד רשת של Thread), ש:

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

אף אחת מהסביבות שהוגדרו לא נדרשת כדי לפתוח את OpenThread ולהבהב את הלוחות ב-Codelab הזה – רק הוראות בסיסיות לפלאש של לוחות ה-Jam. ההנחה היא שכבר השלמת את שיעור ה-Lab Lab Build a Thread Network.

מכונה Linux

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

לוחות NRF52840 נורדיים נורדיים

Codelab הזה משתמש בשלושה nRF52840 PDK לוחות.

a6693da3ce213856.png

התקנת התוכנה

כדי לבנות ולבהב את OpenThread, יש להתקין את SEGGER J-Link, כלי ה-nRF5x Command Line, ARM GNU Toolchain וחבילות Linux שונות. אם השלמת את ה-Lab Network Thread Code לפי הצורך, כבר צריך להתקין את כל מה שצריך. אם לא, יש להשלים את ה-Codelab הזה לפני שממשיכים ליצור ולבנות קובצי Openthread ללוחות פיתוח של nRF52840.

3. שכפול המאגר

OpenThread כולל קוד אפליקציה לדוגמה שאפשר להשתמש בו כנקודת התחלה ל-Codelab הזה.

שכפול ה-nroFic nRF528xx לדוגמה של Openthread

$ git clone --recursive https://github.com/openthread/ot-nrf528xx
$ cd ot-nrf528xx
$ ./script/bootstrap

4. יסודות ה-API של OpenThread

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

  • מידע על המופע והבקרה של OpenThread
  • שירותי אפליקציות כגון IPv6 , UDP ו-CoAP
  • ניהול פרטי כניסה לרשת, יחד עם תפקידי הנציבים והמצטרפים
  • ניהול נתבי גבולות
  • תכונות משופרות כמו 'פיקוח על ילדים' ו'זיהוי Jam'

מידע על קובצי עזר בכל ממשקי ה-API של OpenThread זמין בכתובת openthread.io/reference.

באמצעות API

כדי להשתמש ב-API, יש לכלול את קובץ הכותרת שלו באחד מהקבצים של האפליקציה. אחר כך קוראים לפונקציה הרצויה.

לדוגמה, האפליקציה לדוגמה של CLI הכלולה ב-OpenThread משתמשת בכותרות ה-API הבאות:

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

#include <openthread/config.h>
#include <openthread/cli.h>
#include <openthread/diag.h>
#include <openthread/tasklet.h>
#include <openthread/platform/logging.h>

המופע של OpenThread

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

לדוגמה, המופע של OpenThread מופעל בפונקציה main() של האפליקציה לדוגמה של CLI:

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

int main(int argc, char *argv[])
{
    otInstance *instance

...

#if OPENTHREAD_ENABLE_MULTIPLE_INSTANCES
    // Call to query the buffer size
    (void)otInstanceInit(NULL, &otInstanceBufferLength);

    // Call to allocate the buffer
    otInstanceBuffer = (uint8_t *)malloc(otInstanceBufferLength);
    assert(otInstanceBuffer);

    // Initialize OpenThread with the buffer
    instance = otInstanceInit(otInstanceBuffer, &otInstanceBufferLength);
#else
    instance = otInstanceInitSingle();
#endif

...

    return 0;
}

פונקציות ספציפיות לפלטפורמה

אם ברצונך להוסיף פונקציות ספציפיות לפלטפורמה לאחת מהאפליקציות לדוגמה הכלולות ב-OpenThread, תחילה צריך להצהיר על כך בכותרת ./openthread/examples/platforms/openthread-system.h, באמצעות מרחב השמות otSys לכל הפונקציות. לאחר מכן צריך להטמיע אותם בקובץ מקור ספציפי לפלטפורמה. באופן מופשט, אפשר להשתמש באותן כותרות של פונקציות בפלטפורמות לדוגמה אחרות.

לדוגמה, בפונקציות של GPIO שאנחנו משתמשים כדי לחבר את הלחצנים של nRF52840 ונורות ה-LED צריך להיות מוצהרות ב-openthread-system.h.

פותחים את הקובץ ./openthread/examples/platforms/openthread-system.h בעורך הטקסט המועדף עליכם.

./openthread/examples/platforms/openthread-system.h

פעולה: הוספת הצהרות של פונקציות GPIO ספציפיות לפלטפורמה.

צריך להוסיף את הצהרות הפונקציות האלה אחרי #include בכותרת openthread/instance.h:

/**
 * Init LED module.
 *
 */
void otSysLedInit(void);
void otSysLedSet(uint8_t aLed, bool aOn);
void otSysLedToggle(uint8_t aLed);

/**
* A callback will be called when GPIO interrupts occur.
*
*/
typedef void (*otSysButtonCallback)(otInstance *aInstance);
void otSysButtonInit(otSysButtonCallback aCallback);
void otSysButtonProcess(otInstance *aInstance);

אנחנו ניישם זאת בשלב הבא.

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

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

בשלב הבא, מוסיפים את הכותרת כך שתצטרכו להשתמש בפונקציונליות של 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"

פעולה: מוסיפים קריאות חוזרות (callback) ופונקציות ללא הפרעות עבור לחצן 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;
    }
}

פעולה: צריך להוסיף פונקציות כדי לאתחל ולעבד לחיצות על לחצנים.

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

/**
 * @brief Function to initialize the button.
 */
void otSysButtonInit(otSysButtonCallback aCallback)
{
    nrfx_gpiote_in_config_t in_config = NRFX_GPIOTE_CONFIG_IN_SENSE_LOTOHI(true);
    in_config.pull                    = NRF_GPIO_PIN_PULLUP;

    ret_code_t err_code;
    err_code = nrfx_gpiote_in_init(BUTTON_PIN, &in_config, in_pin1_handler);
    APP_ERROR_CHECK(err_code);

    sButtonHandler = aCallback;
    sButtonPressed = false;

    nrfx_gpiote_in_event_enable(BUTTON_PIN, true);
}

void otSysButtonProcess(otInstance *aInstance)
{
    if (sButtonPressed)
    {
        sButtonPressed = false;
        sButtonHandler(aInstance);
    }
}

פעולה: שמירה וסגירה של הקובץ gpio.c.

6. API: תגובה לשינויים בתפקידים במכשירים

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

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

כדי להפעיל את הפונקציונליות הזו, האפליקציה צריכה לדעת מתי תפקיד המכשיר השתנה ואיך להפעיל את נורת ה-LED הנכונה בתגובה. נעשה שימוש במופע של OpenThread בחלק הראשון, והפשטת פלטפורמת GPIO בקטע השני.

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

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

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

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

#include <openthread/instance.h>
#include <openthread/thread.h>
#include <openthread/thread_ftd.h>

פעולה: הוספת הצהרת פונקציית handler לשינוי מצב המופע של OpenThread.

יש להוסיף את ההצהרה הזו אל main.c, אחרי שהכותרת כוללת לפני ואחרי הצהרות #if. פונקציה זו תוגדר לאחר האפליקציה הראשית.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

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

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

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

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

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

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

#include <string.h>

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

#include "utils/code_utils.h"

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

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

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

#define UDP_PORT 1212

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

ff03::1 היא הכתובת של ריבוי החנויות המקומיות ברשת. הודעות שיישלחו לכתובת זו יישלחו לכל המכשירים המחוברים לרשת. מידע נוסף על תמיכה בריבוי שידורים ב-Openthread זמין במאמר Multicast ב-openthread.io.

פעולה: הוספת הצהרות של פונקציות.

בקובץ main.c, אחרי ההגדרה otTaskletsSignalPending ולפני הפונקציה main(), מוסיפים פונקציות ספציפיות ל-UDP, וכן משתנה סטטי שמייצג שקע UDP:

static void initUdp(otInstance *aInstance);
static void sendUdp(otInstance *aInstance);

static void handleButtonInterrupt(otInstance *aInstance);

void handleUdpReceive(void *aContext, otMessage *aMessage, 
                      const otMessageInfo *aMessageInfo);

static otUdpSocket sUdpSocket;

פעולה: הוספת שיחות לאתחול ה-LED והלחצן של GPIO.

בעוד main.c, יש להוסיף את הקריאות האלה לפונקציה אל הפונקציה main() אחרי השיחה אל otSetStateChangedCallback. הפונקציות האלה מפעילות את הסיכות של GPIO ו-GPIOTE ומגדירות handler של לחצן לטיפול באירועים של לחיצה על לחצנים.

/* init GPIO LEDs and button */
otSysLedInit();
otSysButtonInit(handleButtonInterrupt);

פעולה: הוספה של שיחת האתחול של ה-UDP.

ב-main.c, יש להוסיף את הפונקציה הזו לפונקציה main() אחרי השיחה שנוספה עכשיו ל-otSysButtonInit:

initUdp(instance);

שיחה זו מבטיחה ששקע UDP אותחל בהפעלה של האפליקציה. בלעדיו, המכשיר לא יוכל לשלוח או לקבל הודעות UDP.

פעולה: יש להוסיף שיחה לעיבוד האירוע של לחצן GPIO.

ב-main.c, יש להוסיף את הקריאה לפונקציה הזו לפונקציה main() אחרי הקריאה אל otSysProcessDrivers, בלולאה של while. פונקציית זו, המוצהרת ב-gpio.c, בודקת אם הלחצן נלחץ, ואם כן, מתקשר ל-handler (handleButtonInterrupt) שהוגדר בשלב שלמעלה.

otSysButtonProcess(instance);

פעולה: הטמעה של מנגנון טיפול בהפרעות.

ב-main.c, מוסיפים את ההטמעה של הפונקציה handleButtonInterrupt אחרי הפונקציה handleNetifStateChanged שהוספתם בשלב הקודם:

/**
 * Function to handle button push event
 */
void handleButtonInterrupt(otInstance *aInstance)
{
    sendUdp(aInstance);
}

פעולה: הטמעה של אתחול UDP.

ב-main.c, מוסיפים את ההטמעה של הפונקציה initUdp אחרי הפונקציה handleButtonInterrupt שהוספת:

/**
 * Initialize UDP socket
 */
void initUdp(otInstance *aInstance)
{
    otSockAddr  listenSockAddr;

    memset(&sUdpSocket, 0, sizeof(sUdpSocket));
    memset(&listenSockAddr, 0, sizeof(listenSockAddr));

    listenSockAddr.mPort    = UDP_PORT;

    otUdpOpen(aInstance, &sUdpSocket, handleUdpReceive, aInstance);
    otUdpBind(aInstance, &sUdpSocket, &listenSockAddr, OT_NETIF_THREAD);
}

UDP_PORT היא היציאה שהגדרת קודם לכן (1212). הפונקציה otUdpOpen פותחת את השקע ומתעדת פונקציית קריאה חוזרת (handleUdpReceive) מתי מתקבלת הודעת UDP. otUdpBind מקשר את השקע לממשק הרשת של השרשור על ידי העברה של OT_NETIF_THREAD. לאפשרויות אחרות של ממשק רשת, יש לעיין בספירה של otNetifIdentifier בחומר העזר של UDP API.

פעולה: הטמעת הודעות UDP.

ב-main.c, מוסיפים את ההטמעה של הפונקציה sendUdp אחרי הפונקציה initUdp שהוספת:

/**
 * Send a UDP datagram
 */
void sendUdp(otInstance *aInstance)
{
    otError       error = OT_ERROR_NONE;
    otMessage *   message;
    otMessageInfo messageInfo;
    otIp6Address  destinationAddr;

    memset(&messageInfo, 0, sizeof(messageInfo));

    otIp6AddressFromString(UDP_DEST_ADDR, &destinationAddr);
    messageInfo.mPeerAddr    = destinationAddr;
    messageInfo.mPeerPort    = UDP_PORT;

    message = otUdpNewMessage(aInstance, NULL);
    otEXPECT_ACTION(message != NULL, error = OT_ERROR_NO_BUFS);

    error = otMessageAppend(message, UDP_PAYLOAD, sizeof(UDP_PAYLOAD));
    otEXPECT(error == OT_ERROR_NONE);

    error = otUdpSend(aInstance, &sUdpSocket, message, &messageInfo);

 exit:
    if (error != OT_ERROR_NONE && message != NULL)
    {
        otMessageFree(message);
    }
}

כדאי לשים לב לפקודות המאקרו otEXPECT ו-otEXPECT_ACTION. צריך לוודא שהודעת ה-UDP תקפה ומוקצית כראוי במאגר הנתונים. אם לא, הפונקציה מטפלת בחינה בשגיאות על ידי דילוג אל הבלוק exit, שבו היא משחררת את מאגר הנתונים.

מידע נוסף על הפונקציות המשמשות לאתחול UDP זמין בהפניות IPv6 ו-UDP ב-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: הגדרת רשת השרשורים

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

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

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

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

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

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

#include <openthread/dataset_ftd.h>

פעולה: הוספת הצהרה על פונקציה כדי להגדיר את הרשת.

יש להוסיף את ההצהרה הזו אל main.c, אחרי שהכותרת כוללת לפני ואחרי הצהרות #if. הפונקציה הזו תוגדר אחרי הפונקציה הראשית באפליקציה.

static void setNetworkConfiguration(otInstance *aInstance);

פעולה: הוספת השיחה להגדרת הרשת.

ב-main.c, יש להוסיף את הקריאה הזו לפונקציה לפונקציה main() אחרי השיחה אל otSetStateChangedCallback. פונקציה זו מגדירה את מערך הנתונים ברשת Thread.

/* Override default network credentials */
setNetworkConfiguration(instance);

פעולה: הוספת שיחות כדי להפעיל את ממשק הרשת ואת המקבץ של Thread.

בעוד main.c, יש להוסיף את הקריאות האלה לפונקציה אל הפונקציה main() אחרי השיחה אל otSysButtonInit.

/* Start the Thread network interface (CLI cmd > ifconfig up) */
otIp6SetEnabled(instance, true);

/* Start the Thread stack (CLI cmd > thread start) */
otThreadSetEnabled(instance, true);

פעולה: הטמעה של הגדרת רשת Thread.

ב-main.c, מוסיפים את ההטמעה של הפונקציה setNetworkConfiguration אחרי הפונקציה main():

/**
 * Override default network settings, such as panid, so the devices can join a
 network
 */
void setNetworkConfiguration(otInstance *aInstance)
{
    static char          aNetworkName[] = "OTCodelab";
    otOperationalDataset aDataset;

    memset(&aDataset, 0, sizeof(otOperationalDataset));

    /*
     * Fields that can be configured in otOperationDataset to override defaults:
     *     Network Name, Mesh Local Prefix, Extended PAN ID, PAN ID, Delay Timer,
     *     Channel, Channel Mask Page 0, Network Key, PSKc, Security Policy
     */
    aDataset.mActiveTimestamp.mSeconds             = 1;
    aDataset.mActiveTimestamp.mTicks               = 0;
    aDataset.mActiveTimestamp.mAuthoritative       = false;
    aDataset.mComponents.mIsActiveTimestampPresent = true;

    /* Set Channel to 15 */
    aDataset.mChannel                      = 15;
    aDataset.mComponents.mIsChannelPresent = true;

    /* Set Pan ID to 2222 */
    aDataset.mPanId                      = (otPanId)0x2222;
    aDataset.mComponents.mIsPanIdPresent = true;

    /* Set Extended Pan ID to C0DE1AB5C0DE1AB5 */
    uint8_t extPanId[OT_EXT_PAN_ID_SIZE] = {0xC0, 0xDE, 0x1A, 0xB5, 0xC0, 0xDE, 0x1A, 0xB5};
    memcpy(aDataset.mExtendedPanId.m8, extPanId, sizeof(aDataset.mExtendedPanId));
    aDataset.mComponents.mIsExtendedPanIdPresent = true;

    /* Set network key to 1234C0DE1AB51234C0DE1AB51234C0DE */
    uint8_t key[OT_NETWORK_KEY_SIZE] = {0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE};
    memcpy(aDataset.mNetworkKey.m8, key, sizeof(aDataset.mNetworkKey));
    aDataset.mComponents.mIsNetworkKeyPresent = true;

    /* Set Network Name to OTCodelab */
    size_t length = strlen(aNetworkName);
    assert(length <= OT_NETWORK_NAME_MAX_SIZE);
    memcpy(aDataset.mNetworkName.m8, aNetworkName, length);
    aDataset.mComponents.mIsNetworkNamePresent = true;

    otDatasetSetActive(aInstance, &aDataset);
    /* Set the router selection jitter to override the 2 minute default.
       CLI cmd > routerselectionjitter 20
       Warning: For demo purposes only - not to be used in a real product */
    uint8_t jitterValue = 20;
    otThreadSetRouterSelectionJitter(aInstance, jitterValue);
}

כפי שמתואר בפונקציה, הפרמטרים של רשת Thread שבהם אנחנו משתמשים באפליקציה הזו:

  • ערוץ = 15
  • מזהה PAN = 0x2222
  • מזהה PAN מורחב = C0DE1AB5C0DE1AB5
  • מפתח רשת = 1234C0DE1AB51234C0DE1AB51234C0DE
  • שם הרשת = OTCodelab

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

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

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

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

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

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

./third_party/NordicSemiconductor/CMakeLists.txt

עכשיו מוסיפים עוד התרעות ל-NoricSemiconductor CMakeLists.txt, כדי לוודא שפונקציות GPIO מוגדרות באפליקציה.

פעולה: הוספת דגלים לקובץ CMakeLists.txt .

יש לפתוח את ./third_party/NordicSemiconductor/CMakeLists.txt בעורך הטקסט המועדף עליך ולהוסיף את השורות הבאות בקטע COMMON_FLAG.

...
set(COMMON_FLAG
    -DSPIS_ENABLED=1
    -DSPIS0_ENABLED=1
    -DNRFX_SPIS_ENABLED=1
    -DNRFX_SPIS0_ENABLED=1
    ...

    # Defined in ./third_party/NordicSemiconductor/nrfx/templates/nRF52840/nrfx_config.h
    -DGPIOTE_ENABLED=1
    -DGPIOTE_CONFIG_IRQ_PRIORITY=7
    -DGPIOTE_CONFIG_NUM_OF_LOW_POWER_EVENTS=1
)

...

./src/CMakeLists.txt

כדי להוסיף את קובץ המקור החדש: gpio.c, צריך לערוך את הקובץ ./src/CMakeLists.txt:

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

פעולה: הוספת מנהל ההתקן של gpio לקובץ NordicSemiconductor CMakeLists.txt .

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

...

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

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

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

בניית פרוטוקול OpenThread

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

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

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

$ cd build/bin
$ arm-none-eabi-objcopy -O ihex ot-cli-ftd ot-cli-ftd.hex

הבהוב הלוחות

העברת ה-Flash של הקובץ ot-cli-ftd.hex לכל לוח nRF52840.

מחברים את כבל ה-USB ליציאת ניפוי הבאגים של המיקרו-USB לצד סיכת החשמל החיצונית בלוח ה-nRF52840. לאחר מכן, מחברים אותו למכונה Linux. ההגדרה הנכונה של LED5 מופעלת.

20a3b4b480356447.png

כמו קודם, יש לציין את המספר הסידורי של לוח ה-nRF52840:

c00d519ebec7e5f0.jpeg

עוברים אל המיקום של כלי שורת הפקודה nRFx ולהבהב את קובץ ה-הקסדצימלי של OpenThread CLI FTD ללוח nRF52840 באמצעות המספר הסידורי של ה-Jamboard'

$ 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.

חוזרים על &PLURAL;Flash boards" שלב בשני הלוחות האחרים. כל לוח צריך להיות מחובר ל-Linux באותו אופן, והפקודה לפלאש זהה, מלבד המספר הסידורי של ה-Jamboard. חשוב להשתמש במספר הסידורי הייחודי של כל לוח

nrfjprog פלאש מהבהב.

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

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

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

מחווני תפקידים במכשיר

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

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

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

שידור UDP

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

203dd094acca1f97.png

9bbd96d9b1c63504.png

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

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

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

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

4e1e885861a66570.png

הסרת המנהיג

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

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

4c57c87adb40e0e3.png

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

5f40afca2dcc4b5b.png

איפוס הלוחות

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

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

מחיצות ברשת

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

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

14. הדגמה: שליחת שידורים מרובים בטכנולוגיית UDP

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

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

f186a2618fdbe3fd.png

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

לוחצים על כפתור 1 בלוח אחר ורואים איך לחצן LED4 מופעל בלוחות האחרים. יש ללחוץ על 'לחצן 1' באחד מהלוחות שבהם פועל LED4. LED4 ממשיך לפעול בלוח הזה אבל מוחלף באחרים.

f5865ccb8ab7aa34.png

מחיצות ברשת

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

15. מעולה!

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

הידעתם:

  • איך לתכנת את הלחצנים ואת נורות ה-LED בלוחות פיתוח בסגנון נורדי nRF52840?
  • איך להשתמש בממשקי API נפוצים של Openthread ובכיתת otInstance
  • איך לעקוב אחר השינויים במצב OpenThread ולהגיב להם
  • איך שולחים הודעות UDP לכל המכשירים ברשת שרשור
  • איך משנים את MakeFiles

השלבים הבאים

בהתאם לקוד הזה, כדאי לנסות את התרגילים הבאים:

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

קריאה נוספת

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

הפניה: