All systems operational--:--:-- UTC

Bridging ROS 2 fleets without touching a single node

How the ConsciousLab edge agent subscribes to DDS topics, keeps traffic on the customer's network, and survives 72 hours offline.

Felix Brandt

Staff Engineer, Robotics

Published
Reading time
4 min read

Most robots that arrive at a customer site already run ROS 2. They publish odometry, battery state, joint temperatures and diagnostics on dozens of topics, and the integrator who commissioned them has spent months tuning the nodes. The fastest way to lose that customer is to ask them to change any of it.

So the rule for the Robotics module has always been: connect without rewriting nodes. This post explains how the edge agent does that, the trade-offs we made, and the numbers from 31 sites.

The shape of the problem

A typical warehouse deployment has between 40 and 400 robots on a site network, sometimes split across VLANs, with a mix of Wi-Fi and private LTE. Each robot publishes between 20 and 120 topics. A few of those topics matter to operators: battery, pose, motor temperatures, error states. Most do not leave the robot in normal operation, such as raw lidar scans and point clouds.

DDS, the transport under ROS 2, is designed for discovery on a local network. It is chatty, it uses multicast by default, and it was never meant to cross the public internet. Sending it to a cloud service directly is a bad idea for bandwidth, for latency and for the customer's security team.

One agent per site, not per robot

We run the edge agent on a small gateway at each site, usually an industrial PC or a spare server rack slot, rather than on every robot. The agent joins the site's DDS domain as an ordinary participant. To the robots it looks like one more node subscribing to topics.

The agent is configured with a topic map that says which topics to forward, at what rate, and how to translate them:

domain_id: 42
qos_profile: sensor_data
topics:
  - match: /amr_*/battery_state
    type: sensor_msgs/msg/BatteryState
    channel: battery.percent
    field: percentage
    scale: 100
    rate_hz: 1
  - match: /amr_*/drive/motor_temp
    type: std_msgs/msg/Float32
    channel: drive.motor_temp_c
    rate_hz: 10
    threshold: { above: 60.0, hysteresis: 2.0 }

The match patterns use the robot's namespace to infer the unit ID, so /amr_0398/drive/motor_temp becomes channel drive.motor_temp_c on unit AMR-0398. New robots appear in the console the first time they publish a matched topic.

Thresholds are evaluated at the edge. A breach is sent immediately as an event, even if the channel is downsampled to 1 Hz for the cloud, so an alert never waits for the next sample.

Keeping DDS on the customer's network

The agent never forwards DDS traffic. It decodes messages locally using the type definitions it discovers from the domain, extracts the fields in the topic map, and sends compact samples to our ingest over a single outbound gRPC stream with mutual TLS. No inbound ports are opened on the site network. Security teams consistently tell us this is the property that gets the deployment approved.

Commands work the other way round over the same stream. When an operator pauses a robot from the console, the agent calls the robot's ROS 2 service, for example /amr_0398/pause, and returns the service response as an event. Commands are authorised in the cloud before they are sent and again at the edge against a locally cached policy, so a compromised cloud session cannot command a robot outside its scope.

The one exception is the e-stop. The e-stop service is always forwarded immediately and is never subject to policy, because a safety function that can be blocked by a misconfigured rule is not a safety function.

Surviving the network

Warehouse networks fail. Access points reboot, LTE drops in the cold store, someone unplugs the gateway to vacuum. The agent buffers samples to local disk when the uplink is down, up to 72 hours at default rates, and replays them in order when it reconnects. Samples carry the robot's own timestamp, not the time of upload, so charts fill in correctly after an outage.

Buffered samples are compressed with delta encoding plus zstd. For typical telemetry that is about 11 bytes per sample, so 72 hours for 400 robots at our default rates fits in under 6 GB.

What it costs at the edge

The agent binary is 38 MB, runs on arm64 and x86_64 Linux, and is supported on ROS 2 Humble and Jazzy. On a four-core industrial PC forwarding 400 robots at default rates it uses about 0.6 of a core and 210 MB of memory. The bulk of the CPU goes to CDR deserialisation, which we do lazily: messages on unmatched topics are never decoded.

Numbers from the field

Across 31 sites and 2,418 units, median end-to-end latency from a robot publishing a sample to the value appearing in the console is 310 ms on Wi-Fi and 480 ms on LTE. Command round trip, from pressing Pause to the service response, is 180 ms at p95. Over the last 90 days, 99.97% of samples generated at the edge reached the store, and every one of the missing samples was on a gateway whose disk filled during an outage longer than 72 hours. We now alert on gateway disk usage at 70%.

The devices documentation covers installing the agent and writing topic maps. If your robots do not run ROS 2, the agent also speaks MQTT 5 and can read OPC UA nodes.