Repository navigation
RaftROSSerialLink
The RaftROS Zenoh node can reach its router over the chip's USB-Serial-JTAG port instead of TCP/WiFi: one USB cable carries power, flashing and ROS 2. The router is the stock
rmw_zenohd, listening on the port — no bridge or agent. The link is off unless chosen, and can be switched on and off at run time from the REST API, the console, or application code.Source:
RaftROS/components/RaftROS/Zenoh/(ZenohSerialFramer.h,ZenohSerialLink.h,RaftROSZenoh.cpp). Investigation and measurements:RaftROS/devdocs/RaftROS-zenoh-serial-investigation-plan.md.
Zenoh defines a serial link (z-serial), and the router shipped with ROS 2's rmw_zenoh is built with it (verified for Lyrical's ros-lyrical-rmw-zenoh-cpp 0.10.6). Each Zenoh batch becomes one frame:
| Field | Size | Notes |
|---|---|---|
| header | 1 |
I=0x01 init, A=0x02 ack, R=0x04 reset, 0 for data |
| length | 2 | payload length, little-endian |
| payload | 0..1500 | one Zenoh batch (no TCP length prefix) |
| CRC-32 | 4 | over the payload; z-serial's own CRC, not zlib's |
The frame is COBS-encoded and ended with 0x00. Before the Zenoh session the device sends empty I frames until the router answers I|A; an R means "start again". Above that, INIT/OPEN, the lease, declarations, services and parameters are exactly as over TCP. The device offers a 1500-byte batch on this link.
The router treats any frame it cannot decode on an initialized link as fatal and drops the session — so nothing but frames may be written to the port while the link is on. See Console port sharing.
- A chip with a USB-Serial-JTAG port (ESP32-S3, -C3, -C6, ...).
-
CONFIG_RAFTROS_ZENOH_SERIAL_LINK=y(the default). - If the console is on the same port (the normal build): RaftCore with
RaftConsolePortand a SerialConsole that acceptsreleasePort. With older libraries RaftROS refuses the link and says why.
/api/ros reports "serialAvailable" and, when false, "serialUnavailableReason".
The link setting (tcp default, or usbjtag) layers like any other RaftROS setting:
# Over WiFi
curl 'http://<device-ip>/api/ros/set?link=usbjtag&persist=1' # to the serial link, kept across reboots
curl 'http://<device-ip>/api/ros/set?link=tcp&persist=1' # back to TCP
curl 'http://<device-ip>/api/ros' # link, serialAvailable, countersThe same requests can be typed at the console (ros/set?link=usbjtag), so a board with no WiFi can be switched to the serial link from its own console. Application code can call RaftROS::requestLinkConfig().
A change is applied on the next loop pass: any live session is closed and the node reconnects over the new link.
Add the port to the router's listen endpoints, beside TCP (the ros2 CLI and foxglove_bridge still use TCP):
listen: { endpoints: ["tcp/[::]:7447",
"serial//dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_<MAC>-if00#baudrate=921600"] }ZENOH_ROUTER_CONFIG_URI=<that file> ros2 run rmw_zenoh_cpp rmw_zenohd-
serial/is followed by the path, so an absolute path gives two slashes. - Use the
/dev/serial/by-id/name: the board re-enumerates when it resets. The listener reopens the path on each accept (release_on_close, default true) and keeps retrying while the device is absent. - The router holds the port exclusively:
raft monitor,esptooland terminals cannot open it while it runs. - Opening the port with the router,
raft monitororscreendoes not reset the board; tools that pulse DTR/RTS (esptool) do. - The RaftROS repository's
docker/distros/demo.shdoes this withRAFTROS_SERIAL=<port>.
The console normally lives on the same USB port. While the link is chosen, RaftROS takes the port over:
- It sends
{"cmd":"releasePort"}to SerialConsole (viaSysManager::sendCmdJSON), which stops reading and writing the port. - It logs one last line — "serial link: taking the USB port from the console ..." — and calls
RaftConsolePort::claim(), which silences Raft's log output, ESP-IDF's log output,stdout/stderrand the ROMprintfchannel. - The link handshake starts. Text already queued for the console reaches the router while it waits for an
Iframe, which it ignores.
Choosing tcp again (or deactivating the node) reverses both, and the console is back.
Getting the console back from a terminal. While it holds the port, the link watches the incoming bytes for a typed line ros/set?... or ros/clear and passes it to the REST API. So with the router stopped, open a terminal on the port and type:
ros/set?link=tcp
(The terminal shows the binary I frames until then.) A setting persisted as usbjtag comes back at the next boot unless persist=1 is given.
An image whose console is on a UART needs none of this: nothing is claimed, and the UART console carries on.
With the console silenced, linkLogs=1 makes the firmware write Raft log lines onto the link between frames. A real router would drop the link on them, so use the bridge tool from the RaftROS repository instead: it relays frames to an ordinary TCP router and prints everything else.
python3 tools/zenoh_serial_link.py bridge --serial /dev/serial/by-id/... --router 127.0.0.1:7447The same tool has tap (decode frames on a port), relay (a TCP Zenoh client over serial, to test a serial router with no board) and selftest (CRC/COBS vectors).
| Field | Meaning |
|---|---|
link |
tcp or usbjtag
|
batch |
negotiated batch size (1500 on the serial link) |
serIn / serOut
|
data frames received / sent |
serBytesIn / serBytesOut
|
bytes |
serCrcErr / serBadFrame
|
frames that failed CRC / did not decode |
serInits / serInitsHeld
|
I frames sent / held back because the previous one had not left the TX buffer |
serLinkUps / serResets
|
link handshakes completed / R frames from the router |
serTxFull |
frames held back for TX buffer room (normal under load) |
serInitsHeld exists because, until the host opens the port, frames queue in the device's TX buffer: a queue of I frames would reach the router as a burst and it would answer alternate ones with I|A and R.
| Result | |
|---|---|
| IMU at 20 Hz poll, light at 10 Hz | 18.5–18.7 Hz, 9.6 Hz |
| 2–3 minutes of service and parameter calls, board logging | 285/285 and 423/423 answered, no serial errors at the router |
| Worst loop pass | 4.5 ms |
| Switch over HTTP | serial ready in ~2 s; back on TCP in ~7 s |
| Router restart | node back ~1 s after the router listens |
Getting Started
- Quick Start
- Architecture at a Glance
- Writing Your First SysMod
- Adding a Comms Channel
- Adding an I2C Device Type
- PlatformIO / Arduino
Scaffolding & Building
- Raft CLI
- SysTypes
- Top-Level SysType
- Build Process
- WebUI Build Pipeline
- File System
- Partitions & Flash
- Local Dev Libraries
- Library Developer Guide
Architecture
Built-in SysMods
- NetworkManager
- WiFi Troubleshooting
- BLEManager
- WebServer
- MQTTManager
- SerialConsole
- CommandSerial
- CommandSocket
- CommandFile
- FileManager
- LogManager
- ESPOTAUpdate
- StatePublisher
- Remote Logging
- Data Source Registration
Comms & Protocols
- Stack Overview
- Comms Channels
- ProtocolExchange
- RICREST Protocol
- Real-Time Streams
- Adding REST Endpoints
- Built-in REST Endpoints
- File Download (OKTO)
- OTA Update Flow
Devices & Buses
- DeviceManager
- Device Manager REST API
- Device Factory & Classes
- Device Type Records
- Adding an I2C Device Type
- Device Data Publishing
- Data Logger
- I2C Bus
- I2C Device Scanning
- I2C ID & Polling
- MotorControl Overview
- MotorControl Config
- MotorControl Commands
ROS 2
Helpers
Reference