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



