Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ESP32-C6 OpenThread Border Router

Setup and notes for running a standalone OpenThread Border Router on an ESP32-C6. The ESP32-C6 hosts both Wi-Fi (station) and the 802.15.4 radio, so a single chip bridges a Thread network to the Wi-Fi LAN — no separate RCP module needed.

Note on terminology. In Espressif's reference designs, the "border router" usually runs on an ESP32-S3/C5/P4 connected to an external H2/C6 acting as a Radio Co-Processor (RCP). Here the C6 runs the whole stack itself. The reference example in esp-thread-br/examples/basic_thread_border_router is primarily tailored to the S3/C5/P4 + RCP layout — we keep using it because it adds a web UI and HA-friendly defaults on top of the upstream esp-idf/examples/openthread/ot_br, but several of its defaults (UART RCP transport, RCP auto-update) need to be overridden for the single-chip layout — see the menuconfig table below.

Layout

.
├── esp-idf/           # ESP-IDF SDK (submodule, pinned to v5.5.4)
├── esp-thread-br/     # Espressif Thread Border Router components & examples (submodule)
│   └── examples/
│       └── basic_thread_border_router/   # reference implementation we build from
└── patches/           # local patches applied on top of the submodules — see "Patches"

Submodules

Submodule Repo Pin
esp-idf https://github.com/espressif/esp-idf v5.5.4
esp-thread-br https://github.com/espressif/esp-thread-br tracks default branch

After a fresh clone:

git clone <this-repo>
cd esp32-c6-border-router
git submodule update --init --recursive

--recursive is required because esp-idf itself pulls in many nested submodules (mbedTLS, OpenThread, etc.).

Then apply the local patches (see Patches for what they do):

(cd esp-thread-br && git apply ../patches/basic_thread_border_router-native-radio.patch)

Patches

patches/ holds local diffs applied on top of the unmodified submodule checkout. They live outside the submodule so re-cloning, bumping, or git submodule update --remote doesn't silently lose them — re-apply with git apply after any submodule reset.

Patch Target submodule What it does
basic_thread_border_router-native-radio.patch esp-thread-br Adds a CONFIG_OPENTHREAD_RADIO_NATIVE branch to examples/basic_thread_border_router/main/esp_ot_config.h. The upstream file only handles OPENTHREAD_RADIO_SPINEL_UART / _SPI and falls through to the SPI macro when native radio is selected, which references CONFIG_PIN_TO_RCP_MOSI/MISO/SCLK/CS symbols that don't exist outside SPI mode. Without this patch, the single-chip C6 build fails with 'CONFIG_PIN_TO_RCP_MOSI' undeclared.

To check what's currently applied:

(cd esp-thread-br && git diff)

To regenerate a patch after editing files inside a submodule:

(cd esp-thread-br && git diff <path> > ../patches/<name>.patch)

One-time toolchain install

ESP-IDF ships its own installer that fetches the RISC-V toolchain and Python deps into ~/.espressif/.

cd esp-idf
./install.sh esp32c6
. ./export.sh         # exports IDF_PATH and PATH; run in every new shell
cd ..

You must source esp-idf/export.sh (or wire it into your shell rc) before any idf.py command.

Build & flash

The example is built out-of-tree from esp-thread-br/examples/basic_thread_border_router.

cd esp-thread-br/examples/basic_thread_border_router
idf.py set-target esp32c6
idf.py menuconfig                       # see "menuconfig knobs" below
idf.py -p /dev/cu.usbmodem* build flash monitor

menuconfig knobs

The example's sdkconfig.defaults is tuned for the ESP-Thread-Border-Router devkit (S3 host + external H2 RCP over UART). For a single-chip ESP32-C6 you need to override several of these. Locations below are the path you navigate to inside idf.py menuconfig.

Single-chip C6 essentials (required)

All four of these must be set or the build will fail / produce a broken binary:

Setting menuconfig path Why
OPENTHREAD_RADIO_NATIVE=y Component config → OpenThread → Thread Core Features → Thread 15.4 Radio Link → Config the Thread radio type with 15.4 link → Native 15.4 radio Use the C6's built-in 802.15.4 radio. The default in sdkconfig.defaults is OPENTHREAD_RADIO_SPINEL_UART, which expects an external RCP — switch it.
OPENTHREAD_BORDER_ROUTER=y Component config → OpenThread → Thread Core Features → Enable Border Router Border-router features. Already on via sdkconfig.defaults.
ESP_BR_BOARD_STANDALONE=y ESP Thread Border Router Example → Border router board type → Standalone dev kits Tells the example we are not on the integrated devkit. Overrides the default ESP_BR_BOARD_DEV_KIT.
ESP_CONSOLE_USB_SERIAL_JTAG=y Component config → ESP System Settings → Channel for console output → USB Serial/JTAG Controller C6 devkits expose console over native USB. Pick Default: UART0 instead if you're wired up to the UART pins.

Disable RCP image bundling (required for single-chip)

The defaults try to pack an external RCP firmware into the BR binary, which makes no sense on a single-chip C6 and will fail the build with FileNotFoundError: …/ot_rcp/build/rcp_version if left on. Turn both off:

Setting menuconfig path
AUTO_UPDATE_RCP=n Component config → OpenThread RCP Update → Update RCP automatically
CREATE_OTA_IMAGE_WITH_RCP_FW=n Component config → OpenThread RCP Update → Create the OTA image with rcp for border router

Wi-Fi credentials (headless bring-up)

Setting menuconfig path
OPENTHREAD_BR_AUTO_START=y ESP Thread Border Router Example → Enable the automatic start mode in Thread Border Router.
EXAMPLE_WIFI_SSID Example Connection Configuration → WiFi SSID
EXAMPLE_WIFI_PASSWORD Example Connection Configuration → WiFi Password

With OPENTHREAD_BR_AUTO_START off, you configure Wi-Fi at runtime with the wifi connect -s … -p … CLI.

Optional features

Setting menuconfig path Notes
OPENTHREAD_BR_START_WEB=y ESP Thread Border Router Example → Enable the web server in Thread Border Router. Web GUI for Thread network config. Pulls in commissioner/joiner.
OPENTHREAD_BR_SOFTAP_SETUP=y ESP Thread Border Router Example → Enable SoftAP Wi-Fi configuration mode Falls back to a SoftAP at http://192.168.4.1 when Wi-Fi creds are missing.

After saving, run idf.py build flash monitor (add erase-flash on the first flash to clear stale NVS).

If idf.py set-target hangs or fails with cmake process terminated

CMake auto-initialises ESP-IDF's nested submodules at configure time. The download is large (~1 GB: Wi-Fi, OpenThread, lwIP, mbedTLS, BT libs, …) and if it's interrupted you'll get an exit code of -2 (SIGINT) and a half-populated esp-idf/components/*. Easiest fix is to fetch them yourself with visible progress, then retry:

cd esp-idf
git submodule update --init --recursive --progress
cd ../esp-thread-br/examples/basic_thread_border_router
rm -rf build           # clear the half-finished cmake state
idf.py set-target esp32c6

First boot: bring the Thread interface up

Right after the first flash, ot state returns disabled — the firmware is running but the Thread interface has never been initialised. The example doesn't auto-form a network unless OPENTHREAD_BR_AUTO_START=y is set, so on a default build you do it once from the console.

Open idf.py monitor and run:

esp32c6> ot dataset init new
esp32c6> ot dataset commit active
esp32c6> ot ifconfig up
esp32c6> ot thread start
esp32c6> ot state
detached
...   # within ~10 s, transitions to "leader"

dataset init new generates a fresh Thread network (random network key, PANID, channel, network name). dataset commit active writes it to NVS, so it survives reboots — you only need to do this once per chip. After this, every reboot brings Thread up automatically if auto-start is enabled, otherwise you need ot ifconfig up; ot thread start again.

To make it fully headless (BR boots → joins Wi-Fi → starts Thread, no console interaction): set OPENTHREAD_BR_AUTO_START=y in menuconfig (ESP Thread Border Router Example → Enable the automatic start mode in Thread Border Router.) and reflash. With auto-start on, the dataset created above is reused; you don't need to run the one-shot again.

Save the dataset somewhere you can recover it (printer, password manager) — if you wipe NVS or re-flash without backing it up, you'll be forming a new Thread network and any device already joined to the old one stops talking to the BR.

esp32c6> ot dataset active -x
0e08000000000001000035060004001fffe00208dead00beef00cafe...
Done

Use with Home Assistant

Home Assistant's Thread integration auto-discovers any OpenThread Border Router that advertises _meshcop._udp on the LAN via mDNS. The Espressif OTBR does this automatically once it has joined Wi-Fi and formed a Thread network — no HA add-on is needed.

Required menuconfig settings

On top of the single-chip C6 essentials above:

Setting menuconfig path Why
OPENTHREAD_BR_AUTO_START=y ESP Thread Border Router Example → Enable the automatic start mode in Thread Border Router. Headless boot: connects to Wi-Fi and forms a Thread network on power-up.
EXAMPLE_WIFI_SSID / EXAMPLE_WIFI_PASSWORD Example Connection Configuration → WiFi SSID / WiFi Password 2.4 GHz Wi-Fi credentials.
OPENTHREAD_BR_START_WEB=y ESP Thread Border Router Example → Enable the web server in Thread Border Router. Selects commissioner + joiner. HA needs commissioner support to push its Thread credentials to the BR. Also exposes a web UI at http://<br-ip>/ for inspection.

Optional — pre-seed a deterministic Thread network under Component config → OpenThread → Thread Core Features → Thread Operational Dataset (OPENTHREAD_NETWORK_NAME, _CHANNEL, _PANID, _EXTPANID, _MASTERKEY, _PSKC). Otherwise the BR auto-generates one on first boot; HA will then adopt those credentials when it marks this BR as preferred.

Verifying before adding to HA

From any host on the same L2 segment as the BR and HA:

dns-sd -B _meshcop._udp                 # macOS
avahi-browse -r _meshcop._udp -t        # Linux

One entry for the C6 means mDNS discovery is working.

IPv6 plumbing

The BR advertises an on-mesh IPv6 prefix via ICMPv6 router advertisements on Wi-Fi. For Thread devices to be reachable from HA you need:

  • Your Wi-Fi router must forward RAs (most consumer routers do; some "AP isolation" modes drop them).
  • HA OS / Supervised handles RA acceptance automatically. For HA Container on a plain Linux host, set on the Wi-Fi interface:
    sudo sysctl -w net/ipv6/conf/<wlan-iface>/accept_ra=2
    sudo sysctl -w net/ipv6/conf/<wlan-iface>/accept_ra_rt_info_max_plen=128

Adding to Home Assistant

  1. Settings → Devices & Services — the Thread integration should auto-discover the BR. Accept it.
  2. Open the Thread integration, find the BR entry, and mark it preferred border router if you want HA to push its Thread credentials to it (so Matter/Thread devices commissioned via HA join this BR's mesh).

If you have other border routers on the LAN (HomePod, Nest Hub, SkyConnect with OTBR add-on), they merge networks only when they share credentials — HA's "preferred" flag controls which BR receives credential pushes.

Useful console commands

Connect over USB and run idf.py monitor (or any serial monitor at 115200 baud). The prompt is esp32c6>. Commands are case-sensitive.

OpenThread CLI commands are namespaced behind an ot prefix on the ESP console. Typing state returns Unrecognized command; the working form is ot state. The Thread tables below all need that prefix. The prefix is configurable under Component config → OpenThread → Thread Console → The prefix of the openthread CLI command registered on the esp console (default "ot"; set to empty to drop it).

Reference: OpenThread CLI.

Run help for the full list of top-level commands actually registered on this firmware.

Thread network — state and topology

Command What it shows
state This node's Thread role: leader / router / child / detached / disabled.
version OpenThread version string.
dataset active Active operational dataset (channel, PANID, network name, network key, …).
dataset active -x Same dataset as a single hex blob — paste into HA to share credentials.
partitionid Thread partition ID. Two BRs on different partitions = split network.
leaderdata Leader RLOC16, partition ID, weighting.
channel, panid, extpanid, networkname, extaddr Quick single-parameter reads.

Who's on the mesh

Command What it shows
router table All routers in the partition: RLOC16, next-hop, link quality in/out, age.
child table End-devices directly attached to this BR: RLOC, RSSI, mode, last contact.
child ipaddr IPv6 addresses of each child.
neighbor table Direct radio neighbors with average RSSI and frame error rate — best signal-quality view.
ipaddr This BR's IPv6 addresses on the Thread interface (link-local, mesh-local, OMR, …).
ipmaddr Multicast groups this BR has joined.

Border-router specifics

Command What it shows
br state Border-routing state (running / disabled).
br omrprefix Off-mesh-routable IPv6 prefix advertised to the Thread side.
br onlinkprefix On-link prefix advertised over Wi-Fi via RA.
br nat64prefix NAT64 prefix (if NAT64 is enabled).
br routers Other BRs the routing manager sees on Wi-Fi.
bbr state / bbr config Backbone-router (primary BBR election, sequence number).
ba state / ba port Border Agent — i.e. the _meshcop._udp mDNS responder HA discovers.
srp server state SRP server (Service Registration Protocol — what Thread devices use to publish services).
srp server host / srp server service Devices and services currently registered via SRP — gives you a list of everything advertising itself on the mesh.

Diagnostics

Command What it shows
ping <ipv6> ICMPv6 echo to a Thread or off-mesh address. Works for any reachable IPv6 host.
scan Active scan for Thread networks on all channels.
scan energy Energy detection per 802.15.4 channel — use this to find a quieter channel if Thread perf is bad.
discover Discovery scan: nearby Thread networks plus their network name, ExtPANID, joiner UDP port.
factoryreset Wipe NVS and reboot (will re-form a fresh Thread network).

Wi-Fi side

This example only exposes a minimal wifi command set:

Command What it does
wifi state disconnected / connected / reconnecting.
wifi connect -s <ssid> -p <psk> Manual join (also persisted to NVS).
wifi disconnect Drop the current association.

There is no wifi rssi command. To see Wi-Fi signal strength, watch the wifi: log lines on the monitor (look for bcn_count / rssi= in the periodic Wi-Fi log) or enable iperf and read the AP info from there.

System

Command What it does
free Free heap (bytes).
heap Heap stats incl. largest free block — useful to spot fragmentation.
tasks FreeRTOS task list with stack high-watermarks.
restart Soft-reboot the chip.

OTA

Command What it does
ota download <https-url> Fetch and apply a firmware image from an HTTPS server (cert in server_certs/ca_cert.pem).

Quick recipes

Map the entire Thread mesh from this BR's vantage point:

esp32c6> ot state
esp32c6> ot router table
esp32c6> ot child table
esp32c6> ot neighbor table
esp32c6> ot srp server host
esp32c6> ot srp server service

Copy the Thread credentials to HA (paste the output as the active dataset under HA's Thread integration):

esp32c6> ot dataset active -x

Sanity-check IPv6 to a Thread device once you have its address from ot child ipaddr:

esp32c6> ot ping fd11:1111:1122:0:abcd:abcd:abcd:abcd

Pin to a known-good IDF version

The example currently recommends ESP-IDF v5.5.4, which is the tag this repo pins. To bump, inside esp-idf/:

git fetch --tags
git checkout vX.Y.Z
cd ..
git add esp-idf
git commit -m "Bump esp-idf to vX.Y.Z"

Re-run ./install.sh esp32c6 after switching IDF versions.

References

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors