Programowanie z wykorzystaniem interfejsów API OpenThread

1. Wprowadzenie

26b7f4f6b3ea0700.png

Pakiet OpenThread wydany przez Nest to protokół open source protokołu sieciowego Thread®. Nest wprowadził na rynek OpenThread, by umożliwić deweloperom szerokie udostępnienie technologii Nest w produktach, co przyspiesza tworzenie produktów do domów inteligentnych.

Specyfikacja Thread umożliwia określanie niezawodnego, bezpiecznego i bezpośredniego protokołu komunikacyjnego między urządzeniami w standardzie IPv6. OpenThread implementuje wszystkie warstwy sieciowe Thread, w tym IPv6, 6LoWPAN, IEEE 802.15.4 z zabezpieczeniami MAC, ustanowieniem połączenia sieci typu mesh i routingiem typu mesh.

Dzięki tym ćwiczeniom z programowania możesz korzystać z interfejsów OpenThread API, aby uruchamiać sieć Thread, obserwować zmiany ról i reagować na nie oraz wysyłać wiadomości UDP. Powiążemy te działania z przyciskami i diodami LED na prawdziwym sprzęcie.

2a6db2e258c32237.png

Czego się nauczysz

  • Jak zaprogramować przyciski i diody LED na płytach deweloperskich Norn nRF52840
  • Jak używać popularnych interfejsów API OpenThread i klasy otInstance
  • Monitorowanie zmian stanu OpenThread i reagowanie na nie
  • Jak wysyłać wiadomości UDP do wszystkich urządzeń w sieci typu Thread
  • Jak modyfikować MakeFile

Czego potrzebujesz

Sprzęt:

  • 3 normy północnoprzewodowe nRF52840
  • 3 kable USB do micro-USB do podłączenia płyt
  • komputer z systemem Linux z co najmniej 3 portami USB,

Oprogramowanie:

  • Łańcuch narzędzi GNU
  • Narzędzia wiersza poleceń nRF5x
  • Oprogramowanie Segger J-Link
  • OpenThread
  • Git

O ile nie stwierdzono inaczej, treść tego ćwiczenia z programowania jest dostępna na licencji Creative Commons Attribution 3.0, a przykładowe fragmenty kodu są objęte licencją Apache 2.0.

2. Pierwsze kroki

Ukończ ćwiczenia z programowania

Zanim rozpoczniesz ten moduł z programowania, wykonaj ćwiczenie tworzenia sieci wątków przy użyciu tablic nRF52840 i OpenThread, które:

  • Szczegółowe informacje o oprogramowaniu do tworzenia i błyskania
  • Pokazuje, jak stworzyć OpenThread i uruchomić go na tablicach Nordic nRF52840
  • Prezentuje podstawowe informacje o sieci typu Thread

W tym ćwiczeniu z ćwiczenia nie znajdziesz szczegółowo informacji o środowisku, w którym wymagane jest stworzenie pakietu OpenThread i wykonanie Flasha. Zakładamy, że masz już za sobą ćwiczenia z programowania dotyczące tworzenia wątków.

Maszyna z systemem Linux

To ćwiczenie z programowania zostało stworzone z myślą o korzystaniu z komputera z systemem Linux opartym na i386 lub x86. Wszystkie kroki zostały przetestowane na systemie Ubuntu 14.04.5 LTS (Trusty Tahr).

Deski północnonordowe nRF52840

Te ćwiczenia z programowania obejmują 3 płytki nRF52840 PDK.

A6693da3ce213856.png

Zainstaluj oprogramowanie

Aby skompilować i dodać środowisko OpenThread, należy zainstalować oprogramowanie SEGGER J-Link, narzędzie nRF5x Command Line, łańcuch narzędzi ARM GNU oraz różne pakiety systemu Linux. Jeśli udało Ci się wykonać wymagane ćwiczenia z budowania sieci z wątkami, masz już wszystko, czego potrzebujesz. Jeśli nie, ukończ to ćwiczenie z programowania, zanim przejdziesz dalej, aby upewnić się, że możesz tworzyć i wyświetlać obiekty FlashThread na płytach dev nRF52840.

3. Klonowanie repozytorium

W Watem znajdziesz przykładowy kod aplikacji, który możesz wykorzystać jako punkt wyjścia w tym ćwiczeniu z programowania.

Skopiuj repozytorium OpenThread nRF528xx i utwórz kompilację OpenThread:

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

4. Podstawy interfejsu API OpenThread

Publiczne interfejsy API usługi OpenThread znajdują się pod adresem ./openthread/include/openthread w repozytorium OpenThread. Interfejsy API zapewniają dostęp do różnych funkcji OpenThread, zarówno na poziomie wątku, jak i platformy, do użytku w aplikacjach:

  • Informacje o instancji i elementy sterujące w OpenThread
  • Usługi aplikacji takie jak IPv6, UDP i CoAP
  • Zarządzanie danymi logowania w sieci oraz rolami komisarza i łączenia
  • Zarządzanie granicami routerów
  • Ulepszone funkcje, takie jak nadzór nad dziećmi i wykrywanie Jamów

Informacje referencyjne dotyczące wszystkich interfejsów API OpenThread są dostępne na openthread.io/reference.

Korzystanie z interfejsu API

Aby użyć interfejsu API, umieść jego plik nagłówka w jednym z plików aplikacji. Następnie wywołaj wybraną funkcję.

Na przykład przykładowa aplikacja wiersza poleceń dodana przez OpenThread używa tych nagłówków 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>

Instancja OpenThread

Struktura otInstance będzie często używana podczas pracy z interfejsami API OpenThread. Po zainicjowaniu ta struktura przedstawia statyczną instancję biblioteki OpenThread i umożliwia użytkownikowi wykonywanie wywołań interfejsu OpenThread API.

Na przykład instancja OpenThread jest inicjowana w funkcji main() przykładowej aplikacji wiersza poleceń:

./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;
}

Funkcje specyficzne dla platformy

Jeśli chcesz dodać funkcje związane z platformą do jednej z przykładowych aplikacji uwzględnionych w OpenThread, najpierw zadeklaruj je w nagłówku ./openthread/examples/platforms/openthread-system.h, używając przestrzeni nazw otSys dla wszystkich funkcji. Następnie zastosuj je w pliku źródłowym właściwym dla platformy. W ten sposób możesz używać tych samych nagłówków funkcji dla innych przykładowych platform.

Na przykład funkcje GPIO, których użyjemy do połączenia z przyciskami nRF52840, i diody LED muszą być zadeklarowane w obiekcie openthread-system.h.

Otwórz plik ./openthread/examples/platforms/openthread-system.h w dowolnym edytorze tekstu.

./openthread/examples/platform/openthread-system.h

DZIAŁANIE: dodaj deklaracje funkcji GPIO związane z daną platformą.

Dodaj następujące deklaracje funkcji po elemencie #include w nagłówku 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);

Zaimplementujemy je w następnym kroku.

Pamiętaj, że deklaracja funkcji otSysButtonProcess korzysta z właściwości otInstance. Dzięki temu aplikacja będzie w razie potrzeby mieć dostęp do informacji o instancji OpenThread po naciśnięciu przycisku. Wszystko zależy od potrzeb aplikacji. Jeśli nie potrzebujesz tej funkcji do implementacji funkcji, możesz użyć makra OT_UNUSED_VARIABLE z interfejsu OpenThread API, aby pominąć błędy kompilacji w przypadku nieużywanych zmiennych w niektórych łańcuchach narzędzi. Za chwilę zobaczysz przykłady.

5. Wdrażanie abstrakcji platformy GPIO

W poprzednim kroku omówiliśmy w ./openthread/examples/platforms/openthread-system.h deklaracje funkcji platformy, których można używać na potrzeby GPIO. Aby uzyskać dostęp do przycisków i diod LED na tablicach nRF52840, musisz zaimplementować te funkcje na platformie nRF52840. W tym kodzie dodajesz funkcje, które:

  • Inicjowanie pinezki i trybu GPIO
  • Kontroluj napięcie na styku
  • Włącz powiadomienia GPIO i zarejestruj rozmowy zwrotne

W katalogu ./src/src utwórz nowy plik o nazwie gpio.c. W nowym pliku dodaj te treści.

./src/src/gpio.c (nowy plik)

DZIAŁANIE: dodaj definicje

Określają one abstrakcje między wartościami i zmiennymi nRF52840 używanymi na poziomie aplikacji 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

Więcej informacji o przyciskach i diodach LED nRF52840 znajdziesz w Centrum informacji na temat półprzewodników Nordic.

DZIAŁANIE: dodaj nagłówek zawiera

Następnie dodaj nagłówek zawierający informacje potrzebne do działania funkcji 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"

DZIAŁANIE: dodaj funkcje wywołania zwrotnego i przerywania przycisku 1.

Następnie dodaj kod. Funkcja in_pin1_handler to wywołanie zwrotne rejestrowane po zainicjowaniu funkcji przycisku (później w tym pliku).

Zwróć uwagę na to, że to wywołanie zwrotne używa makra OT_UNUSED_VARIABLE, ponieważ zmienne przekazywane do in_pin1_handler nie są tak naprawdę używane w funkcji.

/* 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;
}

DZIAŁANIE: dodaj funkcję konfigurowania diod LED.

Dodaj ten kod, aby skonfigurować tryb i stan wszystkich diod LED podczas inicjowania.

/**
 * @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);
}

DZIAŁANIE: dodaj funkcję określającą tryb diody LED.

Ta funkcja będzie używana w przypadku zmiany roli urządzenia.

/**
 * @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;
    }
}

DZIAŁANIE: dodaj funkcję przełączania trybu diod LED.

Ta funkcja będzie używana do przełączania diod LED 4, gdy urządzenie otrzyma komunikat 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;
    }
}

DZIAŁANIE: dodaj funkcje inicjowania i przetwarzania naciśnięć przycisku.

Pierwsza funkcja inicjuje tablicę dla naciśnięcia przycisku, a druga wysyła komunikat UDP multicast po naciśnięciu przycisku 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);
    }
}

DZIAŁANIE: Zapisz i zamknij plik gpio.c .

6. Interfejs API: reaguj na zmiany roli urządzenia

Zależy nam, aby różne diody LED świeciły się w zależności od roli urządzenia. Przyjrzyjmy się następującym rolom: lider, router, urządzenie końcowe. Można je przypisywać do diod LED w ten sposób:

  • LED1 = lider
  • LED2 = router
  • LED3 = urządzenie końcowe

Aby włączyć tę funkcję, aplikacja musi wiedzieć, kiedy zmieniła się rola urządzenia i jak włączyć odpowiednią diodę LED. W pierwszej części użyjemy instancji OpenThread, a w drugiej – abstrakcji GPIO.

Otwórz plik ./openthread/examples/apps/cli/main.c w dowolnym edytorze tekstu.

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

DZIAŁANIE: dodaj nagłówek zawiera

W sekcji uwzględniającej plik main.c dodaj pliki nagłówka interfejsu API, których potrzebujesz do zmiany roli.

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

DZIAŁANIE: dodaj deklarację funkcji modułu obsługi dla zmiany stanu instancji OpenThread.

Dodaj tę deklarację do main.c, po nagłówku zawierającym instrukcje #if i przed nimi. Ta funkcja zostanie zdefiniowana po głównej aplikacji.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

DZIAŁANIE: dodaj rejestrację wywołania zwrotnego dla funkcji obsługi zmiany stanu.

W main.c dodaj tę funkcję do funkcji main() po wywołaniu otAppCliInit. Ta rejestracja wywołania zwrotnego sprawia, że funkcja OpenThread wywołuje funkcję handleNetifStateChange za każdym razem, gdy zmieni się stan instancji OpenThread.

/* Register Thread state change handler */
otSetStateChangedCallback(instance, handleNetifStateChanged, instance);

