Communication Patterns Overview
HORUS provides three communication primitives that cover all robotics messaging needs:
| Pattern | Purpose | Example |
|---|---|---|
| Topic | Streaming data (pub/sub) | Sensor readings, motor commands, video frames |
| Action | Long-running tasks (goal/feedback/result) | Navigation, arm motion, calibration |
| Service | Request/response RPC (blocking call) | Query state, trigger a one-shot command, config get/set |
Topics and Actions work locally (shared memory) and across machines (network transport via configuration). A Service is built from a pair of Topics internally, so it follows the same transport rules.
Topic: Streaming Pub/Sub
Topic<T> is the primary communication primitive. It provides ultra-fast pub/sub with automatic optimization — a 16-byte message costs ~63ns to publish when both ends share a process and ~151ns one-way between processes on the reference machine.
use horus::prelude::*;
// Create a topic — backend is auto-detected
let pub_topic: Topic<f32> = Topic::new("sensor.temperature")?;
pub_topic.send(25.3);
// Multiple subscribers can listen (up to 16 participants per topic — see below)
let sub_topic: Topic<f32> = Topic::new("sensor.temperature")?;
if let Some(temp) = sub_topic.recv() {
hlog!(info, "Temperature: {}", temp);
}
Automatic Optimization
You never choose a backend — HORUS detects the optimal one automatically based on:
- Participants: How many publishers and subscribers are attached
- Payload: Whether the message type is plain-old-data (zero-copy ring) or has to be serialized
Where the participants run is not a criterion. Every topic is shared-memory backed and every backend is cross-process, so the same path serves participants whether they share a thread, share a process, or live in separate processes — location changes the latency, not the choice:
| Scenario (16 B message, p50) | Latency |
|---|---|
| Same thread | ~20ns (publish) |
| Same process, separate threads | ~63ns (publish) |
| Cross-process, 1 publisher → 1 subscriber | ~151ns (one-way) |
| Cross-process, 1 publisher → many subscribers | ~191ns (one-way) |
| Cross-process, many publishers → 1 subscriber | ~279ns (one-way) |
Measured by all_paths_latency on an Intel Core i7-10750H — see Benchmarks for the full table.
The communication path can change at runtime — if a second subscriber joins, HORUS automatically migrates to a multi-consumer path. A message in flight across that switch can be lost, so pre-warm a topic to its final topology before it carries traffic that matters; see Topics.
Key Characteristics
Publishers and subscribers share one 16-slot table, counted per process and thread. The
17th registration fails, and that handle receives nothing for its whole life — the role
stays unregistered, so every later recv() fails the same way and returns None.
It says so. The failure is logged at warn naming the topic, the count and the limit,
rate-limited to once a minute per topic so a hot loop cannot flood the log. Sixteen is
plenty for a normal robot, but a fan-out topic read by many nodes — or a process that
opens the same topic from several threads — can reach it, and that warning is the first
place to look when a subscriber that should be receiving never does.
- Non-blocking send:
send()returns()— it never returns an error. Broadcast topologies (multiple subscribers) overwrite the oldest slot — latest-wins, no backpressure. On a point-to-point ringsend()retries briefly (spin + yield) and then drops the newest message if a live subscriber is draining the ring; if nothing is draining it — no subscriber yet, the subscriber's process gone, ortailfrozen for a 5-second lease — it retires the oldest unread slot instead and accepts the new message, counting the retirement indropped_count(). Usetry_send()to get the message back on failure. send_blockingis not a general answer to loss: on any broadcast-backed topic it returnsErr(SendBlockingError::NoBackpressure)without sending. See Topics.- Non-blocking recv:
recv()returnsOption<T>. ReturnsNoneif no message available. - Zero introspection overhead: No timing, logging, or tracing in the default path. Content logging is off by default and is toggled by the topic's verbose header flag (
horus_core::communication::set_topic_verbose), not by a builder method.
For full Topic API details, see Topic Communication.
Action: Long-Running Tasks
Actions handle tasks that take time to complete and benefit from progress feedback and cancellation. They follow the Goal / Feedback / Result pattern: a client sends one goal, the server streams feedback while it works, and the client gets a single result — or cancels partway through.
An action is three message types and a name. A server registers a handler for it; a client sends goals and receives a handle it can poll, await, or cancel.
This page is the overview. The full treatment — server configuration, preemption
policies, goal states and priorities, server metrics, error handling and the
horus action CLI — is in Actions.
| You need | Read |
|---|---|
| The Goal/Feedback/Result types and how to declare them | Defining an Action |
| Handling goals, streaming feedback, honouring cancellation | Action Server |
| Sending a goal and awaiting or cancelling it | Action Client |
| What states a goal moves through, and what preempts what | Goal Lifecycle |
Network Communication
By default, Topics and Actions use shared memory for ultra-fast communication between processes on the same machine. HORUS also supports zero-configuration peer discovery over UDP multicast (group 224.0.69.72, port 9100), letting robots on the same local network find each other automatically. Build with --features net — LAN replication is opt-in and off by default. Tune it with [network] in horus.toml or the HORUS_NET_* env vars.
When to Use What
| Scenario | Use | Why |
|---|---|---|
| Sensor data streaming | Topic | Continuous data, latest value matters |
| Motor commands | Topic | High frequency, low latency |
| Camera frames | Topic | Streaming, multiple consumers |
| Navigate to position | Action | Long-running, needs progress feedback |
| Pick-and-place | Action | Multi-step, cancellable |
| Calibration routine | Action | Takes time, reports progress |
| Emergency stop | Topic | Must be instantaneous, no handshake |
| Diagnostics broadcast | Topic | Many publishers, flexible topology |
| Query a parameter / one-shot command | Service | Caller needs an answer before continuing |
Rule of thumb: If it's a continuous stream of data, use Topic. If it's a task you'd want to start, monitor, and potentially cancel, use Action. If the caller needs an answer back before it can continue, use Service.
Composing Patterns
A typical robot system combines topics and actions in a single node:
struct NavigationSystem {
// Topics for streaming sensor data
lidar_sub: Topic<LaserScan>,
odom_sub: Topic<Odometry>,
// Topics for publishing commands
cmd_vel_pub: Topic<Twist>,
status_pub: Topic<DiagnosticStatus>,
// Action server for goal-based navigation
nav_server: ActionServerNode<NavigateToGoal>,
}
The navigation action server subscribes to sensor topics internally, computes a path, publishes velocity commands via topics, and reports progress back to the action client — all using the same underlying shared memory infrastructure.
Next Steps
- Topic Details - Full Topic API reference
- Actions - Long-running tasks with feedback and cancellation
- Services - Request/response RPC