Repository navigation
RaftROS
A SysMod that makes a Raft application a first-class ROS 2 node: no micro-ROS and no agent process. Every device DeviceManager identifies is published as a typed ROS 2 topic, and application code can subscribe to topics, serve ROS 2 services and expose ROS 2 parameters.
Source: RaftROS (
components/RaftROS/), with a complete application inexamples/ExampleDiscoverable. Background and measurements: Agent-free ROS 2 on an ESP32.
-
A ROS 2 node in
ros2 node list/node info, named after the device by default (see Node naming). -
Auto-publishing: each device
DeviceManagerbrings online gets a topic/raft/<slug>_<bus>_<addrHex>with a standard message type, in SI units, with a QoS profile chosen by device class. Hot-plug withdraws and re-announces the topic. -
Subscriptions to
std_msgs/Stringtopics. -
Services (Zenoh build):
std_srvsTrigger, SetBool and Empty servers, including deferred replies. -
Parameters (Zenoh build): the six
rcl_interfacesservices, soros2 param list/get/set/describe/dumpwork. -
A runtime configuration API (
/api/ros), with changes applied live and optionally persisted.
Exactly one transport is compiled into an image (Kconfig RAFTROS_BACKEND):
| Zenoh (default) | RTPS / DDS | zenoh-pico (experimental) | |
|---|---|---|---|
| Reaches ROS 2 through | one session to an rmw_zenohd router |
multicast + unicast UDP, directly | as Zenoh, using Eclipse zenoh-pico |
| Needs on the ROS side |
rmw_zenohd; tools run with RMW_IMPLEMENTATION=rmw_zenoh_cpp
|
nothing (FastDDS / CycloneDDS) | rmw_zenohd |
| Topics, auto-publish, hot-plug, QoS | yes | yes | yes |
| Services and parameters | yes | no (calls compile to stubs) | yes |
| Link to the router | TCP over WiFi, or the serial link | — | TCP |
Application code is identical for every backend; the services, parameters and link calls exist on all of them and refuse with a reason where unsupported.
# systypes/Common/features.cmake
set(RAFT_COMPONENTS
RaftCore@main
RaftSysMods@main
RaftWebServer@main # for the REST API
RaftI2C@main # for I2C devices
RaftROS@main
)// main.cpp
raftBusSystem.registerBus("I2C", BusI2C::createFn);
raftCoreApp.registerSysMod("RaftROS", RaftROS::create, true);With "active": 1 in its SysTypes block and a router address, the node appears in ROS 2, publishes /chatter and auto-publishes every bus device — with no other code.
| Key | Default | Meaning |
|---|---|---|
active |
the SysMod's enable
|
Whether the node runs (connects, publishes, serves). Live with /api/ros/set?active=
|
domainId |
0 |
ROS 2 domain |
nodeName |
the device's friendly name | See Node naming |
nodeNamespace |
/ |
Node namespace |
routerHost / routerPort
|
Kconfig default (empty) / 7447
|
Zenoh router, IPv4 only (DNS would block the loop) |
link |
tcp |
tcp or usbjtag — see RaftROS Serial Link
|
linkLogs |
0 |
Serial link only: copy log lines onto the link for the bridge tool |
chatterEnable |
1 |
Publish the /chatter test topic |
qosProfiles |
built-ins | Per-topic and per-class QoS overrides |
spdpIntervalMs / leaseDurationSec
|
5000 / 120
|
RTPS discovery |
Settings layer as everywhere in Raft (Configuration): Kconfig default < SysTypes < persisted settings (NVS).
With no nodeName, the node takes Raft's friendly name: the name set with the friendlyname endpoint, or until one is set the SysType's SysManager.DefaultName (else SystemName) followed by the last six hex digits of the MAC. It is lower-cased, and characters that are illegal in ROS names become _ — e.g. DefaultName: "RaftROS" gives /raftros_adbfb6. Two boards running the same image are therefore two distinct nodes. A configured nodeName is used exactly as given.
Message type is chosen from the device type record's clas[] tags (first match), e.g.:
| Device classes | ROS 2 type(s) | Topic slug |
|---|---|---|
ACC + GYRO
|
sensor_msgs/Imu |
imu |
ACC |
sensor_msgs/Imu (accelerometer only) |
accel |
TEMP + RH
|
Temperature + RelativeHumidity
|
temperature, humidity
|
PRES + TEMP
|
FluidPressure + Temperature
|
pressure, temperature
|
PROX + LGHT
|
Illuminance + Range
|
illuminance, proximity
|
PROX |
sensor_msgs/Range |
proximity |
DIST |
sensor_msgs/Range |
range |
LGHT |
sensor_msgs/Illuminance |
illuminance |
MAG |
sensor_msgs/MagneticField |
magnetic_field |
| VL53L5CX / L7CX / L8CX |
sensor_msgs/Image (32FC1 depth) + Range (nearest zone) |
depth, range
|
| anything else |
std_msgs/Float64MultiArray with labelled dims |
data |
Values are read from the decoded poll struct by field name (ax/ay/az in g, gx/gy/gz in °/s, als, prox, dist in mm, temperature, humidity, pressure in hPa ...) and converted to REP-103 SI units. The full table and the QoS profiles are in the example README.
Adding a sensor is a device type record (Adding an I2C Device Type) whose clas and attribute names follow these conventions — no RaftROS code.
Scaling note. RaftROS reads fields through the generated field table, which holds the divisor/addend still to apply: none for a standard decode, the attribute's
d/afor a custom decode (resp.c), whose code stores raw values. Either kind of record publishes in physical units. (RaftCore before this was fixed appliedd/atwice for standard records, and a custom decode withoutnext;reported no records.)
Get the SysMod from another SysMod's setup():
RaftROS* pRos = static_cast<RaftROS*>(getSysManager()->getSysMod("RaftROS"));// Subscribe (both transports)
pRos->addStringSubscription("rt/cmd", "std_msgs::msg::dds_::String_",
[](const uint8_t*, const uint8_t*, const char* text, uint32_t len) { /* ... */ });
// Serve a service (Zenoh). "~/" is relative to the node: /<ns>/<node>/reset
pRos->addService("~/reset", "std_srvs::srv::dds_::Trigger_",
[](const RaftROS::ServiceRequest& req, RaftROS::ServiceReply& reply) {
reply.fields.success = true;
reply.fields.message = "done";
return RaftROS::ServiceOutcome::Replied;
});
// Declare a parameter (Zenoh), optionally refusing values
pRos->declareParameter("gain", 1.5, "Controller gain", false,
[](const RaftROS::ParamValue& v, const char*& reason) {
if (v.doubleValue < 0) { reason = "gain must be >= 0"; return false; }
return true;
});Rules:
-
Nothing blocks the loop. Handlers, parameter callbacks and subscription callbacks run on the main loop. A handler that needs a fresh bus reading returns
ServiceOutcome::Deferredand completes later withcompleteService(token, reply); RaftROS answers the client with an error at its timeout (5 s) if it never does. -
Reading a device from the app without disturbing auto-publishing: subscribe with
DeviceManager::registerForDeviceData(samples fan out to every subscriber) and decode with the device type record'spollResultDecodeFnandpollFieldDescs. The example's~/readservice does this for any device. For an occasional read,peekLatestDecodedPollResponsereturns the latest result without consuming it;getLatestDecodedPollResponsereports only a result that is new since its previous call, so a second reader of it sees nothing.
| Endpoint | Purpose |
|---|---|
ros |
Status: active, router, link, session state, counters |
ros/set?... |
Change settings live: active=0|1, routerHost=<ipv4>, routerPort=<n>, link=tcp|usbjtag, linkLogs=0|1, persist=0|1
|
ros/clear |
Remove the persisted RaftROS settings (SysTypes applies again) |
rosstat |
Session, publishers, samples, service counters, heap, stack, per-phase loop maxima, serial link counters |
A change is applied on the next loop pass; persist=1 also writes it to the settings overlay in NVS. The same requests work typed at the SerialConsole, e.g. ros/set?link=usbjtag&persist=1.
- A node that cannot reach its router logs
ROUTER UNREACHABLEafter three attempts (and every 30 s): the address, whether anything answered, where the address came from, and how to change it. - RaftROS writes at most one log line per loop pass: on a USB-Serial-JTAG console with nothing reading, each line stalls the loop ~10 ms.
- Read loop figures (
loopMaxUsand the per-phase maxima) fromrosstatwith no terminal attached.
- RaftROS Serial Link — Zenoh over the USB cable
- Console Port Sharing — how the serial link takes the console's port
- DeviceManager, Device Type Records, Device Data Publishing
- I2C Bus — bus and connector power configuration
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