DZIAŁANIE: dodaj implementację zmiany stanu

W main.c za funkcje main() zaimplementuj funkcję handleNetifStateChanged. Ta funkcja sprawdza flagę OT_CHANGED_THREAD_ROLE instancji OpenThread i w razie potrzeby włącza lub wyłącza diody 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. Interfejs API: włączanie trybu LED za pomocą multiemisji

W naszej aplikacji chcemy też wysyłać wiadomości UDP do wszystkich pozostałych urządzeń w sieci po naciśnięciu przycisku 1 na jednej tablicy. Aby potwierdzić odbiór wiadomości, w odpowiedzi na płytkach zmieni się wskaźnik LED4.

Aby włączyć tę funkcję, aplikacja musi:

  • Inicjowanie połączenia UDP podczas uruchamiania
  • mieć możliwość wysyłania wiadomości UDP na lokalny adres multish,
  • Obsługa przychodzących wiadomości UDP
  • Przełącz LED4 w odpowiedzi na przychodzące wiadomości UDP

Otwórz plik ./openthread/examples/apps/cli/main.c w dowolnym edytorze tekstu.

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

DZIAŁANIE: dodaj nagłówek zawiera

W sekcji u góry pliku main.c dodaj pliki z nagłówkami interfejsu API, których potrzebujesz do obsługi multiemisji UDP.

#include <string.h>

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

#include "utils/code_utils.h"

Nagłówek code_utils.h jest używany w makrach otEXPECT i otEXPECT_ACTION, które weryfikują warunki działania i łatwo przetwarzają błędy.

DZIAŁANIE: dodaj definicje i stałe:

W pliku main.c, po sekcji „Include” i przed wszelkimi wyrażeniami #if, dodaj stałe typu UDP i określ:

#define UDP_PORT 1212

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

ff03::1 to lokalny adres multicast. Wszystkie wiadomości wysyłane na ten adres będą wysyłane na wszystkie urządzenia z wątkami pełnymi w sieci. Więcej informacji na temat obsługi multiemisji w OpenThread znajdziesz w artykule Multicast on openthread.io.

DZIAŁANIE: dodaj deklaracje funkcji

W pliku main.c, po definicji otTaskletsSignalPending i przed funkcją main(), dodaj funkcje właściwe dla UDP oraz zmienną statyczną reprezentującą gniazdo 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;

DZIAŁANIE: dodaj wywołania inicjujące diody GPIO i przycisk.

W main.c dodaj te wywołania funkcji do funkcji main() po wywołaniu otSetStateChangedCallback. Te funkcje inicjują pinezki GPIO i GPIOTE oraz ustawiają moduł obsługi przycisków do obsługi zdarzeń push przycisku.

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

DZIAŁANIE: dodaj wywołanie inicjowania UDP

W main.c dodaj tę funkcję do funkcji main() po właśnie dodanej rozmowie otSysButtonInit:

initUdp(instance);

To wywołanie gwarantuje zainicjowanie gniazda UDP podczas uruchamiania aplikacji. Bez niego urządzenie nie może wysyłać ani odbierać wiadomości UDP.

DZIAŁANIE: dodaj wywołanie, aby przetworzyć zdarzenie przycisku GPIO.

W main.c dodaj do tej funkcji wywołanie funkcji main() po wywołaniu otSysProcessDrivers, w pętli while. Ta funkcja zadeklarowana w gpio.c wskazuje, czy przycisk został naciśnięty, a jeśli tak, wywołuje moduł obsługi (handleButtonInterrupt), który został ustawiony w poprzednim kroku.

otSysButtonProcess(instance);

DZIAŁANIE: Zaimplementuj przycisk zakłócający działanie modułu

W main.c dodaj implementację funkcji handleButtonInterrupt po funkcji handleNetifStateChanged dodanej w poprzednim kroku:

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

DZIAŁANIE: Zaimplementuj inicjowanie UDP

