Programowanie z wykorzystaniem interfejsów OpenThread API

1. Wprowadzenie

26b7f4f6b3ea0700.png

Opublikowana przez Nest platforma OpenThread to implementacja protokołu open source protokołu sieciowego Thread®. Firma Nest udostępniła rozwiązanie OpenThread, aby udostępnić deweloperom technologię wykorzystywaną w produktach Nest i przyspieszyć proces opracowywania produktów do domów inteligentnych.

Specyfikacja Thread definiuje oparty na protokole IPv6 niezawodny, bezpieczny i oszczędny, bezprzewodowy protokół komunikacyjny między urządzeniami domowymi. OpenThread implementuje wszystkie warstwy sieciowe Thread, w tym IPv6, 6LoWPAN, IEEE 802.15.4 z zabezpieczeniami MAC, ustanowieniem linku mesh i routingiem sieci typu mesh.

W ramach tego ćwiczenia w Codelabs wykorzystasz interfejsy API OpenThread API, aby uruchomić sieć Thread, monitorować zmiany ról urządzeń i reagować na nie, wysyłać komunikaty UDP, a także powiązać 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 Nordic nRF52840
  • Jak używać typowych interfejsów API OpenThread i klasy otInstance.
  • Jak monitorować zmiany stanu OpenThread i na nie reagować
  • Jak wysyłać wiadomości UDP na wszystkie urządzenia w sieci Thread
  • Jak modyfikować pliki Makefiles

Czego potrzebujesz

Sprzęt:

  • 3 płytki deweloperskie Nordic Semiconductor nRF52840
  • 3 kable USB na micro USB do połączenia tablic
  • Komputer z systemem Linux i co najmniej 3 portami USB.

Oprogramowanie:

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

O ile nie stwierdzono inaczej, treść tych ćwiczeń z Codelabs jest objęta licencją Creative Commons Attribution 3.0, a próbki kodu licencją Apache 2.0.

2. Pierwsze kroki

Wykonaj ćwiczenia z programowania

Zanim rozpoczniesz to ćwiczenie w Codelabs, wykonaj ćwiczenia z programowania dotyczące tworzenia sieci wątków przy użyciu tablic nRF52840 i OpenThread, które:

  • Szczegółowe informacje o oprogramowaniu potrzebnym do kompilacji i flashowania
  • Dowiedz się, jak utworzyć interfejs OpenThread i zainstalować go na płytach Nordic nRF52840.
  • Demonstruje podstawy działania sieci Thread.

W tym ćwiczeniu z programowania nie znajdziesz żadnego skonfigurowanego środowiska, które jest wymagane do tworzenia obiektów OpenThread i flashowania tablic. Obejmuje to tylko podstawowe instrukcje dotyczące flashowania tablic. Zakładamy, że masz już za sobą ćwiczenia z programowania w sekcji „Tworzenie sieci Thread”.

Komputer z systemem Linux

To ćwiczenia z programowania zostały opracowane z myślą o użyciu maszyny z systemem Linux z procesorem i386 lub x86 do obsługi we wszystkich płytach deweloperskich Thread. Wszystkie kroki zostały przetestowane w systemie Ubuntu 14.04.5 LTS (Trusty Tahr).

Płyty skandynawskie półprzewodnikowe nRF52840

W tym ćwiczeniu z programowania używane są 3 płyty PDK nRF52840.

a6693da3ce213856.png

Zainstaluj oprogramowanie

Aby utworzyć i zainstalować OpenThread, musisz zainstalować SEGGER J-Link, narzędzia wiersza poleceń nRF5x, łańcuch narzędzi ARM GNU i różne pakiety Linuksa. Po ukończeniu kursu „Build a Thread Network Codelabs” (ćwiczenie z programowania w sieci Thread) masz już zainstalowane wszystko, co jest Ci potrzebne. Jeśli go nie masz, wykonaj najpierw ćwiczenia z Codelab, aby mieć pewność, że możesz kompilować i aktualizować OpenThread na tablicach deweloperskich nRF52840.

3. Klonowanie repozytorium

OpenThread zawiera przykładowy kod aplikacji, którego możesz użyć jako punktu wyjścia w tym ćwiczeniu z programowania.

Sklonuj repozytorium z przykładami OpenThread Nordic nRF528xx i utwórz go:

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

4. Podstawy interfejsu OpenThread API

Publiczne interfejsy API OpenThread znajdują się w repozytorium OpenThread w lokalizacji ./openthread/include/openthread. Te interfejsy API zapewniają dostęp do różnych funkcji OpenThread, które można stosować w aplikacjach zarówno na poziomie Thread, jak i platformy:

  • Informacje o instancji i kontrola nad instancją OpenThread
  • Usługi aplikacji, takie jak IPv6, UDP i CoAP
  • Zarządzanie danymi logowania do sieci wraz z rolami komisarza i łączenia danych
  • Zarządzanie routerami granicznymi
  • Funkcje rozszerzone, takie jak nadzór nad dziećmi i wykrywanie Jamu

Informacje referencyjne na temat wszystkich interfejsów API OpenThread znajdziesz na stronie openthread.io/reference.

Korzystanie z interfejsu API

Aby korzystać z interfejsu API, umieść jego plik nagłówkowy w jednym z plików aplikacji. Następnie wywołaj odpowiednią funkcję.

Na przykład przykładowa aplikacja interfejsu wiersza poleceń dołączana do OpenThread używa tych nagłówków interfejsu 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 to typowa struktura, z której często korzystasz podczas pracy z interfejsami OpenThread API. Po zainicjowaniu struktura reprezentuje statyczną instancję biblioteki OpenThread i pozwala użytkownikowi na wykonywanie wywołań interfejsu OpenThread API.

Na przykład instancja OpenThread jest inicjowana w funkcji main() przykładowej aplikacji interfejsu 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 związane z platformą

Jeśli do jednej z przykładowych aplikacji dostępnych w OpenThread chcesz dodać funkcje zależne od platformy, najpierw zadeklaruj je w nagłówku ./openthread/examples/platforms/openthread-system.h, używając przestrzeni nazw otSys dla wszystkich funkcji. Następnie wdróż je w pliku źródłowym platformy. W ten sposób można użyć tych samych nagłówków funkcji na innych przykładowych platformach.

Na przykład funkcje GPIO, których użyjemy do podłączenia przycisków i diod LED nRF52840, muszą być zadeklarowane w zasadzie openthread-system.h.

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

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

DZIAŁANIE: dodaj deklaracje funkcji GPIO na potrzeby konkretnej platformy.

Dodaj te deklaracje funkcji po fragmencie #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);

Wprowadzimy je w następnym kroku.

Pamiętaj, że deklaracja funkcji otSysButtonProcess używa otInstance. Dzięki temu po naciśnięciu przycisku aplikacja może w razie potrzeby uzyskiwać dostęp do informacji o instancji OpenThread. Wszystko zależy od potrzeb aplikacji. Jeśli nie potrzebujesz go w swojej implementacji funkcji, możesz użyć makra OT_UNUSED_VARIABLE z interfejsu OpenThread API, aby ograniczyć błędy kompilacji związane z nieużywanymi zmiennymi w niektórych łańcuchach narzędzi. Później zobaczymy przykłady.

5. Wdróż abstrakcję platformy GPIO

W poprzednim kroku przyjrzeliśmy się deklaracji funkcji na platformie ./openthread/examples/platforms/openthread-system.h, których można używać na potrzeby GPIO. Aby uzyskać dostęp do przycisków i diod LED na tablicach deweloperskich nRF52840, musisz wdrożyć te funkcje na platformie nRF52840. W tym kodzie dodasz funkcje, które:

  • Zainicjuj piny i tryby GPIO
  • Sterowanie napięciem na styku
  • Włącz przerwy GPIO i zarejestruj wywołanie zwrotne

W katalogu ./src/src utwórz nowy plik o nazwie gpio.c. W nowym pliku dodaj następującą treść.

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

DZIAŁANIE: dodanie definicji.

Definiują one funkcję abstrakcji między wartościami specyficznymi dla nRF52840 a zmiennymi 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 o półprzewodnikach nordyckich.

DZIAŁANIE: Dodaj nagłówek zawiera.

Następnie dodaj nagłówek zawierający informacje potrzebne do działania 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 przerwania dla przycisku 1.

Następnie dodaj ten kod. Funkcja in_pin1_handler to wywołanie zwrotne rejestrowane po zainicjowaniu funkcji naciśnięcia przycisku (w dalszej części tego pliku).

Zwróć uwagę, że wywołanie zwrotne używa makra OT_UNUSED_VARIABLE, ponieważ zmienne przekazywane do in_pin1_handler nie są tak naprawdę używane w tej 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ę ustawiają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łączającą tryb diody LED.

Ta funkcja będzie służyć do przełączania LED4, gdy urządzenie otrzyma wiadomość UDP multiemisji.

/**
 * @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 do inicjowania i przetwarzania naciśnięć przycisków.

Pierwsza funkcja inicjuje tablicę po naciśnięciu przycisku, a druga wysyła komunikat UDP multiemisji 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: reagowanie na zmiany ról urządzenia

W naszej aplikacji chcemy, aby różne diody LED świeciły się w zależności od roli urządzenia. Prześledźmy te role: lider, router, urządzenie końcowe. Możemy je przypisać do diod LED:

  • LED1 = lider
  • LED2 = Router
  • LED3 = Urządzenie końcowe

Aby włączyć tę funkcję, aplikacja musi wiedzieć, kiedy nastąpiła zmiana roli urządzenia i jak włączyć prawidłową diodę LED. W pierwszej części użyjemy instancji OpenThread, a drugiej – abstrakcji na platformie GPIO.

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

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

DZIAŁANIE: Dodaj nagłówek zawiera.

W sekcji „include” w pliku main.c dodaj pliki nagłówka interfejsu API potrzebne do korzystania z funkcji zmiany roli.

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

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

Dodaj tę deklarację do elementu 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. Rejestracja wywołania zwrotnego informuje OpenThread, że ma wywoływać funkcję handleNetifStateChange przy każdej zmianie stanu instancji OpenThread.

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

DZIAŁANIE: Dodaj wdrożenie zmiany stanu.

W main.c, za funkcją main(), zaimplementuj funkcję handleNetifStateChanged. Ta funkcja sprawdza flagę OT_CHANGED_THREAD_ROLE instancji OpenThread i jeśli się zmieniła, 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. API: włączanie multiemisji w celu włączenia diody LED

W naszej aplikacji chcemy też wysyłać wiadomości UDP do wszystkich innych urządzeń w sieci po naciśnięciu przycisku Button1 na jednej płytce. Aby potwierdzić odbiór wiadomości, na innych tablicach przełączymy diody LED4 na innych tablicach.

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

  • Inicjowanie połączenia UDP podczas uruchamiania
  • Mieć możliwość wysłania wiadomości UDP na lokalny adres multicast w sieci typu mesh
  • Obsługa przychodzących wiadomości UDP
  • Przełącz LED4 w odpowiedzi na przychodzące komunikaty UDP

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

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

DZIAŁANIE: Dodaj nagłówek zawiera.

W sekcji „include” na górze pliku main.c dodaj pliki nagłówka interfejsu API potrzebne do korzystania z funkcji multicast 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 sprawdzają warunki działania w czasie działania i sprawnie obsługują błędy.

DZIAŁANIE: Dodaj definicje i stałe:

W pliku main.c, po sekcji „include” i przed instrukcją #if, dodaj stałe specyficzne dla UDP i zdefiniowane:

#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 multiemisji w sieci typu mesh. Wszystkie wiadomości wysyłane na ten adres będą wysyłane do wszystkich urządzeń obsługujących cały wątek w sieci. Więcej informacji na temat obsługi multiemisji w OpenThread.io znajdziesz w artykule Multicast w openthread.io.

DZIAŁANIE: dodaj deklaracje funkcji.

W pliku main.c, po definicji otTaskletsSignalPending i przed funkcją main(), dodaj funkcje specyficzne 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, aby zainicjować diody LED i przycisk GPIO.

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

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

DZIAŁANIE: dodaj wywołanie inicjowania UDP.

W narzędziu main.c dodaj tę funkcję do funkcji main() po dodaniu właśnie wywołania otSysButtonInit:

initUdp(instance);

To wywołanie gwarantuje, że gniazdo UDP zostanie zainicjowane podczas uruchamiania aplikacji. Bez tej opcji urządzenie nie może wysyłać ani odbierać wiadomości UDP.

DZIAŁANIE: dodaj wywołanie przetwarzające zdarzenie przycisku GPIO.

W main.c dodaj tę funkcję do funkcji main() po wywołaniu otSysProcessDrivers, w pętli while. Ta funkcja zadeklarowana w zasadzie gpio.c sprawdza, czy przycisk został naciśnięty, a jeśli tak, wywołuje moduł obsługi (handleButtonInterrupt) ustawiony w kroku powyżej.

otSysButtonProcess(instance);

DZIAŁANIE: wdróż moduł obsługi przerywania przycisków.

W polu 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: zastosuj inicjowanie UDP.

W polu main.c dodaj implementację funkcji initUdp po właśnie dodanej 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 określony wcześniej (1212). Funkcja otUdpOpen otwiera gniazdo i rejestruje funkcję wywołania zwrotnego (handleUdpReceive) po otrzymaniu wiadomości UDP. otUdpBind wiąże gniazdo z interfejsem sieci Thread, przekazując klucz OT_NETIF_THREAD. Informacje o innych opcjach interfejsu sieciowego znajdziesz w wyliczeniu otNetifIdentifier w dokumentacji interfejsu UDP API.

DZIAŁANIE: wdrażanie przesyłania komunikatów UDP.

W polu main.c dodaj implementację funkcji sendUdp po właśnie dodanej 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 wiadomość UDP jest prawidłowa i przydzielona w buforze. Jeśli tak nie jest, funkcja sprawnie obsługuje błędy, przechodząc do bloku exit, gdzie zwolni się bufor.

Więcej informacji o funkcjach używanych do inicjowania UDP znajdziesz w odniesieniach do IPv6 i UDP w openthread.io.

DZIAŁANIE: zaimplementuj obsługę komunikatów UDP.

W polu main.c dodaj implementację funkcji handleUdpReceive po właśnie dodanej funkcji sendUdp. Ta funkcja po prostu przełącza diody 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: konfigurowanie sieci Thread

Chcemy, by urządzenia od razu uruchamiały Thread i łączyły się w sieci, gdy tylko będą włączone. W tym celu użyjemy struktury otOperationalDataset. Ta struktura zawiera wszystkie parametry potrzebne do przesyłania danych logowania do sieci Thread na urządzenie.

Użycie tej struktury spowoduje zastąpienie domyślnych ustawień sieci wbudowanych w OpenThread, co zwiększy bezpieczeństwo naszej aplikacji i ograniczy węzły Thread w naszej sieci tylko do tych, w których aplikacja jest uruchomiona.

Jeszcze raz 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ędniania.

W sekcji „include” na górze pliku main.c dodaj plik nagłówka interfejsu API, który będzie potrzebny do skonfigurowania sieci Thread:

#include <openthread/dataset_ftd.h>

DZIAŁANIE: dodaj deklarację funkcji, która określa konfigurację sieci.

Dodaj tę deklarację do elementu 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 wywołanie tej funkcji do funkcji main() po wywołaniu otSetStateChangedCallback. Ta funkcja konfiguruje sieciowy zbiór danych Thread.

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

DZIAŁANIE: Dodaj wywołania, aby włączyć interfejs sieci 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: wdrażanie konfiguracji sieci Thread.

W polu 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 informacjami w funkcji parametry sieci Thread, których używamy w tej aplikacji:

  • Kanał = 15
  • Identyfikator PAN = 0x2222
  • Rozszerzony identyfikator PAN = C0DE1AB5C0DE1AB5
  • Klucz sieciowy = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Nazwa sieci = OTCodelab

Dodatkowo ograniczamy zakłócenia w wyborze routera, aby nasze urządzenia szybciej zmieniały role w celach demonstracyjnych. Należy pamiętać, że dzieje się to tylko wtedy, gdy węzeł jest FTD (urządzenie z pełnym wątkiem). Więcej informacji na ten temat znajdziesz w następnym kroku.

9. API: funkcje z ograniczeniami

Niektóre interfejsy API OpenThread modyfikują ustawienia, które należy modyfikować tylko w celach demonstracyjnych lub testowych. Tych interfejsów API nie należy używać we wdrażaniu produkcyjnym aplikacji korzystających z OpenThread.

Funkcja otThreadSetRouterSelectionJitter na przykład dostosowuje czas (w sekundach), który musi upłynąć, zanim urządzenie końcowe przejdzie na router. Wartość domyślna to 120 (zgodnie ze specyfikacją wątku). Aby ułatwić korzystanie z tego ćwiczenia z programowania, zmienimy jego wartość na 20, dzięki czemu nie będzie trzeba długo czekać, aż węzeł Thread zmieni role.

Uwaga: urządzenia MTD nie są routerami, a kompilacja MTD nie obsługuje funkcji takiej jak otThreadSetRouterSelectionJitter. Później musimy określić opcję CMake -DOT_MTD=OFF. W przeciwnym razie wystąpi błąd kompilacji.

Możesz to sprawdzić, patrząc na definicję funkcji otThreadSetRouterSelectionJitter, która jest zawarta w dyrektywie wstępnego przetwarzania danych 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. Aktualizuj

Przed skompilowaniem aplikacji musisz wprowadzić kilka drobnych aktualizacji dla trzech plików CMake. Są one używane przez system kompilacji do skompilowania i połączenia aplikacji.

./third_party/NordicSemiconductor/CMakeLists.txt

Teraz dodaj kilka flag do klasy NordicSemiconductor CMakeLists.txt, aby mieć pewność, że funkcje GPIO są zdefiniowane w aplikacji.

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

Otwórz ./third_party/NordicSemiconductor/CMakeLists.txt w wybranym edytorze tekstu i dodaj te 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 gcloud do ./src/CMakeLists.txt pliku.

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

...

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

...

./third_party/NordicSemiconductor/CMakeLists.txt

Na koniec dodaj plik sterownika nrfx_gpiote.c do pliku NordicSemiconductor CMakeLists.txt, aby był dołączony do biblioteki sterowników Nordic.

DZIAŁANIE: Dodaj sterownik gpio do pliku NordicSemiconductor CMakeLists.txt .

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

...

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

11. Skonfiguruj urządzenia

Po zakończeniu wszystkich aktualizacji kodu możesz skompilować aplikację i wdrożyć ją na wszystkich 3 płytach deweloperskich Nordic nRF52840. Każde z nich będzie działać jako urządzenie typu full Thread (FTD).

Kompilowanie OpenThread

Utwórz pliki binarne OpenThread FTD dla platformy nRF52840.

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

Przejdź do katalogu z plikiem binarnym interfejsu wiersza poleceń OpenThread FTD i przekonwertuj go na format szesnastkowy za pomocą łańcucha narzędzi ARM Embedded Toolchain:

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

Rozświetl tablice

Umieść plik ot-cli-ftd.hex na każdej płytce nRF52840.

Podłącz kabel USB do portu debugowania micro USB obok zewnętrznego styku zasilania na płytce nRF52840, a następnie podłącz go do komputera z systemem Linux. Ustaw prawidłowo, LED5 jest włączona.

20a3b4b480356447.png

Tak jak wcześniej, zapisz numer seryjny płyty nRF52840:

c00d519ebec7e5f0.jpeg

Przejdź do lokalizacji narzędzi wiersza poleceń nRFx i prześlij plik szesnastkowy FTD interfejsu wiersza poleceń OpenThread na płytkę nRF52840, używając jej numeru seryjnego:

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

Dioda LED5 na chwilę zgaśnie podczas migania. Po pomyślnym zakończeniu generowania zostaną wygenerowane te dane wyjściowe:

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 tę czynność „Weź tablicę”. dla pozostałych 2 paneli. Każdą płytkę należy podłączyć do komputera z systemem Linux w ten sam sposób. Polecenie Flash jest takie samo, oprócz numeru seryjnego płytki. Użyj unikalnego numeru seryjnego każdej płytki

nrfjprog polecenie flash.

Jeśli się uda, na każdej płytce zaświecą się diody LED1, LED2 lub LED3. Wkrótce po mruganiu może się nawet pojawić przełącznik diody LED z 3 na 2 (lub 2 na 1) (funkcja zmiany roli urządzenia).

12. Funkcje aplikacji

Wszystkie 3 płyty nRF52840 powinny być teraz zasilane i uruchamiać naszą aplikację OpenThread. Jak pisaliśmy wcześniej, ta aplikacja ma 2 główne funkcje.

Wskaźniki roli urządzenia

Dioda LED na każdej płytce wskazuje aktualną rolę węzła Thread:

  • LED1 = lider
  • LED2 = Router
  • LED3 = Urządzenie końcowe

Wraz ze zmianą roli zmienia się podświetlona dioda LED. Te zmiany powinny być widoczne na tablicy w ciągu 20 sekund od włączenia każdego urządzenia.

Multicast UDP

Po naciśnięciu przycisku Button1 na tablicy wysyłana jest wiadomość UDP na lokalny adres multiemisji w sieci typu mesh, który obejmuje wszystkie pozostałe węzły w sieci Thread. W odpowiedzi na ten komunikat dioda LED 4 na wszystkich pozostałych tablicach włącza się lub wyłącza. Dioda LED4 pozostanie włączona lub wyłączona dla każdej płytki, dopóki nie otrzyma kolejnego komunikatu UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Prezentacja: obserwowanie zmian ról urządzenia

Wymienione urządzenia to specyficzny rodzaj urządzenia z pełnym wątkiem (FTD), który jest nazywany urządzeniem końcowym obsługującym router (REED). Oznacza to, że mogą one działać zarówno jako router, jak i urządzenie końcowe, a także przenieść się z urządzenia końcowego do routera.

Thread może obsługiwać do 32 routerów, ale stara się utrzymać ich liczbę między 16 a 23. Jeśli REED zostanie podłączony jako urządzenie końcowe, a liczba routerów jest mniejsza niż 16, automatycznie zmieni się w router. Ta zmiana powinna następować w losowym czasie w ciągu sekund ustawionych w aplikacji jako wartość otThreadSetRouterSelectionJitter (20 sekund).

Każda sieć Thread ma też router Leader, czyli router odpowiedzialny za zarządzanie zestawem routerów w sieci Thread. Gdy wszystkie urządzenia są włączone, po 20 sekundach jedno z nich powinno mieć status Leader (dioda LED 1 włączona), a pozostałe powinny być routerami (dioda LED 2 włączona).

4e1e885861a66570.png

Usuń lidera

Jeśli lider zostanie usunięty z sieci Thread, inny router awansuje się na niego, aby mieć pewność, że sieć nadal ma wskaźnik Leader.

Wyłącz tablicę wyników (tę z diodą LED1) za pomocą przełącznika zasilania. Zaczekaj około 20 sekund. Na jednej z dwóch pozostałych płytek LED2 (router) wyłączy się, a dioda LED1 (Leader) włączy się. To urządzenie jest teraz liderem sieci Thread.

4c57c87adb40e0e3.png

Włącz ponownie oryginalną tablicę wyników. Powinien automatycznie ponownie połączyć się z siecią Thread jako urządzenie końcowe (dioda LED 3 świeci). W ciągu 20 sekund (zakłócenie wyboru routera) urządzenie przejdzie do routera (dioda LED 2 świeci się).

5f40afca2dcc4b5b.png

Zresetuj tablice

Wyłącz wszystkie 3 płyty, włącz je z powrotem i zobacz, jak świecą się diody LED. Pierwsza włączona tablica powinna zacząć pełnić rolę lidera (świeci się dioda LED1) – pierwszy router w sieci Thread automatycznie staje się liderem.

Pozostałe 2 płytki początkowo łączą się z siecią jako urządzenia końcowe (dioda LED 3 świeci się), ale w ciągu 20 sekund powinny przejść do routerów (dioda LED 2 świeci się).

Partycje sieciowe

Jeśli do Twoich płyt nie dociera wystarczająca moc lub połączenie radiowe między nimi jest słabe, sieć Thread może podzielić się na partycje i więcej niż 1 urządzenie może mieć status lidera.

Wątek naprawia się samoczynnie, więc partycje powinny w końcu zostać scalone z powrotem w jedną partycję z jednym liderem.

14. Prezentacja: wysyłanie transmisji grupowej UDP

Jeśli kontynuujesz poprzednie ćwiczenie, na żadnym urządzeniu nie powinna świecić się dioda LED4.

Wybierz dowolną planszę i naciśnij Button1. Dioda LED4 na wszystkich pozostałych płytach w sieci Thread, w których działa aplikacja, powinna przełączyć swój stan. Jeśli będziesz kontynuować z poprzedniego ćwiczenia, powinny teraz być włączone.

f186a2618fdbe3fd.png

Naciśnij Button1 ponownie, by wyświetlić tę samą planszę. Dioda LED4 na wszystkich pozostałych płytach powinna się ponownie przełączyć.

Naciśnij Button1 na innej tablicy i zobacz, jak przełącza się dioda LED4 na pozostałych. Naciśnij Przycisk1 na jednej z płyt, na której włączona jest dioda LED4. Dioda LED4 pozostanie włączona dla tej tablicy, ale przełączy się na pozostałych.

f5865ccb8ab7aa34.png

Partycje sieciowe

Jeśli Twoje tablice są partycjonowane, a wśród nich jest więcej niż jeden lider, wynik transmisji zbiorczej będzie się różnić w zależności od tablic. Jeśli naciśniesz przycisk 1 na tablicy, która jest partycjonowana (i jest jedynym członkiem partycjonowanej sieci Thread), na innych płytkach nie zapali się dioda LED4 na innych płytkach. W takim przypadku zresetuj płyty. Najlepiej, gdyby zostały przekształcone w sieci z pojedynczym Thread, a wiadomości UDP powinny działać prawidłowo.

15. Gratulacje!

Udało Ci się utworzyć aplikację korzystającą z interfejsów OpenThread API.

Wiesz już:

  • Jak zaprogramować przyciski i diody LED na płytach deweloperskich Nordic nRF52840
  • Jak używać typowych interfejsów API OpenThread i klasy otInstance.
  • Jak monitorować zmiany stanu OpenThread i na nie reagować
  • Jak wysyłać wiadomości UDP na wszystkie urządzenia w sieci Thread
  • Jak modyfikować pliki Makefiles

Dalsze kroki

Opierając się na tym ćwiczeniu z programowania, spróbuj wykonać następujące ćwiczenia:

  • Zmodyfikuj moduł GPIO, tak aby używał pinów GPIO zamiast wbudowanych diod LED i podłącz zewnętrzne diody LED RGB, których kolor zależy od roli routera
  • Dodawanie obsługi GPIO dla innej przykładowej platformy
  • Zamiast używać multiemisji do pingowania wszystkich urządzeń po naciśnięciu przycisku, używaj interfejsu Router/Leader API do zlokalizowania i pingowania poszczególnych urządzeń
  • Połącz swoją sieć typu mesh z internetem, korzystając z routera granicowego OpenThread, i przesyłaj je w trybie multiemisji spoza sieci Thread, aby zapalać diody LED.

Więcej informacji

Na stronach openthread.io i GitHub znajdziesz różne zasoby OpenThread, w tym:

Numer referencyjny: