DIY Standalone Thread Border Router using ESP32-C6 and ESP-IDF

Difficulty: Intermediate (comfortable with CLI and flashing firmware)
Cost: ~$5–$10
Time: ~2–3 hours


What This Guide Covers

This guide walks through building a standalone WiFi-based Thread border router using an ESP32-C6 development board. The board runs entirely on its own — no USB connection to HA required — and advertises itself via mDNS so Home Assistant discovers it automatically.

This is not the ESPHome OpenThread component approach, which makes the C6 a Thread end device. This builds a proper border router using the ot_br example from ESP-IDF, equivalent to what a commercial border router does.


Why This Approach?

Most DIY guides cover using an ESP32-C6 or ESP32-H2 as an RCP (Radio Co-Processor) plugged directly into your HA server via USB. That works, but it ties your border router to your server and means a USB cable running to wherever you need coverage.

This guide builds a standalone border router — plug it into any USB charger anywhere in your home, and it connects over WiFi and joins your Thread network automatically.


Hardware Required

  • ESP32-C6 development board with 8MB flash (N8 variant)
    • The Seeed Studio XIAO ESP32-C6 is a popular choice for its small size
    • Any standard ESP32-C6 devkit works
    • Avoid 4MB flash variants — partition space is very tight
  • A Mac or Linux machine for building (this guide uses Mac/Linux)
  • A USB cable for flashing

Note: The ESP32-C6 shares one RF path between WiFi and Thread. This is a minor performance tradeoff vs a dedicated two-chip setup, but works well as a home border router.


Important: What This Firmware Cannot Do

Before starting, understand one key limitation: the ot_br example has no REST API. This means you cannot push Thread credentials to it remotely. The only reliable way to join it to an existing HA Thread network is to flash it with no stored dataset and let HA provision it automatically on first boot. This guide covers exactly how to do that.

:warning: If you already have a Thread border router connected to HA, you must temporarily disconnect it before provisioning the ESP32-C6. With an existing border router present, HA considers the Thread network already active and will not provision a new device — the ESP32-C6 will boot with no dataset and form its own separate Thread network instead of joining yours.

The process is: disconnect your existing border router first → power up the ESP32-C6 → wait for HA to discover and provision it → then reconnect your existing border router. Once provisioned, both border routers will be on the same network and can run simultaneously.


Part 1: Set Up the Build Environment

Install Dependencies (Mac)

brew install cmake ninja dfu-util python3 git

Clone ESP-IDF

mkdir -p ~/esp && cd ~/esp
git clone -b v5.4.2 --recursive https://github.com/espressif/esp-idf.git

Run the Installer

cd ~/esp/esp-idf
./install.sh esp32c6

Activate the Environment

Run this at the start of every new terminal session before building:

. ~/esp/esp-idf/export.sh

Part 2: Configure the Firmware

Navigate to the example:

cd ~/esp/esp-idf/examples/openthread/ot_br

Open the configuration menu:

idf.py menuconfig

Work through each section below. These settings are not optional — skipping any of them will cause the build to fail or the device to misbehave.


Flash Size

Serial flasher config → Flash size → 8MB

Must match your board’s actual flash size.


Radio Mode ← CRITICAL

Component config → OpenThread → Thread 15.4 Radio Link

Select: Native 15.4 radio

This is the most important setting. Without it, the firmware tries to talk to an external radio chip that doesn’t exist, and nothing will work.


Console Type

Component config → OpenThread → Thread Console

Select: USB Serial/JTAG

This prevents a watchdog crash caused by a conflict between the UART console task and the USB serial connection used for flashing and monitoring.


Disable OpenThread CLI

Component config → OpenThread → Thread Extended Features

Disable: Enable OpenThread CLI

Not needed for a standalone border router and causes issues when enabled alongside the USB Serial/JTAG console.


WiFi Credentials

Example Connection Configuration

  • WiFi SSID: your network name
  • WiFi Password: your password
  • Auth mode: WPA2 PSK

The default auth mode is Open — you must change this or the device will fail to connect to any secured network.


Auto-Start

Example Configuration

Enable: Enable the automatic start mode in Thread Border Router

Without this, the device boots but does not start the border router automatically.


Thread Dataset (Optional)

Component config → OpenThread → Thread Core Features → Thread Operational Dataset

You can optionally pre-configure your Thread network credentials here. However, it is recommended to leave these blank and let HA provision the device on first boot instead. See Part 4 for why.


Save and exit menuconfig.


Part 3: Build and Flash

Find your device port:

ls /dev/cu.*        # Mac
ls /dev/ttyUSB*     # Linux

Build and flash:

idf.py build && idf.py -p /dev/cu.usbmodem101 flash monitor

Replace /dev/cu.usbmodem101 with your actual port.

Successful Boot — What to Look For

OpenThread attached to netif
Got IPv4 event: address: 192.168.x.x
mDNS task will be created from internal RAM

Exit the monitor with Ctrl+].

Verify mDNS Discovery

From your Mac or Linux machine:

dns-sd -B _meshcop._udp local       # Mac
avahi-browse _meshcop._udp          # Linux

You should see esp-ot-br listed. This confirms Home Assistant will be able to discover the device.


Part 4: Join Your Existing HA Thread Network

When the device first boots with no stored dataset, HA will automatically provision it with your preferred Thread network credentials. This is the cleanest way to ensure it joins the right network.

If the device previously had a dataset stored (from a prior flash or from pre-configuring credentials in menuconfig), it will form its own separate Thread network instead of joining yours. To fix this:

  1. Erase the flash to clear the stored dataset:
idf.py -p /dev/cu.usbmodem101 erase-flash
  1. Reflash:
idf.py -p /dev/cu.usbmodem101 flash
  1. On boot, the device will have no dataset. HA discovers it via mDNS and provisions it with your preferred network credentials automatically.

After provisioning, check Settings → Devices & Services → Thread in HA. Your esp-ot-br border router should appear under your preferred network.


Part 5: Deploy

Once confirmed working:

  1. Note the IP address from the boot log or your router’s DHCP table
  2. Assign a static DHCP reservation so the IP doesn’t change between reboots
  3. Unplug from your Mac and plug into any USB charger — the device is now standalone

Monitoring

Since there is no REST API and no web UI in this firmware, your options for checking the device is alive are:

  • Uptime Kuma — add a ping monitor for the device’s reserved IP
  • Home Assistant Thread page — if esp-ot-br disappears from the border router list, it’s offline
  • mDNS querydns-sd -B _meshcop._udp local from any Mac on the network

Re-flashing Reference

If you need to rebuild in a future session:

. ~/esp/esp-idf/export.sh
cd ~/esp/esp-idf/examples/openthread/ot_br
# sdkconfig stores your settings — only re-run menuconfig if starting fresh
idf.py build && idf.py -p /dev/cu.usbmodem101 flash monitor

Gotchas Summary

Gotcha Fix
CONFIG_OPENTHREAD_RADIO_NATIVE not set by default Set manually in menuconfig → Thread 15.4 Radio Link
Watchdog crash on boot Switch console to USB Serial/JTAG in menuconfig
Default WiFi auth is Open Change to WPA2 PSK in menuconfig
Auto-start not enabled by default Enable in Example Configuration
Device forms its own Thread network Erase flash, reflash, let HA provision
esp-thread-br SDK won’t work for single-chip builds Use ot_br from esp-idf instead — esp-thread-br is two-chip only

Software Versions Used

  • ESP-IDF: v5.4.2
  • HA OpenThread Border Router add-on: v2.15.3
  • Home Assistant OS: tested on current stable

Notes

  • The esp-thread-br SDK does not support single-chip native radio mode. It requires a two-chip setup (e.g., ESP32-S3 host + ESP32-H2 radio). Use the ot_br example from esp-idf for single-chip C6 builds.
  • If you want a border router with a web UI and REST API (for easier credential management), look at the Espressif ESP Thread Border Router board (S3 + H2 two-chip design) or a commercial option like the GL-S20.
  • Multiple border routers on the same Thread network are fully supported by HA and improve mesh reliability and coverage.

This took a fair amount of trial and error to get working and the documentation for the single-chip native radio path is sparse.

Hi David, thanks for this guide, it’s just what I have been looking for, but unfortunately I don’t have either a Mac or Linux machine I can use to set this up, and some of the terminology is a bit alien to me. Are you by any chance going to write this again for setting up with Windows machines or Python on Windows ?

I don’t use Windows often so I won’t be re-writing this for Windows but if you use WSL on windows these instructions should work fine. I used Brew to install dependencies on the Mac (and you can install Brew on Linux and WSL if you want) but you can also just use the normal apt commands like

sudo apt install cmake ninja-build dfu-util python3 git

I managed to borrow a mates laptop that runs Linux Mate to give this a try. First time I got to Part 2 but then got an error about cmake not being installed so I went back to the start and saw the command to install it, but as it said (Mac) I ignored it.
I then removed the esp directory and started again with command “sudo apt install cmake ninja-build dfu-util python3 git” and carried on again as far as part 2, but when I ran “idf.py menuconfig” I got a screen full of CMake errors in RED which says it can’t find the compiler files. Is there an environment variable that needs setting somewhere?

Unfortunately, I would have no way of knowing what the differences are between your environment and mine. Best bet would be to give the error messages to Claude, or your favorite AI and see if it can help resolve the dependency issues.