Mit OpenThread APIs entwickeln

1) Einführung

26b7f4f6b3ea0700.png

OpenThread, das von Nest veröffentlicht wurde, ist eine Open-Source-Implementierung des Thread®-Netzwerkprotokolls. Nest hat OpenThread veröffentlicht, um die Technologie, die in Nest-Produkten zum Einsatz kommt, allgemein für Entwickler verfügbar gemacht. So lässt sich die Entwicklung von Produkten für das Smart Home beschleunigen.

Die Thread-Spezifikation definiert ein IPv6-basiertes zuverlässiges, sicheres und energiesparendes Kommunikationsprotokoll für kabellose Geräte zwischen Geräten. 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 werden Sie mit OpenThread APIs ein Thread-Netzwerk starten, Änderungen an Geräterollen überwachen und darauf reagieren sowie UDP-Nachrichten senden sowie diese Aktionen mit Schaltflächen und LEDs auf echter Hardware verknüpfen.

2a6db2e258c32237.png

Lerninhalte

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

Voraussetzungen

Hardware:

  • 3 Nordic Semiconductor nRF52840 Entwicklungsplatten
  • 3 USB-auf-Micro-USB-Kabel zum Verbinden der Boards
  • Ein Linux-Computer mit mindestens 3 USB-Ports

Software:

  • GNU-Toolchain
  • Nordic nRF5x-Befehlszeilentools
  • Segger J-Link-Software
  • OpenThread
  • Git

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

2. Erste Schritte

Hardware-Codelab erstellen

Bevor Sie mit dem Codelab beginnen, sollten Sie das Codelab Thread-Netzwerk mit nRF52840-Boards und OpenThread erstellen. Dieses hat folgende Möglichkeiten:

  • Details zur Software, die zum Erstellen und Blinken erforderlich ist
  • Hier lernen Sie, wie Sie OpenThread erstellen und auf skandinavischen nRF52840-Boards blinken
  • Demonstration der Grundlagen eines Thread-Netzwerks

Keine der Umgebungen, die zum Erstellen von OpenThread und zum Flashen der Boards erforderlich sind, wird in diesem Codelab beschrieben. Sie enthält lediglich eine grundlegende Anleitung zum Flashen. Es wird davon ausgegangen, dass Sie bereits ein Thread-Netzwerk-Build erstellt haben.

Linux-Computer

Dieses Codelab wurde entwickelt, um einen i386- oder x86-basierten Linux-Computer zum Flashen aller Thread-Entwicklungsplatinen zu verwenden. Alle Schritte wurden auf Ubuntu 14.04.5 LTS (Trusty Tahr) getestet.

Nordic Semiconductor nRF52840 Platinen

Dieses Codelab verwendet drei nRF52840 PDK-Boards.

a6693da3ce213856.png

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 Erstellen eines Thread-Netzwerks nach Bedarf abgeschlossen haben, ist alles installiert, was Sie benötigen. Falls nicht, schließe das Codelab ab, bevor du fortfährst. Dann stellst du sicher, dass du OpenThread auf nRF52840-Entwicklerplatinen erstellen und darauf verlinken kannst.

3. Repository klonen

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

Klonen Sie das Beispiel-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 befinden sich unter ./openthread/include/openthread im OpenThread-Repository. Diese APIs bieten Zugriff auf eine Vielzahl von OpenThread-Funktionen sowohl auf Thread- als auch auf Plattformebene zur Verwendung in Ihren Anwendungen:

  • Informationen und Einstellungen zu OpenThread-Instanzen
  • Anwendungsdienste wie IPv6, UDP und CoAP
  • Anmeldedaten für das Netzwerk zusammen mit den Rollen „Provisionen“ und „Mitbearbeiter“
  • Border Router-Verwaltung
  • Erweiterte Funktionen wie die Elternaufsicht und die Jam-Erkennung

Referenzinformationen zu allen OpenThread APIs finden Sie unter openthread.io/reference.

API verwenden

Fügen Sie die Header-Datei in einer Ihrer Anwendungsdateien ein, um eine API zu verwenden. Rufen Sie dann die gewünschte Funktion auf.

Die in OpenThread enthaltene Beispiel-App 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 Struktur otInstance wird häufig verwendet, wenn Sie mit den OpenThread APIs arbeiten. Nach der Initialisierung stellt diese Struktur eine statische Instanz der OpenThread-Bibliothek dar und ermöglicht dem Nutzer, OpenThread API-Aufrufe auszuführen.

Die OpenThread-Instanz wird beispielsweise mit der main()-Funktion der Beispiel-App 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 in OpenThread enthaltenen Beispielanwendungen 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 dann in einer plattformspezifischen Quelldatei. Auf diese Weise können Sie dieselben Funktion-Header für andere Beispielplattformen verwenden.

Die GPIO-Funktionen, mit denen wir den nRF52840 betätigen werden, müssen beispielsweise in openthread-system.h deklariert werden.

Öffnen Sie die Datei ./openthread/examples/platforms/openthread-system.h in einem Texteditor.

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

AKTION: Plattformspezifische GPIO-Funktionsdeklarationen hinzufügen.

Fügen Sie diese Funktionsdeklarationen nach #include für den Header openthread/instance.h ein:

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

Sie werden im nächsten Schritt implementiert.

Für die otSysButtonProcess-Funktionsdeklaration wird ein otInstance verwendet. So kann die Anwendung bei Bedarf auf die OpenThread-Instanz zugreifen, wenn eine Schaltfläche gedrückt wird. Das 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. Beispiele dafür sehen wir später.

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 Schaltflächen und LEDs auf den nRF52840-Entwicklerplatinen müssen Sie diese Funktionen für die nRF52840-Plattform implementieren. Mit diesem Code fügen Sie Funktionen hinzu, die:

  • GPIO-Pins und -Modi initialisieren
  • Spannung an einer Stift steuern
  • GPIO-Unterbrechungen aktivieren und Callbacks registrieren

Erstellen Sie im Verzeichnis ./src/src eine neue Datei mit dem Namen gpio.c. Fügen Sie in diese neue Datei den folgenden Inhalt ein.

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

AKTION: Definition hinzufügen

Diese definieren als Abstraktionen zwischen nRF52840-spezifischen Werten und Variablen, die auf OpenThread-Anwendungsebene 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 Schaltflächen und LEDs nRF52840 finden Sie im Nordic Semiconductor Infocenter.

ACTION: Header Header hinzufügen:

Fügen Sie als Nächstes den Header hinzu, den Sie für die GPIO-Funktion benötigen.

/* 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ügen Sie Callback- und Unterbrechungsfunktionen für Schaltfläche 1 hinzu.

Gib als Nächstes diesen Code ein. Die Funktion in_pin1_handler ist der Callback, der registriert wird, wenn die Funktion zum Drücken der Schaltfläche initialisiert wird (später in dieser Datei).

Bei diesem Callback wird 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: Funktion zum Konfigurieren der LEDs hinzufügen

Fügen Sie diesen Code hinzu, um den Modus und 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);
}

AUFGABE: Funktion zum Einstellen des LED-Modus hinzufügen

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

Diese Funktion wird verwendet, um LED4 ein- bzw. auszuschalten, wenn das Gerät eine Multicast-UDP-Nachricht empfängt.

/**
 * @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 Tastendrücken hinzufügen.

Die erste Funktion initialisiert das Board für den Tastendruck und die zweite sendet die Multicast-UDP-Nachricht, wenn die 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 Änderungen an Geräterollen reagieren

In unserer App sollen verschiedene LEDs je nach Geräterolle leuchten. Wir verfolgen die folgenden Rollen: Leader, Router, Endgerät. Sie können LEDs so zuweisen:

  • LED1 = Leader
  • LED2 = Router
  • LED3 = Endgerät

Damit diese Funktion aktiviert werden kann, muss die App wissen, wann die Geräterolle geändert wurde und wie die richtige LED als Reaktion aktiviert wird. Wir verwenden die OpenThread-Instanz für den ersten Teil und die GPIO-Plattformabstraktion für den ersten Teil.

Öffnen Sie die Datei ./openthread/examples/apps/cli/main.c in einem Texteditor.

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

ACTION: Header Header hinzufügen:

Fügen Sie im Abschnitt „Einschließen“ der Datei main.c die API-Headerdateien ein, die Sie für die Funktion zur Rollenänderung benötigen.

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

ACTION: Deklaration der Handler-Funktion für die Änderung des OpenThread-Instanzstatus hinzufügen

Fügen Sie diese Deklaration „main.c“ nach dem Header und vor den #if-Anweisungen ein. Diese Funktion wird nach der Hauptanwendung definiert.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

ACTION: Callback-Registrierung für die Funktion zur Statusänderungs-Handler hinzufügen

Fügen Sie diese Funktion in main.c nach dem Aufruf otAppCliInit der Funktion main() hinzu. Diese Callback-Registrierung weist OpenThread an, die Funktion handleNetifStateChange aufzurufen, sobald sich der Status der OpenThread-Instanz ändert.

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

AKTION: Implementierung der Statusänderung hinzufügen

Implementieren Sie in main.c nach der Funktion main() die Funktion handleNetifStateChanged. 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 verwenden, um eine LED einzuschalten

In unserer App sollen 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, schalten wir als Reaktion die LEDs auf den anderen Boards ein.

Damit diese Funktion aktiviert werden kann, muss die Anwendung:

  • UDP-Verbindung beim Start initialisieren
  • UDP-Nachricht an die lokale Mesh-Multicast-Adresse senden
  • Eingehende UDP-Nachrichten verarbeiten
  • LED4 bei eingehenden UDP-Nachrichten wechseln

Öffnen Sie die Datei ./openthread/examples/apps/cli/main.c in einem Texteditor.

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

ACTION: Header Header hinzufügen:

Fügen Sie oben im Abschnitt „Includes“ (main.c) die API-Headerdateien 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, um Laufzeitbedingungen zu validieren und Fehler reibungslos zu beheben.

AKTION: Definitionen und Konstanten hinzufügen:

Fügen Sie in der Datei main.c nach dem Einschlussbereich und vor #if-Anweisungen UDP-spezifische Konstanten ein 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 lokale Multicast-Adresse des Mesh-Netzwerks. Alle Nachrichten, die an diese Adresse gesendet werden, 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 Definition otTaskletsSignalPending und vor der Funktion main() 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 zum Initialisieren der GPIO-LEDs und der Schaltfläche hinzufügen

Fügen Sie in main.c diese Funktionsaufrufe nach dem otSetStateChangedCallback-Aufruf zur main()-Funktion hinzu. Diese Funktionen initialisieren die GPIO- und GPIOTE-PINs und legen einen Schaltflächen-Handler fest, um Schaltflächen-Push-Ereignisse zu verarbeiten.

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

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

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

initUdp(instance);

Dieser Aufruf stellt sicher, dass ein UDP-Socket beim Start der Anwendung initialisiert wird. Andernfalls 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 der Funktion main() nach dem otSysProcessDrivers-Aufruf in der while-Schleife hinzu. Mit der in gpio.c deklarierten Funktion wird geprüft, ob die Schaltfläche gedrückt wurde. Wenn ja, wird der Handler (handleButtonInterrupt) aufgerufen, der im vorherigen Schritt festgelegt wurde.

otSysButtonProcess(instance);

MAẞNAHME: Handler für Unterbrechungen der Schaltfläche implementieren

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

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

ACTION: UDP-Initialisierung implementieren

Fügen Sie 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 eingeht. otUdpBind bindet den Socket an die Thread-Netzwerkschnittstelle. Dazu wird OT_NETIF_THREAD übergeben. Informationen zu weiteren Optionen für die Netzwerkschnittstelle finden Sie in der Aufzählung otNetifIdentifier in der UDP-API-Referenz.

ACTION: UDP-Messaging implementieren

Fügen Sie 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. So wird sichergestellt, dass die UDP-Nachricht gültig ist und im Zwischenspeicher korrekt zugewiesen wird. Wenn nicht, verarbeitet die Funktion Fehler, indem sie zum Block exit springt, wo sie den Zwischenspeicher freigibt.

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

Aktion: UDP-Nachrichtenverarbeitung implementieren

Fügen Sie in main.c die Implementierung der Funktion handleUdpReceive nach der soeben hinzugefügten Funktion sendUdp hinzu. Mit dieser Funktion wird einfach 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 leichteren Demo möchten wir, dass unsere Geräte Thread sofort starten und sich in einem Netzwerk verbinden, wenn sie eingeschaltet sind. Dazu verwenden wir die otOperationalDataset-Struktur. Diese Struktur enthält alle Parameter, die zum Übertragen von Anmeldedaten für das Thread-Netzwerk an ein Gerät erforderlich sind.

Durch die Verwendung dieser Struktur werden die in OpenThread integrierten Netzwerkstandards überschrieben, um unsere Anwendung sicherer zu machen und die Thread-Knoten in unserem Netzwerk auf die Netzwerke zu beschränken, auf denen die Anwendung ausgeführt wird.

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

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

AKTION: Header Include hinzufügen.

Fügen Sie oben in der Datei main.c im Abschnitt „Einschließen“ die API-Header-Datei ein, die Sie zum Konfigurieren des Thread-Netzwerks benötigen:

#include <openthread/dataset_ftd.h>

AKTION: Fügen Sie eine Funktionsdeklaration hinzu, um die Netzwerkkonfiguration festzulegen.

Fügen Sie diese Deklaration „main.c“ nach dem Header und vor den #if-Anweisungen ein. Diese Funktion wird nach der Hauptanwendungsfunktion definiert.

static void setNetworkConfiguration(otInstance *aInstance);

AKTION: Fügen Sie den Aufruf für die Netzwerkkonfiguration hinzu.

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

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

ACTION: Fügen Sie Aufrufe hinzu, um die Thread-Netzwerkschnittstelle und den Stack zu aktivieren.

Fügen Sie in main.c diese Funktionsaufrufe nach dem otSysButtonInit-Aufruf zur main()-Funktion 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ügen Sie 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, werden für diese Anwendung die Thread-Netzwerkparameter verwendet, die wir verwenden:

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

Darüber hinaus reduzieren wir den Jitter der Router-Auswahl, sodass die Geräte die Rollen zu Demozwecken schneller ändern können. Dies gilt nur, wenn der Knoten ein FTD-Gerät (Full Thread Device) ist. Im nächsten Schritt erfahren Sie mehr darüber.

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 gemäß der Thread-Spezifikation 120. Der Einfachheit halber ändern wir den Wert in 20. So müssen Sie nicht lange warten, bis ein Thread-Knoten die Rolle wechselt.

Hinweis: MTD-Geräte werden nicht zu Routern. Die Unterstützung einer 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.

Sie können dies anhand der Funktionsdefinition otThreadSetRouterSelectionJitter prüfen, die in einer Präprozessor-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, sind einige kleinere Updates für drei CMake-Dateien erforderlich. Diese werden vom Build-System verwendet, um Ihre Anwendung zu kompilieren und zu verknüpfen.

./third_party/NordicSemiconductor/CMakeLists.txt.

Fügen Sie nun 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 ein.

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

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

AKTION: Füge die gpio-Quelle der Datei ./src/CMakeLists.txt hinzu.

Öffne ./src/CMakeLists.txt in deinem bevorzugten Texteditor und füge die Datei dem Abschnitt NRF_COMM_SOURCES hinzu.

...

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

...

./third_party/NordicSemiconductor/CMakeLists.txt.

Zum Schluss fügen Sie die Treiberdatei nrfx_gpiote.c in die Datei CMakeLists.txt im NordicSemiconductor ein, sodass sie der Bibliothek der skandinavischen Treiber hinzugefügt wird.

Aktion: Gpio-Treiber in die Datei NordicSemiconductor CMakeLists.txt einfügen

Öffne ./third_party/NordicSemiconductor/CMakeLists.txt in deinem bevorzugten Texteditor und füge die Datei dem 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-Entwicklungsplatinen blinken lassen. Jedes Gerät fungiert als Full-Thread-Gerät (Full Thread Device, FTD).

OpenThread erstellen

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

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

Wechseln Sie in das Verzeichnis mit der OpenThread FTD CLI-Binärdatei und konvertieren Sie sie mit der eingebetteten ARM-Toolchain in das Hexadezimalformat:

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

Flash-Anzeigen

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

Verbinden Sie das USB-Kabel mit dem USB-Debugging-Port neben dem externen Stromanschluss des nRF52840-Boards und stecken Sie dieses in Ihren Linux-Computer ein. Setzen Sie den richtigen Wert auf LED5 ist an.

20a3b4b480356447.png

Notieren Sie wie zuvor die Seriennummer der nRF52840-Platine:

c00d519ebec7e5f0.jpeg.

Wechseln Sie zum Speicherort der nRFx-Befehlszeilentools und blinken Sie die Hexadezimaldatei „OpenThread CLI FTD“ auf der nRF52840-Karte mit der 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 während des Blinkens kurz 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 diesen Schritt 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 der einzelnen Boards im

nrfjprog Flash-Befehl.

Wenn der Vorgang erfolgreich war, leuchtet entweder LED1, LED2 oder LED3 auf den Platinen. Möglicherweise sehen Sie kurz nach dem Blinken die LED-Anzeige von 3 auf 2 (oder 2 zu 1).

12. Anwendungsfunktionen

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

Indikatoren für Geräterollen

Die LED auf jedem Board entspricht der aktuellen Rolle des Thread-Knotens:

  • LED1 = Leader
  • LED2 = Router
  • LED3 = Endgerät

Während sich die Rolle ändert, ändert sich auch die LED. Die Änderungen sollten auf einem oder zwei Boards innerhalb von 20 Sekunden nach dem Einschalten des Geräts sichtbar sein.

UDP-Multicast

Wenn auf einem Board Taste1 gedrückt wird, wird eine UDP-Nachricht an die lokale Mesh-Multicast-Adresse gesendet, die alle anderen Knoten im Thread-Netzwerk enthält. Als Reaktion auf diese Meldung kann LED4 auf allen anderen Boards aktiviert oder deaktiviert werden. LED4 bleibt für jedes Board aktiviert oder deaktiviert, bis es eine weitere UDP-Nachricht erhält.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demo: Änderungen an Geräterollen beobachten

Die von dir Flash-Geräte sind eine bestimmte Art von Full-Thread-Gerät (FTD), das als Router berechtigtes Endgerät (REED) bezeichnet wird. Das bedeutet, dass sie entweder als Router oder Endgerät fungieren und sich selbst von einem Endgerät zu einem Router hochstufen lassen.

Thread unterstützt bis zu 32 Router, versucht aber, die Anzahl der Router zwischen 16 und 23 zu halten. Wenn ein REED als Endgerät angehängt wird und die Anzahl der Router unter 16 liegt, erklingt es automatisch zu einem Router. Diese Änderung sollte zu einem beliebigen Zeitpunkt innerhalb der Anzahl der Sekunden erfolgen, die Sie in der Anwendung als otThreadSetRouterSelectionJitter-Wert festlegen (20 Sekunden).

Jedes Thread-Netzwerk hat auch einen Leader. Das ist ein Router, der für die Verwaltung der Gruppe von Routern in einem Thread-Netzwerk verantwortlich ist. Wenn alle Geräte eingeschaltet sind, sollte nach 20 Sekunden eines der Geräte mit „Führungskraft (LED1 an)“ und die anderen Geräte Router (LED2) eingeschaltet sein.

4e1e885861a66570.png

Leader entfernen

Wenn der Leiter aus dem Thread-Netzwerk entfernt wird, befördert ein anderer Router einen Leiter, damit das Netzwerk immer noch einen Leiter hat.

Schalten Sie die Führungsplatine mit dem Ein-/Aus-Schalter aus. Warten Sie etwa 20 Sekunden. Auf einer der beiden anderen Platinen schaltet sich die LED2 (Router) aus und die LED1 (Leiter) schaltet sich ein. Dieses Gerät ist jetzt der Leiter des Thread-Netzwerks.

4c57c87adb40e0e3.png

Aktivieren Sie das ursprüngliche Leader-Board wieder. Das Thread-Netzwerk sollte sich automatisch wieder als Endgerät (LED3) beleuchten. Innerhalb von 20 Sekunden (der „Router zur Auswahl“ Jitter) wandert er zu einem Router (LED2 leuchtet).

5f40afca2dcc4b5b.png

Spielfelder zurücksetzen

Deaktivieren Sie alle drei Boards, schalten Sie sie dann wieder ein und beobachten Sie die LEDs. Das erste Board sollte mit der Leader-Rolle beginnen (LED1 leuchtet). Der erste Router in einem Thread-Netzwerk wird automatisch zum Leader.

Die anderen beiden Boards verbinden sich anfangs mit dem Netzwerk, als die Endgeräte (LED3) angestrahlt sind, sollten sich aber innerhalb von 20 Sekunden bei den Routern befinden (LED2).

Netzwerkpartitionen

Wenn Ihre Boards nicht ausreichend mit Strom versorgt werden oder die Funkverbindung zwischen ihnen schwach ist, wird das Thread-Netzwerk möglicherweise in Partitionen aufgeteilt und es wird möglicherweise mehr als ein Gerät als Leader angezeigt.

Thread ist selbstreparierend. Daher sollten Partitionen wieder zu einer einzigen Partition mit einem Leader zusammengeführt werden.

14. Demo: UDP-Multicast senden

Wenn du mit dem vorherigen Training fortfährst, sollte LED4 auf keinem Gerät leuchten.

Wähle ein beliebiges Board aus und drücke auf Taste1. Die LED4 auf allen anderen Boards im Thread-Netzwerk, auf denen die Anwendung ausgeführt wird, sollte in den Status wechseln. Wenn du mit der vorherigen Übung fortfährst, sollte sie jetzt aktiviert sein.

f186a2618fdbe3fd.png

Drücken Sie noch einmal die Taste 1 für dasselbe Board. Die LED4 auf allen anderen Boards sollte wieder ein- und ausgeschaltet werden.

Drücke die Taste1 auf einer anderen Platte und sieh zu, wie die LED4 auf den anderen Boards ein- und ausgeschaltet wird. Drücke die Taste1 auf einem der Boards, bei denen LED4 eingeschaltet ist. Die LED4 für das Board bleibt eingeschaltet, die Ein-/Aus-Schaltflächen werden jedoch aktiviert.

f5865ccb8ab7aa34.png.

Netzwerkpartitionen

Wenn Ihre Boards partitioniert sind und es mehr als einen Leader dafür gibt, unterscheidet sich das Ergebnis der Multicast-Nachricht zwischen den Boards. Wenn Sie die Taste 1 auf einem Board mit partitioniertem Board drücken und somit das einzige Mitglied des partitionierten Thread-Netzwerks ist, leuchtet die LED4 auf den anderen Boards nicht. Setzen Sie in diesem Fall die Boards zurück. Im Idealfall bilden sie ein einzelnes Thread-Netzwerk und UDP-Messaging sollte korrekt funktionieren.

15. Glückwunsch!

Sie haben eine Anwendung erstellt, die OpenThread APIs verwendet.

Sie wissen jetzt:

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

Nächste Schritte

Aufbauend auf diesem Codelab können Sie folgende Übungen ausführen:

  • Ändern Sie das GPIO-Modul so, dass GPIO-Pins anstelle der integrierten LEDs verwendet werden, und schließen Sie externe RGB-LEDs an, die die Farbe je nach Rolle des Routers ändern.
  • GPIO-Unterstützung für eine andere Beispielplattform hinzufügen
  • Verwenden Sie statt Multicast das Pingen aller Geräte per Knopfdruck, nutzen Sie die Router/Leader API, um ein einzelnes Gerät zu finden und zu kontaktieren.
  • Verbinden Sie Ihr Mesh-Netzwerk über einen OpenThread-Border-Router mit dem Internet und streamen Sie sie über das Thread-Netzwerk, um die LEDs zu beleuchten.

Weitere Informationen

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

Referenz: