Skip to content

dunknowcoding/ArduinoNRF-Thread

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NiusThread

Thread (OpenThread) IPv6 mesh networking for the ArduinoNRF board package, running entirely on the nRF52840's own 2.4 GHz radio. The full OpenThread FTD stack (plus mbedtls) is vendored into the library and sits on a register-level IEEE 802.15.4 driver written for this core — no SoftDevice, no nrfx, no external radio module.

Four calls take a bare board into a self-healing mesh; after that every node has real IPv6 and UDP across the network, and the complete official OpenThread C API stays one Thread.instance() away.

Highlights

  • A mesh in four calls. begin(), setNetwork(...), start(), and process() in loop(). The first board promotes itself to Leader; every further board with the same credentials attaches and gets promoted to Router as the mesh grows.
  • CoAP built in. Register text resources with onCoapGet()/onCoapPost() and reach any node (or the whole mesh at ff03::1) with coapPost()/coapGet() — the same protocol Matter devices and Linux coap-client speak.
  • Survives reboots. The operational dataset, network key sequence and frame counters persist in two dedicated flash pages (OpenThread's two-swap settings store over raw NVMC); a rebooted node re-attaches by itself.
  • Two API levels. The friendly Thread object for bring-up and role/state queries — and the official OpenThread C API (otXxx() from <openthread/...>) for everything else, via Thread.instance(). Nothing is hidden.
  • Own radio driver, hardware-verified. The 802.15.4 PHY/MAC driver (src/ot_radio_nrf52840.cpp) programs the RADIO peripheral directly: IRQ-driven RX, software address filter, immediate-acks with source-match frame-pending, single-shot hardware CCA. The OpenThread sub-MAC does CSMA backoff, retransmits, ack timeout and TX security in software.
  • Real interop evidence. Frames were cross-checked against a TI CC2530 sniffer during bring-up, and a two-node mesh (Leader + Child → Router, bidirectional UDP multicast) runs on real boards.
  • Self-contained vendoring. OpenThread fa3213ec + mbedtls 3.6.5 live in src/, pinned and patched in exactly three marked places (ARDUINONRF-PATCH) — see docs/VENDORING.md.

Requirements

  • An ArduinoNRF board (nRF52840) with the ArduinoNRF board package installed — the library uses its NrfRtc / NrfPeripherals core drivers.
  • Peripherals claimed: RADIO (exclusive with NimBLE / Zigbee / NrfRadio), RTC2 (millisecond alarm), TIMER3 (microsecond alarm + timestamps).
  • Footprint with the UDP example: ~225 KB flash (28 %), ~35 KB RAM (14 %).

Install

In the Arduino Library Manager it is published as NiusThread. The GitHub repository keeps the ArduinoNRF-Thread name because it is developed alongside the ArduinoNRF core.

Or compile an example straight from a checkout:

arduino-cli compile \
  --fqbn arduinonrf:nrf52:promicro_nrf52840 \
  --library <path-to>/ArduinoNRF-Thread \
  <path-to>/ArduinoNRF-Thread/examples/FormNetwork

Bootloader layout note: on nice!nano-style bootloaders without a SoftDevice (INFO_UF2.TXT says SoftDevice: not found), build with the no-SoftDevice bootloader option (e.g. :bootloader=promicroserialnosd) so the sketch links at 0x1000 — otherwise it uploads fine but never runs.

Existing sketches that did #include <Thread.h> keep working: the ArduinoNRF package ships a compatibility shim that forwards to this library.

Quick start

#include <NiusThread.h>

const uint8_t key[16] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
                         0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};

void setup() {
  Thread.begin();
  Thread.setNetwork("MyMesh", 11, 0xBEEF, key);  // name, channel, PAN id, key
  Thread.start();
}

void loop() {
  Thread.process();              // keep the stack running
  if (Thread.isAttached()) {
    // IPv6/UDP across the mesh - see the UdpBroadcast example
  }
}

Flash the same sketch to several boards: the first becomes leader, the rest join as child and get promoted to router.

Examples

Example What it shows
FormNetwork The hello-world: form/join a mesh and watch the role change on the serial monitor
NetworkInfo A live mesh dashboard — role, addresses, partition, neighbor table with RSSI — using the official otXxx() API
UdpBroadcast Real data across the mesh: every node multicasts a numbered hello over UDP and prints what it hears
CoapLamp A CoAP server on every node (GET/POST /led) — the boards toggle each other's LEDs over the mesh
ThreadCli The full official OpenThread CLI on the serial monitor — state, dataset, ping, udp, everything ot-cli-ftd can do
CommissionerNode Real Thread commissioning, admitting side: form a network and admit joiners by passphrase (PSKd)
JoinerNode Real Thread commissioning, joining side: no credentials in the sketch — discover, EC-JPAKE with the PSKd, receive the network key over DTLS, attach

Verified two-board output of UdpBroadcast (board 2's serial monitor):

UdpBroadcast: attaching...
role -> detached
role -> child
TX  "hello #1 from 0xE401"
RX  from fd00:db8:0:0:535d:5645:501c:5ef2  "hello #31 from 0xE400"
TX  "hello #32 from 0xC000"          <- promoted from child to router
RX  from fd00:db8:0:0:535d:5645:501c:5ef2  "hello #62 from 0xE400"

The API

Thread.begin();                  // platform + OpenThread instance up
Thread.setNetwork(name, channel /*11..26*/, panId, key16 [, extPanId8]);
Thread.start();   Thread.stop();
Thread.process();                // call from loop() - alarms, radio, tasklets

Thread.setLinkMode(ThreadClass::LINK_SED, 1000); // sleepy end device,
                                 // 1 s poll; LINK_MED = always-on end device,
                                 // LINK_ROUTER = full device (default)

// Real commissioning (MeshCoP) - admit devices by passphrase instead of
// sharing the network key:
Thread.commissionerStart();                  // on an attached node
Thread.commissionerAddJoiner("J01NME", 600); // open a 10-min joiner window
// ...and on the factory-fresh device (no setNetwork() needed):
Thread.joinNetwork("J01NME");                // poll Thread.joinResult()
Thread.eraseNetwork();                       // forget stored credentials

Thread.role();                   // ROLE_DISABLED/DETACHED/CHILD/ROUTER/LEADER
Thread.roleString();             // "leader", ...
Thread.isAttached();             // child, router or leader
Thread.rloc16();                 // mesh-internal short address
Thread.getEui64(buf8);           // factory-unique 64-bit id (FICR)

// CoAP, text payloads (full otCoap API available via instance()):
Thread.coapStart();                          // server on :5683
Thread.onCoapGet("led", []() { return "on"; });
Thread.onCoapPost("led", [](const char *s) { /* act on s */ });
Thread.coapPost("ff03::1", "led", "toggle"); // POST to one node or the mesh
Thread.coapGet(addr, "led", [](const char *reply) { /* NULL on timeout */ });

otInstance *ot = Thread.instance();   // the official OpenThread C API

NiusThread is an alias for Thread if another library in your sketch already uses that name.

How it works

sketch  ->  Thread (Thread.h)           friendly Arduino API
        ->  OpenThread core (vendored)  MLE, mesh forwarding, MeshCoP, IPv6, UDP
        ->  platform_impl.cpp           alarms (RTC2/TIMER3), entropy (TRNG),
                                        RAM settings, logging -> Serial
        ->  ot_radio_nrf52840.cpp       register-level 802.15.4 RADIO driver

Everything is polled from process() — interrupts only move bytes and set flags, so the (non-ISR-safe) OpenThread core always runs in thread context. The build is configured by src/arduino-ot-config.h (FTD role, software MAC, no CSL/commissioner/joiner/border-router) since Arduino cannot pass per-library compiler flags.

Persistent settings use OpenThread's two-swap flash store over raw NVMC writes, in two 4 KB pages directly below the bootloader (default 0xF2000, override with NIUSTHREAD_SETTINGS_FLASH_BASE) — clear of the application, the core's EEPROM region, and the bootloader itself.

Current limitations, by design of the bring-up scope:

  • No CSL receiver (sleepy-child long polling), commissioner, joiner or border router roles yet.
  • One outstanding coapGet() at a time; CoAP payloads are text up to ~120 bytes (use the full otCoap API for more).
  • Logging defaults to OT_LOG_LEVEL_NOTE on Serial.

License

Apache-2.0 for the library code — see LICENSE. Vendored components keep their own licenses: OpenThread (BSD-3-Clause) and Mbed TLS (Apache-2.0), unmodified except for the marked ARDUINONRF-PATCH config hooks.

About

OpenThread-compatible Arduino library for ArduinoNRF board library developed by NiusRobotLab, powered by Claude Fable 5 and Opus4.8

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors