Skip to content

RaftROS

Rob Dobson edited this page Oct 12, 2026 · 1 revision

RaftROS — ROS 2 Node

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 in examples/ExampleDiscoverable. Background and measurements: Agent-free ROS 2 on an ESP32.

What it provides

  • A ROS 2 node in ros2 node list / node info, named after the device by default (see Node naming).
  • Auto-publishing: each device DeviceManager brings 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/String topics.
  • Services (Zenoh build): std_srvs Trigger, SetBool and Empty servers, including deferred replies.
  • Parameters (Zenoh build): the six rcl_interfaces services, so ros2 param list/get/set/describe/dump work.
  • A runtime configuration API (/api/ros), with changes applied live and optionally persisted.

Transports

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.

Adding it to an application

# 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.

Configuration (RaftROS block)

"RaftROS": {
    "active": 1,
    "domainId": 0,
    "nodeNamespace": "/",
    "routerHost": "192.168.1.50",
    "link": "tcp",
    "qosProfiles": { "classDefaults": {} }
}
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).

Node naming

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.

Auto-publishing

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/a for a custom decode (resp.c), whose code stores raw values. Either kind of record publishes in physical units. (RaftCore before this was fixed applied d/a twice for standard records, and a custom decode without next; reported no records.)

Application API

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::Deferred and completes later with completeService(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's pollResultDecodeFn and pollFieldDescs. The example's ~/read service does this for any device. For an occasional read, peekLatestDecodedPollResponse returns the latest result without consuming it; getLatestDecodedPollResponse reports only a result that is new since its previous call, so a second reader of it sees nothing.

REST API

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.

Diagnostics

  • A node that cannot reach its router logs ROUTER UNREACHABLE after 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 (loopMaxUs and the per-phase maxima) from rosstat with no terminal attached.

Related

Clone this wiki locally