ESP32-C6 OpenThread RCP on Jetson Orin Nano - Project Guide¶
Goal: Bring up a second ESP32-C6 as an OpenThread Radio Co-Processor (RCP) for the Jetson Orin Nano 8GB Developer Kit, so the Jetson can run OpenThread Border Router (OTBR) or OpenThread Daemon while keeping your first ESP32-C6 dedicated to ESP-Hosted Wi-Fi/BLE.
Hub: Network and Connectivity
Related local guides: ESP32-C6 ESP-Hosted over SPI on Jetson Orin Nano · Peripheral Access
1. Why this project matters¶
Once your Jetson already has:
wlan0from ESP-Hosted over SPIhci0from the same ESP32-C6 for BLE
the clean next step for Thread is not to overload that same chip. The cleaner architecture is:
- keep the first ESP32-C6 as the Jetson's Wi-Fi/BLE coprocessor
- add a second ESP32-C6 dedicated to 802.15.4 / Thread
- let the Jetson run the host-side OpenThread stack
This follows the standard RCP design from OpenThread:
- the host processor runs the OpenThread stack
- the RCP only handles the Thread radio / MAC layer
- host and RCP talk over Spinel
That model is a good fit for Jetson because the Linux host is always on and already powerful enough to run OTBR and other edge services.
2. Target architecture¶
Jetson Orin Nano
|
|-- SPI -> ESP32-C6 #1 -> ESP-Hosted -> wlan0 + hci0
|
|-- UART (recommended first) -> ESP32-C6 #2 -> OpenThread RCP
| ^
| |
| Spinel protocol
|
+--> Linux host side
|-- otbr-agent (for Thread Border Router)
|-- or ot-daemon (for lighter RCP host use)
|
+--> wpan0
Target outcome
- Jetson keeps using
wlan0from the first ESP32-C6 as its normal Wi-Fi interface - Jetson gains a Thread radio path through
wpan0 otbr-agentorot-daemoncan talk to the second ESP32-C6 over Spinel- the second ESP32-C6 is dedicated to Thread RCP use and is not shared with ESP-Hosted
3. Why use a second ESP32-C6¶
This guide intentionally avoids the single-chip “do everything on one ESP32-C6” path.
Why:
- ESP-Hosted is a Linux Wi-Fi/BLE coprocessor solution
- OpenThread RCP is a different host/co-processor model
- both want control of the same radio and host transport path
- the integration effort to merge them into one stable firmware is much higher than using two chips
Espressif officially documents:
- ESP32-C6 supports RCP mode
- the RCP transport can be SPI or UART
- for Wi-Fi-based Thread Border Router products, a dual-SoC architecture is recommended for better coexistence behavior
So this guide uses the safer engineering split:
- ESP32-C6 #1 for Jetson Wi-Fi/BLE
- ESP32-C6 #2 for Thread RCP
4. Hardware and software prerequisites¶
Hardware¶
- Jetson Orin Nano 8GB Developer Kit
- your existing ESP32-C6 #1 already working with ESP-Hosted over SPI
- a second ESP32-C6 dev board
- jumper wires for a direct UART link between Jetson and the second ESP32-C6
- USB cable for flashing and monitoring the second ESP32-C6
- optionally, one more Thread-capable device for validation:
- another ESP32-C6
- ESP32-H2
- a Matter-over-Thread end device
Software¶
- JetPack 6.x / L4T 36.x on Jetson
- ESP-IDF installed on a Linux build/flash machine
- OpenThread Border Router host software on Jetson:
ot-br-posixif you want a real Thread Border Router- or OpenThread POSIX
ot-daemonif you want a lighter RCP host setup
Recommended first transport¶
Although ESP-IDF says RCP can use SPI or UART, this guide uses the Jetson's 40-pin header UART1 as the first real host transport.
Why:
- less risk of colliding with the SPI bus already used by ESP-Hosted
- it gives you a stable Jetson device path on this validated setup:
/dev/ttyTHS1 - it reflects the real host/RCP wiring model instead of hiding it behind a USB-UART bridge
You can still use the ESP board's USB connection for:
- flashing
- serial monitor logs
- power during bring-up
You can optimize to SPI later if you need lower latency.
5. The official model you are implementing¶
OpenThread's RCP design means:
- the OpenThread core lives on the host processor
- the radio chip runs a minimal controller firmware
- host and controller talk through Spinel
On Jetson, that means:
- Jetson runs
otbr-agentorot-daemon - ESP32-C6 #2 runs the
ot_rcpfirmware from ESP-IDF
OpenThread's own coprocessor docs describe this as an RCP design, and OT Daemon is explicitly documented as the POSIX-side component for RCP setups.
For the backbone / infrastructure side, you have multiple valid choices on Jetson:
l4tbr0if you want to use the Jetson USB device networking path to a host PCwlan0if the Jetson reaches the network through your first ESP32-C6 and ESP-HostedenP8p1s0if you later use Ethernet as the OTBR backbone
On your current setup, l4tbr0 is the right choice for USB networking, not usb0 or usb1, because those interfaces are members of the l4tbr0 bridge.
6. Build and flash the ESP32-C6 RCP firmware¶
Use the ESP-IDF ot_rcp example for the second ESP32-C6.
On your Linux build host:
cd $IDF_PATH/examples/openthread/ot_rcp
# Select the chip once
idf.py set-target esp32c6
# Optional: inspect settings
idf.py menuconfig
# Build
idf.py build
Flash it to the second ESP32-C6:
# Use the actual serial port for your board
# On many ESP32-C6 dev boards with CP210x this is /dev/ttyUSB0
idf.py -p /dev/ttyUSB0 flash monitor
Important menuconfig choices for this guide¶
If the ot_rcp menu shows:
that matches the direct Jetson UART wiring documented below.
For this guide:
- keep
Configure RCP UART pin manuallyenabled - set RCP RX pin =
4 - set RCP TX pin =
5 - leave external coexist wire disabled for first bring-up
That means:
- Jetson TX must go to ESP GPIO4 (
RCP RX) - Jetson RX must come from ESP GPIO5 (
RCP TX)
Port naming note¶
Do not hardcode /dev/ttyACM0 just because some upstream OTBR examples show it.
On Espressif dev boards:
- one board may show up as
/dev/ttyUSB0 - another may show up as
/dev/ttyACM0
Use the actual port that belongs to ESP32-C6 #2 on your machine.
If you are using an Espressif board with a CP210x bridge, /dev/ttyUSB0 is common.
This USB serial port is for:
- flashing the
ot_rcpimage - reading ESP boot and log output
It is not the same thing as the Jetson's real UART transport used later by otbr-agent.
7. Enable UART1 on the Jetson and wire it to the RCP¶
The Jetson Orin Nano 40-pin header exposes UART1 as:
| Function | Jetson pin | Linux device | Direction |
|---|---|---|---|
| UART1_TXD | 8 |
/dev/ttyTHS1 on your current image |
Jetson -> ESP |
| UART1_RXD | 10 |
/dev/ttyTHS1 on your current image |
ESP -> Jetson |
| Ground | 6 or 9 or 14 |
-- | common reference |
For the ESP32-C6 RCP side in this guide:
| Function | ESP32-C6 pin | Direction |
|---|---|---|
| RCP RX | GPIO4 |
Jetson TX -> ESP RX |
| RCP TX | GPIO5 |
ESP TX -> Jetson RX |
| Ground | GND |
common reference |
So the exact wiring is:
Jetson pin 8 (UART1_TXD) -> ESP32-C6 GPIO4
Jetson pin 10 (UART1_RXD) <- ESP32-C6 GPIO5
Jetson GND -> ESP32-C6 GND
Jetson UART prerequisites¶
On Jetson, the main thing that usually blocks use of the header UART is nvgetty, which can claim the user UART device.
Check the device first:
Then disable the serial getty if it is active:
Verify it is no longer active:
Basic Jetson UART configuration¶
Configure the port to the baud rate you intend to use with the RCP host link:
This guide uses 460800 because that is a common ot_rcp / OTBR pairing on Espressif examples. If you later choose a different baud in your OpenThread configuration, keep the Jetson and ESP sides aligned.
Electrical warning¶
The Jetson 40-pin UART is 3.3 V logic only.
Do not connect it to:
- RS-232 voltage levels
- 5 V UART
- any external adapter that drives outside 3.3 V logic levels
For an ESP32-C6 dev board, direct 3.3 V UART wiring is fine.
Optional sanity test¶
Before involving the ESP board, you can do a Jetson loopback test:
- temporarily short pin 8 to pin 10
- run:
If the text echoes back, the Jetson side UART path is alive.
8. Install OTBR on the Jetson¶
If you want a real Thread Border Router on the Jetson, use ot-br-posix.
sudo apt update
sudo apt install -y git
git clone --depth=1 https://github.com/openthread/ot-br-posix
cd ot-br-posix
./script/bootstrap
INFRA_IF_NAME=l4tbr0 ./script/setup
Why INFRA_IF_NAME=l4tbr0 on your current Jetson:
- OTBR needs a backbone / infrastructure interface
- your Jetson currently exposes the USB networking path as the bridge
l4tbr0 usb0andusb1are members of that bridge, sol4tbr0is the real backbone interface
If you later want the backbone to be:
- ESP-Hosted Wi-Fi, use
INFRA_IF_NAME=wlan0 - Ethernet, use
INFRA_IF_NAME=enP8p1s0once that link is active
After installation:
The official OTBR native install guide shows the agent running like this at a high level:
For your setup, the important part is the -B <backbone> choice. On your current Jetson USB networking path, that backbone should be l4tbr0.
9. Point OTBR at the real ESP32-C6 RCP port¶
OTBR uses /etc/default/otbr-agent to define the Radio URL.
Edit it:
Set the RCP path and baud rate. For the direct Jetson header UART path in this guide:
If you intentionally choose a USB-serial path instead, replace /dev/ttyTHS1 with the real USB serial device such as /dev/ttyUSB0 or /dev/ttyACM0.
If you later move the OTBR backbone to Wi-Fi, change -B l4tbr0 to -B wlan0.
Then restart the service:
sudo systemctl restart otbr-agent
sudo systemctl status otbr-agent
sudo journalctl -u otbr-agent -n 100 --no-pager
Why 460800:
- Espressif's Thread BR FAQ notes that OTBR often defaults to
115200 - but the Espressif
ot_rcpexample commonly uses460800 - if the baud rate is wrong, host/RCP communication will fail even though the serial device exists
10. Validate the host/RCP link first¶
Before forming a Thread network, prove that the Jetson can talk to the RCP reliably.
Check that the expected Jetson UART device exists:
Check OTBR service health:
What success looks like:
otbr-agentisactive (running)wpan0exists- logs do not show repeated Spinel timeouts
If wpan0 does not appear, stop there and fix the RCP link before trying to form a network.
11. Form a Thread network on the Jetson¶
Once OTBR is healthy, use ot-ctl on the Jetson.
sudo ot-ctl state
sudo ot-ctl dataset init new
sudo ot-ctl dataset commit active
sudo ot-ctl ifconfig up
sudo ot-ctl thread start
sudo ot-ctl state
Typical progression:
- first
disabled - then
detached - finally
leaderif this is the first Thread node in the new network
Useful follow-up commands:
At this point, the Jetson is no longer just “connected to an RCP.” It is actively running the OpenThread host stack.
How to read the real OTBR and ot-ctl output¶
The most useful habit in this project is to read the host-side state in layers instead of looking for one magic success line.
For this validated Jetson setup, the first important line is:
That line means the Linux host successfully talked to the ESP32-C6 over:
spinel+hdlc+uart/dev/ttyTHS1460800baud
If that line is missing and wpan0 never appears, the problem is still in the transport layer: wrong UART device, wrong baud, wrong wiring, bad RCP image, or an RCP reset.
Once the link is healthy, the next useful snapshot is usually:
For example, a real session may show:
extaddr: fac5eb4acbada19e
rloc16: a400
ipaddr:
fd3f:d825:5faf:9782:0:ff:fe00:a400
fd3f:d825:5faf:9782:8b89:772a:39cd:737a
fe80::f8c5:eb4a:cbad:a19e
state: detached
Read that output like this:
extaddris the 64-bit IEEE 802.15.4 radio identity.rloc16is the Thread mesh locator assigned inside the partition logic.fd...ff:fe00:a400is the RLOC IPv6 address derived from the mesh-local prefix andrloc16.fd...8b89:...is the Mesh-Local EID, a more stable mesh-local identity.fe80::...is the normal IPv6 link-local address.
This is the key nuance: you can have valid Thread addresses and still be detached. That means the dataset is present and the host/RCP stack is alive, but the node has not yet completed attachment to a partition.
What the common OTBR log lines mean¶
These log lines are the most important ones to recognize:
Mle-----------: Send Link Request (ff02::2)
MeshForwarder-: Sent IPv6 UDP msg ... dst:[ff02::2]:19788
Settings------: Read NetworkInfo {rloc:0xa400, extaddr:..., role:leader, ...}
BorderAgent---: Registering service OpenThread BorderRouter #A19E _meshcop._udp
How to interpret them:
Mle-----------means the Thread control plane is actively trying to discover or attach.MeshForwarder-proves the radio path is transmitting real Thread traffic.Settings------shows persisted OpenThread state being read or written.BorderAgent--- ... _meshcop._udpshows the commissioning-facing Border Agent service being registered by OTBR.
One subtle line confuses many people:
This does not prove the node is currently leader. It only means the stack read previously saved network state that remembered a leader role. The live role still comes from sudo ot-ctl state.
Why detached can still be a healthy intermediate state¶
In a fresh lab setup, the expected state progression is:
disableddetachedleaderfor a one-node partition, orrouter/childwhen joining an existing mesh
So detached is not the same as "the UART is broken." In your actual logs, it appeared together with:
- a real
Radio Co-processor versionline wpan0creation- valid
extaddr,rloc16, andipaddroutput - MLE attach attempts in the logs
That combination means the host/RCP link is already working. The remaining work is in Thread attachment and partition formation, not low-level serial bring-up.
How to read the attach-attempt logs¶
Lines like these:
Attach attempt 8, AnyPartition
Send Parent Request to routers
Send Parent Request to routers and REEDs
Attach attempt 8 unsuccessful, will try again in 32.128 seconds
mean the node is alive and behaving like a Thread node, but it has not yet completed the attach process. This is a protocol-state problem, not automatically a transport problem.
If you instead see:
that points back to the host/RCP path and you should debug the UART, baud, RCP image, or board stability first.
Session-socket and restart messages¶
Two more lines are easy to over-interpret:
In normal use, those often just mean that ot-ctl connected to OTBR's control socket and then exited. They are not automatically a radio failure.
Similarly, after restarting otbr-agent, it is normal to see:
wpan0exist but still bestate DOWNsudo ot-ctl statereturndisabled
until you run:
sudo ot-ctl dataset init new
sudo ot-ctl dataset commit active
sudo ot-ctl ifconfig up
sudo ot-ctl thread start
For this Jetson project, the known-good host-side baseline is:
- RCP transport:
spinel+hdlc+uart - serial device:
/dev/ttyTHS1 - baud:
460800 - OTBR backbone:
l4tbr0
Current validated status on this Jetson image¶
At this point, the current validated status of the project is:
- the ESP32-C6
ot_rcpimage is working - Jetson can talk to the RCP over Spinel + UART
- the validated host path is:
/dev/ttyTHS1460800l4tbr0- OTBR creates
wpan0 ot-ctlcan manage the Thread dataset and interface- the node can progress from
disabledtodetached - in a foreground OTBR run, the node eventually becomes
leader
The most important leader-formation lines from the real run were:
Allocate router id 41
RLOC16 fffe -> a400
Role detached -> leader
Partition ID 0x1a1d09c0
Route table ... me - leader
Those lines prove that the Thread stack itself is functioning correctly on this hardware split. In other words, the RCP link, dataset handling, MLE attach logic, and one-node partition formation all work on the current system.
However, the same foreground run later hit:
That is the key current limitation. It means OTBR was able to bring up the Thread node and even reach leader, but then failed while enabling Linux-side border-routing / multicast-routing features.
The kernel check confirmed the reason:
So the correct current conclusion is:
- Thread-over-RCP on Jetson is working
- full OTBR border-router functionality is blocked on this Jetson image
- the blocker is missing kernel multicast-routing support, not UART, not Spinel, and not the ESP32-C6 RCP firmware
This is exactly the kind of mixed-software boundary problem that this project is meant to teach: a system can be successful as a Thread node and still be incomplete as a full border-router product because the Linux kernel is missing one feature OTBR expects.
12. Optional lighter path: OT Daemon instead of OTBR¶
If you want an RCP host path without the full border-router stack, use OpenThread Daemon.
According to OpenThread's official coprocessor docs:
ot-daemonis the POSIX service for RCP designs- it uses a Spinel Radio URL just like OTBR
Build it:
git clone --depth=1 https://github.com/openthread/openthread
cd openthread
./script/bootstrap
./script/cmake-build posix -DOT_DAEMON=ON
Run it against the real RCP:
In another terminal:
Use this path if:
- you want to validate host/RCP behavior first
- you do not yet need a full Thread Border Router
- you want a smaller debugging surface than OTBR
On your current Jetson image, this path is especially useful because the kernel currently lacks:
CONFIG_IP_MROUTECONFIG_IPV6_MROUTE
That means ot-daemon is the cleaner current host path for Thread/RCP work while you defer a JetPack or kernel rebuild for full OTBR border-routing support.
If you choose the USB-serial path instead of Jetson header UART, substitute the real USB device path.
13. Common failure modes¶
otbr-agent is running, but wpan0 is missing¶
Usually means:
- wrong serial device
- wrong baud rate
- RCP firmware is not actually
ot_rcp nvgettyis still owning the user UART device
Check:
Spinel timeout warnings¶
Typical causes:
- wrong UART baud rate
- unstable USB serial path
- wrong serial port after replug or reflashing
- Jetson UART1 is not actually free for application use
Espressif's FAQ shows this family of symptom as:
/dev/ttyTHS1 exists, but OTBR still cannot talk to the RCP¶
Usually means one of:
- Jetson pin
8/10are not actually cross-wired to ESPGPIO4/5 - TX/RX are swapped incorrectly
nvgettystill owns the port- the ESP side UART pins in
menuconfigdo not match the actual wiring
For this guide, the correct direct wiring is:
Confusing the two ESP32-C6 boards¶
Keep them clearly separated:
- ESP32-C6 #1 = ESP-Hosted Wi-Fi/BLE over SPI
- ESP32-C6 #2 = Thread RCP over UART
Do not flash ot_rcp onto the board currently providing wlan0.
OTBR installs correctly but routing does not work¶
If OTBR can talk to the RCP, forms wpan0, and even reaches leader, but then exits with a message like:
the problem is no longer the ESP32-C6 or the UART link. It is usually a host-kernel limitation.
On the validated Jetson run for this project, the exact cause was:
Check on the Jetson with:
or, if /proc/config.gz is unavailable:
If those options are missing, the practical choices are:
- use
ot-daemonfor current Thread/RCP work - rebuild or replace the kernel / JetPack image later with multicast-routing support enabled
Also make sure the backbone interface is right:
wlan0if Jetson reaches the network through ESP-Hostedeth0only if you intentionally use Ethernet as the Thread BR backbone
14. Good next steps after first bring-up¶
- keep this current image for RCP and Thread learning, using
ot-daemonwhen you do not need full OTBR border routing - rebuild JetPack or the Jetson kernel later with
CONFIG_IP_MROUTEandCONFIG_IPV6_MROUTEenabled - add a second Thread device and have it join the network
- validate
ipaddr,ping, and service discovery over Thread - keep the RCP on UART first, then evaluate SPI later only if needed
- if you want Matter later, keep this Jetson + RCP split and build on top of it
15. References¶
Official upstream references¶
- OpenThread co-processor designs
- OpenThread Daemon (RCP host mode)
- OpenThread Border Router native install
- ESP-IDF OpenThread on ESP32-C6
- ESP-IDF Thread / esp_openthread API reference
- ESP Thread Border Router SDK
- ESP Thread BR FAQ: OTBR with
ot_rcp - ESP32-C6 RF coexistence guidance