OpenThread: a real Thread node
A real emulated ESP32-C6 running the unmodified OpenThread stack. No typing needed: the sketch commits a complete operational dataset in setup() and starts Thread, so the node comes up as leader on its own; the radio encrypts its MLE traffic with the real hardware AES-CCM* path. The full OpenThread CLI stays on the serial monitor (`state`, `dataset active`, `ipaddr`), the Code tab holds the exact sketch the firmware was built from, and the 📡 Sniffer shows the 802.15.4 frames. Open the Thread mesh example to see a second board join it.

How it works
One sealed C6 board runs genuine OpenThread as compiled Arduino firmware. On boot it commits a complete operational dataset (channel 15, PAN ID 0x1234, network key, mesh-local prefix), starts Thread, and walks the real state machine: detached, then leader of its own one-node network, with the actual routing and MLE logic deciding that. The full OpenThread CLI stays live on serial at the ot> prompt: the same commands you'd type at a Nordic or Espressif devkit work here.
What's on the bench
- Battery
- ESP32-C6
How it's wired
- Battery · pos→ESP32-C6 · vin
- Battery · neg→ESP32-C6 · gnd
The code
The exact Arduino C++ sketch(es) the bundled firmware was compiled from — shown in the simulator’s Code tab, where you can edit them and press Compile & upload to rebuild the board’s firmware.
// OpenThread leader — auto-forms a Thread network on boot, no typing needed. // // setup() drives the real OpenThread CLI programmatically: it commits a // COMPLETE fixed operational dataset (timestamp, channel, PAN IDs, network // key, PSKc, mesh-local prefix, security policy) and starts Thread. With no // one to join, the node goes detached → leader in a few seconds. The CLI // stays fully interactive on the serial monitor — try `state`, // `dataset active`, `ipaddr`, `child table`. // // loop() pumps serial ↔ CLI itself instead of OThreadCLI.startConsole(): // the library's console worker busy-polls the stream and never lets the // CPU idle, while this pump sleeps between polls — much friendlier to // power (and to the emulator's idle-skip). // // A second board running the matching OpenThreadNode sketch (identical // dataset, delayed start) attaches to this network as a child. #include <Arduino.h> #include "OThread.h" #include "OThreadCLI.h" #include "OThreadCLI_Util.h" OpenThread node; // The Breadboard Thread network — every value fixed so each boot forms the // identical network, and any board with the same dataset can join it. static const char *DATASET[][2] = { {"dataset activetimestamp", "1"}, {"dataset channel", "15"}, // The channel mask is a required Active-Dataset TLV — the Joiner Entrust // (commissioning) can't be built without it. 0x07fff800 = channels 11-26. {"dataset channelmask", "0x07fff800"}, {"dataset panid", "0x1234"}, {"dataset extpanid", "1122334455667788"}, {"dataset networkname", "Breadboard"}, {"dataset networkkey", "00112233445566778899aabbccddeeff"}, {"dataset pskc", "00112233445566778899aabbccddeeff"}, {"dataset meshlocalprefix", "fd11:2233:4455:6677::"}, {"dataset securitypolicy", "672 onrc"}, {"dataset commit", "active"}, {"ifconfig", "up"}, {"thread", "start"}, }; void setup() { Serial.begin(115200); OThread.begin(false); // fresh start — don't resume a dataset from NVS OThreadCLI.begin(); Serial.println("OpenThread starting (auto-forming): committing the Breadboard dataset."); for (auto &cmd : DATASET) { if (!otExecCommand(cmd[0], cmd[1])) { Serial.printf("[thread] '%s %s' failed\r\n", cmd[0], cmd[1]); } } Serial.println("Thread started - CLI ready, try `state`."); Serial.print("ot> "); } // Narrate role changes: disabled → detached → leader. static void narrateRole() { static ot_device_role_t lastRole = OT_ROLE_DISABLED; ot_device_role_t role = node.otGetDeviceRole(); if (role == lastRole) { return; } lastRole = role; Serial.printf("\r\n[thread] role: %s\r\n", node.otGetStringDeviceRole()); if (role == OT_ROLE_LEADER) { Serial.printf("[thread] network \"%s\" formed: channel %u, panid 0x%04x\r\n", node.getNetworkName().c_str(), node.getChannel(), node.getPanId()); Serial.printf("[thread] mesh-local EID: %s\r\n", node.getMeshLocalEid().toString().c_str()); Serial.printf("[thread] rloc16: 0x%04x - waiting for children\r\n", node.getRloc16()); } } // Serial ↔ CLI pump. Typed characters go to the CLI task (it executes on // newline); responses stream back, with a fresh prompt after each command // (the CLI ends every command with "Done" or "Error ..."). static void pumpCli() { while (Serial.available() > 0) { char ch = (char)Serial.read(); Serial.write(ch); // echo what you type OThreadCLI.write((uint8_t)ch); } static String lineBuf; while (OThreadCLI.available() > 0) { char ch = (char)OThreadCLI.read(); Serial.write(ch); if (ch == '\n') { if (lineBuf.startsWith("Done") || lineBuf.startsWith("Error")) { Serial.print("ot> "); } lineBuf = ""; } else if (ch != '\r') { lineBuf += ch; } } } void loop() { static uint32_t lastRoleCheck = 0; if (millis() - lastRoleCheck >= 250) { lastRoleCheck = millis(); narrateRole(); } pumpCli(); delay(50); }
Firmware licence
Notice: this firmware uses the LGPL-licensed Arduino core library
arduino-openthread-leader-c6.bin is statically linked with Arduino-ESP32 3.3.10. Parts of this library are licensed under the GNU Lesser General Public License, version 2.1 or (at your option) any later version. You may modify this work for your own use and reverse engineer it to debug those modifications. Download the relink package and corresponding source to rebuild it with a changed library.
- Relink package: openthread-leader-c6-relink.zip (0.9 MB)
- Required shared inputs: arduino-esp32-3.3.10-esp32c6-sdk.zip (10.0 MB), arduino-esp32-3.3.10-notices.zip (0.1 MB)
- Corresponding source: arduino-esp32-3.3.10-core-source.zip (1.2 MB), arduino-esp32-3.3.10-esp32c6-headers-source.zip (2.1 MB)
- Licence texts and relink instructions
Try this
- Type state at the ot> prompt: 'leader', once formation settles.
- Type dataset active to read back the committed network parameters.
- Type ipaddr and meet the mesh-local and link-local addresses Thread derived.
- Click the C6's 802.15.4 Inspector tab: PAN ID 0x1234, live, once the network forms.



