Programowanie z wykorzystaniem interfejsów OpenThread API

1. Wprowadzenie

26b7f4f6b3ea0700.png

OpenThread to udostępniona przez Nest implementacja open source protokołu sieciowego Thread®. Firma Nest udostępniła OpenThread, aby technologia używana w produktach Nest była powszechnie dostępna dla programistów i przyspieszyła rozwój produktów do domów inteligentnych.

Specyfikacja Thread definiuje niezawodny, bezpieczny i energooszczędny protokół komunikacji bezprzewodowej między urządzeniami oparty na IPv6, przeznaczony do zastosowań domowych. OpenThread implementuje wszystkie warstwy sieciowe Thread, w tym IPv6, 6LoWPAN, IEEE 802.15.4 z zabezpieczeniami MAC, ustanawianie połączeń siatkowych i routing siatkowy.

W tym ćwiczeniu w Codelabs użyjesz interfejsów OpenThread API, aby uruchomić sieć Thread, monitorować zmiany ról urządzeń i na nie reagować oraz wysyłać wiadomości UDP. Połączysz też 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łytkach deweloperskich Nordic nRF52840
  • Jak korzystać z popularnych interfejsów OpenThread API i klasy otInstance
  • Monitorowanie zmian stanu OpenThread i reagowanie na nie
  • Jak wysyłać wiadomości UDP do wszystkich urządzeń w sieci Thread
  • Jak modyfikować pliki Makefiles

Czego potrzebujesz

Sprzęt:

  • 3 płytki deweloperskie Nordic Semiconductor nRF52840
  • 3 kable USB do micro USB do podłączenia tablic
  • komputer z systemem Linux 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 Codelabu jest objęta licencją Creative Commons Uznanie autorstwa 3.0, a przykładowy kod – licencją Apache 2.0.

2. Pierwsze kroki

Ukończ ćwiczenia z programowania dotyczące sprzętu

Zanim rozpoczniesz to ćwiczenie, wykonaj ćwiczenie Tworzenie sieci Thread za pomocą płytek nRF52840 i OpenThread, które:

  • Zawiera wszystkie informacje o oprogramowaniu potrzebnym do tworzenia i flashowania.
  • Wyjaśnia, jak skompilować OpenThread i wgrać go na płytki Nordic nRF52840.
  • Wyjaśnia podstawy sieci Thread

W tym module Codelabs nie znajdziesz szczegółowych informacji o konfiguracji środowiska potrzebnego do skompilowania OpenThread i wgrania go na płytki. Znajdziesz tu tylko podstawowe instrukcje dotyczące wgrywania. Zakładamy, że masz już za sobą ćwiczenie programistyczne Tworzenie sieci Thread.

Urządzenie z systemem Linux

Te warsztaty zostały zaprojektowane z myślą o używaniu komputera z systemem Linux opartego na architekturze i386 lub x86 do flashowania wszystkich płytek deweloperskich Thread. Wszystkie kroki zostały przetestowane w systemie Ubuntu 14.04.5 LTS (Trusty Tahr).

Płytki Nordic Semiconductor nRF52840

W tym ćwiczeniu w Codelabs używane są 3 płytki nRF52840 PDK.

a6693da3ce213856.png

Instalowanie oprogramowania

Aby skompilować i wgrać OpenThread, musisz zainstalować SEGGER J-Link, narzędzia wiersza poleceń nRF5x, ARM GNU Toolchain i różne pakiety Linux. Jeśli zgodnie z instrukcjami ukończysz ćwiczenia z programowania dotyczące tworzenia sieci Thread, będziesz mieć już zainstalowane wszystko, czego potrzebujesz. Jeśli nie, wykonaj to ćwiczenie, zanim przejdziesz dalej, aby mieć pewność, że możesz tworzyć i wgrywać OpenThread na płytki deweloperskie nRF52840.

3. Klonowanie repozytorium

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

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 Thread i platformy, które można wykorzystać w aplikacjach:

  • Informacje o instancji OpenThread i sterowanie nią
  • usługi aplikacji, takie jak IPv6, UDP i CoAP;
  • zarządzanie danymi logowania do sieci, a także role Komisarz i Dołączający;
  • Zarządzanie routerem granicznym
  • ulepszone funkcje, takie jak nadzór nad dziećmi i wykrywanie zakłóceń;

Informacje referencyjne o wszystkich interfejsach OpenThread API są dostępne na stronie openthread.io/reference.

Korzystanie z interfejsu API

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

Na przykład 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

Struktura otInstance jest często używana podczas pracy z interfejsami OpenThread API. Po zainicjowaniu ta struktura reprezentuje statyczną instancję biblioteki OpenThread i umożliwia użytkownikowi wywoływanie interfejsu OpenThread API.

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

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

int main(int argc, char *argv[])
{
    otInstance *instance

...

#if OPENTHREAD_ENABLE_MULTIPLE_INSTANCES
    // Call to query the buffer size
    (void)otInstanceInit(NULL, &otInstanceBufferLength);

    // Call to allocate the buffer
    otInstanceBuffer = (uint8_t *)malloc(otInstanceBufferLength);
    assert(otInstanceBuffer);

    // Initialize OpenThread with the buffer
    instance = otInstanceInit(otInstanceBuffer, &otInstanceBufferLength);
#else
    instance = otInstanceInitSingle();
#endif

...

    return 0;
}

Funkcje specyficzne dla platformy

Jeśli chcesz dodać funkcje specyficzne dla 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 zaimplementuj je w pliku źródłowym dla danej platformy. W ten sposób możesz używać tych samych nagłówków funkcji na innych platformach przykładowych.

Na przykład funkcje GPIO, których użyjemy do podłączenia przycisków i diod LED 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

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

Po #include w nagłówku openthread/instance.h dodaj te deklaracje funkcji:

/**
 * 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 znaku otInstance. Dzięki temu aplikacja może w razie potrzeby uzyskać dostęp do informacji o instancji OpenThread po naciśnięciu przycisku. Wszystko zależy od potrzeb aplikacji. Jeśli nie jest Ci potrzebny w implementacji funkcji, możesz użyć makra OT_UNUSED_VARIABLE z interfejsu OpenThread API, aby uniknąć błędów kompilacji związanych z nieużywanymi zmiennymi w przypadku niektórych łańcuchów narzędzi. Przykłady tego zobaczysz później.

5. Implementowanie abstrakcji platformy GPIO

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

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

W katalogu ./src/src utwórz nowy plik o nazwie gpio.c. W nowym pliku dodaj te wiersze:

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

DZIAŁANIE: dodaj definicje.

Te definicje służą jako abstrakcje 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 Nordic Semiconductor.

DZIAŁANIE: dodaj pliki nagłówkowe.

Następnie dodaj pliki nagłówkowe, które będą potrzebne 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"

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, które jest rejestrowane podczas inicjowania funkcji naciśnięcia przycisku (w dalszej części tego pliku).

Zwróć uwagę, że w tym wywołaniu zwrotnym używane jest makro OT_UNUSED_VARIABLE, ponieważ zmienne przekazywane do in_pin1_handler nie są w rzeczywistości 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ę, która skonfiguruje diody 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ę, która ustawia 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 będzie używana do włączania i wyłączania diody LED4, gdy urządzenie otrzyma komunikat UDP multicast.

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

DZIAŁANIE: dodaj funkcje do inicjowania i przetwarzania naciśnięć przycisków.

Pierwsza funkcja inicjuje płytkę w odpowiedzi na naciśnięcie przycisku, a druga wysyła wiadomość UDP do wielu odbiorców 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 ról urządzeń

W naszej aplikacji chcemy, aby w zależności od roli urządzenia zapalały się różne diody LED. Śledźmy te 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 odpowiedzi na to włączyć odpowiednią diodę LED. W pierwszej części użyjemy instancji OpenThread, a w drugiej – 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 pliki nagłówkowe.

W sekcji includes pliku main.c dodaj pliki nagłówkowe interfejsu API, które będą potrzebne do funkcji zmiany roli.

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

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

Dodaj tę deklarację do main.c po nagłówkach i przed instrukcjami #if. 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 pliku main.c dodaj tę funkcję do funkcji main() po wywołaniu otAppCliInit. Ta rejestracja wywołania zwrotnego informuje OpenThread, że ma wywoływać funkcję handleNetifStateChange za każdym razem, gdy zmieni się stan instancji OpenThread.

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

DZIAŁANIE: dodaj implementację zmiany stanu.

W 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. API: włączanie diody LED za pomocą multiemisji

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

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

  • Inicjowanie połączenia UDP przy uruchamianiu
  • możliwość wysyłania wiadomości UDP na lokalny adres multicastowy sieci mesh;
  • Obsługa przychodzących wiadomości UDP
  • Przełączanie diody LED4 w odpowiedzi 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 pliki nagłówkowe.

W sekcji includes u góry pliku main.c dodaj pliki nagłówkowe interfejsu API, które będą potrzebne do korzystania z funkcji UDP multicast.

#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 otEXPECTotEXPECT_ACTION, które sprawdzają warunki w czasie działania i prawidłowo obsługują błędy.

DZIAŁANIE: dodaj definicje i stałe:

W pliku main.c po sekcji includes i przed instrukcjami #if dodaj stałe i definicje 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 lokalny adres multicast sieci mesh. Wszystkie wiadomości wysyłane na ten adres będą przesyłane do wszystkich urządzeń Full Thread w sieci. Więcej informacji o obsłudze multiemisji w OpenThread znajdziesz na stronie Multicast on openthread.io.

DZIAŁANIE: dodaj deklaracje funkcji.

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

W pliku main.c dodaj te wywołania funkcji do funkcji main() po wywołaniu otSetStateChangedCallback. Te funkcje inicjują piny GPIO i GPIOTE oraz ustawiają procedurę obsługi przycisku, która będzie obsługiwać zdarzenia naciśnięcia przycisku.

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

DZIAŁANIE: dodaj wywołanie inicjowania UDP.

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

initUdp(instance);

To wywołanie zapewnia zainicjowanie gniazda UDP przy uruchamianiu aplikacji. Bez tego urządzenie nie może wysyłać ani odbierać wiadomości UDP.

DZIAŁANIE: dodaj wywołanie do przetworzenia zdarzenia przycisku GPIO.

W pliku main.c dodaj to wywołanie funkcji 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 procedurę obsługi (handleButtonInterrupt) ustawioną w poprzednim kroku.

otSysButtonProcess(instance);

DZIAŁANIE: zaimplementuj procedurę obsługi 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);
}

DZIAŁANIE: wdróż inicjowanie UDP.

W pliku main.c dodaj implementację funkcji initUdp po dodanej 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 funkcję wywołania zwrotnego (handleUdpReceive), która jest wywoływana po otrzymaniu wiadomości UDP. otUdpBind wiąże gniazdo z interfejsem sieci Thread, przekazując OT_NETIF_THREAD. Inne opcje interfejsu sieciowego znajdziesz w wyliczeniu otNetifIdentifierdokumentacji interfejsu UDP API.

DZIAŁANIE: wdróż przesyłanie wiadomości UDP.

W pliku main.c dodaj implementację funkcji sendUdp po dodanej 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ść UDP jest prawidłowa i prawidłowo przydzielona w buforze. Jeśli nie, funkcja obsługuje błędy, przechodząc do bloku exit, w którym zwalnia bufor.

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

DZIAŁANIE: zaimplementuj obsługę wiadomości UDP.

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

Aby ułatwić demonstrację, chcemy, aby urządzenia natychmiast uruchamiały Thread i łączyły się w sieć po włączeniu. Użyjemy do tego otOperationalDataset. Ta struktura zawiera wszystkie parametry potrzebne do przesłania do urządzenia danych logowania do sieci Thread.

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

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

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

DZIAŁANIE: dodaj dołączenie nagłówka.

W sekcji includes u góry pliku main.c dodaj plik nagłówkowy interfejsu API, który będzie potrzebny 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łówkach i przed instrukcjami #if. Ta funkcja zostanie zdefiniowana po głównej funkcji aplikacji.

static void setNetworkConfiguration(otInstance *aInstance);

DZIAŁANIE: dodaj wywołanie konfiguracji sieci.

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

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

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

W pliku 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: wdróż konfigurację sieci Thread.

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

/**
 * Override default network settings, such as panid, so the devices can join a
 network
 */
void setNetworkConfiguration(otInstance *aInstance)
{
    static char          aNetworkName[] = "OTCodelab";
    otOperationalDataset aDataset;

    memset(&aDataset, 0, sizeof(otOperationalDataset));

    /*
     * Fields that can be configured in otOperationDataset to override defaults:
     *     Network Name, Mesh Local Prefix, Extended PAN ID, PAN ID, Delay Timer,
     *     Channel, Channel Mask Page 0, Network Key, PSKc, Security Policy
     */
    aDataset.mActiveTimestamp.mSeconds             = 1;
    aDataset.mActiveTimestamp.mTicks               = 0;
    aDataset.mActiveTimestamp.mAuthoritative       = false;
    aDataset.mComponents.mIsActiveTimestampPresent = true;

    /* Set Channel to 15 */
    aDataset.mChannel                      = 15;
    aDataset.mComponents.mIsChannelPresent = true;

    /* Set Pan ID to 2222 */
    aDataset.mPanId                      = (otPanId)0x2222;
    aDataset.mComponents.mIsPanIdPresent = true;

    /* Set Extended Pan ID to C0DE1AB5C0DE1AB5 */
    uint8_t extPanId[OT_EXT_PAN_ID_SIZE] = {0xC0, 0xDE, 0x1A, 0xB5, 0xC0, 0xDE, 0x1A, 0xB5};
    memcpy(aDataset.mExtendedPanId.m8, extPanId, sizeof(aDataset.mExtendedPanId));
    aDataset.mComponents.mIsExtendedPanIdPresent = true;

    /* Set network key to 1234C0DE1AB51234C0DE1AB51234C0DE */
    uint8_t key[OT_NETWORK_KEY_SIZE] = {0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE, 0x1A, 0xB5, 0x12, 0x34, 0xC0, 0xDE};
    memcpy(aDataset.mNetworkKey.m8, key, sizeof(aDataset.mNetworkKey));
    aDataset.mComponents.mIsNetworkKeyPresent = true;

    /* Set Network Name to OTCodelab */
    size_t length = strlen(aNetworkName);
    assert(length <= OT_NETWORK_NAME_MAX_SIZE);
    memcpy(aDataset.mNetworkName.m8, aNetworkName, length);
    aDataset.mComponents.mIsNetworkNamePresent = true;

    otDatasetSetActive(aInstance, &aDataset);
    /* Set the router selection jitter to override the 2 minute default.
       CLI cmd > routerselectionjitter 20
       Warning: For demo purposes only - not to be used in a real product */
    uint8_t jitterValue = 20;
    otThreadSetRouterSelectionJitter(aInstance, jitterValue);
}

Zgodnie z opisem funkcji parametry sieci Thread, których używamy w tej aplikacji, to:

  • Channel = 15
  • PAN ID = 0x2222
  • Rozszerzony identyfikator PAN = C0DE1AB5C0DE1AB5
  • Klucz sieciowy = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Network Name = OTCodelab

Dodatkowo zmniejszamy tu drgania wyboru routera, aby urządzenia szybciej zmieniały role na potrzeby demonstracji. Pamiętaj, że jest to wykonywane 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. Tych interfejsów API nie należy używać w środowisku produkcyjnym aplikacji korzystającej z OpenThread.

Na przykład funkcja otThreadSetRouterSelectionJitter dostosowuje czas (w sekundach), po którym urządzenie końcowe może awansować na router. Zgodnie ze specyfikacją Thread domyślna wartość to 120. Aby ułatwić korzystanie z tego laboratorium, zmienimy tę wartość na 20, dzięki czemu nie będziesz musiał długo czekać, aż węzeł Thread zmieni rolę.

Uwaga: urządzenia MTD nie stają się 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 kompilacja się nie powiedzie.

Możesz to sprawdzić, przyglądając się definicji funkcji otThreadSetRouterSelectionJitter, która znajduje się w dyrektywie 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 utworzysz aplikację, musisz wprowadzić kilka drobnych zmian w 3 plikach CMake. Są one używane przez system kompilacji do kompilowania i łączenia aplikacji.

./third_party/NordicSemiconductor/CMakeLists.txt

Teraz dodaj kilka flag do urządzenia 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 ulubionym 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 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 CMakeLists.txt NordicSemiconductor, aby był on uwzględniony w kompilacji biblioteki sterowników Nordic.

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

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 zmian w kodzie możesz skompilować aplikację i wgrać ją na wszystkie 3 płytki deweloperskie Nordic nRF52840. Każde urządzenie będzie działać jako urządzenie Thread o pełnej funkcjonalności (FTD).

Kompilowanie OpenThread

Skompiluj 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 binarnym plikiem CLI OpenThread FTD i przekonwertuj go na format szesnastkowy za pomocą ARM Embedded Toolchain:

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

Flash the boards

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łytce nRF52840, a następnie podłącz go do urządzenia z systemem Linux. Ustaw prawidłowo, LED5 jest włączona.

20a3b4b480356447.png

Jak poprzednio, zanotuj numer seryjny płytki nRF52840:

c00d519ebec7e5f0.jpeg

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

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

Podczas migania dioda LED5 na chwilę się wyłączy. Jeśli operacja się powiedzie, pojawią się 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 krok „Flash the boards” (Wgraj oprogramowanie na płytki) w przypadku pozostałych 2 płytek. Każda płytka powinna być połączona z urządzeniem z systemem Linux w ten sam sposób, a polecenie flashowania jest takie samo, z wyjątkiem numeru seryjnego płytki. Upewnij się, że używasz unikalnego numeru seryjnego każdej tablicy w

nrfjprog polecenie migania.

Jeśli się to uda, na każdej płytce zaświeci się dioda LED1, LED2 lub LED3. Wkrótce po błyskaniu dioda LED może zmienić kolor z 3 na 2 (lub z 2 na 1) (funkcja zmiany roli urządzenia).

12. Funkcje aplikacji

Wszystkie 3 płytki nRF52840 powinny być teraz zasilane i uruchamiać naszą 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łytce odzwierciedla bieżącą rolę węzła Thread:

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

Wraz ze zmianą roli zmienia się też podświetlenie diody LED. Zmiany powinny być widoczne na 1–2 tablicach w ciągu 20 sekund od włączenia każdego urządzenia.

Multicast UDP

Gdy na płytce zostanie naciśnięty przycisk Button1, do lokalnego adresu multicastowego sieci mesh zostanie wysłana wiadomość UDP, która obejmuje wszystkie inne węzły w sieci Thread. W odpowiedzi na otrzymanie tej wiadomości dioda LED4 na wszystkich pozostałych płytkach włącza się lub wyłącza. Dioda LED4 pozostaje włączona lub wyłączona na każdej płytce, dopóki nie otrzyma kolejnej wiadomości UDP.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Prezentacja: obserwowanie zmian ról urządzeń

Urządzenia, na których zainstalowano oprogramowanie, to specjalny rodzaj urządzenia FTD (Full Thread Device) o nazwie REED (Router Eligible End Device). Oznacza to, że mogą one pełnić funkcję routera lub urządzenia końcowego, a także mogą zmienić swój status z urządzenia końcowego na router.

Sieć Thread może obsługiwać do 32 routerów, ale staramy się, aby ich liczba wynosiła od 16 do 23. Jeśli urządzenie REED zostanie podłączone jako urządzenie końcowe, a liczba routerów będzie mniejsza niż 16, automatycznie zostanie ono przekształcone w router. Ta zmiana powinna nastąpić w losowym momencie w ciągu liczby sekund, na którą ustawisz wartość otThreadSetRouterSelectionJitter w aplikacji (20 sekund).

Każda sieć Thread ma też lidera, czyli router, który zarządza zestawem routerów w sieci Thread. Po włączeniu wszystkich urządzeń po 20 sekundach jedno z nich powinno być urządzeniem głównym (dioda LED1 włączona), a pozostałe dwa – routerami (dioda LED2 włączona).

4e1e885861a66570.png

Usuwanie lidera

Jeśli lider zostanie usunięty z sieci Thread, inny router awansuje na lidera, aby zapewnić, że sieć nadal ma lidera.

Wyłącz tablicę wyników (tę, na której świeci się dioda LED1) za pomocą przełącznika zasilania. Poczekaj około 20 sekund. Na jednej z pozostałych dwóch 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łącz oryginalną tabelę wyników. Powinien automatycznie ponownie dołączyć do sieci Thread jako urządzenie końcowe (dioda LED3 świeci). W ciągu 20 sekund (Router Selection Jitter) urządzenie promuje się na router (dioda LED2 świeci).

5f40afca2dcc4b5b.png

Resetowanie tablic

Wyłącz wszystkie 3 płytki, a następnie włącz je ponownie i obserwuj diody LED. Pierwsza włączona płytka powinna zacząć działać w roli lidera (dioda LED1 jest włączona) – 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 LED3 świeci), ale w ciągu 20 sekund powinny zostać przekształcone w routery (dioda LED2 świeci).

Partycje sieci

Jeśli Twoje urządzenia nie otrzymują wystarczającej ilości energii lub połączenie radiowe między nimi jest słabe, sieć Thread może podzielić się na partycje i może się okazać, że więcej niż jedno urządzenie jest liderem.

Sieć Thread sama się naprawia, więc partycje powinny się ostatecznie połączyć z powrotem w jedną partycję z jednym liderem.

14. Prezentacja: wysyłanie multiemisji UDP

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

Wybierz dowolną deskę i naciśnij przycisk 1. Dioda LED4 na wszystkich pozostałych płytkach w sieci Thread, na których działa aplikacja, powinna zmienić stan. Jeśli kontynuujesz poprzednie ćwiczenie, powinny być teraz włączone.

f186a2618fdbe3fd.png

Ponownie naciśnij przycisk 1 na tej samej tablicy. Dioda LED4 na wszystkich pozostałych płytkach powinna ponownie się przełączyć.

Naciśnij przycisk 1 na innej płytce i obserwuj, jak dioda LED4 przełącza się na pozostałych płytkach. Naciśnij przycisk 1 na jednej z płytek, na której dioda LED4 jest obecnie włączona. Dioda LED4 pozostaje włączona na tej tablicy, ale na pozostałych jest włączana i wyłączana.

f5865ccb8ab7aa34.png

Partycje sieci

Jeśli tablice są podzielone na partycje i jest wśród nich więcej niż 1 lider, wynik wiadomości multicast będzie się różnić w zależności od tablicy. Jeśli naciśniesz przycisk 1 na płytce, która jest podzielona (a tym samym jest jedynym członkiem podzielonej sieci Thread), dioda LED 4 na pozostałych płytkach nie zaświeci się w odpowiedzi. W takim przypadku zresetuj płytki. Powinny one utworzyć jedną sieć Thread, a komunikacja UDP powinna 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 deweloperskich Nordic nRF52840
  • Jak korzystać z popularnych interfejsów OpenThread API i klasy otInstance
  • Monitorowanie zmian stanu OpenThread i reagowanie na nie
  • Jak wysyłać wiadomości UDP do wszystkich urządzeń w sieci Thread
  • Jak modyfikować pliki Makefiles

Dalsze kroki

Wykonaj te ćwiczenia, korzystając z tego samouczka:

  • Zmodyfikuj moduł GPIO, aby używać pinów GPIO zamiast wbudowanych diod LED, i podłącz zewnętrzne diody LED RGB, które zmieniają kolor w zależności od roli routera.
  • Dodawanie obsługi GPIO dla innej platformy przykładowej
  • Zamiast używać multiemisji do wysyłania pingów do wszystkich urządzeń po naciśnięciu przycisku, użyj interfejsu Router/Leader API, aby zlokalizować i pingować poszczególne urządzenia.
  • Połącz sieć typu mesh z internetem za pomocą routera granicznego OpenThread i przesyłaj do niej komunikaty multicast z zewnątrz sieci Thread, aby włączać diody LED.

Więcej informacji

Na stronach openthread.ioGitHub znajdziesz różne materiały dotyczące OpenThread, w tym:

Źródło: