libraries/OpenThread/examples/Native/README.md
This folder contains sketches that use the OpenThread Native API from Arduino code.
The Native API is the typed C++ interface exposed by Arduino wrappers such as:
OThreadDataSetOThreadUDPOThreadCoAP (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.
OThreadOThread 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):
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):
| Role | Order after begin(false) |
|---|---|
| Joiner | networkInterfaceUp() → startJoiner(PSKD) → start() on success |
| Commissioner | commitDataSet() (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.
initNew() vs NVS resumeMulti-board demos behave differently depending on how each sketch obtains its Thread identity:
| Pattern | Typical sketches | After server reboot |
|---|---|---|
initNew() every boot | Most 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 flows | Same 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 RouterNode | Joins 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 — JoinerNode | Needs 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.
| Pattern | When addJoiner() runs | Examples |
|---|---|---|
| Automatic | Right after startCommissioner() succeeds | CoAP Light Switch light, UDP Light Switch light, CoAP CRUD notes_server, CoAP Secure/Greenhouse servers |
| Button-gated | On user button press after Commissioner is active | Thread 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).
DataSetDataSet wraps the Thread Operational Dataset. It is used when a sketch needs
to form or pre-configure a network with known parameters:
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.
OThreadUDPOThreadUDP 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:
otUdp.begin(localPort);
otUdp.beginPacket(peerAddress, peerPort);
otUdp.write(payload, length);
otUdp.endPacket();
OThreadCoAPOThreadCoAP* 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):
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:
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.
OThreadScanOThreadScan 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.
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.
| Topic | Native approach | CLI approach |
|---|---|---|
| Interface style | Typed methods/functions | String commands |
| Error handling | Return values (otError, booleans) | Parse CLI responses (Done, Error ...) |
| Best for | Production-oriented application logic, compile-time checks, maintainability | Feature parity with the OT shell, quick prototyping, command-driven flows |
| Debug visibility | Cleaner runtime logic, less text parsing | Very explicit command/response logs |
| Dependency on wrappers | Depends on wrapper coverage for each feature | Low: can use features directly if CLI supports them |
Use Native API when you want to:
OThreadUDP for application UDP traffic over Thread.Use CLI when you want to:
Yes. The Arduino OpenThread Library supports mixed usage.
Common mixed pattern:
Prefer one dominant style per sketch for readability.
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=yCONFIG_SOC_IEEE802154_SUPPORTED=ySome examples also require:
CONFIG_OPENTHREAD_JOINER=y for Joiner sketchesCONFIG_OPENTHREAD_COMMISSIONER=y for Commissioner sketches| Folder | Sketches | What it demonstrates |
|---|---|---|
| Native StackShutdown | StackShutdown | Graceful teardown then restart: OThreadCoAPServer.stop() → OThreadUDP.stop() → OThread.end() → setup(). |
| Simple Thread Network | LeaderNode (network former), RouterNode (joiner) | Basic Native network formation and joining using OThread and DataSet. |
| Thread Commissioning | CommissionerNode (server), JoinerNode (client) | Thread commissioning: a Commissioner opens a joiner window and a Joiner obtains the dataset using only a PSKd. |
| Native UDP examples | UDP Light Switch, UDP Sensor Network | Native UDP application traffic (ports 5050/5051). See Native UDP examples overview. |
| Native CoAP examples | CoAP SimpleGet, CoAP Light Switch, CoAP Sensor, CoAP CRUD, CoAP Secure, CoAP Greenhouse | Native CoAP / CoAPS on ports 5683/5684. See Native CoAP examples overview. |
| Native ThreadScan | ThreadScan_Discover, ThreadScan_Async, ThreadScan_Callback | OThreadScan.discoverNetworks() — MLE Thread discovery with OThreadNetworkInfo results. CLI reference: CLI ThreadScan. |
OThread.end(), plus a setup() restart without chip reset.OThreadCoAPClient and
OThreadCoAPServer.OThreadCoAPResourceStore.loop().otError results from
Joiner / Commissioner calls.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).setup() restart) or CLI StackShutdown (CLI-only).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.
| Symptom | Likely cause |
|---|---|
| Joiner or client cannot attach | Server/Leader not running yet, join window closed, or client started before server was ready — reset client after server is up. |
| Attached but application traffic fails | Server not listening, wrong port, stale Leader RLOC, or dataset mismatch between boards. |
| Works once, fails after reboot | Demo 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 Joiner | Called start() before startJoiner() — use Joiner call order from the table above. |
| Build errors for Joiner/Commissioner | Missing CONFIG_OPENTHREAD_JOINER=y or CONFIG_OPENTHREAD_COMMISSIONER=y in sdkconfig for that sketch. |
Apache License 2.0.