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

1. מבוא

26b7f4f6b3ea0700.png

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

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

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

2a6db2e258c32237.png

מה תלמדו

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

מה נדרש

חומרה:

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

תוכנה:

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

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

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

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

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

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

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

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

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

מעתיקים את המאגר של דוגמאות OpenThread Nordic nRF528xx ומפתחים את OpenThread:

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

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

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

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

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

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

פעולה: מוסיפים רכיבים של כותרת עליונה.

בשלב הבא, מוסיפים את ה-include של הכותרות הנדרשות לפונקציונליות של GPIO.

/* Header for the functions defined here */
#include "openthread-system.h"

#include <string.h>

/* Header to access an OpenThread instance */
#include <openthread/instance.h>

/* Headers for lower-level nRF52840 functions */
#include "platform-nrf5.h"
#include "hal/nrf_gpio.h"
#include "hal/nrf_gpiote.h"
#include "nrfx/drivers/include/nrfx_gpiote.h"

פעולה: מוסיפים פונקציות קריאה חוזרת והפרעה ללחצן 1.

מוסיפים את הקוד הבא. הפונקציה in_pin1_handler היא פונקציית הקריאה החוזרת (callback) שמירתמת כשפונקציית לחיצת הלחצן מופעלת (בהמשך הקובץ).

שימו לב שהקריאה החוזרת (callback) הזו משתמשת במאקרו OT_UNUSED_VARIABLE, כי המשתנים שהועברו אל in_pin1_handler לא משמשים בפועל בפונקציה.

/* Declaring callback function for button 1. */
static otSysButtonCallback sButtonHandler;
static bool                sButtonPressed;

/**
 * @brief Function to receive interrupt and call back function
 * set by the application for button 1.
 *
 */
static void in_pin1_handler(uint32_t pin, nrf_gpiote_polarity_t action)
{
    OT_UNUSED_VARIABLE(pin);
    OT_UNUSED_VARIABLE(action);
    sButtonPressed = true;
}

פעולה: מוסיפים פונקציה להגדרת נוריות ה-LED.

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

/**
 * @brief Function for configuring: PIN_IN pin for input, PIN_OUT pin for output,
 * and configures GPIOTE to give an interrupt on pin change.
 */

void otSysLedInit(void)
{
    /* Configure GPIO mode: output */
    nrf_gpio_cfg_output(LED_1_PIN);
    nrf_gpio_cfg_output(LED_2_PIN);
    nrf_gpio_cfg_output(LED_3_PIN);
    nrf_gpio_cfg_output(LED_4_PIN);

    /* Clear all output first */
    nrf_gpio_pin_write(LED_1_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_2_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_3_PIN, GPIO_LOGIC_LOW);
    nrf_gpio_pin_write(LED_4_PIN, GPIO_LOGIC_LOW);

    /* Initialize gpiote for button(s) input.
     Button event handlers are set in the application (main.c) */
    ret_code_t err_code;
    err_code = nrfx_gpiote_init();
    APP_ERROR_CHECK(err_code);
}

פעולה: מוסיפים פונקציה להגדרת המצב של נורית LED.

הפונקציה הזו תופעל כשהתפקיד של המכשיר ישתנה.

/**
 * @brief Function to set the mode of an LED.
 */

void otSysLedSet(uint8_t aLed, bool aOn)
{
    switch (aLed)
    {
    case 1:
        nrf_gpio_pin_write(LED_1_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 2:
        nrf_gpio_pin_write(LED_2_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 3:
        nrf_gpio_pin_write(LED_3_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    case 4:
        nrf_gpio_pin_write(LED_4_PIN, (aOn == GPIO_LOGIC_HI));
        break;
    }
}

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

הפונקציה הזו תשמש להפעלה או להשבתה של LED4 כשהמכשיר יקבל הודעת UDP של Multicast.

/**
 * @brief Function to toggle the mode of an LED.
 */
void otSysLedToggle(uint8_t aLed)
{
    switch (aLed)
    {
    case 1:
        nrf_gpio_pin_toggle(LED_1_PIN);
        break;
    case 2:
        nrf_gpio_pin_toggle(LED_2_PIN);
        break;
    case 3:
        nrf_gpio_pin_toggle(LED_3_PIN);
        break;
    case 4:
        nrf_gpio_pin_toggle(LED_4_PIN);
        break;
    }
}

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

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

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

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

    sButtonHandler = aCallback;
    sButtonPressed = false;

    nrfx_gpiote_in_event_enable(BUTTON_PIN, true);
}

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

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

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

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

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

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

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

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

פעולה: מוסיפים רכיבים לכותרת.

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

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

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

מוסיפים את ההצהרה הזו ל-main.c, אחרי שכותרת ה-include וקודם להצהרות #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: שימוש ב-multicast כדי להפעיל נורית LED

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

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

  • איך מפעילים חיבור UDP בזמן ההפעלה
  • יכולת לשלוח הודעת UDP לכתובת ה-multicast המקומית של הרשת
  • טיפול בהודעות 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 היא כתובת ה-multicast המקומית ברשת התאמה. כל ההודעות שנשלחות לכתובת הזו יישלחו לכל המכשירים ברשת עם תמיכה בשרשור מלא. מידע נוסף על תמיכה ב-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;

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

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

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

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

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

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

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

אפשר לאמת זאת על ידי בדיקת הגדרת הפונקציה otThreadSetRouterSelectionJitter, שמופיעה בהוראת מעבד הטקסט של OPENTHREAD_FTD:

./openthread/src/core/api/thread_ftd_api.cpp

#if OPENTHREAD_FTD

#include <openthread/thread_ftd.h>

...

void otThreadSetRouterSelectionJitter(otInstance *aInstance, uint8_t aRouterJitter)
{
    Instance &instance = *static_cast<Instance *>(aInstance);

    instance.GetThreadNetif().GetMle().SetRouterSelectionJitter(aRouterJitter);
}

...

#endif // OPENTHREAD_FTD

10. עדכונים ב-CMake

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

./third_party/NordicSemiconductor/CMakeLists.txt

עכשיו מוסיפים כמה דגלים ל-CMakeLists.txt של NordicSemiconductor, כדי לוודא שפונקציות ה-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

לבסוף, מוסיפים את קובץ ה-driver‏ nrfx_gpiote.c לקובץ CMakeLists.txt של NordicSemiconductor, כדי שהוא ייכלל ב-build של הספרייה של מנהלי ההתקנים של Nordic.

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

פותחים את ./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

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

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

איך מאפסים את הלוחות

מעבירים את הקובץ ot-cli-ftd.hex לכל לוח nRF52840.

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

20a3b4b480356447.png

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

c00d519ebec7e5f0.jpeg

עוברים למיקום של כלי שורת הפקודה של nRFx, ומעבירים את קובץ ה-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 באותו אופן, והפקודה להעברת קובץ האימג' זהה, מלבד המספר הסידורי של הלוח. חשוב להקפיד להשתמש במספר הסידורי הייחודי של כל לוח

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

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

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

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

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

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

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

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

UDP Multicast

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

203dd094acca1f97.png

9bbd96d9b1c63504.png

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

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

השרשור יכול לתמוך ב-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. הלוח הראשון שהופעל אמור להתחיל בתפקיד 'מנהיג' (נורית LED1 דולקת) – הנתב הראשון ברשת Thread הופך באופן אוטומטי למנהיג.

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

מחיצות רשת

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

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

14. הדגמה: שליחת UDP multicast

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

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

f186a2618fdbe3fd.png

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

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

f5865ccb8ab7aa34.png

מחיצות רשת

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

15. מעולה!

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

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

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

השלבים הבאים

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

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

קריאה נוספת

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

מקור: