Mit OpenThread APIs entwickeln

1. Einführung

26b7f4f6b3ea0700.png

OpenThread von Nest ist eine Open-Source-Implementierung des Netzwerkprotokolls Thread®. Nest hat OpenThread veröffentlicht, um die Technologie, die in Nest-Produkten zum Einsatz kommt, Entwicklern zur Verfügung gestellt, um die Entwicklung von Produkten für das Smart Home zu beschleunigen.

Die Thread-Spezifikation definiert ein IPv6-basiertes zuverlässiges, sicheres und leistungsstarkes Kommunikationsprotokoll für Geräte zu Hause. OpenThread implementiert alle Thread-Netzwerkebenen, einschließlich IPv6, 6LoWPAN, IEEE 802.15.4 mit MAC-Sicherheit, Mesh-Link-Einrichtung und Mesh-Routing.

In diesem Codelab verwenden Sie OpenThread APIs, um ein Thread-Netzwerk zu starten, Änderungen an Geräterollen zu überwachen und darauf zu reagieren sowie UDP-Nachrichten zu senden und diese Aktionen mit Schaltflächen und LEDs auf echter Hardware zu verknüpfen.

2a6db2e258c32237.png

Lerninhalte

  • Tasten und LEDs auf den Nordic nRF52840-Entwicklerboards programmieren
  • Gemeinsame OpenThread APIs und die otInstance-Klasse verwenden
  • OpenThread-Statusänderungen überwachen und darauf reagieren
  • So senden Sie UDP-Nachrichten an alle Geräte in einem Thread-Netzwerk
  • Makefiles ändern

Voraussetzungen

Hardware:

  • 3 Nordische Halbleiter-nRF52840-Entwicklungsboards
  • 3 USB-zu-Micro-USB-Kabel zum Verbinden der Leiterplatten
  • Linux-Computer mit mindestens 3 USB-Ports

Software:

  • GNU-Toolkette
  • Nordische nRF5x-Befehlszeilentools
  • Segger-J-Link-Software
  • OpenThread
  • Git

Sofern nicht anders angegeben, sind die Inhalte dieses Codelabs unter der Creative Commons-Lizenz „Namensnennung 3.0“ lizenziert und Codebeispiele sind unter der Apache 2.0-Lizenz lizenziert.

2. Erste Schritte

Schließe das Hardware-Codelab ab.

Bevor Sie dieses Codelab starten, sollten Sie das Codelab Build a Thread Network with nRF52840 Boards and OpenThread erstellen, das:

  • Detaillierte Informationen zur Software, die Sie zum Erstellen und Flashen benötigen
  • Zeigt, wie Sie OpenThread erstellen und auf nordischen nRF52840-Boards blitzen
  • Veranschaulicht die Grundlagen eines Thread-Netzwerks

Keine der für das Erstellen von OpenThread und das Flashing der Boards erforderlichen Umgebungen ist in diesem Codelab beschrieben. Sie enthält nur eine grundlegende Anleitung zum Flashen. Es wird davon ausgegangen, dass Sie bereits das Codelab für „Thread-Netzwerk erstellen“ abgeschlossen haben.

Linux-Computer

Dieses Codelab wurde entwickelt, um auf einem i386- oder x86-basierten Linux-Computer alle Thread-Entwicklungsboards zu blinken zu lassen. Alle Schritte wurden unter Ubuntu 14.04.5 LTS (Trusty Tahr) getestet.

Nordic Semiductor nRF52840-Boards

Dieses Codelab verwendet drei nRF52840 PDK-Boards.

a6693da3ce213856

Software installieren

Zum Erstellen und Flashen von OpenThread müssen Sie SEGGER J-Link, die nRF5x-Befehlszeilentools, die ARM GNU Toolchain und verschiedene Linux-Pakete installieren. Wenn Sie das Codelab für das Thread-Netzwerk erstellt haben, haben Sie alles, was Sie installieren müssen. Wenn nicht, schließen Sie dieses Codelab ab, bevor Sie fortfahren. Prüfen Sie dann, ob Sie OpenThread erstellen und auf nRF52840-Entwicklerboards weiterleiten können.

3. Repository klonen

OpenThread enthält einen Beispielanwendungscode, den Sie als Ausgangspunkt für dieses Codelab verwenden können.

Klonen Sie das Repository „OpenThread Nordic nRF528xx“ und erstellen Sie OpenThread:

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

4. OpenThread API – Grundlagen

Die öffentlichen APIs von OpenThread finden Sie unter ./openthread/include/openthread im OpenThread-Repository. Diese APIs bieten Zugriff auf eine Vielzahl von OpenThread-Funktionen auf Thread- und Plattformebene zur Verwendung in Ihren Anwendungen:

  • OpenThread-Instanzinformationen und -Steuerung
  • Anwendungsdienste wie IPv6, UDP und CoAP
  • Verwaltung von Netzwerkanmeldedaten sowie Provisions- und Joiner-Rollen
  • Border-Router verwalten
  • Erweiterte Funktionen wie die Elternaufsicht und die Erkennung von Jams

Referenzinformationen zu allen OpenThread APIs sind unter openthread.io/reference verfügbar.

API verwenden

Fügen Sie dazu eine Header-Datei in eine Ihrer Anwendungsdateien ein. Rufen Sie dann die gewünschte Funktion auf.

Die in OpenThread enthaltene Beispiel-Befehlszeile verwendet beispielsweise die folgenden API-Header:

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

Die OpenThread-Instanz

Die otInstance-Struktur wird häufig verwendet, wenn Sie mit den OpenThread APIs arbeiten. Nach der Initialisierung stellt diese Struktur eine statische Instanz der OpenThread-Bibliothek dar. Der Nutzer kann nun OpenThread API-Aufrufe ausführen.

Die Instanz „OpenThread“ wird beispielsweise in der Beispielanwendung „CLI“ in der Funktion main() initialisiert:

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

Plattformspezifische Funktionen

Wenn Sie einer der Beispielanwendungen in OpenThread plattformspezifische Funktionen hinzufügen möchten, deklarieren Sie sie zuerst im Header ./openthread/examples/platforms/openthread-system.h mit dem Namespace otSys für alle Funktionen. Implementieren Sie sie anschließend in einer plattformspezifischen Quelldatei. Auf diese Weise können Sie dieselben Funktionsheader für andere Beispielplattformen verwenden.

Beispiel: Die GPIO-Funktionen, die wir zum Einbinden der nRF52840-Schaltflächen und LEDs verwenden möchten, müssen in openthread-system.h deklariert werden.

Öffne die ./openthread/examples/platforms/openthread-system.h-Datei in deinem bevorzugten Texteditor.

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

AKTION: Plattformspezifische GPIO-Funktionsdeklarationen hinzufügen

Füge diese Funktionsdeklarationen nach #include für den Header openthread/instance.h hinzu:

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

Diese werden im nächsten Schritt implementiert.

Hinweis: Für die Funktionsdeklaration otSysButtonProcess wird otInstance verwendet. So kann die Anwendung bei Bedarf auf Informationen zur OpenThread-Instanz zugreifen, wenn eine Schaltfläche gedrückt wird. Dies hängt von den Anforderungen Ihrer Anwendung ab. Wenn Sie es in Ihrer Implementierung der Funktion nicht benötigen, können Sie das OT_UNUSED_VARIABLE-Makro aus der OpenThread API verwenden, um Build-Fehler bei nicht verwendeten Variablen für einige Toolchains zu unterdrücken. Wir sehen uns später Beispiele dazu an.

5. GPIO-Plattformabstraktion implementieren

Im vorherigen Schritt haben wir die plattformspezifischen Funktionsdeklarationen in ./openthread/examples/platforms/openthread-system.h behandelt, die für GPIO verwendet werden können. Für den Zugriff auf die Tasten und LEDs der nRF52840-Entwicklerboards müssen Sie diese Funktionen für die nRF52840-Plattform implementieren. In diesem Code fügen Sie Funktionen hinzu, die:

  • GPIO-PINs und -Modi initialisieren
  • Spannung eines Pins steuern
  • GPIO-Unterbrechungen aktivieren und Rückruf registrieren

Erstellen Sie im Verzeichnis ./src/src eine neue Datei mit dem Namen gpio.c. Fügen Sie in dieser neuen Datei die folgenden Inhalte hinzu.

./src/src/gpio.c (neue Datei)

AKTION: Definition hinzufügen

Diese Definitionen dienen als Abstraktionen zwischen nRF52840-spezifischen Werten und Variablen, die auf Ebene von OpenThread verwendet werden.

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

Weitere Informationen zu nRF52840-Schaltflächen und LEDs finden Sie im Nordic Semiconductor Infocenter.

AKTION: Header enthält

Als Nächstes fügen Sie den Header hinzu, damit die GPIO-Funktionalität verfügbar ist.

/* 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"

AKTION: Füge Callback- und Unterbrechungsfunktionen für Schaltfläche 1 hinzu.

Fügen Sie als Nächstes diesen Code hinzu. Die Funktion in_pin1_handler ist der Callback, der registriert wird, wenn die Funktion zum Drücken von Schaltflächen initialisiert wird (später in dieser Datei).

Beachten Sie, dass dieser Callback das Makro OT_UNUSED_VARIABLE verwendet, da die an in_pin1_handler übergebenen Variablen in der Funktion nicht verwendet werden.

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

AKTION: Fügen Sie eine Funktion hinzu, mit der Sie die LEDs konfigurieren können.

Fügen Sie diesen Code hinzu, um den Modus und den Status aller LEDs während der Initialisierung zu konfigurieren.

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

AKTION: Fügen Sie eine Funktion hinzu, um den Modus einer LED festzulegen.

Diese Funktion wird verwendet, wenn sich die Rolle des Geräts ändert.

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

AKTION: Füge eine Funktion hinzu, um den Modus einer LED zu ändern.

Diese Funktion wird zum Ein- und Ausschalten von LED4 verwendet, wenn das Gerät eine Multicast-UDP-Nachricht erhält.

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

AKTION: Funktionen zum Initialisieren und Verarbeiten von Tastendrucken hinzufügen.

Mit der ersten Funktion wird das Board für einen Tastendruck initialisiert. Mit der zweiten wird die Multicast-UDP-Nachricht gesendet, wenn Taste 1 gedrückt wird.

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

AKTION: Speichern und schließen Sie die Datei gpio.c.

6. API: Auf Geräterollenänderungen reagieren

In unserer App sollen je nach Geräterolle verschiedene LEDs aufleuchten. Erfasst die folgenden Rollen: Leader, Router, End Device. Dazu können wir LEDs wie die folgenden zuweisen:

  • LED1 = Beste Variante
  • LED2 = Router
  • LED3 = Endgerät

Um diese Funktion verwenden zu können, muss die App wissen, wann sich die Geräterolle geändert hat und wie die richtige LED aktiviert wird. Für den ersten Teil verwenden wir die OpenThread-Instanz und für den zweiten Teil die GPIO-Plattform.

Öffne die ./openthread/examples/apps/cli/main.c-Datei in deinem bevorzugten Texteditor.

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

AKTION: Header enthält

Füge im Abschnitt „Enthält“ der Datei main.c die API-Header-Dateien hinzu, die du für die Funktion zur Rollenänderung benötigst.

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

ACTION: Handler-Funktionsdeklaration für die Änderung des OpenThread-Instanzstatus hinzufügen

Füge diese Deklaration main.c nach dem Header und vor #ifAnweisungen hinzu. Diese Funktion wird nach der Hauptanwendung definiert.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

AKTION: Fügen Sie eine Callback-Registrierung für die Funktion zum Ändern des Bundesstaats hinzu.

Fügen Sie diese Funktion in main.c nach dem otAppCliInit-Aufruf in die Funktion main() ein. Durch diese Callback-Registrierung wird OpenThread angewiesen, die handleNetifStateChange-Funktion aufzurufen, wenn sich der Status der OpenThread-Instanz ändert.

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

AKTION: Fügen Sie die Implementierung der Statusänderung hinzu.

Implementiere nach der main()-Funktion die main.c-Funktion in main.c. Diese Funktion prüft das Flag OT_CHANGED_THREAD_ROLE der OpenThread-Instanz und schaltet die LEDs bei Bedarf ein/aus.

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: Multicast zum Aktivieren einer LED verwenden

In unserer Anwendung sollen auch UDP-Nachrichten an alle anderen Geräte im Netzwerk gesendet werden, wenn „Button1“ auf einem Board gedrückt wird. Um den Empfang der Nachricht zu bestätigen, aktivieren wir LED4 auf den anderen Boards als Antwort.

Damit diese Funktion aktiviert werden kann, muss Folgendes möglich sein:

  • UDP-Verbindung beim Start initialisieren
  • UDP-Nachricht an die lokale Mesh-Multicast-Adresse senden können
  • Eingehende UDP-Nachrichten verarbeiten
  • LED4 bei eingehenden UDP-Nachrichten ein- und ausschalten

Öffne die ./openthread/examples/apps/cli/main.c-Datei in deinem bevorzugten Texteditor.

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

AKTION: Header enthält

Fügen Sie oben in der Datei main.c im Abschnitt „Einschließen“ die API-Header-Dateien ein, die Sie für die Multicast-UDP-Funktion benötigen.

#include <string.h>

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

#include "utils/code_utils.h"

Der Header code_utils.h wird für die Makros otEXPECT und otEXPECT_ACTION verwendet, die Laufzeitbedingungen validieren und Fehler ordnungsgemäß beheben.

AKTION: Definitionen und Konstanten hinzufügen:

Fügen Sie in der Datei main.c nach dem Include-Abschnitt und vor allen #if-Anweisungen UDP-spezifische Konstanten hinzu und definieren Sie Folgendes:

#define UDP_PORT 1212

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

ff03::1 ist die Mesh-Local-Multicast-Adresse. Alle an diese Adresse gesendeten Nachrichten werden an alle Full Thread-Geräte im Netzwerk gesendet. Weitere Informationen zur Multicast-Unterstützung in OpenThread finden Sie unter Multicast auf openthread.io.

AKTION: Funktionsdeklarationen hinzufügen.

Fügen Sie in der Datei main.c nach der otTaskletsSignalPending-Definition und vor der main()-Funktion UDP-spezifische Funktionen sowie eine statische Variable hinzu, die einen UDP-Socket darstellt:

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;

AKTION: Aufrufe hinzufügen, um die GPIO-LEDs und die Schaltfläche zu initialisieren

Fügen Sie in main.c diese Funktionsaufrufe der Funktion main() nach dem otSetStateChangedCallback-Aufruf hinzu. Diese Funktionen initialisieren die GPIO- und GPIOTE-PINs und legen einen Schaltflächen-Handler für die Verarbeitung von Schaltflächen-Push-Ereignissen fest.

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

AKTION: Fügen Sie den UDP-Initialisierungsaufruf hinzu.

Fügen Sie in main.c die Funktion main() nach dem gerade hinzugefügten otSysButtonInit-Aufruf ein:

initUdp(instance);

Durch diesen Aufruf wird sichergestellt, dass ein UDP-Socket beim Starten der Anwendung initialisiert wird. Ansonsten kann das Gerät keine UDP-Nachrichten senden oder empfangen.

AKTION: Fügen Sie einen Aufruf hinzu, um das Ereignis der GPIO-Schaltfläche zu verarbeiten.

Fügen Sie in main.c diesen Funktionsaufruf nach der Funktion otSysProcessDrivers in der while-Schleife der main()-Funktion hinzu. Diese in gpio.c deklarierte Funktion überprüft, ob die Schaltfläche gedrückt wurde, und ruft dann den Handler (handleButtonInterrupt) auf, der im obigen Schritt festgelegt wurde.

otSysButtonProcess(instance);

AKTION: Implementierung von Schaltflächen-Unterbrechungs-Handlern.

Fügen Sie in main.c die Implementierung der handleButtonInterrupt-Funktion nach der handleNetifStateChanged-Funktion hinzu, die Sie im vorherigen Schritt hinzugefügt haben:

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

Aktion: UDP-Initialisierung implementieren

Füge in main.c die Implementierung der Funktion initUdp nach der soeben hinzugefügten Funktion handleButtonInterrupt hinzu:

/**
 * 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 ist der zuvor definierte Port (1212). Die Funktion otUdpOpen öffnet den Socket und registriert eine Callback-Funktion (handleUdpReceive), wenn eine UDP-Nachricht empfangen wird. otUdpBind bindet den Socket an die Thread-Netzwerkschnittstelle, indem er OT_NETIF_THREAD übergibt. Informationen zu anderen Optionen für die Netzwerkschnittstelle finden Sie in der otNetifIdentifier-Aufzählung in der UDP API-Referenz.

ACTION: UDP-Messaging implementieren

Füge in main.c die Implementierung der Funktion sendUdp nach der soeben hinzugefügten Funktion initUdp hinzu:

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

Beachten Sie die Makros otEXPECT und otEXPECT_ACTION. Dadurch wird sichergestellt, dass die UDP-Nachricht gültig und korrekt im Puffer zugewiesen ist. Ist dies nicht der Fall, verarbeitet die Funktion Fehler, indem der Nutzer in den exit-Block springen kann, in dem der Puffer freigegeben wird.

Weitere Informationen zu den Funktionen zum Initialisieren von UDP finden Sie in den Referenzen zu IPv6 und UDP auf openthread.io.

AKTION: Verarbeitung von UDP-Nachrichten implementieren

Füge in main.c die Implementierung der Funktion handleUdpReceive nach der sendUdp-Funktion hinzu, die du gerade hinzugefügt hast. Mit dieser Funktion wird einfach die LED4 eingeschaltet.

/**
 * 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: Thread-Netzwerk konfigurieren

Zur Veranschaulichung möchten wir, dass unsere Geräte Thread sofort starten und zu einem Netzwerk zusammenfügen, wenn sie eingeschaltet sind. Dazu verwenden wir die Struktur otOperationalDataset. In dieser Struktur sind alle Parameter enthalten, die zur Übertragung der Anmeldedaten für das Thread-Netzwerk an ein Gerät erforderlich sind.

Die Verwendung dieser Struktur überschreibt die in OpenThread integrierten Netzwerkeinstellungen, um unsere Anwendung sicherer zu machen und Thread-Knoten in unserem Netzwerk auf diejenigen zu beschränken, die die Anwendung ausführen.

Öffnen Sie noch einmal die Datei ./openthread/examples/apps/cli/main.c in Ihrem bevorzugten Texteditor.

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

ACTION: Header hinzufügen

Füge im Abschnitt „Einschließen“ oben in der Datei main.c die API-Header-Datei hinzu, die du das Thread-Netzwerk konfigurieren musst:

#include <openthread/dataset_ftd.h>

AKTION: Funktionsdeklaration zum Festlegen der Netzwerkkonfiguration hinzufügen

Füge diese Deklaration main.c nach dem Header und vor #ifAnweisungen hinzu. Diese Funktion wird nach der Hauptanwendungsfunktion definiert.

static void setNetworkConfiguration(otInstance *aInstance);

AKTION: Fügen Sie den Netzwerkkonfigurationsaufruf hinzu.

Fügen Sie in main.c diesen Funktionsaufruf der main()-Funktion nach dem otSetStateChangedCallback-Aufruf hinzu. Mit dieser Funktion wird das Thread-Netzwerk-Dataset konfiguriert.

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

AKTION: Aufrufe hinzufügen, um die Thread-Netzwerkschnittstelle und den Stack zu aktivieren

Fügen Sie in main.c diese Funktionsaufrufe der Funktion main() nach dem otSysButtonInit-Aufruf hinzu.

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

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

AKTION: Thread-Netzwerkkonfiguration implementieren

Füge in main.c die Implementierung der Funktion setNetworkConfiguration nach der Funktion main() hinzu:

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

Wie in der Funktion beschrieben, gelten die folgenden Thread-Netzwerkparameter für diese Anwendung:

  • Kanal = 15
  • PAN-ID = 0x2222
  • Erweiterte PAN-ID = C0DE1AB5C0DE1AB5
  • Netzwerkschlüssel = 1234C0DE1AB51234C0DE1AB51234C0DE
  • Netzwerkname = OTCodelab

Außerdem verringern wir an dieser Stelle den Jitter-Router, sodass unsere Geräte zu Demonstrationszwecken die Rollen schneller ändern. Beachten Sie, dass dies nur durchgeführt wird, wenn der Knoten ein FTD-Gerät (Full Thread Device) ist. Im nächsten Schritt erfahren Sie mehr dazu.

9. API: Eingeschränkte Funktionen

Einige der APIs von OpenThread ändern Einstellungen, die nur zu Demo- oder Testzwecken geändert werden sollten. Diese APIs sollten nicht in einer Produktionsbereitstellung einer Anwendung mit OpenThread verwendet werden.

Mit der Funktion otThreadSetRouterSelectionJitter wird beispielsweise die Zeit in Sekunden angepasst, die ein Endgerät benötigt, um sich zu einem Router hochzustufen. Der Standardwert für diesen Wert ist 120 (gemäß der Threadspezifikation). Für die einfache Nutzung dieses Codelabs wird er auf 20 geändert. Sie müssen also nicht mehr lange warten, bis ein Thread-Knoten die Rollen ändert.

Hinweis: MTD-Geräte werden nicht als Router verwendet. Auch eine Funktion wie otThreadSetRouterSelectionJitter ist in einem MTD-Build nicht enthalten. Später müssen wir die CMake-Option -DOT_MTD=OFF angeben, andernfalls tritt ein Build-Fehler auf.

Das können Sie anhand der Funktionsdefinition otThreadSetRouterSelectionJitter prüfen, die in einer Preprocessor-Anweisung von OPENTHREAD_FTD enthalten ist:

./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. CMake-Updates

Bevor Sie Ihre Anwendung erstellen, gibt es einige kleinere Updates für drei CMake-Dateien. Sie werden vom Build-System verwendet, um Ihre Anwendung zu kompilieren und zu verknüpfen.

./third_party/NordicSemiconductor/CMakeLists.txt

Fügen Sie jetzt dem NordicSemiconductor CMakeLists.txt einige Flags hinzu, um sicherzustellen, dass GPIO-Funktionen in der Anwendung definiert sind.

AKTION: Flags zur Datei CMakeLists.txt hinzufügen.

Öffnen Sie ./third_party/NordicSemiconductor/CMakeLists.txt in Ihrem bevorzugten Texteditor und fügen Sie im Abschnitt COMMON_FLAG die folgenden Zeilen hinzu.

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

Bearbeite die Datei ./src/CMakeLists.txt, um die neue Quelldatei gpio.c hinzuzufügen:

AKTION: Fügen Sie die Gpio-Quelle der ./src/CMakeLists.txt-Datei hinzu.

Öffnen Sie ./src/CMakeLists.txt in einem Texteditor Ihrer Wahl und fügen Sie die Datei im Abschnitt „NRF_COMM_SOURCES“ hinzu.

...

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

...

./third_party/NordicSemiconductor/CMakeLists.txt

Fügen Sie die Treiberdatei nrfx_gpiote.c für NordicSemiconductor zur Datei CMakeLists.txt hinzu, damit sie im Bibliotheksaufbau der nordischen Treiber enthalten ist.

AKTION: Fügen Sie den Gpio-Treiber der NordicSemiconductor-Datei CMakeLists.txt hinzu.

Öffnen Sie ./third_party/NordicSemiconductor/CMakeLists.txt in einem Texteditor Ihrer Wahl und fügen Sie die Datei im Abschnitt „COMMON_SOURCES“ hinzu.

...

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

11. Geräte einrichten

Nachdem Sie den Code aktualisiert haben, können Sie die Anwendung erstellen und auf alle drei Nordic nRF52840-Entwicklungsboards laden. Jedes Gerät fungiert als Full Thread Device (FTD).

OpenThread erstellen

Erstellen Sie die OpenThread-FTD-Binärprogramme für die nRF52840-Plattform.

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

Gehen Sie zu dem Verzeichnis mit der OpenThread-FTD-Befehlszeile und konvertieren Sie es in ein Hex-Format mit der ARM Embedded Toolchain:

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

Die Boards blinken

Legen Sie die Datei ot-cli-ftd.hex auf jedes nRF52840-Board fest.

Verbinden Sie das USB-Kabel mit dem Micro-USB-Debugging-Port neben der externen Steckdose auf der nRF52840-Karte und schließen Sie es dann an Ihren Linux-Computer an. Prüfen Sie, ob LED5 aktiviert ist.

20a3b4b480356447.png

Beachten Sie wie zuvor die Seriennummer des nRF52840-Boards:

c00d519ebec7e5f0.jpeg

Gehen Sie zum Speicherort der nRFx-Befehlszeilentools und öffnen Sie die Hexadezimaldatei der OpenThread-Befehlszeile auf nRF52840. Verwenden Sie dabei die Seriennummer des Boards:

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

LED5 schaltet sich kurzzeitig für das Blinken aus. Bei Erfolg wird die folgende Ausgabe generiert:

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.

Wiederholen Sie den Schritt & „blitzen Sie die Boards“ für die anderen beiden Boards. Jedes Board sollte auf die gleiche Weise mit dem Linux-Computer verbunden werden und der Befehl zum Flashen ist bis auf die Seriennummer des Boards identisch. Achten Sie darauf, dass Sie die eindeutige Seriennummer jedes Boards in der

nrfjprog-Flashing-Befehl.

Bei Erfolg leuchtet auf jeder Karte entweder LED1, LED2 oder LED3. Möglicherweise leuchtet kurz nach dem Blitzen die LED-Schalteranzeige von 3 auf 2 (oder 2 auf 1).

12. Funktionen der Anwendung

Alle drei nRF52840-Boards sollten nun mit Strom versorgt werden und unsere OpenThread-Anwendung ausführen. Wie bereits beschrieben, hat diese Anwendung zwei Hauptfunktionen.

Anzeige der Geräterollen

Die leuchtende LED auf einem Board spiegelt die aktuelle Rolle des Thread-Knotens wider:

  • LED1 = Beste Variante
  • LED2 = Router
  • LED3 = Endgerät

Ändert sich die Rolle, ändern sich auch die leuchtenden LEDs. Diese Änderungen sollten bereits innerhalb von 20 Sekunden nach dem Aufladen des Geräts auf einem Board oder auf zwei Platinen zu sehen sein.

UDP-Multicast

Wenn Schaltfläche1 auf einem Board gedrückt wird, wird eine UDP-Nachricht an die Mesh-Netzwerk-Multicast-Adresse gesendet, die alle anderen Knoten im Thread-Netzwerk enthält. Daraufhin wird LED4 auf allen anderen Boards ein- oder ausgeschaltet. Die LED4 bleibt auf jedem Board so lange aktiviert, bis er eine weitere UDP-Nachricht erhält.

203dd094acca1f97

9bbd96d9b1c63504.png

13. Demo: Änderungen an Geräterollen beobachten

Die Flash-Geräte sind eine bestimmte Art von Full Thread-Gerät (FTD), das sogenannte Router-fähige Endgeräte (REED). Das bedeutet, dass sie entweder als Router oder Endgerät fungieren und sich selbst von einem Endgerät zu einem Router hochstufen können.

Thread unterstützt bis zu 32 Router, versucht jedoch, die Anzahl der Router zwischen 16 und 23 beizubehalten. Wenn ein REED als Endgerät angehängt ist und die Anzahl der Router unter 16 liegt, wird er automatisch zu einem Router hochgestuft. Diese Änderung sollte zufällig innerhalb der Anzahl von Sekunden erfolgen, die Sie in der Anwendung auf den Wert von otThreadSetRouterSelectionJitter (20 Sekunden) festlegen.

Jedes Thread-Netzwerk hat auch eine führende Rolle. Das ist ein Router, der für die Verwaltung der Gruppe von Routern in einem Thread-Netzwerk verantwortlich ist. Jetzt sollten alle Geräte eingeschaltet sein. Nach 20 Sekunden sollte eines der Geräte als Leader (LED1 an) und die anderen beiden Router (LED2) eingeschaltet sein.

4e1e885861a66570.png

Beste Variante entfernen

Wenn die führende Person aus dem Thread-Netzwerk entfernt wird, stuft ein anderer Router sich selbst als Leader ein, um dafür zu sorgen, dass das Netzwerk immer noch einen Leader hat.

Deaktivieren Sie die Leiterplatte (das Gerät mit LED1-LED) über den Ein-/Aus-Schalter. Warten Sie etwa 20 Sekunden. Auf einem der verbleibenden zwei Boards schaltet sich LED2 (Router) aus und LED1 (Leader) schaltet sich ein. Dieses Gerät ist jetzt der Leader des Thread-Netzwerks.

4c57c87adb40e0e3.png

Aktivieren Sie das ursprüngliche Leader Board wieder. Das Thread-Netzwerk sollte sich automatisch wieder als Endgerät verbinden (LED 3 ist eingeschaltet). Innerhalb von 20 Sekunden (der Router-Auswahl-Jitter) hochstufen Sie zu einem Router (LED2 leuchtet auf).

5f40afca2dcc4b5b

Boards zurücksetzen

Schalten Sie alle drei Boards aus, schalten Sie sie wieder ein und beobachten Sie die LEDs. Das erste Board, das hochgestuft wurde, sollte die Rolle „Leader“ haben (LED1 ist erleuchtet). Der erste Router in einem Thread-Netzwerk wird automatisch zum Leader.

Die anderen beiden Boards verbinden sich anfangs mit dem Netzwerk als LEDs (LED3 werden beleuchtet), sollten aber innerhalb von 20 Sekunden zu Routern wechseln (LED2 leuchten).

Netzwerkpartitionen

Wenn Ihre Boards nicht genügend Strom erhalten oder die Radioverbindung zwischen ihnen schwach ist, wird das Thread-Netzwerk in Partitionen aufgeteilt. Möglicherweise wird mehr als ein Gerät als Leader angezeigt.

Thread ist selbstreparierend, sodass Partitionen mit einer führenden Partition wieder zusammengeführt werden können.

14. Demo: UDP-Multicast senden

Wenn Sie beim vorherigen Training fortfahren, sollte LED4 auf keinem Gerät beleuchtet sein.

Wählen Sie ein beliebiges Board aus und drücken Sie die Taste 1. Der Status aller anderen Boards im Thread-Netzwerk, in denen die Anwendung ausgeführt wird, sollte ihren Status ändern. Wenn Sie bei der vorherigen Übung fortfahren, sollte sie jetzt aktiviert sein.

f186a2618fdbe3fd

Drücken Sie noch einmal die Taste 1 für dasselbe Board. Auf allen anderen Boards sollte die LED4 wieder ein- und ausschalten.

Drücken Sie die Taste1 auf einer anderen Karte und beobachten Sie, wie die LED4 auf den anderen Tasten reagiert. Drücken Sie die Taste1 auf einem der Platinen, auf denen LED4 gerade eingeschaltet ist. Die LED4 bleibt für dieses Board eingeschaltet, wird aber auf den anderen Umschaltern eingeschaltet.

f5865ccb8ab7aa34.png

Netzwerkpartitionen

Wenn Ihre Boards partitioniert sind und mehr als ein Leader die Boards umfasst, sind die Ergebnisse der Multicast-Nachricht unterschiedlich. Wenn Sie auf einem Board, das partitioniert ist und somit das einzige Mitglied des partitionierten Thread-Netzwerks ist, auf die Taste 1 drücken, wird die LED4 auf den anderen Boards nicht beleuchtet. Setzen Sie in diesem Fall die Boards zurück. Im Idealfall bilden sie ein einzelnes Thread-Netzwerk und die UDP-Nachrichten sollten korrekt funktionieren.

15. Glückwunsch!

Sie haben eine Anwendung erstellt, die OpenThread APIs verwendet.

Sie wissen jetzt:

  • Tasten und LEDs auf den Nordic nRF52840-Entwicklerboards programmieren
  • Gemeinsame OpenThread APIs und die otInstance-Klasse verwenden
  • OpenThread-Statusänderungen überwachen und darauf reagieren
  • So senden Sie UDP-Nachrichten an alle Geräte in einem Thread-Netzwerk
  • Makefiles ändern

Weitere Informationen

Aufbauen Sie anhand dieses Codelabs die folgenden Übungen:

  • Ändern Sie das GPIO-Modul so, dass GPIO-Stifte anstelle der integrierten LEDs verwendet werden. Verbinden Sie dann externe RGB-LEDs, deren Farbe sich nach der Rolle des Routers ändert.
  • GPIO-Unterstützung für eine andere Beispielplattform hinzufügen
  • Anstatt mehrere Geräte per Tastendruck zu pingen, können Sie einzelne Geräte mit der Router/Leader API orten und pingen.
  • Mesh-Netzwerk mit dem Internet verbinden: OpenThread-Border-Router verwenden und von außerhalb des Thread-Netzwerks streamen, um die LEDs zu beleuchten

Weitere Informationen

Auf openthread.io und GitHub finden Sie eine Vielzahl von OpenThread-Ressourcen:

Referenz: