1. Einführung

OpenThread von Nest ist eine Open-Source-Implementierung des Thread®-Netzwerkprotokolls. Nest hat OpenThread veröffentlicht, um die in Nest-Produkten verwendete Technologie Entwicklern zur Verfügung zu stellen 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 Protokoll für die drahtlose Kommunikation zwischen Geräten für Anwendungen im Haushalt. OpenThread implementiert alle Thread-Netzwerkschichten, einschließlich IPv6, 6LoWPAN, IEEE 802.15.4 mit MAC-Sicherheit, Mesh Link Establishment und Mesh Routing.
In diesem Codelab verwenden Sie OpenThread-APIs, um ein Thread-Netzwerk zu starten, Änderungen an Geräterollen zu überwachen und darauf zu reagieren sowie UDP-Nachrichten zu senden. Außerdem binden Sie diese Aktionen an Schaltflächen und LEDs auf echter Hardware.

Lerninhalte
- Tasten und LEDs auf Nordic nRF52840-Entwicklerboards programmieren
- Verwendung gängiger OpenThread-APIs und der Klasse
otInstance - OpenThread-Statusänderungen überwachen und darauf reagieren
- UDP-Nachrichten an alle Geräte in einem Thread-Netzwerk senden
- Makefiles ändern
Voraussetzungen
Hardware:
- 3 Nordic Semiconductor nRF52840-Entwicklerboards
- 3 USB-zu-Micro-USB-Kabel zum Verbinden der Boards
- Ein Linux-Computer mit mindestens drei USB-Anschlüssen
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-Lizenz „Namensnennung 3.0“ und die Codebeispiele unter der Apache 2.0-Lizenz lizenziert.
2. Erste Schritte
Hardware-Codelab abschließen
Bevor Sie mit diesem Codelab beginnen, sollten Sie das Codelab Build a Thread Network with nRF52840 Boards and OpenThread durcharbeiten. Darin wird Folgendes behandelt:
- Hier finden Sie alle Software, die Sie zum Erstellen und Flashen benötigen.
- Hier erfahren Sie, wie Sie OpenThread erstellen und auf Nordic nRF52840-Boards flashen.
- Grundlagen eines Thread-Netzwerks
Die für das Erstellen von OpenThread und das Flashen der Boards erforderliche Einrichtung der Umgebung wird in diesem Codelab nicht beschrieben. Es werden nur grundlegende Anleitungen zum Flashen der Boards gegeben. Es wird davon ausgegangen, dass Sie das Codelab „Thread-Netzwerk erstellen“ bereits abgeschlossen haben.
Linux-Computer
Dieses Codelab wurde für die Verwendung einer i386- oder x86-basierten Linux-Maschine zum Flashen aller Thread-Entwicklungsboards entwickelt. 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.

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 „Thread-Netzwerk erstellen“ wie erforderlich durchlaufen haben, ist bereits alles installiert, was Sie benötigen. Wenn nicht, führen Sie dieses Codelab aus, bevor Sie fortfahren, damit Sie OpenThread auf nRF52840-Entwicklerboards erstellen und flashen können.
3. Repository klonen
OpenThread enthält Beispielanwendungscode, den Sie als Ausgangspunkt für dieses Codelab verwenden können.
Klonen Sie das OpenThread-Beispiel-Repository für 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 im OpenThread-Repository unter ./openthread/include/openthread. Diese APIs bieten Zugriff auf eine Vielzahl von OpenThread-Funktionen und -Funktionalitäten sowohl auf Thread- als auch auf Plattformebene für die Verwendung in Ihren Anwendungen:
- OpenThread-Instanzinformationen und ‑Steuerung
- Anwendungsdienste wie IPv6, UDP und CoAP
- Verwaltung von Netzwerkanmeldedaten sowie Commissioner- und Joiner-Rollen
- Border-Router-Verwaltung
- Erweiterte Funktionen wie die Elternaufsicht und die Stauerkennung
Referenzinformationen zu allen OpenThread-APIs finden Sie unter openthread.io/reference.
API verwenden
Wenn Sie eine API verwenden möchten, müssen Sie die zugehörige Header-Datei in eine Ihrer Anwendungsdateien einfügen. Rufen Sie dann die gewünschte Funktion auf.
Die in OpenThread enthaltene CLI-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 otInstance-Struktur wird häufig verwendet, wenn Sie mit den OpenThread-APIs arbeiten. Nach der Initialisierung stellt diese Struktur eine statische Instanz der OpenThread-Bibliothek dar und ermöglicht dem Nutzer, OpenThread-API-Aufrufe zu tätigen.
Die OpenThread-Instanz wird beispielsweise in der Funktion main() der CLI-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 Beispielanwendungen, die in OpenThread enthalten sind, plattformspezifische Funktionen hinzufügen möchten, deklarieren Sie sie zuerst im ./openthread/examples/platforms/openthread-system.h-Header und verwenden Sie für alle Funktionen den otSys-Namespace. Implementieren Sie sie dann in einer plattformspezifischen Quelldatei. Auf diese Weise können Sie dieselben Funktionsheadern für andere Beispielplattformen verwenden.
Die GPIO-Funktionen, die wir verwenden, um die nRF52840-Schaltflächen und ‑LEDs anzusteuern, 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
ACTION: Add platform-specific GPIO function declarations.
Fügen Sie diese Funktionsdeklarationen nach dem #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);
Wir werden sie im nächsten Schritt implementieren.
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 Makro OT_UNUSED_VARIABLE aus der OpenThread API verwenden, um Build-Fehler im Zusammenhang mit nicht verwendeten Variablen für einige Toolchains zu unterdrücken. Dazu sehen wir uns später Beispiele an.
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. Wenn Sie auf Tasten und LEDs auf den nRF52840-Entwicklerboards zugreifen möchten, 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-Interrupts aktivieren und Callback 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)
ACTION: Add defines.
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 mesh extender 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-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 Interrupt-Funktionen 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 Schaltflächenfunktion 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;
}
ACTION: Add a function to configure the LEDs. (AKTION: Füge 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);
}
ACTION: Add a function to set the mode of an LED. (AKTION: Füge eine Funktion zum Festlegen des Modus einer LED hinzu.)
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;
}
}
ACTION: Add a function to toggle the mode of an LED. (AKTION: Füge eine Funktion hinzu, um den Modus einer LED umzuschalten.)
Diese Funktion wird verwendet, um LED4 zu aktivieren oder zu deaktivieren, 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 einen Tastendruck 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ätefunktion unterschiedliche LEDs aufleuchten. Wir verfolgen die folgenden Rollen: Leader, Mesh Extender, Endgerät. Wir können sie so LEDs zuweisen:
- LED1 = Leader
- LED2 = Mesh-Extender
- LED3 = Endgerät
Damit diese Funktion aktiviert werden kann, muss die Anwendung wissen, wann sich die Geräterolle ändert, und wie die richtige LED als Reaktion darauf eingeschaltet wird. Für den ersten Teil verwenden wir 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ügen Sie im Abschnitt „includes“ der Datei main.c die API-Headerdateien hinzu, die Sie für die Funktion zum Ändern von Rollen benötigen.
#include <openthread/instance.h> #include <openthread/thread.h> #include <openthread/thread_ftd.h>
ACTION: Add handler function declaration for the OpenThread instance state change.
Fügen Sie diese Deklaration in main.c nach den Header-Includes und vor allen #if-Anweisungen ein. Diese Funktion wird nach der Hauptanwendung definiert.
void handleNetifStateChanged(uint32_t aFlags, void *aContext);
ACTION: Add a callback registration for the state change handler function.
Fügen Sie in main.c diese Funktion der Funktion main() nach dem Aufruf von otAppCliInit 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: Implementierung der Statusänderung hinzufügen
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 LEDs bei Bedarf ein oder aus.
void handleNetifStateChanged(uint32_t aFlags, void *aContext)
{
if ((aFlags & OT_CHANGED_THREAD_ROLE) != 0)
{
otDeviceRole changedRole = otThreadGetDeviceRole(aContext);
switch (changedRole)
{
case OT_DEVICE_ROLE_LEADER:
otSysLedSet(1, true);
otSysLedSet(2, false);
otSysLedSet(3, false);
break;
case OT_DEVICE_ROLE_ROUTER:
otSysLedSet(1, false);
otSysLedSet(2, true);
otSysLedSet(3, false);
break;
case OT_DEVICE_ROLE_CHILD:
otSysLedSet(1, false);
otSysLedSet(2, false);
otSysLedSet(3, true);
break;
case OT_DEVICE_ROLE_DETACHED:
case OT_DEVICE_ROLE_DISABLED:
/* Clear LED4 if Thread is not enabled. */
otSysLedSet(4, false);
break;
}
}
}
7. API: Multicast zum Einschalten einer LED verwenden
In unserer Anwendung möchten wir auch UDP-Nachrichten an alle anderen Geräte im Netzwerk senden, wenn auf einer Platine die Schaltfläche 1 gedrückt wird. Um den Empfang der Nachricht zu bestätigen, schalten wir LED4 auf den anderen Boards ein.
Damit diese Funktion aktiviert werden kann, muss die Anwendung Folgendes tun:
- Beim Starten eine UDP-Verbindung initialisieren
- UDP-Nachricht an die Mesh-lokale Multicast-Adresse senden können
- Eingehende UDP-Nachrichten verarbeiten
- LED4 als Reaktion auf eingehende 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 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, mit denen Laufzeitbedingungen validiert und Fehler ordnungsgemäß behandelt werden.
AKTION: Defines 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 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 von 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 den ‑Button zu initialisieren.
Fügen Sie in main.c diese Funktionsaufrufe der Funktion main() nach dem Aufruf von otSetStateChangedCallback hinzu. Diese Funktionen initialisieren die GPIO- und GPIOTE-Pins und legen einen Schaltflächen-Handler fest, der Schaltflächen-Ereignisse verarbeitet.
/* init GPIO LEDs and button */ otSysLedInit(); otSysButtonInit(handleButtonInterrupt);
AKTION: UDP-Initialisierungsaufruf hinzufügen
Fügen Sie in main.c diese Funktion der Funktion main() nach dem Aufruf otSysButtonInit hinzu, den Sie gerade hinzugefügt haben:
initUdp(instance);
Mit diesem Aufruf wird dafür gesorgt, 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 Aufruf von otSysProcessDrivers in der while-Schleife hinzu. Diese Funktion, die in gpio.c deklariert ist, prüft, ob die Schaltfläche gedrückt wurde. Wenn ja, wird der Handler (handleButtonInterrupt) aufgerufen, der im vorherigen Schritt festgelegt wurde.
otSysButtonProcess(instance);
WICHTIG: Button-Interrupt-Handler 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);
}
MAßNAHME: 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 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. Mit otUdpBind wird der Socket an die Thread-Netzwerkschnittstelle gebunden, indem OT_NETIF_THREAD übergeben wird. Weitere Optionen für die Netzwerkschnittstelle 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);
}
}
Achten Sie auf die Makros otEXPECT und otEXPECT_ACTION. Diese sorgen dafür, dass die UDP-Nachricht gültig ist und korrekt im Puffer zugewiesen wird. Andernfalls werden Fehler von der Funktion ordnungsgemäß behandelt, indem zum exit-Block gesprungen wird, in dem 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-Nachrichtenverarbeitung implementieren
Fügen Sie in main.c die Implementierung der Funktion handleUdpReceive nach der Funktion sendUdp ein, 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
Um die Demonstration zu vereinfachen, sollen unsere Geräte sofort mit Thread beginnen und sich zu einem Netzwerk verbinden, wenn sie eingeschaltet werden. Dazu verwenden wir die Struktur otOperationalDataset. Diese Struktur enthält alle Parameter, die zum Übertragen von Thread-Netzwerk-Anmeldedaten 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 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>
ACTION: Add function declaration for setting the network configuration.
Fügen Sie diese Deklaration in main.c nach den Header-Includes und vor allen #if-Anweisungen ein. Diese Funktion wird nach der Hauptanwendungsfunktion definiert.
static void setNetworkConfiguration(otInstance *aInstance);
ACTION: Add the network configuration call. (AKTION: Netzwerkkonfigurationsaufruf hinzufügen.)
Fügen Sie in main.c diesen Funktionsaufruf der Funktion main() nach dem Aufruf von otSetStateChangedCallback hinzu. Mit dieser Funktion wird das Thread-Netzwerk-Dataset konfiguriert.
/* 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 der Funktion main() nach dem Aufruf von otSysButtonInit 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, sind die Thread-Netzwerkparameter, die wir für diese Anwendung verwenden:
- Channel = 15
- PAN-ID = 0x2222
- Erweiterte PAN-ID = C0DE1AB5C0DE1AB5
- Netzwerkschlüssel = 1234C0DE1AB51234C0DE1AB51234C0DE
- Netzwerkname = OTCodelab
Außerdem wird hier der Router Selection Jitter verringert, damit unsere Geräte für Demozwecke schneller die Rolle wechseln. Dies erfolgt nur, wenn der Knoten ein FTD (Full Thread Device) ist. Mehr dazu im nächsten Schritt.
9. API: Eingeschränkte Funktionen
Einige OpenThread-APIs ändern Einstellungen, die nur zu Demo- oder Testzwecken geändert werden sollten. Diese APIs sollten nicht in einer Produktionsbereitstellung einer Anwendung verwendet werden, die OpenThread verwendet.
Die Funktion otThreadSetRouterSelectionJitter passt beispielsweise die Zeit (in Sekunden) an, die ein Endgerät benötigt, um sich selbst zu einem Mesh-Extender zu machen. Der Standardwert für diesen Wert ist gemäß der Thread-Spezifikation 120. Zur besseren Nutzung in diesem Codelab ändern wir den Wert in 20, damit du nicht lange warten musst, bis ein Thread-Knoten die Rolle ändert.
Hinweis: MTD-Geräte werden nicht zu Mesh-Extendern 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 es sonst zu einem Build-Fehler kommt.
Sie können dies bestätigen, indem Sie sich die otThreadSetRouterSelectionJitter-Funktionsdefinition ansehen, die in einer Präprozessordirektive 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, müssen Sie drei CMake-Dateien aktualisieren. Sie werden vom Build-System verwendet, um Ihre Anwendung zu kompilieren und zu verknüpfen.
./third_party/NordicSemiconductor/CMakeLists.txt
Fügen Sie nun einige Flags zum NordicSemiconductor-Gerät CMakeLists.txt hinzu, damit GPIO-Funktionen in der Anwendung definiert werden.
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 die GPIO-Quelle der Datei ./src/CMakeLists.txt hinzu.
Öffnen Sie ./src/CMakeLists.txt in Ihrem bevorzugten Texteditor und fügen Sie die Datei in den Abschnitt NRF_COMM_SOURCES ein.
... set(NRF_COMM_SOURCES ... src/gpio.c ... ) ...
./third_party/NordicSemiconductor/CMakeLists.txt
Fügen Sie zum Schluss die Treiberdatei nrfx_gpiote.c der Datei CMakeLists.txt von NordicSemiconductor hinzu, damit sie in den Bibliotheksbuild der Nordic-Treiber aufgenommen wird.
ACTION: Add the gpio driver to the NordicSemiconductor CMakeLists.txt file.
Öffnen Sie ./third_party/NordicSemiconductor/CMakeLists.txt in Ihrem bevorzugten Texteditor und fügen Sie die Datei in den Abschnitt COMMON_SOURCES ein.
... set(COMMON_SOURCES ... nrfx/drivers/src/nrfx_gpiote.c ... ) ...
11. Geräte einrichten
Nachdem Sie alle Code-Updates vorgenommen haben, 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
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 binären Datei der OpenThread FTD-Befehlszeile 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 auf dem nRF52840-Board an und stecken Sie es dann in Ihren Linux-Computer. Stellen Sie die richtig ein, LED5 leuchtet.

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

Rufen Sie den Speicherort der nRFx Command Line Tools auf und flashen Sie die OpenThread CLI FTD-Hex-Datei auf das nRF52840-Board. Verwenden Sie dazu die Seriennummer des Boards:
$ cd ~/nrfjprog
$ ./nrfjprog -f nrf52 -s 683704924 --verify --chiperase --program \
~/openthread/output/nrf52840/bin/ot-cli-ftd.hex --reset
LED5 schaltet sich 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 („Boards flashen“) für die beiden anderen Boards. Jedes Board sollte auf dieselbe Weise mit dem Linux-Computer verbunden werden. Der Befehl zum Flashen ist derselbe, mit Ausnahme der Seriennummer des Boards. Verwenden Sie die eindeutige Seriennummer jedes Boards im
nrfjprog Befehl zum Flashen
Wenn der Vorgang erfolgreich ist, leuchtet auf jeder Platine entweder LED1, LED2 oder LED3. Möglicherweise wechselt die leuchtende LED kurz nach dem Blinken von 3 zu 2 (oder von 2 zu 1) (Funktion zum Ändern der Geräterolle).
12. App-Funktionen
Alle drei nRF52840-Boards sollten jetzt mit Strom versorgt werden und unsere OpenThread-Anwendung ausführen. Wie bereits beschrieben, hat diese Anwendung zwei Hauptfunktionen.
Geräterollenindikatoren
Die leuchtende LED auf jeder Platine gibt die aktuelle Rolle des Thread-Knotens an:
- LED1 = Leader
- LED2 = Mesh-Extender
- LED3 = Endgerät
Mit der Rolle ändert sich auch die leuchtende LED. Sie sollten diese Änderungen innerhalb von 20 Sekunden nach dem Einschalten der Geräte auf einem oder zwei Boards sehen.
UDP-Multicast
Wenn auf einem Board die Schaltfläche 1 gedrückt wird, wird eine UDP-Nachricht an die Mesh-lokale Multicast-Adresse gesendet, die alle anderen Knoten im Thread-Netzwerk enthält. Wenn diese Nachricht empfangen wird, wird LED4 auf allen anderen Boards ein- oder ausgeschaltet. LED4 bleibt auf jedem Board an oder aus, bis eine weitere UDP-Nachricht empfangen wird.


13. Demo: Änderungen an Geräterollen beobachten
Die Geräte, die du geflasht hast, sind eine spezielle Art von Full Thread Device (FTD), die als Extender-Capable Device (ECD) bezeichnet wird. Das bedeutet, dass sie entweder als Mesh-Extender oder als Standby-ECD fungieren können und sich selbst von einem Standby-ECD zu einem Mesh-Extender hochstufen können.
Thread kann bis zu 32 Mesh-Extender unterstützen, versucht aber, die Anzahl der Mesh-Extender zwischen 16 und 23 zu halten. Wenn ein ECD als Endgerät verbunden wird und die Anzahl der Mesh-Extender unter 16 liegt, wird es automatisch zu einem Mesh-Extender hochgestuft. Diese Änderung sollte zu einem zufälligen Zeitpunkt innerhalb der Anzahl von Sekunden erfolgen, die Sie in der Anwendung für den otThreadSetRouterSelectionJitter-Wert festgelegt haben (20 Sekunden).
Jedes Thread-Netzwerk hat auch einen Leader, einen Mesh-Extender, der für die Verwaltung der Mesh-Extender in einem Thread-Netzwerk verantwortlich ist. Nach 20 Sekunden sollte eines der Geräte ein Leader (LED1 leuchtet) und die beiden anderen Mesh-Repeater (LED2 leuchtet) sein.

Führungsperson entfernen
Wenn der Leader aus dem Thread-Netzwerk entfernt wird, wird ein anderer Mesh-Extender zum Leader ernannt, damit das Netzwerk weiterhin einen Leader hat.
Schalten Sie das Leaderboard (mit der leuchtenden LED1) mit dem Ein/Aus-Schalter aus. Warten Sie etwa 20 Sekunden. Auf einem der verbleibenden beiden Boards schaltet sich LED2 (Mesh Extender) aus und LED1 (Leader) ein. Dieses Gerät ist jetzt der Leader des Thread-Netzwerks.

Aktivieren Sie die ursprüngliche Bestenliste wieder. Es sollte sich automatisch wieder mit dem Thread-Netzwerk als Endgerät verbinden (LED3 leuchtet). Innerhalb von 20 Sekunden (dem Router Selection Jitter) wird es zu einem Mesh-Extender hochgestuft (LED2 leuchtet).

Boards zurücksetzen
Schalte alle drei Boards aus und wieder ein und beobachte die LEDs. Das erste Board, das eingeschaltet wurde, sollte die Rolle des Leaders übernehmen (LED1 leuchtet). Der erste Mesh-Extender in einem Thread-Netzwerk wird automatisch zum Leader.
Die anderen beiden Boards stellen zuerst als Endgeräte eine Verbindung zum Netzwerk her (LED3 leuchtet), sollten sich aber innerhalb von 20 Sekunden zu Mesh-Extendern hochstufen (LED2 leuchtet).
Netzwerkpartitionen
Wenn deine Boards nicht ausreichend mit Strom versorgt werden oder die Funkverbindung zwischen ihnen schwach ist, kann das Thread-Netzwerk in Partitionen aufgeteilt werden und du hast möglicherweise mehr als ein Gerät, das als Leader angezeigt wird.
Der Thread heilt sich selbst. Partitionen sollten also irgendwann wieder zu einer einzigen Partition mit einem Leader zusammengeführt werden.
14. Demo: UDP-Multicast senden
Wenn Sie mit der vorherigen Übung fortfahren, sollte LED4 auf keinem Gerät leuchten.
Wähle ein beliebiges Board aus und drücke die Taste 1. LED4 auf allen anderen Boards im Thread-Netzwerk, auf denen die Anwendung ausgeführt wird, sollte ihren Status ändern. Wenn Sie mit der vorherigen Übung fortfahren, sollten sie jetzt eingeschaltet sein.

Drücken Sie noch einmal die Taste 1 für dasselbe Board. LED4 sollte auf allen anderen Boards wieder umschalten.
Drücken Sie die Taste 1 auf einem anderen Board und beobachten Sie, wie LED4 auf den anderen Boards umgeschaltet wird. Drücken Sie die Taste 1 auf einer der Platinen, auf denen LED4 gerade leuchtet. LED4 bleibt auf dieser Platine an, blinkt aber auf den anderen.

Netzwerkpartitionen
Wenn Ihre Boards partitioniert sind und es mehr als einen Leader gibt, unterscheidet sich das Ergebnis der Multicast-Nachricht zwischen den Boards. Wenn Sie die Taste 1 auf einem Board drücken, das partitioniert ist und somit das einzige Mitglied des partitionierten Thread-Netzwerks ist, leuchtet die LED 4 auf den anderen Boards nicht auf. Wenn das passiert, setze die Boards zurück. Idealerweise bilden sie dann ein einzelnes Thread-Netzwerk und die UDP-Nachrichtenübertragung sollte korrekt funktionieren.
15. Glückwunsch!
Sie haben eine Anwendung erstellt, die OpenThread-APIs verwendet.
Sie wissen jetzt:
- Tasten und LEDs auf Nordic nRF52840-Entwicklerboards programmieren
- Verwendung gängiger OpenThread-APIs und der Klasse
otInstance - 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, die folgenden Übungen zu absolvieren:
- Ändern Sie das GPIO-Modul so, dass GPIO-Pins anstelle der integrierten LEDs verwendet werden, und schließen Sie externe RGB-LEDs an, die ihre Farbe je nach Rolle des Mesh-Extenders ändern.
- GPIO-Unterstützung für eine andere Beispielplattform hinzufügen
- Anstatt Multicast zu verwenden, um alle Geräte durch Drücken einer Taste zu pingen, verwenden Sie die Router-/Leader API, um ein einzelnes Gerät zu finden und zu pingen.
- Verbinde dein Mesh-Netzwerk über einen OpenThread Border Router mit dem Internet und sende Multicast-Pakete von außerhalb des Thread-Netzwerks, um die LEDs zu aktivieren.
Weitere Informationen
Auf openthread.io und GitHub finden Sie eine Vielzahl von OpenThread-Ressourcen, darunter:
- Unterstützte Plattformen – Hier finden Sie alle Plattformen, die OpenThread unterstützen.
- OpenThread erstellen – Weitere Informationen zum Erstellen und Konfigurieren von OpenThread
- Thread Primer – eine gute Referenz zu Thread-Konzepten
Referenz: