Programowanie z wykorzystaniem interfejsów API OpenThread

1. Wstęp

26b7f4f6b3ea0700.png

OpenThread udostępniany przez Nest to implementacja protokołu open source protokołu Thread®. Firma Nest udostępniła OpenThread, by umożliwić deweloperom szeroko dostępną technologię związaną z produktami Nest. Przyspiesza to tworzenie produktów dla inteligentnego domu.

Specyfikacja wątku definiuje w protokole komunikacyjnym między urządzeniami niezawodne, bezpieczne i bezprzewodowe protokół IPv6 na potrzeby aplikacji domowych. OpenThread wdraża wszystkie warstwy sieciowe Thread, w tym IPv6, 6LoWPAN, IEEE 802.15.4 z zabezpieczeniami MAC, ustanowieniem linku w sieci typu mesh i routingiem sieci typu mesh.

W ramach tego ćwiczenia użyjesz interfejsów API OpenThread, aby uruchamiać sieć Thread, sprawdzać i reagować na zmiany ról urządzeń oraz wysyłać wiadomości UDP, a także powiązać te czynności z przyciskami i diodami LED na prawdziwym sprzęcie.

2a6db2e258c32237.png

Czego się nauczysz

  • Jak zaprogramować przyciski i diody LED na płytkach programistycznych nRF52840
  • Jak używać popularnych interfejsów API OpenThread i klasy otInstance
  • Jak monitorować zmiany stanu OpenThread i reagować na nie
  • Jak wysyłać wiadomości UDP do wszystkich urządzeń w sieci Thread
  • Jak zmodyfikować elementy Makefiles

Czego potrzebujesz

Sprzęt:

  • Płyty dev-NrF52840 dla 3 północnych północnoprzewodników
  • 3 kable USB do micro-USB do podłączenia płytek.
  • Maszyna z systemem Linux z co najmniej 3 portami USB

Oprogramowanie:

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

O ile nie stwierdzono inaczej, treść tego ćwiczenia z ćwiczeniami z programowania jest objęta licencją Creative Commons Attribution 3.0, a próbki kodu są objęte licencją Apache 2.0.

2. Pierwsze kroki

Ukończ ćwiczenia z programowania

Zanim rozpoczniesz to ćwiczenia z programowania, wykonaj ćwiczenie tworzenia sieci wątków z tablicami nRF52840 i OpenThread, które:

  • Szczegółowe informacje o oprogramowaniu potrzebnym do tworzenia i błyskania
  • Naucz się przygotowywać OpenThread i błyskawicznie na płytkach Nordic nRF52840
  • Prezentuje podstawowe informacje o sieci Thread

W tym ćwiczeniu z programowania nie znajdziesz szczegółowo informacji na temat środowisk, które nie są wymagane do utworzenia OpenThread i Flasha. Zakładamy, że masz już za sobą ćwiczenia z programowania kompilacji w wątkach.

Maszyna z systemem Linux

To ćwiczenie zostało wykonane z użyciem komputera z systemem Linux opartego na i386 lub x86. Są one przeznaczone do flashowania wszystkich płytek programistycznych Thread. Wszystkie kroki zostały przetestowane na systemie Ubuntu 14.04.5 LTS (Trusty Tahr).

Płyty północnoprzewodnikowe nRF52840

W ćwiczeniach z programowania korzystasz z 3 pamięci PDR55840.

A6693da3ce213856.png

Zainstaluj oprogramowanie

Aby skompilować i uruchomić oprogramowanie FlashThread, należy zainstalować SEGGER J-Link, narzędzia nRF5x Command Line, ARM GNU Toolchain i różne pakiety Linux. Jeżeli masz już za sobą ćwiczenia z programowania kompilacji sieci zgodnie z wymaganiami, masz już wszystko, czego potrzebujesz. Jeśli nie, dokończ ćwiczenia z programowania, aby mieć pewność, że możesz kompilować i dodawać obiekty FlashThread to nRF52840.

3. Kopiowanie repozytorium

W Wtyce znajdziesz przykładowy kod aplikacji, który możesz wykorzystać jako punkt wyjścia do ćwiczeń z programowania.

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

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

4. Podstawowe informacje dotyczące interfejsu OpenThread API

Publiczne interfejsy API OpenThread znajdują się pod adresem ./openthread/include/openthread w repozytorium OpenThread. Te interfejsy API zapewniają dostęp do różnych funkcji OpenThread, zarówno na poziomie wątku, jak i platformy. Można ich używać w aplikacjach:

  • Informacje o instancji OpenControl i kontrola nad nią
  • Usługi aplikacji, takie jak IPv6, UDP i CoAP
  • Zarządzanie danymi logowania w sieci wraz z rolami Komisarza i Połączenie
  • Zarządzanie routerem granicznym
  • Funkcje rozszerzone, takie jak nadzór rodzicielski 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 korzystać z 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ń włączona 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 jest często wykorzystywana podczas pracy z interfejsami API OpenThread. Po zainicjowaniu ta struktura reprezentuje statyczną instancję biblioteki OpenThread i umożliwia użytkownikowi wykonywanie wywołań interfejsu OpenThread API.

Na przykład instancja OpenThread została zainicjowana w funkcji main() w 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 platformy

Jeśli chcesz dodać funkcje typowe dla platformy 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 zaimplementuj je w pliku źródłowym danej 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óre będą używane do łączenia się z przyciskami nRF52840 i diodami LED, należy zadeklarować w polu openthread-system.h.

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

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

DZIAŁANIE: dodaj deklaracje funkcji GPIO dla danej platformy.

Dodaj następujące deklaracje funkcji po #include dla nagłówka 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 wykorzystuje otInstance. Dzięki temu w razie potrzeby po kliknięciu przycisku aplikacja uzyska dostęp do informacji o instancji OpenThread. To zależy od potrzeb Twojej aplikacji. Jeśli nie potrzebujesz go w 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. Później zobaczysz przykłady.

5. Wdrażanie abstrakcji platformy GPIO

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

  • Zainicjuj kody PIN i tryby 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 następującą treść.

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

DZIAŁANIE: dodaj definicję.

Te definicje służą do rozróżniania wartości i zmiennych nRF52840 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 na temat przycisków i diod LED nRF52840 znajdziesz w centrum informacji o półprzewodnikach Nordic.

ACTION: dodaj nagłówek, który zawiera

Następnie dodaj nagłówek „Ty” potrzebujesz 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 przerwania w przypadku przycisku 1.

Następnie dodaj ten kod. Funkcja in_pin1_handler to wywołanie zwrotne, które jest rejestrowane, gdy zostanie zainicjowane działanie przycisku (później w tym pliku).

Pamiętaj, że to wywołanie zwrotne używa makra OT_UNUSED_VARIABLE, ponieważ zmienne przekazywane do in_pin1_handler nie są faktycznie 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, by skonfigurować tryb i stan wszystkich diod 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);
}

ACTION: Dodaj funkcję, aby ustawić 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ę zmieniającą tryb diody LED.

Ta funkcja będzie używana do przełączania diod LED4, gdy urządzenie otrzyma multiemisję wiadomości 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;
    }
}

CZYNNOŚĆ: dodaj funkcje inicjowania i przetwarzania naciśnięć przycisku.

Pierwsza z nich inicjuje tablicę po naciśnięciu przycisku, a następnie wysyła multiemisję wiadomości UDP 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: reagować na zmiany ról urządzenia

W mojej aplikacji chcesz, żeby różne diody LED świeciły w zależności od roli urządzenia. Możesz śledzić następujące role: Lider, Router, Urządzenie końcowe. Możemy przypisać je 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 odpowiedzi. W pierwszej części użyjemy instancji OpenThread, a w drugiej – abstrakcji platformy GPIO.

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

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

ACTION: dodaj nagłówek, który zawiera

W sekcji „include” pliku 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 obsługi obsługi zmiany stanu instancji OpenThread.

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

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 informuje funkcję OpenThread, aby wywoływała funkcję handleNetifStateChange za każdym razem, gdy zmieni się stan 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 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: używanie trybu multicast do włączania diody LED

W naszej aplikacji chcemy też wysyłać wiadomości UDP do wszystkich innych urządzeń w sieci po naciśnięciu przycisku 1 na jednej z platform. Aby potwierdzić odbiór wiadomości, w reakcji na pozostałe płytki wyłączymy LED4.

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

  • Uruchamianie połączenia UDP po uruchomieniu
  • Wysyłanie wiadomości UDP na adres multicast sieci typu mesh
  • Obsługa przychodzących wiadomości UDP
  • Włącz lub wyłącz LED4 w odpowiedzi na przychodzące wiadomości UDP

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

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

ACTION: dodaj nagłówek, który zawiera

W sekcji „includes” (zawiera) u góry pliku main.c dodaj pliki nagłówka interfejsu API, których będziesz potrzebować w przypadku 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 weryfikują warunki działania i płynnie obsługują błędy.

DZIAŁANIE: dodaj definicje i stałe:

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

#define UDP_PORT 1212

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

ff03::1 to adres multicast sieci typu mesh. Wszystkie wiadomości wysyłane na ten adres będą wysyłane do wszystkich urządzeń w całej sieci. Więcej informacji o obsłudze multiemisji w OpenThread znajdziesz w artykule Multicast na 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, a także 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ń przekazywania przycisku.

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

ACTION: dodaj wywołanie inicjowania UDP.

W main.c dodaj tę funkcję do funkcji main() po nowo dodanym wywołaniu otSysButtonInit:

initUdp(instance);

To wywołanie gwarantuje zainicjowanie gniazda UDP po uruchomieniu aplikacji. Bez tego urządzenia nie można wysyłać ani odbierać wiadomości UDP.

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

W main.c dodaj tę funkcję do funkcji main() po wywołaniu otSysProcessDrivers w pętli while. Ta funkcja zadeklarowana w gpio.c sprawdza, 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: Wdrażanie modułu zakłócającego działanie przycisku.

W sekcji 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 funkcję initUdp po dodaniu przed chwilą 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 otrzymania wiadomości UDP. otUdpBind łączy gniazdo z interfejsem sieciowym wątku, przekazując OT_NETIF_THREAD. Informacje o innych opcjach interfejsu sieciowego znajdziesz w artykule otNetifIdentifier na stronie UDP API Reference (Informacje o interfejsie API UDP).

DZIAŁANIE: Zaimplementuj wiadomości przez UDP.

W main.c dodaj funkcję sendUdp po dodaniu przed chwilą 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ści UDP są prawidłowe i przypisane do bufora, a jeśli nie, funkcja ta płynnie obsługuje błędy, przeskakując do bloku exit, gdzie zwalnia bufor.

Więcej informacji o funkcjach inicjowanych przez UDP znajdziesz w dokumentacji IPv6 i UDP na stronie openthread.io.

Działanie: wdrożenie obsługi wiadomości UDP.

W main.c dodaj implementację funkcji handleUdpReceive po nowo dodanej funkcji sendUdp. Ta funkcja włącza po prostu 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

Aby ułatwić demonstrację, chcemy, aby nasze urządzenia od razu uruchamiały wątek i łączyły się, gdy są włączone. W tym celu użyjemy struktury otOperationalDataset. Ta struktura zawiera wszystkie parametry potrzebne do przesłania danych logowania w sieci do urządzenia.

Użycie tej struktury zastąpi ustawienia domyślne sieci wbudowane w OpenThread, by poprawić bezpieczeństwo aplikacji i ograniczyć węzły Thread w naszej sieci tylko do tych, które działają w aplikacji.

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

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

DZIAŁANIE: Dodaj nagłówek

W sekcji include u góry pliku main.c dodaj plik nagłówka interfejsu API, który musisz skonfigurować w wątku:

#include <openthread/dataset_ftd.h>

ACTION: dodaj deklarację funkcji do konfigurowania sieci.

Dodaj tę deklarację do tagu main.c, po nagłówku zawierającym instrukcje #if i przed nimi. Ta funkcja zostanie zdefiniowana za główną funkcją 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 konfiguruje zbiór danych z sieci dla wątków.

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

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

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żaj konfigurację sieci 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 informacją o tej funkcji parametry sieci Thread, których używamy w tej aplikacji, to:

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

Dodatkowo w tym miejscu zmniejszamy wpływ zakłóceń w wyborze routera, więc nasze urządzenia szybciej zmieniają role w celach demonstracyjnych. Pamiętaj, że jest to możliwe tylko wtedy, gdy węzeł jest urządzeniem FTD (Full Thread Device). Więcej informacji znajdziesz w następnym kroku.

9. Interfejs API: funkcje z ograniczeniami

Niektóre interfejsy API OpenThread&#39's modyfikują ustawienia, które należy modyfikować tylko do celów demonstracyjnych lub testowania. Tych interfejsów API nie należy używać w środowisku produkcyjnym aplikacji z użyciem OpenThread.

Na przykład funkcja otThreadSetRouterSelectionJitter dostosowuje czas (w sekundach), który musi upłynąć, aby urządzenie końcowe awansowało się na router. Wartość domyślna to 120, zgodnie ze specyfikacją wątku. Aby można było łatwo korzystać z tych ćwiczeń z programowania, zmienimy je na 20, dlatego nie będzie trzeba długo czekać na zmianę ról w wątku.

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

Możesz to potwierdzić, sprawdzając definicję funkcji otThreadSetRouterSelectionJitter, która znajduje się w dyrektywie preprocessora 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. Zaktualizuj

Zanim utworzysz aplikację, musisz wykonać kilka drobnych aktualizacji w przypadku 3 plików CMake. Są one używane przez system kompilacji do kompilowania i łączenia aplikacji.

./third_party/NordicSemiconductor/CMakeLists.txt

Teraz dodaj kilka flag do NordicSemiconduct CMakeLists.txt, aby mieć pewność, że funkcje GPIO zostały zdefiniowane w aplikacji.

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

Otwórz ./third_party/NordicSemiconductor/CMakeLists.txt w dowolnym 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:

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

Otwórz ./src/CMakeLists.txt w dowolnym 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 NordicSemiconoror CMakeLists.txt, aby można go było uwzględnić w kompilacji biblioteki sterowników nordyckich.

DZIAŁANIE: Dodaj sterownik gpio do pliku NordicSemiconductorCMakeLists.txt.

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

...

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

11. Skonfiguruj urządzenia

Po zaktualizowaniu kodu możesz utworzyć i uruchomić aplikację na wszystkich trzech tablicach Nord nRF52840. Każde urządzenie będzie działać jako urządzenie z wątkiem.

Utwórz OpenThread

Utwórz pliki binarne binarne 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 CLI OpenThread FTD i przekonwertuj go na format szesnastkowy w łańcuchu narzędzi ARM.

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

Płytki

Umieść plik ot-cli-ftd.hex na każdej tablicy nRF52840.

Podłącz kabel USB do portu debugowania USB micro USB obok zewnętrznego gniazda zasilania na karcie nRF52840, a potem podłącz go do komputera z systemem Linux. Prawidłowo ustaw przełącznik LED5.

20a3b4b480356447

Zapisz numer seryjny płyty nRF52840:

C00D519ebec7e5f0.jpeg

Przejdź do lokalizacji narzędzi wiersza poleceń nRFx i uruchom plik szesnastkowy OpenThread CLI FTD na płytce nRF52840, używając numeru seryjnego tablicy Board:

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

Podczas błyskania LED5 wyłącza się na chwilę. W przypadku sukcesu generowane są te dane:

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 te kroki „&” („Miksuj tablice”) i dodaj je do pozostałych płytek. Każda płytka powinna być podłączona do komputera z systemem Linux w ten sam sposób, a polecenie Flash jest takie samo, z wyjątkiem numeru seryjnego tablicy. Pamiętaj, aby używać niepowtarzalnego numeru seryjnego każdej płyty w

nrfjprog polecenie błyskania.

Jeśli wszystko będzie w porządku, na każdej płytce będzie świecić się dioda LED1, LED2 lub LED3. Krótko przed podświetleniem przełącznika LED może pojawić się 3 lub 2 (lub od 2 do 1).

12. Funkcje aplikacji

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

Wskaźniki roli urządzenia

Dioda LED na każdej tablicy odzwierciedla bieżącą rolę węzła w wątku:

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

W miarę zmiany roli zmienia się dioda LED. Zmiany te powinny być już widoczne na tablicy lub w ciągu 20 sekund od momentu ponownego uruchomienia urządzenia.

multiemisja UDP

Po naciśnięciu na tablicy przycisku 1 wiadomość UDP jest wysyłana na adres multicast sieci lokalnej typu mesh, co obejmuje pozostałe węzły w sieci Thread. W odpowiedzi na ten komunikat dioda LED 4 na wszystkich pozostałych płytach włącza się lub wyłącza.4

203dd094aca1f97.png

9bbd96d9b1c63504.png

13. Wersja demonstracyjna: obserwowanie zmian roli urządzenia

Miganie urządzeń to konkretny typ urządzenia z wątkiem Full Thread (FTD), zwanego urządzeniem spełniającym wymagania routera (REED). Oznacza to, że mogą działać jako router lub urządzenie końcowe i mogą się wzajemnie reklamować.

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 jest podłączone jako urządzenie końcowe, a liczba routerów jest mniejsza niż 16, automatycznie zmienia się na router. Ta zmiana powinna nastąpić w losowym momencie w ciągu sekund ustawionych przez Ciebie w aplikacji otThreadSetRouterSelectionJitter (20 sekund).

Każda sieć Thread ma również lidera, czyli router, który odpowiada za zarządzanie zestawem routerów w sieci Thread. Gdy wszystkie urządzenia są włączone, po 20 sekundach tylko jedno powinno być liderem (LD1 włączone), a drugie to routery (LED2 włączone).

4e1e885861a66570.png

Usuń lidera

Jeśli Lider zostanie usunięty z sieci Threader, jego inny awansuje na siebie.

Wyłącz tablicę lidera (tę, która świeci się przez LED1) przełącznikiem Zasilanie. Zaczekaj około 20 sekund. Na jednej z pozostałych płytek LED2 (router) wyłączy się, a dioda LED1 (lider) zostanie włączona. To urządzenie jest teraz liderem sieci Thread.

4c57c87adb40e0e3.png

Włącz pierwotną tablicę liderów. Urządzenie powinno automatycznie ponownie połączyć się z siecią Thread jako urządzenie końcowe (dioda LED jest podświetlona). W ciągu 20 sekund (zakłócenie wyboru routera) zmienia się w router (dioda LED 2 jest podświetlona).

5f40afca2dcc4b5b.png

Zresetuj tablice

Wyłącz wszystkie 3 płyty, włącz je ponownie i obserwuj diody LED. Pierwsza włączona tablica powinna mieć rolę lidera (LED1 – świeci się) – pierwszy router w sieci z wątkiem automatycznie staje się liderem.

Pozostałe 2 płytki początkowo łączą się z siecią w taki sposób, jak urządzenia końcowe (przy oświetleniu LEDD) świecą się, ale w ciągu 20 sekund powinny zmienić się na Routery (LED2).

Partycje sieciowe

Jeśli płytki nie są wystarczająco zasilane lub połączenie między nimi jest słabe, sieć Thread może zostać podzielona na partycje i więcej niż jedno urządzenie może być oznaczone jako lider.

Wątek naprawia się samoistnie, więc partycje z czasem powinny się ponownie przekształcić w pojedynczą partycję z jednym liderem.

14. Prezentacja: wysyłanie multiemisji UDP

Jeśli przejdziesz do poprzedniego ćwiczenia, dioda LED4 nie powinna świecić się na żadnym urządzeniu.

Wybierz dowolną planszę i naciśnij przycisk 1. Na wszystkich pozostałych płytach w sieci wątków uruchomionych przez aplikację aplikacja powinna przełączyć stan. Jeśli przejdziesz do poprzedniego ćwiczenia, powinny być teraz włączone.

F186a2618fdbe3fd.png

Ponownie naciśnij przycisk 1 na tej samej tablicy. Na wszystkich pozostałych płytach LED4 włączą się ponownie.

Naciśnij przycisk 1 na innej płytce i zobacz, jak diody LED4 włączają się na innych tablicach. Naciśnij przycisk 1 na tablicy, w której dioda LED4 jest obecnie włączona. Dioda LED4 pozostaje włączona na tej tablicy, ale włącza się na pozostałych.

F5865ccb8ab7aa34.png

Partycje sieciowe

Jeśli tablice są podzielone na partycje, a wśród nich jest więcej niż lider, wynik komunikatu multicast będzie się różnić w zależności od tablicy. Jeśli na płytce, która ma partycję (i jedyny jej element jest podzielony na partycje), naciśnij przycisk 1, dioda LED4 na innych płytach nie będzie świecić w odpowiedzi. Jeśli tak się stanie, zresetuj tablice. Najlepiej będzie zreformować jedną sieć Thread, a przesyłanie wiadomości UDP powinno działać prawidłowo.

15. Gratulacje!

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

Teraz wiesz już:

  • Jak zaprogramować przyciski i diody LED na płytkach programistycznych nRF52840
  • Jak używać popularnych interfejsów API OpenThread i klasy otInstance
  • Jak monitorować zmiany stanu OpenThread i reagować na nie
  • Jak wysyłać wiadomości UDP do wszystkich urządzeń w sieci Thread
  • Jak zmodyfikować elementy Makefiles

Dalsze kroki

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

  • Zmodyfikuj moduł GPIO, aby używać pinezek GPIO zamiast wbudowanych diod LED, i podłącz zewnętrzne diody RGB, które zmieniają kolor w zależności od roli routera
  • Dodaj obsługę GPIO na innej przykładowej platformie
  • Zamiast używać funkcji multicast do wysyłania pingów do wszystkich urządzeń za pomocą przycisku, użyj interfejsu Router/Leader API, aby zlokalizować i wysłać ping do pojedynczego urządzenia.
  • Połącz sieć typu mesh z internetem za pomocą routera gratowego OpenThread i transmituj je spoza sieci nici, aby zapalić diody LED

Więcej informacji

Zapoznaj się z materiałami openthread.io i GitHub. Znajdziesz tam wiele materiałów dotyczących OpenThread, w tym:

Źródła wiedzy: