Mit OpenThread-APIs entwickeln

1. Einführung

26b7f4f6b3ea0700.png

OpenThread von Nest ist eine Open-Source-Implementierung des Thread®-Netzwerkprotokolls. Nest hat OpenThread veröffentlicht, um die in Nest-Produkten verwendete Technologie für Entwickler allgemein verfügbar zu machen und so die Entwicklung von Produkten für das vernetzte Zuhause zu beschleunigen.

Die Thread-Spezifikation definiert ein zuverlässiges, sicheres und energieeffizientes IPv6-basiertes drahtloses Protokoll für die Kommunikation zwischen Geräten für Anwendungen im Zuhause. 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 verwenden Sie OpenThread APIs, um ein Thread-Netzwerk zu starten, Änderungen an Geräterollen zu überwachen und darauf zu reagieren, UDP-Nachrichten zu senden und diese Aktionen mit Schaltflächen und LEDs auf echter Hardware zu verknüpfen.

2a6db2e258c32237.png

Lerninhalte

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

Voraussetzungen

Hardware:

  • 3 Entwicklerboards nRF52840 von Nordic Semiconductor
  • 3 USB-auf-Micro-USB-Kabel zum Verbinden der Boards
  • Einen Linux-Computer mit mindestens drei USB-Anschlüssen

Software:

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

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

2. Erste Schritte

Codelab zur Hardware abschließen

Bevor Sie mit diesem Codelab beginnen, sollten Sie das Codelab Ein Thread-Netzwerk mit nRF52840-Boards und OpenThread erstellen absolvieren. Dabei lernen Sie Folgendes:

  • Alle Software, die Sie zum Erstellen und Flashen benötigen
  • Hier erfahren Sie, wie Sie OpenThread erstellen und auf Nordic nRF52840-Boards flashen.
  • Die Grundlagen eines Thread-Netzwerks

In diesem Codelab wird keine der Umgebungen beschrieben, die zum Erstellen von OpenThread und zum Flashen der Boards erforderlich sind. Es wird nur eine grundlegende Anleitung zum Flashen der Boards bereitgestellt. Es wird davon ausgegangen, dass Sie das Codelab zum Erstellen eines Thread-Netzwerks bereits abgeschlossen haben.

Linux-Rechner

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

Nordic Semiconductor nRF52840-Boards

In diesem Codelab werden drei nRF52840 PDK-Boards verwendet.

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 zum Erstellen eines Thread-Netzwerks wie erforderlich abgeschlossen haben, ist bereits alles installiert, was Sie benötigen. Falls nicht, schließen Sie dieses Codelab ab, bevor Sie fortfahren, damit Sie OpenThread auf nRF52840-Entwicklerboards erstellen und flashen können.

3. Repository klonen

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

Klonen Sie das OpenThread Nordic nRF528xx-Beispiel-Repository 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 finden Sie im OpenThread-Repository unter ./openthread/include/openthread. Diese APIs bieten Zugriff auf eine Vielzahl von OpenThread-Funktionen sowohl auf Thread- als auch auf Plattformebene für die Verwendung in Ihren Anwendungen:

  • Informationen zu und Steuerung von OpenThread-Instanzen
  • Anwendungsdienste wie IPv6, UDP und CoAP
  • Netzwerk-Anmeldedatenverwaltung sowie Rollen „Beauftragter“ und „Teilnehmer“
  • Border-Router-Verwaltung
  • Erweiterte Funktionen wie Elternaufsicht und Funkstörungserkennung

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

API verwenden

Wenn Sie eine API verwenden möchten, fügen Sie die zugehörige Headerdatei in eine Ihrer Anwendungsdateien ein. Rufen Sie dann die gewünschte Funktion auf.

Die in OpenThread enthaltene Beispiel-CLI-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 bei der Arbeit mit den OpenThread APIs verwendet. Nach der Initialisierung stellt diese Struktur eine statische Instanz der OpenThread-Bibliothek dar und ermöglicht dem Nutzer OpenThread API-Aufrufe.

In der Beispiel-CLI-App wird die OpenThread-Instanz beispielsweise in der main()-Funktion 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 platformspezifische Funktionen hinzufügen möchten, deklarieren Sie sie zuerst in der ./openthread/examples/platforms/openthread-system.h-Headerdatei und verwenden Sie den Namespace otSys für alle Funktionen. Implementieren Sie sie dann in einer plattformspezifischen Quelldatei. So können Sie dieselben Funktionsheader für andere Beispielplattformen verwenden.

Beispielsweise müssen die GPIO-Funktionen, die wir zum Anschließen an die Schaltflächen und LEDs des nRF52840 verwenden, 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: Plattformspezifische GPIO-Funktionsdeklarationen hinzufügen

Fügen Sie diese Funktionsdeklarationen nach dem #include für den openthread/instance.h-Header 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 implementieren wir im nächsten Schritt.

Beachten Sie, dass in der Funktionsdeklaration für otSysButtonProcess ein otInstance verwendet wird. So kann die Anwendung bei Bedarf auf Informationen zur OpenThread-Instanz zugreifen, wenn eine Schaltfläche gedrückt wird. Das hängt von den Anforderungen Ihrer Anwendung ab. Wenn Sie sie in Ihrer Implementierung der Funktion nicht benötigen, können Sie das OT_UNUSED_VARIABLE-Makro aus der OpenThread API verwenden, um Buildfehler bei nicht verwendeten Variablen für einige Toolchains zu unterdrücken. Später sehen wir uns Beispiele dafür an.

5. GPIO-Plattformabstraktion implementieren

Im vorherigen Schritt haben wir die plattformspezifischen Funktionsdeklarationen in ./openthread/examples/platforms/openthread-system.h besprochen, die für GPIO verwendet werden können. Damit Sie auf die Schaltflächen und LEDs auf den nRF52840-Entwicklerboards zugreifen können, müssen Sie diese Funktionen für die nRF52840-Plattform implementieren. In diesem Code fügen Sie Funktionen hinzu, die Folgendes tun:

  • GPIO-Pins und -Modi initialisieren
  • Spannung an einem Pin steuern
  • GPIO-Unterbrechungen aktivieren und einen Rückruf registrieren

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

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

AKTION: Definitionen hinzufügen

Diese Definitionen dienen als Abstraktion 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 den Tasten und LEDs des 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-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 Rückruf- 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 der Schaltfläche initialisiert wird (später in dieser Datei).

Beachten Sie, dass in diesem Callback das Makro OT_UNUSED_VARIABLE verwendet wird, 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 zum Konfigurieren der LEDs hinzu.

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ügen Sie eine Funktion hinzu, um den Modus einer LED umzuschalten.

Mit dieser Funktion wird LED4 ein- und ausgeschaltet, 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 eine Tastenbetätigung und die zweite 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 der Geräterolle reagieren

In unserer Anwendung sollen je nach Geräterolle unterschiedliche LEDs leuchten. Beobachten wir die folgenden Rollen: Leader, Router und Endgerät. Wir können sie den LEDs so zuweisen:

  • LED1 = Führungsgerät
  • LED2 = Router
  • LED3 = Endgerät

Damit diese Funktion aktiviert werden kann, muss die Anwendung wissen, wann sich die Geräterolle geändert hat und wie die richtige LED aktiviert werden soll. Für den ersten Teil verwenden wir die OpenThread-Instanz und für den zweiten Teil 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ügen Sie im Abschnitt „includes“ der Datei main.c die API-Headerdateien hinzu, die Sie für die Funktion zum Ändern der Rolle benötigen.

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

AKTION: Deklaration der Handlerfunktion für die Änderung des OpenThread-Instanzstatus hinzufügen

Fügen Sie diese Deklaration main.c hinzu, nach dem Header-Include und vor allen #if-Anweisungen. Diese Funktion wird nach der Hauptanwendung definiert.

void handleNetifStateChanged(uint32_t aFlags, void *aContext);

AKTION: Callback-Registrierung für die Statusänderungs-Handlerfunktion hinzufügen

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

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

AKTION: Implementieren Sie die Statusänderung.

Implementieren Sie in main.c nach der Funktion main() die Funktion handleNetifStateChanged. Diese Funktion prüft das OT_CHANGED_THREAD_ROLE-Flag der OpenThread-Instanz und schaltet die LEDs bei Bedarf ein oder aus, wenn sich das Flag geändert hat.

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: LED mit Multicast einschalten

In unserer Anwendung möchten wir außerdem UDP-Nachrichten an alle anderen Geräte im Netzwerk senden, wenn auf einem Board die Taste 1 gedrückt wird. Als Bestätigung des Empfangs der Nachricht schalten wir LED4 auf den anderen Boards ein und aus.

Damit diese Funktion aktiviert werden kann, muss die Anwendung Folgendes erfüllen:

  • UDP-Verbindung beim Starten initialisieren
  • UDP-Nachricht an die mesh-lokale Multicast-Adresse senden können
  • Eingehende UDP-Nachrichten verarbeiten
  • LED4 als Reaktion auf eingehende UDP-Nachrichten ein- und ausschalten

Ö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 im Abschnitt „includes“ oben in der Datei main.c 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 code_utils.h-Header wird für die Makros otEXPECT und otEXPECT_ACTION verwendet, die Laufzeitbedingungen prüfen und Fehler ordnungsgemäß behandeln.

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 und Definitionen hinzu:

#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-lokale Multicast-Adresse. Alle an diese Adresse gesendeten Nachrichten werden an alle Geräte mit vollständigem Thread 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 von 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: Fügen Sie Aufrufe hinzu, um die GPIO-LEDs und die Schaltfläche zu initialisieren.

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

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

AKTION: UDP-Initialisierungsaufruf hinzufügen

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

initUdp(instance);

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

AKTION: Rufen Sie eine Funktion auf, um das GPIO-Tastenereignis 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 die Schaltfläche gedrückt wurde, und ruft andernfalls den Handler (handleButtonInterrupt) auf, der im obigen Schritt festgelegt wurde.

otSysButtonProcess(instance);

AKTION: Implementieren Sie einen Schaltflächen-Unterbrechungs-Handler.

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

AKTION: UDP-Initialisierung implementieren

Fügen Sie in main.c die Implementierung der Funktion initUdp nach der Funktion handleButtonInterrupt 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 zuvor definierte Port (1212). Die Funktion otUdpOpen öffnet den Socket und registriert eine Callback-Funktion (handleUdpReceive), die aufgerufen wird, wenn eine UDP-Nachricht empfangen wird. otUdpBind bindet den Socket an die Thread-Netzwerkschnittstelle, indem OT_NETIF_THREAD übergeben wird. Weitere Netzwerkschnittstellenoptionen finden Sie in der otNetifIdentifier-Aufzählung in der UDP API-Referenz.

AKTION: UDP-Messaging implementieren

Fügen Sie in main.c die Implementierung der Funktion sendUdp nach der Funktion initUdp 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);
    }
}

Notieren Sie sich die Makros otEXPECT und otEXPECT_ACTION. So wird sichergestellt, dass die UDP-Nachricht gültig ist und im Puffer korrekt zugewiesen wird. Andernfalls werden Fehler durch einen Sprung zum Block exit verwaltet, wo der Puffer freigegeben wird.

Weitere Informationen zu den Funktionen, die zum Initialisieren von UDP verwendet werden, finden Sie in den IPv6- und UDP-Referenzen auf openthread.io.

AKTION: UDP-Nachrichtenbehandlung implementieren

Fügen Sie in main.c die Implementierung der Funktion handleUdpReceive nach der Funktion sendUdp hinzu, die Sie gerade hinzugefügt haben. Diese Funktion schaltet LED4 einfach um.

/**
 * 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 besseren Veranschaulichung sollen unsere Geräte sofort nach dem Einschalten Thread starten und sich zu einem Netzwerk zusammenschließen. Dazu verwenden wir die Struktur otOperationalDataset. Diese Struktur enthält alle Parameter, die zum Übertragen von Thread-Netzwerkanmeldedaten an ein Gerät erforderlich sind.

Durch die Verwendung dieser Struktur werden die in OpenThread integrierten Netzwerkstandardeinstellungen überschrieben, um unsere Anwendung sicherer zu machen und Thread-Knoten in unserem Netzwerk auf diejenigen 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 „includes“ 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 main.c hinzu, nach dem Header-Include und vor allen #if-Anweisungen. Diese Funktion wird nach der Hauptfunktion der Anwendung definiert.

static void setNetworkConfiguration(otInstance *aInstance);

AKTION: Den Aufruf zur Netzwerkkonfiguration hinzufügen

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

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

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

Fügen Sie in main.c nach dem otSysButtonInit-Aufruf die folgenden Funktionsaufrufe zur 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: Thread-Netzwerkkonfiguration implementieren

Fügen Sie in main.c nach der Funktion main() die Implementierung der Funktion setNetworkConfiguration 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 Thread-Netzwerkparameter:

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

Außerdem verringern wir hier den Jitter bei der Routerauswahl, damit unsere Geräte zu Demonstrationszwecken schneller die Rolle wechseln. Hinweis: Dies geschieht nur, wenn es sich bei dem Knoten um ein FTD (Full Thread Device) handelt. Mehr dazu im nächsten Schritt.

9. API: Eingeschränkten Funktionen

Einige der OpenThread-APIs ändern Einstellungen, die nur zu Demo- oder Testzwecken geändert werden sollten. Diese APIs sollten nicht in der 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 bei einem Router anzumelden. Gemäß der Thread-Spezifikation ist der Standardwert für diesen Wert 120. Für die bessere Nutzung in diesem Codelab ändern wir ihn zu 20, damit Sie nicht so lange warten müssen, bis ein Threadknoten die Rolle wechselt.

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

Das können Sie an der otThreadSetRouterSelectionJitter-Funktionsdefinition sehen, die in einer OPENTHREAD_FTD-Vorverarbeitungsanweisung 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 Änderungen an drei CMake-Dateien erforderlich. Sie werden vom Build-System zum Kompilieren und Verknüpfen Ihrer Anwendung verwendet.

./third_party/NordicSemiconductor/CMakeLists.txt

Fügen Sie nun dem NordicSemiconductor-CMakeLists.txt einige Flags hinzu, damit die GPIO-Funktionen in der Anwendung definiert sind.

AKTION: Fügen Sie der Datei CMakeLists.txt Flags hinzu.

Öffnen Sie ./third_party/NordicSemiconductor/CMakeLists.txt in Ihrem bevorzugten Texteditor und fügen Sie die folgenden Zeilen im Abschnitt COMMON_FLAG 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 Quelldatei gpio.c 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 dem 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 der Datei CMakeLists.txt von NordicSemiconductor hinzu, damit sie in den Bibliotheksbuild der Nordic-Treiber aufgenommen wird.

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

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

...

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

11. Geräte einrichten

Nachdem alle Codeupdates abgeschlossen sind, können Sie die Anwendung erstellen und auf alle drei Nordic nRF52840-Entwicklerboards flashen. Jedes Gerät fungiert als Full Thread Device (FTD).

OpenThread erstellen

Erstelle 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

Rufen Sie das Verzeichnis mit der OpenThread-FTD-Befehlszeilen-Binärdatei auf und konvertieren Sie sie mit der ARM Embedded Toolchain in das Hexadezimalformat:

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

Boards flashen

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

Schließen Sie das USB-Kabel an den Micro-USB-Debug-Port neben dem externen Stromanschluss des nRF52840-Boards an und dann an Ihren Linux-Computer. Wenn die Einstellungen korrekt sind, leuchtet LED5.

20a3b4b480356447.png

Notieren Sie sich wie zuvor die Seriennummer des nRF52840-Boards:

c00d519ebec7e5f0.jpeg

Rufen Sie den Speicherort der nRFx-Befehlszeilentools auf und flashen Sie die OpenThread-CLI-FTD-Hexdatei mit der Seriennummer des Boards auf das nRF52840-Board:

$ 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 zum Flashen der Boards für die anderen beiden Boards. Jedes Board sollte auf dieselbe Weise mit dem Linux-Computer verbunden sein und der Befehl zum Flashen ist mit Ausnahme der Seriennummer des Boards identisch. Verwenden Sie in der

nrfjprog Befehl zum Flashen

Wenn der Vorgang erfolgreich war, leuchtet auf jedem Board entweder LED1, LED2 oder LED3. Möglicherweise sehen Sie kurz nach dem Blinken sogar, dass die leuchtende LED von 3 auf 2 (oder von 2 auf 1) umschaltet (Geräterollenänderung).

12. App-Funktionen

Alle drei nRF52840-Boards sollten jetzt eingeschaltet sein und unsere OpenThread-Anwendung ausführen. Wie bereits erwähnt, hat diese Anwendung zwei Hauptfunktionen.

Indikatoren für die Geräterolle

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

  • LED1 = Führungsgerät
  • LED2 = Router
  • LED3 = Endgerät

Wenn sich die Rolle ändert, ändert sich auch die Farbe der LED. Diese Änderungen sollten Sie bereits innerhalb von 20 Sekunden nach dem Einschalten auf einem oder zwei Boards sehen.

UDP-Multicast

Wenn Taste 1 auf einem Steuerfeld gedrückt wird, wird eine UDP-Nachricht an die mesh-lokale Multicast-Adresse gesendet, die alle anderen Knoten im Thread-Netzwerk umfasst. Als Reaktion auf den Empfang dieser Nachricht blinkt LED4 auf allen anderen Boards an oder aus. LED4 bleibt auf jedem Board an oder aus, bis eine weitere UDP-Nachricht empfangen wird.

203dd094acca1f97.png

9bbd96d9b1c63504.png

13. Demo: Änderungen an Geräterollen beobachten

Die von Ihnen geflashten Geräte sind eine bestimmte Art von Full Thread Device (FTD), das als Router Eligible End Device (REED) bezeichnet wird. Das bedeutet, dass sie entweder als Router oder Endgerät fungieren und sich von einem Endgerät zu einem Router befördern 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 verbunden ist und die Anzahl der Router unter 16 liegt, wird es automatisch zu einem Router. Diese Änderung sollte zu einer zufälligen Zeit innerhalb der Anzahl von Sekunden erfolgen, die Sie in der Anwendung für den otThreadSetRouterSelectionJitter-Wert festgelegt haben (20 Sekunden).

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

4e1e885861a66570.png

Führungsperson entfernen

Wenn der Leader aus dem Thread-Netzwerk entfernt wird, übernimmt ein anderer Router die Rolle des Leaders, damit das Netzwerk immer einen Leader hat.

Schalten Sie die Bestenliste (die mit der leuchtenden LED 1) mit dem Ein/Aus-Schalter aus. Warten Sie etwa 20 Sekunden. Auf einem der verbleibenden beiden Boards schaltet sich LED2 (Router) aus und LED1 (Leiter) ein. Dieses Gerät ist jetzt der Leader des Thread-Netzwerks.

4c57c87adb40e0e3.png

Aktiviere die ursprüngliche Bestenliste wieder. Es sollte automatisch wieder als Endgerät mit dem Thread-Netzwerk verbunden werden (LED 3 leuchtet). Innerhalb von 20 Sekunden (der Jitter für die Routerauswahl) stuft es sich selbst als Router ein (LED2 leuchtet).

5f40afca2dcc4b5b.png

Boards zurücksetzen

Schalten Sie alle drei Boards aus, schalten Sie sie dann wieder ein und beobachten Sie die LEDs. Das erste Board, das eingeschaltet wurde, sollte die Rolle „Leiter“ übernehmen (LED1 leuchtet). Der erste Router in einem Thread-Netzwerk wird automatisch zum Leiter.

Die anderen beiden Boards stellen sich zunächst als Endgeräte (LED 3 leuchtet) mit dem Netzwerk in Verbindung, sollten sich aber innerhalb von 20 Sekunden zu Routern (LED 2 leuchtet) hochstufen.

Netzwerkpartitionen

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

Der Thread ist selbstheilend. Daher sollten Partitionen schließlich wieder zu einer einzelnen Partition mit einem einzelnen Leader zusammengeführt werden.

14. Demo: UDP-Multicast senden

Wenn Sie mit der vorherigen Übung fortfahren, sollte LED4 auf keinem Gerät leuchten.

Wählen Sie ein beliebiges Board aus und drücken Sie Taste 1. Die LED4 auf allen anderen Boards im Thread-Netzwerk, in dem die Anwendung ausgeführt wird, sollte ihren Status wechseln. Wenn Sie mit der vorherigen Übung fortfahren, sollten sie jetzt aktiviert sein.

f186a2618fdbe3fd.png

Drücken Sie noch einmal Taste 1 für dasselbe Board. LED4 auf allen anderen Boards sollte wieder blinken.

Drücken Sie Taste 1 auf einem anderen Board und beobachten Sie, wie LED 4 auf den anderen Boards ein- und ausgeschaltet wird. Drücken Sie auf einer der Platinen, auf der LED4 gerade leuchtet, die Taste 1. LED4 bleibt für dieses Board an, wechselt aber bei den anderen Boards.

f5865ccb8ab7aa34.png

Netzwerkpartitionen

Wenn Ihre Boards partitioniert sind und es mehr als einen Leiter gibt, unterscheidet sich das Ergebnis der Multicast-Nachricht zwischen den Boards. Wenn Sie auf eine Taste 1 auf einem Board drücken, das partitioniert ist (und somit das einzige Mitglied des partitionierten Thread-Netzwerks ist), leuchtet LED 4 auf den anderen Boards nicht auf. In diesem Fall müssen Sie die Boards zurücksetzen. Idealerweise wird dann ein einzelnes Thread-Netzwerk neu erstellt und die UDP-Messaging-Funktion sollte wieder ordnungsgemäß funktionieren.

15. Glückwunsch!

Sie haben eine Anwendung erstellt, die OpenThread APIs verwendet.

Sie wissen jetzt:

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

Nächste Schritte

Versuchen Sie, anhand dieses Codelabs die folgenden Übungen auszufü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 Routerrolle ändern.
  • GPIO-Unterstützung für eine andere Beispielplattform hinzufügen
  • Anstatt mit Multicast alle Geräte per Tastenanschlag zu pingen, verwenden Sie die Router/Leader API, um ein einzelnes Gerät zu finden und zu pingen.
  • Verbinden Sie Ihr Mesh-Netzwerk über einen OpenThread-Border-Router mit dem Internet und senden Sie einen Multicast von außerhalb des Thread-Netzwerks, um die LEDs zu beleuchten.

Weitere Informationen

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

Referenz: