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 Entwicklern die in Nest-Produkten verwendete Technologie allgemein zur Verfügung zu stellen und so die Entwicklung von Produkten für das vernetzte Zuhause zu beschleunigen.

Die Thread-Spezifikation definiert ein IPv6-basiertes, zuverlässiges, sicheres und energiesparendes Kommunikationsprotokoll für drahtlose Geräte zwischen Geräten für Heimanwendungen. OpenThread implementiert alle Thread-Netzwerkschichten, einschließlich IPv6, 6LoWPAN, IEEE 802.15.4 mit MAC-Sicherheit, Mesh-Link-Einrichtung und Mesh-Routing.

In diesem Codelab nutzen Sie OpenThread-APIs, um ein Thread-Netzwerk zu starten, Änderungen der Geräterollen zu beobachten und darauf zu reagieren und UDP-Nachrichten zu senden. Außerdem verknüpfen Sie diese Aktionen mit Tasten und LEDs auf echter Hardware.

2a6db2e258c32237.png

Lerninhalte

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

Voraussetzungen

Hardware:

  • 3 Entwicklungsboards von Nordic Semiconductor nRF52840
  • 3 USB-auf-Micro-USB-Kabel zum Verbinden der Karten
  • Ein Linux-Computer mit mindestens 3 USB-Ports

Software:

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

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

2. Erste Schritte

Hardware-Codelab absolvieren

Bevor Sie mit diesem Codelab beginnen, sollten Sie das Codelab Build a Thread Network with nRF52840 Boards and OpenThread absolvieren. Dabei erhalten Sie folgende Informationen:

  • Beschreibt die gesamte Software, die Sie zum Erstellen und Flashen benötigen
  • Zeigt Ihnen, wie Sie OpenThread erstellen und auf Nordic nRF52840-Boards flashen können
  • Veranschaulicht die Grundlagen eines Thread-Netzwerks

In diesem Codelab wird keine Umgebung beschrieben, die zum Erstellen von OpenThread und zum Flashen der Boards erforderlich ist – nur grundlegende Anweisungen zum Flashen der Boards. Es wird davon ausgegangen, dass Sie das Codelab „Thread-Netzwerk erstellen“ bereits abgeschlossen haben.

Linux-Computer

Dieses Codelab wurde für die Verwendung eines i386- oder x86-basierten Linux-Computers entwickelt, um alle Thread-Entwicklungsboards zu flashen. Alle Schritte wurden unter Ubuntu 14.04.5 LTS (Trusty Tahr) getestet.

nRF52840-Platinen von Nordic Semiconductor

In diesem Codelab werden drei nRF52840-PDK-Boards verwendet.

a6693da3ce213856.png

Software installieren

Um OpenThread zu erstellen und zu flashen, müssen Sie SEGGER J-Link, die nRF5x-Befehlszeilentools, die ARM GNU-Toolchain und verschiedene Linux-Pakete installieren. Wenn Sie das Codelab zum Erstellen eines Thread-Netzwerks abgeschlossen haben, ist bereits alles installiert, was Sie benötigen. Falls nicht, schließen Sie dieses Codelab ab, bevor Sie fortfahren, um sicherzustellen, dass Sie OpenThread erstellen und in nRF52840-Entwicklungsboards flashen können.

3. Repository klonen

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

Klonen Sie das Repository mit den Beispielen für OpenThread Nordic nRF528xx und erstellen Sie OpenThread:

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

4. Grundlagen der OpenThread API

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

  • Informationen und Steuerung der OpenThread-Instanz
  • Anwendungsdienste wie IPv6, UDP und CoAP
  • Verwaltung von Netzwerk-Qualifikationsnachweisen sowie Positionen als Commissioner und Joiner
  • Border-Router-Verwaltung
  • Erweiterte Funktionen wie Elternaufsicht und Jam-Erkennung

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

API verwenden

Um eine API zu verwenden, fügen Sie deren Headerdatei in eine Ihrer Anwendungsdateien ein. Rufen Sie dann die gewünschte Funktion auf.

Die in OpenThread enthaltene CLI-Beispielanwendung 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 werden Sie häufig verwenden, 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 durchzuführen.

Die OpenThread-Instanz wird beispielsweise in der Funktion main() der Beispiel-App für die Befehlszeile 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, müssen Sie diese zuerst im Header ./openthread/examples/platforms/openthread-system.h deklarieren. Verwenden Sie dabei für alle Funktionen den Namespace otSys. Implementieren Sie sie dann in einer plattformspezifischen Quelldatei. Abstrahiert können Sie dieselben Funktions-Header auch für andere Beispielplattformen verwenden.

Die GPIO-Funktionen, die wir zum Einbinden in die nRF52840-Tasten verwenden, müssen beispielsweise in openthread-system.h deklariert werden.

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

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

AKTION: Füge plattformspezifische GPIO-Funktionsdeklarationen hinzu.

Fügen Sie für den openthread/instance.h-Header diese Funktionsdeklarationen nach #include 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);

Diese werden im nächsten Schritt implementiert.

Die Funktionsdeklaration otSysButtonProcess verwendet ein otInstance. Auf diese Weise kann die Anwendung bei Bedarf auf Informationen über die OpenThread-Instanz zugreifen, wenn eine Schaltfläche gedrückt wird. Alles hängt von den Anforderungen Ihrer Anwendung ab. Wenn Sie es bei der Implementierung der Funktion nicht benötigen, können Sie das Makro OT_UNUSED_VARIABLE aus der OpenThread API verwenden, um Build-Fehler in Bezug auf nicht verwendete Variablen für einige Toolchains zu unterdrücken. Beispiele dafür sehen wir später.

5. GPIO-Plattformabstraktion implementieren

Im vorherigen Schritt haben wir uns die plattformspezifischen Funktionsdeklarationen in ./openthread/examples/platforms/openthread-system.h angesehen, die für GPIO verwendet werden können. Damit Sie auf Schaltflächen und LEDs auf den nRF52840-Entwicklungsboards zugreifen können, 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 an einem Pin steuern
  • GPIO-Unterbrechungen aktivieren und Callback registrieren

Erstellen Sie im Verzeichnis ./src/src eine neue Datei mit dem Namen gpio.c. Fügen Sie der neuen Datei den folgenden Inhalt hinzu.

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

AKTION: Definitionen hinzufügen.

Diese Definitionen dienen als Abstraktionen zwischen nRF52840-spezifischen Werten und Variablen, die auf der 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 den Tasten und LEDs des Typs nRF52840 finden Sie im Nordic Semiconductor Infocenter.

AKTION: Header-Includes hinzufügen.

Fügen Sie als Nächstes die Header-Includes hinzu, die Sie für die GPIO-Funktionalität 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üge Callback- und Unterbrechungsfunktionen für Schaltfläche 1 hinzu.

Fügen Sie diesen Code als Nächstes hinzu. Die in_pin1_handler-Funktion ist der Callback, der registriert wird, wenn die Tastendruckfunktion initialisiert wird (später in dieser Datei).

Beachten Sie, wie bei diesem Callback das Makro OT_UNUSED_VARIABLE verwendet wird, da die an in_pin1_handler übergebenen Variablen nicht in der Funktion 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üge eine Funktion zum Konfigurieren der LEDs hinzu.

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

AKTION: Füge 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 zum Umschalten des LED-Modus hinzu.

Mit dieser Funktion wird die LED4 aktiviert, 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 Tastendrucken hinzufügen.

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

In unserer Anwendung sollen je nach Geräterolle verschiedene LEDs aufleuchten. Sehen wir uns die folgenden Rollen an: „Leader“, „Router“, „Endgerät“. Wir können sie den LEDs so zuweisen:

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

Um diese Funktion zu aktivieren, muss die App wissen, wann sich die Geräterolle geändert hat und wie die richtige LED daraufhin eingeschaltet werden soll. Wir verwenden für den ersten Teil die OpenThread-Instanz und für den zweiten die GPIO-Plattformabstraktion.

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

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

AKTION: Header-Includes hinzufügen.

Füge im Abschnitt „Includes“ der Datei main.c die API-Headerdateien hinzu, die du für die Funktion zum Ändern der Rolle benötigst.

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

AKTION: Deklaration der Handler-Funktion für die Statusänderung der OpenThread-Instanz hinzugefügt.

Fügen Sie diese Deklaration in „main.c“ nach dem Header „Einschließen“ und vor allen #if-Anweisungen hinzu. Diese Funktion wird nach der Hauptanwendung definiert.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

AKTION: Callback-Registrierung für die Handler-Funktion für Statusänderung hinzufügen

Fügen Sie diese Funktion in main.c der Funktion main() nach dem otAppCliInit-Aufruf hinzu. Diese Callback-Registrierung weist OpenThread an, die Funktion handleNetifStateChange jedes Mal aufzurufen, wenn 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. Wenn es sich geändert hat, werden die LEDs bei Bedarf ein- bzw. ausgeschaltet.

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: LEDs per Multicast einschalten

In unserer Anwendung möchten wir auch UDP-Nachrichten an alle anderen Geräte im Netzwerk senden, wenn Button1 auf einer Platine gedrückt wird. Um den Empfang der Nachricht zu bestätigen, schalten wir als Antwort LED4 auf den anderen Boards ein.

Damit diese Funktion aktiviert werden kann, muss die Anwendung folgende Schritte ausführen:

  • UDP-Verbindung beim Start initialisieren
  • Sie müssen in der Lage sein, eine UDP-Nachricht an die lokale Mesh-Multicast-Adresse zu senden.
  • Eingehende UDP-Nachrichten verarbeiten
  • LED4 bei eingehenden UDP-Nachrichten umschalten

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

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

AKTION: Header-Includes hinzufügen.

Fügen Sie oben in der Datei main.c im Abschnitt „Einschließen“ die API-Headerdateien hinzu, 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 problemlos verarbeiten.

AKTION: Definitionen und Konstanten hinzufügen:

Fügen Sie in der Datei main.c nach dem Abschnitt „Includes“ 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 lokale Multicast-Adresse des Mesh-Netzwerks. 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 Definition otTaskletsSignalPending und vor der Funktion main() UDP-spezifische Funktionen sowie eine statische Variable zur Darstellung eines UDP-Sockets hinzu:

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: Füge Aufrufe hinzu, um die GPIO-LEDs und die Taste zu initialisieren.

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

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

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

Fügen Sie in main.c der Funktion main() nach dem otSysButtonInit-Aufruf, den Sie gerade hinzugefügt haben, diese Funktion hinzu:

initUdp(instance);

Mit diesem Aufruf wird sichergestellt, dass beim Start der Anwendung ein UDP-Socket initialisiert wird. Andernfalls kann das Gerät keine UDP-Nachrichten senden oder empfangen.

AKTION: Füge einen Aufruf hinzu, um das GPIO-Schaltflächenereignis zu verarbeiten.

Fügen Sie in main.c diesen Funktionsaufruf der Funktion main() nach dem otSysProcessDrivers-Aufruf in der while-Schleife hinzu. Diese in gpio.c deklarierte Funktion prüft, ob auf die Schaltfläche geklickt wurde. Ist dies der Fall, wird der Handler (handleButtonInterrupt) aufgerufen, der im obigen Schritt festgelegt wurde.

otSysButtonProcess(instance);

AKTION: Implementiere den Handler „Button Interrupt“.

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: Implementieren Sie die UDP-Initialisierung.

Fügen Sie in main.c die Implementierung der initUdp-Funktion nach der handleButtonInterrupt-Funktion hinzu, die Sie gerade hinzugefügt haben:

/**
 * 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 Port, den Sie zuvor definiert haben (1212). Die Funktion otUdpOpen öffnet den Socket und registriert eine Callback-Funktion (handleUdpReceive) für den Empfang einer UDP-Nachricht. otUdpBind bindet den Socket über OT_NETIF_THREAD an die Thread-Netzwerkschnittstelle. Weitere Optionen für Netzwerkschnittstellen finden Sie in der Aufzählung otNetifIdentifier in der UDP API-Referenz.

AKTION: Implementieren Sie UDP-Messaging.

Fügen Sie in main.c die Implementierung der sendUdp-Funktion nach der initUdp-Funktion hinzu, die Sie gerade hinzugefügt haben:

/**
 * 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. Damit wird sichergestellt, dass die UDP-Nachricht im Zwischenspeicher gültig und korrekt zugewiesen ist. Ist dies nicht der Fall, verarbeitet die Funktion Fehler ordnungsgemäß, 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 zu IPv6 und UDP auf openthread.io.

AKTION: UDP-Nachrichtenverarbeitung implementieren

Fügen Sie in main.c die Implementierung der handleUdpReceive-Funktion nach der gerade hinzugefügten sendUdp-Funktion hinzu. Mit dieser Funktion wird LED4 einfach ein- und ausgeschaltet.

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

Der Einfachheit halber sollen unsere Geräte Thread sofort starten und sich beim Einschalten zu einem Netzwerk verbinden. Dazu verwenden wir die Struktur otOperationalDataset. Diese Struktur enthält alle Parameter, die zum Übertragen von Thread-Netzwerkanmeldedaten an ein Gerät erforderlich sind.

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

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

Fügen Sie im Abschnitt „Includes“ oben in der Datei main.c die API-Headerdatei hinzu, die Sie zum Konfigurieren des Thread-Netzwerks benötigen:

#include <openthread/dataset_ftd.h>

AKTION: Funktionsdeklaration zum Festlegen der Netzwerkkonfiguration hinzufügen.

Fügen Sie diese Deklaration in „main.c“ nach dem Header „Einschließen“ und vor allen #if-Anweisungen hinzu. Diese Funktion wird nach der Hauptfunktion der Anwendung definiert.

static void setNetworkConfiguration(otInstance *aInstance);

AKTION: Fügen Sie den Netzwerkkonfigurationsaufruf hinzu.

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

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

AKTION: Füge Aufrufe hinzu, um die Thread-Netzwerkschnittstelle und den Thread-Stack zu aktivieren.

Fügen Sie in main.c diese Funktionsaufrufe nach dem otSysButtonInit-Aufruf der Funktion main() 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: Implementieren Sie die Thread-Netzwerkkonfiguration.

Fügen Sie in main.c die Implementierung der setNetworkConfiguration-Funktion nach der main()-Funktion 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, verwenden wir für diese Anwendung die folgenden Netzwerkparameter für Thread:

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

Außerdem verringern wir hier den Router-Auswahljitter, damit unsere Geräte zu Demozwecken schneller ihre Rollen ändern. Dies erfolgt nur, wenn der Knoten ein FTD (Full Thread Device) ist. Mehr dazu im nächsten Schritt.

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.

Beispielsweise passt die Funktion otThreadSetRouterSelectionJitter die Zeit (in Sekunden) an, die ein Endgerät benötigt, um sich zu einem Router hochzustufen. Der Standardwert für diesen Wert ist 120 gemäß der Thread-Spezifikation. Zur einfacheren Verwendung in diesem Codelab ändern wir den Wert in 20, damit Sie nicht lange warten müssen, bis ein Thread-Knoten die Rollen ändert.

Hinweis: MTD-Geräte werden nicht zu Routern und die Unterstützung für eine Funktion wie otThreadSetRouterSelectionJitter ist nicht in einem MTD-Build enthalten. Später müssen wir die CMake-Option -DOT_MTD=OFF angeben, da andernfalls ein Build-Fehler auftritt.

Sie können dies in 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. Updates vornehmen

Bevor Sie Ihre Anwendung erstellen, sind für drei CMake-Dateien einige kleinere Updates 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 CMakeLists.txt von NordicSemiconductor einige Flags hinzu, damit GPIO-Funktionen in der Anwendung definiert sind.

AKTION: Füge der Datei CMakeLists.txt Flags hinzu.

Ö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

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

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

Öffnen Sie ./src/CMakeLists.txt in Ihrem bevorzugten Texteditor 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 zuletzt der CMakeLists.txt-Datei von NordicSemiconductor die Treiberdatei nrfx_gpiote.c hinzu, damit sie in der Bibliotheks-Build der Nordic-Treiber enthalten ist.

AKTION: Fügen Sie der Datei „NordicSemiconductorCMakeLists.txt “ den gpio-Treiber hinzu.

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

...

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

11. Geräte einrichten

Wenn alle Code-Updates abgeschlossen sind, können Sie die Anwendung erstellen und in allen drei Nordic nRF52840-Entwicklungsboards verwenden. Jedes Gerät fungiert als 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 zum Verzeichnis mit der OpenThread-FTD-CLI-Binärdatei und konvertieren Sie es mit der ARM Embedded Toolchain in das Hex-Format:

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

Die Tafeln blinken lassen

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

Stecken Sie das USB-Kabel in den Micro-USB-Debug-Port neben dem externen Stromanschluss der nRF52840-Platine und schließen Sie es dann an Ihren Linux-Computer an. Stellen Sie die LED5 richtig ein.

20a3b4b480356447.png

Notieren Sie sich wie zuvor die Seriennummer der nRF52840-Platine:

c00d519ebec7e5f0.jpeg

Navigieren Sie zum Speicherort der nRFx-Befehlszeilentools und flashen Sie die FTD-Hexadezimaldatei der OpenThread CLI mithilfe der Seriennummer auf der nRF52840-Platine:

$ 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 dies: „Die Tafeln blitzen“ für die anderen beiden Boards. Jede Karte sollte auf die gleiche Weise mit dem Linux-Computer verbunden werden und der Befehl zum Flashen bleibt derselbe, abgesehen von der Seriennummer der Karte. Achte darauf, die eindeutige Seriennummer jeder Karte im

nrfjprog blinkender Befehl.

Im Erfolgsfall leuchten auf jeder Platine LED1, LED2 oder LED3 auf. Kurz nach dem Blinken (Funktion zum Ändern der Geräterolle) wechselt der LED-Lichter von 3 auf 2 (oder von 2 auf 1).

12. Anwendungsfunktionen

Alle drei nRF52840-Karten sollten jetzt mit Strom versorgt werden und unsere OpenThread-Anwendung ausführen. Wie bereits erwähnt, verfügt diese Anwendung über zwei Hauptfunktionen.

Geräterollenanzeigen

Die LED auf jeder Platine zeigt die aktuelle Rolle des Thread-Knotens an:

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

Wenn sich die Rolle ändert, ändert sich auch die leuchtende LED. Sie sollten diese Änderungen bereits auf ein oder zwei Platinen innerhalb von 20 Sekunden nach dem Einschalten jedes Geräts gesehen haben.

UDP-Multicast

Wenn Button1 auf einer Tafel gedrückt wird, wird eine UDP-Nachricht an die lokale Multicast-Adresse des Mesh-Netzwerks gesendet, zu der auch alle anderen Knoten im Thread-Netzwerk gehören. Als Reaktion auf diese Meldung wird die LED4 an allen anderen Karten ein- oder ausgeschaltet. LED4 bleibt für jede Karte an oder aus, bis eine weitere UDP-Nachricht empfangen wird.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demo: Änderungen an Geräterollen beobachten

Bei den Geräten, die Sie geflasht haben, handelt es sich um eine bestimmte Art von Full-Thread-Gerät (Full Thread Device, FTD), auch als Router Allowed End Device (REED) bezeichnet. Das bedeutet, dass sie entweder als Router oder als Endgerät fungieren und sich von einem Endgerät zu einem Router hochstufen können.

Thread kann bis zu 32 Router unterstützen, versucht jedoch, 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, wird es automatisch zu einem Router hochgestuft. Diese Änderung sollte zufällig innerhalb der Anzahl von Sekunden erfolgen, die Sie in der Anwendung für den Wert otThreadSetRouterSelectionJitter festgelegt haben (20 Sekunden).

Jedes Thread-Netzwerk hat auch einen Leader, also einen Router, der für die Verwaltung der Routergruppe in einem Thread-Netzwerk verantwortlich ist. Wenn alle Geräte eingeschaltet sind, sollte nach 20 Sekunden eines der Geräte ein Leader (LED1 an) und die anderen beiden Router (LED2 an) sein.

4e1e885861a66570.png

Führungselement entfernen

Wird der Leader aus dem Thread-Netzwerk entfernt, stuft sich ein anderer Router zum Leader hoch, um sicherzustellen, dass das Netzwerk weiterhin einen Leader hat.

Schalten Sie die Bestenliste (mit LED1-Licht) mithilfe des Ein-/Aus-Schalters aus. Warten Sie etwa 20 Sekunden. Auf einer der verbleibenden zwei Karten schaltet sich LED2 (Router) aus und LED1 (Leader) leuchtet auf. Dieses Gerät ist jetzt das führende Thread-Netzwerk.

4c57c87adb40e0e3.png

Aktivieren Sie die ursprüngliche Bestenliste wieder. Es sollte automatisch wieder als Endgerät mit dem Thread-Netzwerk verbunden werden (LED3 leuchtet). Innerhalb von 20 Sekunden (Router-Auswahljitter) wird er zu einem Router hochgestuft (LED2 leuchtet auf).

5f40afca2dcc4b5b.png

Boards zurücksetzen

Schalten Sie alle drei Leiterplatten aus und wieder ein und beobachten Sie, ob die LEDs leuchten. Das erste eingeschaltete Board sollte die Leader-Rolle haben (LED1 leuchtet auf) – der erste Router in einem Thread-Netzwerk wird automatisch zum Leader.

Die anderen beiden Platinen werden zuerst als Endgeräte mit dem Netzwerk verbunden (LED3 leuchtet auf), sollten sich jedoch innerhalb von 20 Sekunden zu Routern hochstufen (LED2 leuchtet).

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 selbst repariert sich selbst, sodass Partitionen irgendwann wieder zu einer einzigen Partition mit einem Leader zusammengeführt werden sollten.

14. Demo: UDP-Multicast senden

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

Wähle ein beliebiges Board und drücke Button1. LED4 auf allen anderen Boards im Thread-Netzwerk, auf denen die Anwendung ausgeführt wird, sollte ihren Status umschalten. Wenn Sie mit der vorherigen Übung fortfahren, sollten diese jetzt eingeschaltet sein.

f186a2618fdbe3fd.png

Drücke noch einmal Button1 für dasselbe Board. LED4 auf allen anderen Karten sollte wieder umgeschaltet werden.

Drücke „Button1“ auf einer anderen Karte und sieh zu, wie LED4 bei den anderen Leitern umschaltet. Drücke Taste1 auf einer der Platinen, bei der LED4 derzeit eingeschaltet ist. LED4 bleibt für diese Karte eingeschaltet, schaltet sich jedoch auf den anderen ein.

f5865ccb8ab7aa34.png

Netzwerkpartitionen

Wenn Ihre Boards partitioniert sind und mehr als ein Leader vorhanden ist, unterscheidet sich das Ergebnis der Multicast-Nachricht zwischen den Boards. Wenn Sie Button1 auf einer Platine drücken, die partitioniert ist (und damit das einzige Mitglied des partitionierten Thread-Netzwerks ist), leuchtet LED4 auf den anderen Boards nicht auf. Wenn dies der Fall ist, setzen Sie die Boards zurück. Idealerweise stellen sie ein einzelnes Thread-Netzwerk um und UDP-Messaging sollte korrekt funktionieren.

15. Glückwunsch!

Sie haben eine Anwendung erstellt, die OpenThread-APIs verwendet.

Jetzt wissen Sie:

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

Nächste Schritte

Probiere die folgenden Übungen aus, die auf diesem Codelab aufbauen:

  • Stellen Sie das GPIO-Modul so ein, dass GPIO-Pins anstelle der integrierten LEDs verwendet werden. Schließen Sie externe RGB-LEDs an, die die Farbe je nach Routerrolle ändern.
  • GPIO-Unterstützung für eine andere Beispielplattform hinzufügen
  • Anstatt Multicast zu verwenden, um alle Geräte per Tastendruck anzupingen, verwenden Sie die Router/Leader API, um ein einzelnes Gerät zu finden und zu pingen.
  • Verbinden Sie Ihr Mesh-Netzwerk mithilfe eines OpenThread-Border-Routers mit dem Internet und führen Sie einen Multicast von außerhalb des Thread-Netzwerks aus, um die LEDs zu beleuchten.

Weitere Informationen

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

Referenz: