Programowanie z wykorzystaniem interfejsów OpenThread API

Informacje o tym ćwiczeniu (w Codelabs)
schedule60 minut
subjectOstatnia aktualizacja: 5 maja 2025
account_circleAutorzy: Jeff Bumgardner

1. Wprowadzenie

26b7f4f6b3ea0700.png

OpenThread, opracowany przez Nest, to implementacja protokołu sieciowego Thread® w wersji open source. Firma Nest udostępniła OpenThread, aby udostępnić programistom technologię używaną w produktach Nest i przyspieszyć rozwój produktów do domów połączonych.

Specyfikacja Thread definiuje niezawodny, bezpieczny i energooszczędny protokół komunikacji bezprzewodowej między urządzeniami na potrzeby zastosowań domowych. OpenThread obsługuje wszystkie warstwy sieciowe Thread, w tym IPv6, 6LoWPAN, IEEE 802.15.4 z zabezpieczeniami MAC, ustanowieniem połączenia w sieci Mesh i routingiem w sieci Mesh.

W tym ćwiczeniu w Codelab użyjesz interfejsów OpenThread API, aby uruchomić sieć Thread, monitorować i reagować na zmiany w rolach urządzenia oraz wysyłać wiadomości UDP. Wykorzystasz też te działania do obsługi przycisków i diod LED na rzeczywistym sprzęcie.

2a6db2e258c32237.png

Czego się nauczysz

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

Czego potrzebujesz

Sprzęt:

  • 3 płyty rozwojowe Nordic Semiconductor nRF52840
  • 3 kable USB-micro USB do łączenia płytek
  • komputer z Linuksem i co najmniej 3 portami USB;

Oprogramowanie:

  • GNU Toolchain
  • Narzędzia wiersza poleceń Nordic nRF5x
  • Oprogramowanie Segger J-Link
  • OpenThread
  • Git

O ile nie stwierdzono inaczej, zawartość tego Codelab jest objęta licencją Creative Commons Attribution 3.0, a próbki kodu – licencją Apache 2.0.

2. Pierwsze kroki

Ukończenie ćwiczeń z programowania dotyczących sprzętu

Zanim rozpoczniesz pracę z tym ćwiczeniem, wykonaj ćwiczenie Tworzenie sieci Thread za pomocą płytek nRF52840 i OpenThread, które:

  • szczegółowe informacje o oprogramowaniu potrzebnym do kompilacji i flashowania;
  • Dowiedz się, jak tworzyć i flashować OpenThread na płytkach Nordic nRF52840
  • Podstawy dotyczące sieci Thread

W tym Codelab nie ma szczegółowych informacji o konfiguracji środowiska potrzebnej do tworzenia OpenThread i flashowania płytek. Znajdziesz tu tylko podstawowe instrukcje flashowania płytek. Zakładamy, że masz już ukończone Codelab Tworzenie sieci Thread.

Urządzenie z systemem Linux

To Codelab zostało zaprojektowane do korzystania z komputera z systemem Linux i procesorem i386 lub x86 do flashowania wszystkich płytek rozwojowych Thread. Wszystkie czynności zostały przetestowane w Ubuntu 14.04.5 LTS (Trusty Tahr).

płyty Nordic Semiconductor nRF52840,

W tym ćwiczeniu Codelab używamy 3 płyt nRF52840 PDK.

a6693da3ce213856.png

Instalowanie oprogramowania

Aby kompilować i flashować OpenThread, musisz zainstalować SEGGER J-Link, narzędzia wiersza poleceń nRF5x, pakiet ARM GNU Toolchain oraz różne pakiety Linuxa. Jeśli zgodnie z wymaganiami ukończysz Codelab Tworzenie sieci Thread, będziesz mieć już zainstalowane wszystko, czego potrzebujesz. Jeśli nie, przed kontynuacją wykonaj to Codelab, aby mieć pewność, że możesz skompilować i przeflashować OpenThread na płytkach deweloperskich nRF52840.

3. Klonowanie repozytorium

OpenThread zawiera przykładowy kod aplikacji, który możesz wykorzystać jako punkt wyjścia w tym Codelab.

Sklonuj repozytorium przykładów OpenThread Nordic nRF528xx i skompiluj OpenThread:

$ 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 pod adresem ./openthread/include/openthread. Te interfejsy API zapewniają dostęp do różnych funkcji OpenThread na poziomie wątku i platformy, które można wykorzystywać w aplikacjach:

  • Informacje o instancjach OpenThread i ich kontrola
  • usługi aplikacji, takie jak IPv6, UDP i CoAP;
  • zarządzanie danymi logowania do sieci, a także role Komisarza i Uczestnika;
  • Zarządzanie routerem granicznym
  • Ulepszone funkcje, takie jak nadzór rodzicielski i wykrywanie zakłóceń

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

Korzystanie z interfejsu API

Aby korzystać z interfejsu API, dołącz jego plik nagłówka do jednego z plików aplikacji. Następnie wywołaj odpowiednią funkcję.

Przykładowa aplikacja CLI dołą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,

Struktury otInstance będziesz często używać podczas pracy z interfejsami OpenThread API. Po zainicjowaniu ta struktura reprezentuje stałą instancję biblioteki OpenThread i pozwala użytkownikowi wykonywać wywołania interfejsu OpenThread API.

Na przykład instancja OpenThread jest inicjowana 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 związane z danymi platformami

Jeśli chcesz dodać funkcje specyficzne dla danej platformy do jednej z przykładowych aplikacji dołączonych do 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 dla danej platformy. Dzięki temu możesz używać 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 do nRF52840, muszą być zadeklarowane w openthread-system.h.

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

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

DZIANIE: dodaj deklaracje funkcji GPIO dla konkretnej platformy.

Dodaj te 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);

Wprowadzimy je w następnym kroku.

Pamiętaj, że deklaracja funkcji otSysButtonProcess używa otInstance. Dzięki temu aplikacja może w razie potrzeby uzyskać dostęp do informacji o instancji OpenThread, gdy zostanie naciśnięty przycisk. Wszystko zależy od potrzeb Twojej aplikacji. Jeśli nie potrzebujesz go w implementacji funkcji, możesz użyć makra OT_UNUSED_VARIABLE z OpenThread API, aby pominąć błędy kompilacji dotyczące nieużywanych zmiennych w przypadku niektórych łańcuchów narzędzi. Przykłady tego omówimy później.

5. Implementacja abstrakcji platformy GPIO

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

  • Inicjowanie pinów i trybów GPIO
  • Sterowanie napięciem na pinie
  • Włączanie przerw GPIO i rejestrowanie wywołania zwrotnego

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

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

DZIANIE: dodaj definicje.

Te definicje służą jako abstrakcje między wartościami i zmiennymi specyficznymi dla 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 modułu nRF52840 znajdziesz w Infocenter Nordic Semiconductor.

DZIAŁANIE: dodaj nagłówek zawiera.

Następnie dodaj nagłówek z elementami, których potrzebujesz do obsługi 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"

DZIAJ: dodaj funkcję wywołania zwrotnego i funkcję przerywania dla przycisku 1.

Następnie dodaj ten kod. Funkcja in_pin1_handler to wywołanie zwrotne zarejestrowane podczas inicjowania funkcji naciśnięcia przycisku (później w tym pliku).

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

/* 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ę, aby ustawić tryb diody LED.

Ta funkcja będzie używana, gdy zmieni się rola 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 diody LED.

Ta funkcja służy do przełączania diody LED4, gdy urządzenie otrzyma wiadomość mUDP.

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

DZIANIE: dodaj funkcje inicjowania i przetwarzania naciśnięć przycisków.

Pierwsza funkcja inicjuje płytkę po naciśnięciu przycisku, a druga wysyła wiadomość multicast 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 gpio.c plik.

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

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

  • LED1 = Lider
  • LED2 = Router
  • LED3 = urządzenie końcowe

Aby umożliwić tę funkcję, aplikacja musi wiedzieć, kiedy zmieniła się rola urządzenia i jak włączyć odpowiedni diodę LED. W pierwszym przypadku użyjemy instancji OpenThread, a w drugim – abstrakcji platformy 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 includes pliku main.c dodaj pliki nagłówka interfejsu API, których potrzebujesz do funkcji zmiany roli.

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

DZIANIE: dodaj deklarację funkcji obsługi dla zmiany stanu instancji OpenThread.

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

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

DZIANIE: dodaj rejestrację wywołania zwrotnego dla funkcji obsługi zmiany stanu.

W pliku main.c dodaj tę funkcję do funkcji main() po wywołaniu funkcji otAppCliInit. Ta rejestracja wywołania zwrotnego instruuje OpenThread, aby wywoływał funkcję handleNetifStateChange, gdy tylko zmieni się stan instancji OpenThread.

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

DZIANIE: dodaj implementację zmiany stanu.

W pliku main.c po funkcji 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 diody LED za pomocą multicastu

W naszej aplikacji chcemy też wysyłać wiadomości UDP do wszystkich innych urządzeń w sieci, gdy na jednej płycie zostanie naciśnięty przycisk 1. Aby potwierdzić otrzymanie wiadomości, włączymy diodę LED4 na innych płytkach.

Aby umożliwić tę funkcję, aplikacja musi:

  • Inicjowanie połączenia UDP po uruchomieniu
  • wysyłać wiadomość UDP do adresu multicast lokalnego w sieci mesh;
  • Obsługa przychodzących wiadomości UDP
  • Włączanie i wyłączanie diody LED4 w reakcji na przychodzące wiadomości 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 includes u góry pliku main.c dodaj pliki nagłówka interfejsu API, których potrzebujesz do obsługi funkcji multicast UDP.

#include <string.h>

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

#include "utils/code_utils.h"

Nagłówek code_utils.h służy do obsługi makr otEXPECTotEXPECT_ACTION, które weryfikują warunki w czasie wykonywania i odpowiednio reagują na błędy.

DZIANIE: dodaj definicje i stałe:

W pliku main.c po sekcji includes i przed każdym poleceniem #if dodaj definicje i stałe związane z protokołem 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 lokalny w sieci mesh. Wszystkie wiadomości wysyłane na ten adres będą wysyłane do wszystkich urządzeń z pełnym wątkiem w sieci. Więcej informacji o obsługiwaniu multicastu w OpenThread znajdziesz na stronie Multicast na openthread.io.

DZIANIE: dodaj deklaracje funkcji.

W pliku main.c po definicji otTaskletsSignalPending i przed funkcją main() dodaj funkcje związane z 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;

DZIANIE: dodaj wywołania, aby zainicjować diody LED i przycisk GPIO.

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

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

DZIANIE: dodaj wywołanie inicjowania UDP.

W funkcji main.c dodaj tę funkcję do funkcji main() po wywołaniu funkcji otSysButtonInit, które właśnie dodano:

initUdp(instance);

Ten wywołanie zapewnia, że gniazdo UDP jest inicjowane po uruchomieniu aplikacji. Bez tego urządzenia nie mogą wysyłać ani odbierać wiadomości UDP.

DZIANIE: dodaj wywołanie do przetworzenia zdarzenia przycisku GPIO.

W funkcji main.c dodaj to wywołanie funkcji do funkcji main() po wywołaniu funkcji 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ł skonfigurowany w powyższym kroku.

otSysButtonProcess(instance);

DZIAŁANIE: zaimplementuj obsługę przerwania przycisku.

W pliku 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);
}

DZIANIE: zaimplementuj inicjalizację UDP.

W pliku 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 zdefiniowany wcześniej port (1212). Funkcja otUdpOpen otwiera gniazdo i rejestruje funkcję wywołania zwrotnego (handleUdpReceive) na wypadek otrzymania wiadomości UDP. otUdpBind łączy gniazdo z interfejsem sieci Thread, przekazując OT_NETIF_THREAD. Inne opcje interfejsu sieciowego znajdziesz w enumeracji otNetifIdentifierprzewodniku po interfejsie UDP API.

DZIAJ: zaimplementuj przesyłanie wiadomości UDP.

W pliku 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 otEXPECTotEXPECT_ACTION. Dzięki temu wiadomość UDP jest prawidłowa i prawidłowo przydzielona w buforze. Jeśli nie, funkcja łagodnie obsługuje błędy, przeskakując do bloku exit, gdzie zwalnia bufor.

Więcej informacji o funkcjach używanych do inicjowania UDP znajdziesz w dokumentacji IPv6UDP na stronie openthread.io.

DZIANIE: zaimplementuj obsługę wiadomości UDP.

W pliku main.c dodaj implementację funkcji handleUdpReceive po właśnie dodanej funkcji sendUdp. Ta funkcja po prostu włącza i wyłącza diodę 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 Thread i po połączeniu tworzyły sieć. W tym celu użyjemy struktury otOperationalDataset. Ta struktura zawiera wszystkie parametry potrzebne do przesłania danych logowania do sieci Thread na urządzenie.

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

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

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

DZIAŁANIE: dodaj nagłówek include.

W sekcji „includes” u góry pliku main.c dodaj plik nagłówka interfejsu API, którego potrzebujesz do skonfigurowania sieci Thread:

#include <openthread/dataset_ftd.h>

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

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

static void setNetworkConfiguration(otInstance *aInstance);

DZIAJ: dodaj wywołanie konfiguracji sieci.

W funkcji main.c dodaj to wywołanie funkcji do funkcji main() po wywołaniu funkcji otSetStateChangedCallback. Ta funkcja konfiguruje zbiór danych sieci Thread.

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

DZIANIE: dodaj wywołania, aby włączyć interfejs sieci i zbiór Thread.

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

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

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

DZIAJ: wdrożyć konfigurację sieci Thread.

W pliku main.c po funkcji main() dodaj implementację funkcji setNetworkConfiguration:

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

Jak opisano w funkcji, parametry sieci wątku, których używamy w ramach tej aplikacji, to:

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

Ponadto zmniejszamy jitter w wyborze routera, aby urządzenia szybciej zmieniały role na potrzeby demonstracji. Pamiętaj, że jest to możliwe tylko wtedy, gdy węzeł jest urządzeniem FTD (Full Thread Device). Więcej informacji na ten temat znajdziesz w następnym kroku.

9. Interfejs API: funkcje z ograniczeniami

Niektóre interfejsy API OpenThread modyfikują ustawienia, które powinny być zmieniane tylko na potrzeby demonstracji lub testów. Nie należy używać tych interfejsów API w produkcyjnym wdrożeniu aplikacji korzystającej z OpenThread.

Na przykład funkcja otThreadSetRouterSelectionJitter dostosowuje czas (w sekundach), jaki jest potrzebny urządzeniu końcowemu do promowania się do routera. Wartość domyślna to 120 (zgodnie ze specyfikacją Thread). Aby ułatwić Ci korzystanie z tego Codelab, zmienimy tę wartość na 20, dzięki czemu nie będziesz musiał(-a) długo czekać na zmianę ról węzła wątku.

Uwaga: urządzenia MTD nie stają się routerami, a obsługa funkcji takich jak otThreadSetRouterSelectionJitter nie jest uwzględniana w kompilacji MTD. Później musimy określić opcję CMake -DOT_MTD=OFF, w przeciwnym razie kompilacja zakończy się niepowodzeniem.

Możesz to sprawdzić, patrząc na definicję funkcji otThreadSetRouterSelectionJitter, która jest zawarta w instrukcji preprocesora 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. Aktualizacje CMake

Zanim skompilujesz aplikację, musisz wprowadzić kilka drobnych zmian w 3 plikach CMake. System kompilacji używa ich do kompilowania i linkowania aplikacji.

./third_party/NordicSemiconductor/CMakeLists.txt

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

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

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

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

Aby dodać nowy plik źródłowy gpio.c, otwórz plik ./src/CMakeLists.txt i zrób w nim to:

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

Otwórz plik ./src/CMakeLists.txt w preferowanym edytorze tekstu i dodaj go 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ł uwzględniony w kompilacji biblioteki sterowników Nordic.

DZIAJ: dodaj sterownik gpio do pliku CMakeLists.txt NordicSemiconductor.

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

...

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

11. Konfigurowanie urządzeń

Po wprowadzeniu wszystkich aktualizacji kodu możesz skompilować aplikację i przeflashować ją na wszystkich 3 płytach rozwojowych Nordic nRF52840. Każde urządzenie będzie działać jako urządzenie z pełnym wątkiem (FTD).

Tworzenie OpenThread

Utwórz pliki binarne OpenThread FTD na platformę nRF52840.

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

Otwórz katalog z binarnym plikiem OpenThread FTD CLI i konwertuj go na format szesnastkowy za pomocą zestawu narzędzi ARM Embedded:

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

wyświetlanie tablic,

Wgraj plik ot-cli-ftd.hex na każdą płytkę nRF52840.

Podłącz kabel USB do portu debugowania micro-USB obok pinu zasilania zewnętrznego na płycie nRF52840, a potem do komputera z Linuxem. Ustaw prawidłowo, LED5 jest włączony.

20a3b4b480356447.png

Podobnie jak poprzednio, zanotuj numer seryjny płytki nRF52840:

c00d519ebec7e5f0.jpeg

Przejdź do lokalizacji narzędzi wiersza poleceń nRFx i za pomocą numeru seryjnego płytki zapisz plik FTD OpenThread CLI na płytce nRF52840:

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

LED5 na chwilę wyłączy się podczas migania. Po pomyślnym wykonaniu polecenia zostanie wygenerowany następujący wynik:

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 „Flash the boards” (Aktywuj płyty) w przypadku pozostałych 2 płyt. Każda płytka powinna być podłączona do maszyny z Linuxem w taki sam sposób, a polecenie do flashowania jest takie samo, z wyjątkiem numeru seryjnego płytki. Upewnij się, że w

nrfjprog polecenie migania.

W przypadku powodzenia na każdej płycie zaświeci się dioda LED1, LED2 lub LED3. Możesz nawet zauważyć, że po chwili migania dioda LED zmieni kolor z 3 na 2 (lub z 2 na 1) (funkcja zmiany roli urządzenia).

12. Funkcje aplikacji

Wszystkie 3 karty nRF52840 powinny być teraz włączone i uruchomić aplikację OpenThread. Jak już wspomnieliśmy, ta aplikacja ma 2 główne funkcje.

Wskaźniki roli urządzenia

Świecąca dioda LED na każdej płycie odpowiada bieżącej roli węzła wątku:

  • LED1 = Lider
  • LED2 = Router
  • LED3 = urządzenie końcowe

Zmiany roli powodują zmiany w działaniu diody LED. Te zmiany powinny być widoczne na jednej lub dwóch tablicach w ciągu 20 sekund od włączenia każdego urządzenia.

Multicast UDP

Gdy na płycie zostanie naciśnięty przycisk 1, wiadomość UDP zostanie wysłana na adres multicast mesh-local, który obejmuje wszystkie inne węzły w sieci Thread. W odpowiedzi na tę wiadomość LED4 na wszystkich innych płytkach włącza się lub wyłącza. LED4 pozostaje włączony lub wyłączony na każdej płycie, dopóki nie otrzyma kolejnej wiadomości UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demonstracja: obserwowanie zmian roli urządzenia

Urządzenia, które zostały zaktualizowane, to specyficzny rodzaj pełnego urządzenia obsługującego Thread (FTD) zwanego urządzeniem końcowym z możliwością podłączenia do routera (REED). Oznacza to, że mogą działać jako router lub urządzenie końcowe i mogą przekształcić się z urządzenia końcowego w router.

Sieć Thread może obsługiwać do 32 routerów, ale stara się utrzymywać ich liczbę na poziomie od 16 do 23. Jeśli REED łączy się jako urządzenie końcowe, a liczba routerów jest mniejsza niż 16, automatycznie staje się routerem. Ta zmiana powinna nastąpić w losowym momencie w ciągu liczby sekund ustawionej w aplikacji jako wartość otThreadSetRouterSelectionJitter (20 sekund).

Każda sieć Thread ma też lidera, czyli router, który odpowiada za zarządzanie zestawem routerów w tej sieci. Po włączeniu wszystkich urządzeń po 20 sekundach jedno z nich powinno być liderem (LED1 włączony), a pozostałe dwa routerami (LED2 włączony).

4e1e885861a66570.png

Usuwanie lidera

Jeśli lider zostanie usunięty z sieci Thread, inny router zostanie awansowany do lidera, aby zapewnić sieci lidera.

Wyłącz tablicę liderów (ta z podświetlonym LED1) za pomocą przełącznika Power. Zaczekaj około 20 sekund. Na jednej z pozostałych 2 płytek dioda LED2 (Router) wyłączy się, a dioda LED1 (Leader) włączy się. To urządzenie jest teraz liderem sieci Thread.

4c57c87adb40e0e3.png

ponownie włączyć pierwotną tablicę wyników; Powinien on automatycznie ponownie dołączyć do sieci Thread jako urządzenie końcowe (LED3 jest podświetlony). W ciągu 20 sekund (Router Selection Jitter) awansuje do Routera (LED2 jest podświetlony).

5f40afca2dcc4b5b.png

Zresetuj tablice

Wyłącz wszystkie 3 płyty, a potem włącz je ponownie i obserwuj diody LED. Pierwsza płytka, która została włączona, powinna rozpocząć działanie w roli lidera (LED1 jest podświetlony). Pierwszy router w sieci Thread automatycznie staje się liderem.

Pozostałe 2 płyty początkowo łączą się z siecią jako urządzenia końcowe (dioda LED3 jest włączona), ale w ciągu 20 sekund powinny się przełączyć na routery (dioda LED2 jest włączona).

partycje sieciowe,

Jeśli płytki nie otrzymują wystarczającej ilości energii lub połączenie radiowe między nimi jest słabe, sieć Thread może się podzielić na partycje, a więcej niż jedno urządzenie może być wyświetlane jako lider.

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

14. Demonstracja: wysyłanie strumienia UDP

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

Wybierz dowolną tablicę i naciśnij przycisk 1. Diody LED4 na wszystkich innych płytkach w sieci Thread, na których działa aplikacja, powinny zmieniać stan. Jeśli kontynuujesz pracę z poprzedniego ćwiczenia, powinny być one włączone.

f186a2618fdbe3fd.png

Ponownie naciśnij przycisk 1, aby wybrać tę samą tablicę. LED4 na wszystkich pozostałych tablicach powinien się ponownie przełączyć.

Naciśnij przycisk 1 na innej płycie i obserwuj, jak dioda LED 4 włącza się i wyłącza na pozostałych płytach. Naciśnij przycisk 1 na jednej z kart, na której dioda LED4 jest włączona. LED4 pozostaje włączony na tej płycie, ale włącza się na pozostałych.

f5865ccb8ab7aa34.png

partycje sieciowe,

Jeśli Twoje tablice zostały podzielone i jest na nich więcej niż 1 kierownik, wynik wiadomości wielodostępnej będzie się różnić w zależności od tablicy. Jeśli naciśniesz przycisk 1 na płycie, która została podzielona (i jest jedynym elementem w podzielonej sieci Thread), dioda LED 4 na pozostałych płytach nie zaświeci się w odpowiedzi na to działanie. W takim przypadku zresetuj tablice. W idealnej sytuacji utworzą one pojedynczą sieć wątków, a wiadomości UDP powinny działać prawidłowo.

15. Gratulacje!

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

Wiesz już:

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

Dalsze kroki

Na podstawie tego ćwiczenia Codelab wykonaj te ćwiczenia:

  • Zmodyfikuj moduł GPIO, aby używać pinów GPIO zamiast wbudowanych diod LED, i podłącz zewnętrzne diody RGB, które zmieniają kolor w zależności od roli routera.
  • Dodawanie obsługi GPIO na innej przykładowej platformie
  • Zamiast korzystania z multicastu do pingowania wszystkich urządzeń po naciśnięciu przycisku, użyj interfejsu Router/Leader API, aby zlokalizować i wysłać ping do konkretnego urządzenia.
  • Połącz sieć mesh z internetem za pomocą routera OpenThread Border i prześlij je z zewnętrznej sieci Thread, aby włączyć diody LED.

Więcej informacji

Odwiedź stronę openthread.io i GitHub, aby znaleźć różne zasoby OpenThread, w tym:

Materiały referencyjne: