Back to Arduino Esp32

OpenThread Native API Examples (Arduino)

libraries/OpenThread/examples/Native/README.md

3.3.1118.3 KB
Original Source

OpenThread Native API Examples (Arduino)

This folder contains sketches that use the OpenThread Native API from Arduino code.

What is the OpenThread Native API?

The Native API is the typed C++ interface exposed by Arduino wrappers such as:

  • OThread
  • DataSet
  • OThreadUDP
  • OThreadCoAP (OThreadCoAPClient, OThreadCoAPServer, …)
  • OThreadScan (network scanning — Native API)

Instead of sending textual OpenThread CLI commands, sketches call methods directly, for example:

  • OThread.begin(false)
  • OThread.commitDataSet(ds)
  • OThread.networkInterfaceUp()
  • OThread.start()
  • OThread.startJoiner(...) then OThread.start() on success (Joiner side)
  • OThread.start() then OThread.startCommissioner() when attached (Commissioner side)
  • otUdp.begin(...)
  • otUdp.beginPacket(...)

This style keeps application logic in structured C++ code, gives clearer return values (otError, booleans, typed getters), and avoids parsing CLI text output.

Core Native classes

OThread

OThread is the main entry point for managing the OpenThread stack. It provides helpers for stack startup, Thread interface control, role checks, address queries, dataset handling, and, when enabled, Joiner / Commissioner operations.

Typical usage (form or resume a network with a known dataset):

cpp
OThread.begin(false);
OThread.networkInterfaceUp();
OThread.start();

Serial.println(OThread.otGetStringDeviceRole());
Serial.println(OThread.getMeshLocalEid());

Commissioning call order (see Thread Commissioning examples and Native UDP examples):

RoleOrder after begin(false)
JoinernetworkInterfaceUp()startJoiner(PSKD)start() on success
CommissionercommitDataSet() (or NVS resume) → networkInterfaceUp()start() → wait for attach → startCommissioner()addJoiner()

Do not call start() before startJoiner(); OpenThread returns OT_ERROR_INVALID_STATE if Thread is already enabled during Joiner commissioning.

Network identity: initNew() vs NVS resume

Multi-board demos behave differently depending on how each sketch obtains its Thread identity:

PatternTypical sketchesAfter server reboot
initNew() every bootMost CoAP servers (SimpleGet, Light Switch, CRUD, Sensor server, Secure/Greenhouse servers)Forms a new partition (new Extended PAN ID). Clients must reset or re-flash to re-join.
NVS resume (begin(true) or commitDataSet without initNew)UDP Light Switch light, some Commissioner flowsSame network identity when NVS is intact. Clients usually re-attach without erase.
Network key only (client)CoAP SimpleGet client, CoAP Sensor sensor_client, Native/CLI RouterNodeJoins whichever Leader matches NETKEY; fails if server rebooted with a fresh initNew() partition.
Joiner + PSKd (no local dataset)CoAP Light Switch switch, UDP Light Switch switch, Thread Commissioning — JoinerNodeNeeds an open Commissioner window (addJoiner); not the same as NETKEY-only join.

Before changing dataset constants in source, erase flash (or factory-reset the OpenThread dataset) on all boards. If a client shows Started as Leader or attach timeout after a server reboot, the server likely started a fresh initNew() network — reset the client after the server is Leader again.

Commissioner join-window patterns

PatternWhen addJoiner() runsExamples
AutomaticRight after startCommissioner() succeedsCoAP Light Switch light, UDP Light Switch light, CoAP CRUD notes_server, CoAP Secure/Greenhouse servers
Button-gatedOn user button press after Commissioner is activeThread Commissioning — CommissionerNode

If you flash a joiner sketch while no Commissioner window is open, attach fails until the server calls addJoiner() (or you press the Commissioner button).

DataSet

DataSet wraps the Thread Operational Dataset. It is used when a sketch needs to form or pre-configure a network with known parameters:

cpp
DataSet ds;
ds.initNew();
ds.setNetworkName("ESP_OpenThread");
ds.setChannel(15);
ds.setPanId(0x1234);
ds.setNetworkKey(networkKey);
OThread.commitDataSet(ds);

Use this approach for examples that form a network, such as Leader or Commissioner sketches.

OThreadUDP

OThreadUDP is an Arduino UDP-compatible class backed directly by the OpenThread otUdpSocket API. It is used by the Native UDP examples for IPv6 UDP traffic over the Thread mesh without using lwIP.

Typical usage:

cpp
otUdp.begin(localPort);
otUdp.beginPacket(peerAddress, peerPort);
otUdp.write(payload, length);
otUdp.endPacket();

OThreadCoAP

OThreadCoAP* classes wrap the OpenThread Application CoAP API (otCoap*) in an Arduino-style interface. Plain CoAP uses UDP port 5683; CoAPS (DTLS) uses port 5684 when enabled at build time.

Typical server usage (global singleton — do not declare a local server variable):

cpp
static void onHello(OThreadCoAPRequest &req, OThreadCoAPResponse &resp, void *ctx) {
  resp.setCode(OT_COAP_RESP_OK);
  resp.setPayload("Hello from CoAP!");
  resp.send();
}

OThreadCoAPServer.on("hello", OT_COAP_METHOD_GET, onHello);
OThreadCoAPServer.begin();

Typical client usage:

cpp
OThreadCoAPClient client;
client.setConfirmable(true);
int code = client.GET(serverIp, "hello");

See the Native CoAP examples for full two-board CoAP demos. CLI-based CoAP examples remain under CLI CoAP examples for reference.

OThreadScan

OThreadScan discovers nearby Thread networks via MLE discover (otThreadDiscover() / CLI discover). Each result is an OThreadNetworkInfo with Thread identity and 802.15.4 link fields — the same primitive Matter uses during commissioning.

cpp
OThread.begin(false);
OThread.networkInterfaceUp();

int n = OThreadScan.discoverNetworks();
for (int i = 0; i < n; ++i) {
  Serial.println(OThreadScan.getResult(i).networkNameStr());
}
OThreadScan.scanDelete();

Indexed accessors (getResult(), getResultCount(), …) are valid only after discovery completes; use onResult() while a scan is still running.

See Native ThreadScan examples for blocking, async, and callback patterns. Raw 802.15.4 beacon scan remains available via CLI ThreadScan if needed.

Native vs CLI: quick comparison

TopicNative approachCLI approach
Interface styleTyped methods/functionsString commands
Error handlingReturn values (otError, booleans)Parse CLI responses (Done, Error ...)
Best forProduction-oriented application logic, compile-time checks, maintainabilityFeature parity with the OT shell, quick prototyping, command-driven flows
Debug visibilityCleaner runtime logic, less text parsingVery explicit command/response logs
Dependency on wrappersDepends on wrapper coverage for each featureLow: can use features directly if CLI supports them

When should I use Native API?

Use Native API when you want to:

  • build production-oriented logic with stronger typing,
  • reduce string parsing and command formatting code,
  • simplify long-term maintenance and refactoring,
  • keep network state checks and application behavior in structured C++ code,
  • use OThreadUDP for application UDP traffic over Thread.

When should I use CLI?

Use CLI when you want to:

  • mirror known OpenThread shell commands directly,
  • validate behavior quickly with command-like flows,
  • use a capability exposed in CLI before a Native wrapper exists,
  • keep behavior close to CLI/manual operational procedures.

Can I mix Native and CLI in the same sketch?

Yes. The Arduino OpenThread Library supports mixed usage.

Common mixed pattern:

  • use Native APIs for startup, state checks, and application logic,
  • use CLI commands for diagnostics or for OpenThread features that do not yet have a Native wrapper,
  • document clearly which layer owns each operation.

Prefer one dominant style per sketch for readability.

Required target support

These examples require an ESP32 target with IEEE 802.15.4 / Thread support, such as ESP32-H2, ESP32-C6, or ESP32-C5.

Common requirements:

  • CONFIG_OPENTHREAD_ENABLED=y
  • CONFIG_SOC_IEEE802154_SUPPORTED=y

Some examples also require:

  • CONFIG_OPENTHREAD_JOINER=y for Joiner sketches
  • CONFIG_OPENTHREAD_COMMISSIONER=y for Commissioner sketches
FolderSketchesWhat it demonstrates
Native StackShutdownStackShutdownGraceful teardown then restart: OThreadCoAPServer.stop()OThreadUDP.stop()OThread.end()setup().
Simple Thread NetworkLeaderNode (network former), RouterNode (joiner)Basic Native network formation and joining using OThread and DataSet.
Thread CommissioningCommissionerNode (server), JoinerNode (client)Thread commissioning: a Commissioner opens a joiner window and a Joiner obtains the dataset using only a PSKd.
Native UDP examplesUDP Light Switch, UDP Sensor NetworkNative UDP application traffic (ports 5050/5051). See Native UDP examples overview.
Native CoAP examplesCoAP SimpleGet, CoAP Light Switch, CoAP Sensor, CoAP CRUD, CoAP Secure, CoAP GreenhouseNative CoAP / CoAPS on ports 5683/5684. See Native CoAP examples overview.
Native ThreadScanThreadScan_Discover, ThreadScan_Async, ThreadScan_CallbackOThreadScan.discoverNetworks() — MLE Thread discovery with OThreadNetworkInfo results. CLI reference: CLI ThreadScan.

Choosing an example

  • Use Native StackShutdown if you need a working Native teardown sequence with UDP and CoAP before OThread.end(), plus a setup() restart without chip reset.
  • Start with Simple Thread Network if you only need to learn how to form and join a Thread network with a preconfigured dataset.
  • Use Thread Commissioning examples if devices should join securely using a PSKd instead of carrying the network key in source code.
  • Use UDP Light Switch if you want a compact command/ACK example over UDP.
  • Use UDP Sensor Network if you want a more robust telemetry pattern with application-level acknowledgments and node liveness tracking.
  • Use CoAP SimpleGet for the smallest introduction to OThreadCoAPClient and OThreadCoAPServer.
  • Use CoAP Light Switch for multicast command/response patterns over CoAP port 5683.
  • Use CoAP Sensor for a read-only resource with changing values and NON polling.
  • Use CoAP CRUD for REST collections with OThreadCoAPResourceStore.
  • Use CoAP Secure or CoAP Greenhouse when CoAPS (DTLS) is required.
  • Use ThreadScan_Discover for blocking Thread network discovery.
  • Use ThreadScan_Async for non-blocking discovery polling in loop().
  • Use ThreadScan_Callback for per-network streaming callbacks.

Practical guidance

  • Check return values from Native APIs, especially otError results from Joiner / Commissioner calls.
  • Bind UDP sockets only after the device has attached to a Thread network.
  • Avoid OpenThread-reserved UDP ports for application traffic. For UDP examples, 5050 and 5051 are used instead of CoAP / TMF ports. For CoAP examples, use application ports 5683 (plain) and 5684 (CoAPS) — not 61631 (Thread TMF CoAP).
  • If a sketch resumes a dataset from NVS, erase NVS or factory-reset the OpenThread dataset before expecting changed dataset constants to take effect.
  • For commissioning examples, make sure the Commissioner joiner window is open before starting or retrying the Joiner.
  • To stop Thread without rebooting, follow the shutdown order in the OpenThread library README: Native StackShutdown (Native UDP + CoAP teardown and setup() restart) or CLI StackShutdown (CLI-only).

Troubleshooting

Multi-board demos (UDP, CoAP, SimpleThreadNetwork, ThreadCommissioning) share the same bring-up rule: start the server / Leader / Commissioner / collector sketch first, wait until Serial reports attached (and Commissioner ready when commissioning is used), then flash or reset client / Joiner / router boards that booted too early.

SymptomLikely cause
Joiner or client cannot attachServer/Leader not running yet, join window closed, or client started before server was ready — reset client after server is up.
Attached but application traffic failsServer not listening, wrong port, stale Leader RLOC, or dataset mismatch between boards.
Works once, fails after rebootDemo may use initNew() (new network each boot) vs NVS resume — erase NVS or reset clients to re-join; see the specific example README.
OT_ERROR_INVALID_STATE on JoinerCalled start() before startJoiner() — use Joiner call order from the table above.
Build errors for Joiner/CommissionerMissing CONFIG_OPENTHREAD_JOINER=y or CONFIG_OPENTHREAD_COMMISSIONER=y in sdkconfig for that sketch.

License

Apache License 2.0.