W main.c dodaj implementację funkcji initUdp po dodaniu funkcji 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 to port zdefiniowany wcześniej (1212). Funkcja otUdpOpen otwiera gniazdo i rejestruje wywołanie zwrotne (handleUdpReceive) w przypadku odebrania wiadomości UDP. Domena otUdpBind łączy gniazdo z interfejsem sieciowym wątku, przekazując wartość OT_NETIF_THREAD. Informacje o innych opcjach interfejsu sieciowego znajdziesz w opisie otNetifIdentifier w przewodniku po interfejsie API UDP.

DZIAŁANIE: Zaimplementuj przesyłanie przez UDP

W main.c dodaj implementację funkcji sendUdp po dodaniu funkcji 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);
    }
}

Zwróć uwagę na makra otEXPECT i otEXPECT_ACTION. Dzięki temu komunikat UDP jest prawidłowy i przydzielony poprawnie w buforze. Jeśli tak się nie stanie, funkcja płynnie obsługuje błędy, przechodząc do bloku exit, w którym zwalnia bufor.

Więcej informacji na temat funkcji używanych do inicjowania protokołu UDP znajdziesz w dokumentach IPv6 i UDP na temat openthread.io.

DZIAŁANIE: wdróż obsługę wiadomości UDP

W main.c dodaj implementację funkcji handleUdpReceive po dodanej przed chwilą funkcji sendUdp. Ta funkcja jedynie włącza 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. Interfejs API: konfigurowanie sieci Thread

Aby ułatwić demonstrację, zależy nam na tym, aby nasze urządzenia natychmiast rozpoczynały wątki i łączyły się z siecią, gdy są włączone. Aby to zrobić, użyjemy struktury otOperationalDataset. Ta struktura zawiera wszystkie parametry wymagane do przesyłania danych logowania do sieci typu Thread na urządzenie.

Użycie tej struktury zastąpi domyślne ustawienia sieci wbudowane w OpenThread, aby zwiększyć bezpieczeństwo naszej aplikacji i ograniczyć węzły w nazwie naszej sieci tylko do tych, które ją uruchamiają.

Otwórz plik ./openthread/examples/apps/cli/main.c w preferowanym edytorze tekstu.

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

DZIAŁANIE: dodaj nagłówek „Uwzględnij”.

Dodaj sekcję nagłówka u góry pliku main.c, by dodać plik nagłówka interfejsu API, którego potrzebujesz do skonfigurowania sieci Thread:

#include <openthread/dataset_ftd.h>

DZIAŁANIE: dodaj deklarację funkcji do konfiguracji sieci.

Dodaj tę deklarację do main.c, po nagłówku zawierającym instrukcje #if i przed nimi. Ta funkcja zostanie zdefiniowana po głównej funkcji aplikacji.

static void setNetworkConfiguration(otInstance *aInstance);

DZIAŁANIE: dodaj wywołanie konfiguracji sieci.

W main.c dodaj tę funkcję do funkcji main() po wywołaniu otSetStateChangedCallback. Ta funkcja umożliwia skonfigurowanie zbioru danych z sieci typu Thread.

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

DZIAŁANIE: dodaj wywołania, aby włączyć interfejs sieciowy i stos Thread.

W main.c dodaj te wywołania funkcji do funkcji main() po wywołaniu otSysButtonInit.

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

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

DZIAŁANIE: Zaimplementuj konfigurację sieci typu Thread.

W main.c dodaj implementację funkcji setNetworkConfiguration po funkcji 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);
}

Zgodnie z opisem w tej funkcji parametry sieci Thread używane przez tę aplikację są następujące:

  • Kanał = 15
  • Numer PAN = 0x2222
  • Identyfikator rozszerzonego numeru PAN = C0DE1AB5C0DE1AB5
  • Klucz sieci = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nazwa sieci = OTCodelab

Dodatkowo w tym miejscu zmniejszamy zakłócenia w wyborze routera, więc nasze urządzenia szybciej zmieniają role na potrzeby wersji demonstracyjnej. Należy to zrobić tylko wtedy, gdy węzeł jest FTD (urządzenia z pełnym wątkiem). Więcej na ten temat w następnym kroku.

9. Interfejs API: funkcje z ograniczeniami

Niektóre interfejsy API OpenThread modyfikują ustawienia, które należy modyfikować tylko na potrzeby wersji demonstracyjnej lub testowej. Te interfejsy API nie powinny być używane w środowisku produkcyjnym aplikacji za pomocą OpenThread.

Na przykład funkcja otThreadSetRouterSelectionJitter dostosowuje czas (w sekundach) potrzebny do wypromowania urządzenia przez router. Wartość domyślna to 120 zgodnie ze specyfikacją wątku. Aby ułatwić korzystanie z nich w ćwiczeniach z programowania, zastąpimy je wartością 20, dzięki czemu nie trzeba będzie długo czekać na zmianę roli w wątku.

Uwaga: urządzenia MTD nie stają się routerami, a kompilacja MTD nie obejmuje obsługi funkcji takiej jak otThreadSetRouterSelectionJitter. Musimy później określić opcję CMake -DOT_MTD=OFF. W przeciwnym razie napotkamy błąd kompilacji.

Możesz to sprawdzić, znajdując się w definicji funkcji otThreadSetRouterSelectionJitter zawartej w dyrektywie Preprocessor do 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. Wprowadzanie zmian

Zanim skompilujesz aplikację, musisz wprowadzić kilka drobnych aktualizacji w 3 plikach CMake. Są one używane przez system kompilacji do kompilowania i łączenia aplikacji.

./third_party/Nordicpółprzewodnik/CMakeLists.txt

Teraz dodaj flagi do Nordicpółprzewodnika CMakeLists.txt, aby upewnić się, że funkcje GPIO są zdefiniowane w aplikacji.

DZIAŁANIE: Dodaj flagi do pliku CMakeLists.txt.

Otwórz ./third_party/NordicSemiconductor/CMakeLists.txt w preferowanym edytorze tekstu i dodaj następujące wiersze w sekcji 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

Edytuj plik ./src/CMakeLists.txt, aby dodać nowy plik źródłowy gpio.c:

DZIAŁANIE: dodaj źródło gpio do pliku ./src/CMakeLists.txt

Otwórz ./src/CMakeLists.txt w preferowanym edytorze tekstu i dodaj plik w sekcji NRF_COMM_SOURCES.

...

set(NRF_COMM_SOURCES
  ...
  src/gpio.c
  ...
)

...

./third_party/Nordicpółprzewodnik/CMakeLists.txt

Na koniec dodaj plik sterownika nrfx_gpiote.c do pliku Nordicpółprzewodnika CMakeLists.txt, aby uwzględnić go w bibliotece kompilacji skandynawskich sterowników.

DZIAŁANIE: Dodaj sterownik gpio do pliku NordicpółprzewodnikaCMakeLists.txt .

Otwórz ./third_party/NordicSemiconductor/CMakeLists.txt w preferowanym edytorze tekstu i dodaj plik w sekcji COMMON_SOURCES.

...

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

11. Konfigurowanie urządzeń

Po zaktualizowaniu kodu możesz utworzyć aplikację i uruchomić ją na wszystkich 3 panelach programisty Norn nRF52840. Każde urządzenie będzie działać jako urządzenie z pełnym wątkiem (FTD).

Utwórz otwarty wątek

Utwórz pliki binarne FTD OpenThread dla platformy nRF52840.

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

Przejdź do katalogu za pomocą pliku binarnego interfejsu wiersza poleceń OpenThread FTD i przekonwertuj go na format szesnastkowy za pomocą wbudowanego łańcucha narzędzi ARM:

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

Migaj deski

Zapisz plik ot-cli-ftd.hex na każdej tablicy nRF52840.

Kabel USB podłącza się do portu debugowania obok zewnętrznego gniazda zasilania na płytce nRF52840, a następnie podłącz do komputera z systemem Linux. Prawidłowo ustaw tryb LED5.

20a3b4b480356447.png

Jak zwykle zwróć uwagę na numer seryjny płyty nRF52840:

C00D519ebec7e5f0.jpeg

Przejdź do lokalizacji narzędzi wiersza poleceń nRFx i umieść na tablicy nRF52840 plik szesnastkowy OpenThread CLI, używając numeru seryjnego tablicy:

$ cd ~/nrfjprog
$ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \
       ~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset

Podczas błysku lampka LED 5 na chwilę się wyłączy. Po wykonaniu tych czynności dane wyjściowe są generowane:

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.

Powtórz ten krok dla Flasha na każdej tablicy. Każda tablica powinna być podłączona do komputera z systemem Linux w ten sam sposób. Polecenie Flash jest takie samo, z wyjątkiem numeru seryjnego tablicy. Pamiętaj, aby używać unikalnego numeru seryjnego każdej tablicy w

nrfjprog polecenie Flashing.

Jeśli uda się go zapalić, na każdej tablicy będzie świecić dioda LED1, LED2 lub LED3. Wkrótce po miganiu lampki LED może zmienić się nawet z 3 na 2 (lub od 2 do 1).

12. Funkcje aplikacji

Wszystkie 3 płyty nRF52840 powinny być teraz zasilane i uruchamiać aplikację OpenThread. Jak już wspomnieliśmy, ta aplikacja ma 2 główne funkcje.

Wskaźniki ról urządzeń

Oświetlona dioda LED na każdej tablicy odzwierciedla aktualną rolę węzła Thread:

  • LED1 = lider
  • LED2 = router
  • LED3 = urządzenie końcowe

Wraz ze zmianą roli zmienia się również dioda LED. Te zmiany powinny być widoczne na tablicy.

Multicast Multicast

Po naciśnięciu na przycisku 1 wiadomość UDP jest przesyłana na lokalny adres typu mesh w trybie multicast, który obejmuje wszystkie pozostałe węzły w sieci typu Thread. W odpowiedzi na tę wiadomość LED4 na wszystkich innych tablicach włącza się lub wyłącza. LED4 pozostaje włączony lub wyłączony na każdej tablicy do momentu otrzymania kolejnego komunikatu UDP.

203dd094acca1f97.png

9bbd96d9b1c63504

13. Wersja demonstracyjna: obserwowanie zmian roli urządzenia

Migające urządzenia to specjalne urządzenia z pełnym wątkiem (FTD) nazywane urządzeniem spełniającym warunki routera (REED). Oznacza to, że mogą pełnić funkcję routera lub urządzenia końcowego i mogą promować się jako urządzenie końcowe do routera.

Thread może obsługiwać do 32 routerów, ale stara się utrzymać ich liczbę między 16 a 23. Jeśli urządzenie REED zostanie podłączone jako urządzenie końcowe, a liczba routerów jest mniejsza niż 16, zostanie automatycznie zmieniona na router. Ta zmiana powinna zostać wprowadzona losowo w ciągu sekund, o których ustawiasz wartość otThreadSetRouterSelectionJitter w aplikacji (20 sekund).

Każda sieć Thread ma też lidera, czyli router, który jest odpowiedzialny za zarządzanie zestawem routerów w sieci Thread. Kiedy wszystkie urządzenia są włączone, po 20 sekundach jednym z nich powinno być lider (włączona lampa 1), a pozostałe routery (LED2 włączone).

4e1e885861a66570.png

Usuń lidera

Jeśli lider zostanie usunięty z sieci Thread, inny router awansuje się do roli lidera, aby mieć pewność, że w sieci nadal jest Lider.

Wyłącz tablicę lidera (tętniową diodą LED 1) za pomocą przełącznika zasilania. Zaczekaj około 20 sekund. Na jednej z dwóch pozostałych płytek świeci się dioda LED2 (Router) i LED1 (1). To urządzenie jest teraz liderem sieci Thread.

4c57c87adb40e0e3.png

Włącz z powrotem pierwotną tablicę liderów. Powinno automatycznie połączyć się ponownie z siecią Thread jako urządzeniem końcowym (oświetlona dioda LED 3). W ciągu 20 sekund (zakłócenie wyboru routera) zmienia się w router (oświetlenie LED 2).

5f40afca2dcc4b5b.png

Zresetuj tablice

Wyłącz wszystkie trzy tablice, a następnie włącz je ponownie i obserwuj diody LED. Pierwsza włączona tablica powinna zaczynać się od roli lidera (LED1 świeci się) – pierwszy router w sieci typu Thread automatycznie staje się liderem.

Dwie pozostałe tablice łączą się z siecią w ciągu 20 sekund i podłączone są do niej jako urządzenia końcowe, czyli dioda LED 3, ale w ciągu 20 sekund powinna awansować się do konfiguracji routerów (LED2).

Partycje sieciowe

Jeśli płyty nie mają wystarczającej mocy lub połączenie radiowe jest między nimi słabe, sieć Thread może być podzielona na partycje i więcej niż jedno urządzenie może pokazywać się jako lider.

Wątek jest samonaprawiający, więc partycje powinny ostatecznie przekształcić się w jedną partycję z jednym liderem.

14. Wersja demonstracyjna: przesyłanie multiemisji UDP

Jeśli będziesz kontynuować od poprzedniego ćwiczenia, dioda LED4 nie powinna świecić się na żadnym urządzeniu.

Wybierz dowolną planszę i naciśnij przycisk 1. Na wszystkich innych tablicach w sieci wątków, w których działa aplikacja, powinna działać tryb LED4. Jeśli przejdziesz do poprzedniego ćwiczenia, powinno ono zostać włączone.

F186a2618fdbe3fd.png

Jeszcze raz naciśnij przycisk 1 na tej samej tablicy. W przypadku innych płytek interfejs LED4 powinien się przełączyć.

Naciśnij przycisk 1 na innej tablicy i sprawdź, jak dioda LED 4 przełącza się na pozostałych. Naciśnij przycisk 1 na jednej z tablic, w których świeci obecnie LED4. Dioda LED 4 pozostaje włączona na tej tablicy, ale przełącza się na pozostałych.

F5865ccb8ab7aa34.png

Partycje sieciowe

Jeśli partycje są podzielone na partycje i znajduje się w nich więcej niż lider, wynik multicastu będzie różny. Jeśli naciśniesz przycisk 1 na płytce, która jest partycjonowana (i więc jest jedynym jej członkiem w sieci partycji), dioda LED 4 na innych tablicach nie zapali się w odpowiedzi. W takim przypadku zresetuj tablice. Najlepiej, aby zmodyfikowały pojedynczą sieć Thread, a wiadomości UDP powinny działać prawidłowo.

15. Gratulacje!

Utworzono aplikację, która korzysta z interfejsów API OpenThread.

Teraz wiesz już:

  • Jak zaprogramować przyciski i diody LED na płytach deweloperskich Norn nRF52840
  • Jak używać popularnych interfejsów API OpenThread i klasy otInstance
  • Monitorowanie zmian stanu OpenThread i reagowanie na nie
  • Jak wysyłać wiadomości UDP do wszystkich urządzeń w sieci typu Thread
  • Jak modyfikować MakeFile

Dalsze kroki

Na podstawie tych ćwiczeń z programowania wypróbuj te ćwiczenia:

  • Zmodyfikuj moduł GPIO, aby używać pinezek GPIO zamiast wbudowanych diod LED oraz podłączyć zewnętrzne diody RGB, które zmieniają kolor w zależności od roli routera
  • Dodaj obsługę GPIO dla innej przykładowej platformy
  • Zamiast używać multiemisji do pingowania wszystkich urządzeń po naciśnięciu przycisku, możesz używać interfejsu Router/Leader API do znajdowania i pingowania pojedynczego urządzenia
  • Połącz sieć typu mesh z internetem za pomocą routera granicznego OpenThread i emituj je zbiorczo poza sieć, aby podświetlać diody LED

Więcej informacji

Odwiedź strony openthread.io i GitHub, aby sprawdzić różne zasoby OpenThread, w tym:

Materiały referencyjne: