Communication Overview

Think about how people communicate at work. Sometimes you leave a note on a shared whiteboard for anyone who walks by to read — you do not care who reads it or when, you just put the information out there. Other times, you pick up the phone and ask a colleague a direct question: "What is the status of order 4217?" You need an answer before you can continue. And sometimes you delegate an entire task: "Ship this package to Berlin, and let me know when it arrives." You want updates along the way, and you want the ability to say "actually, cancel that" if plans change.

Robots have exactly the same three communication needs. A LiDAR sensor continuously publishes scan data for anyone who wants it — the path planner, the obstacle detector, the logger. A calibration routine needs to ask the map server for a specific region of the map and wait for the response before it can proceed. A navigation system needs to send the robot to a destination, receive progress updates ("3 meters remaining"), and cancel the trip if a higher-priority task arrives.

HORUS gives you one communication primitive for each of these patterns: Topics for continuous data streams, Services for quick request/response exchanges, and Actions for long-running tasks with feedback and cancellation. Every robotics communication problem maps to one of these three. The rest of this page helps you understand what each one does and when to pick which.

The Three Primitives

Topics — Continuous Data Streams

A topic is like a radio station. The broadcaster transmits whether anyone is listening or not. Listeners tune in when they want and tune out when they are done. The broadcaster does not know or care how many listeners exist — zero, one, or fifty.

In HORUS, a topic is a named channel that carries a specific type of data. Publishers call send() to put data on the channel. Subscribers call recv() to read the latest value. The publisher and subscriber do not need to know about each other. They just agree on a topic name and a data type.

// simplified
use horus::prelude::*;

// Publisher
let pub_topic: Topic<f32> = Topic::new("sensor.temperature")?;
pub_topic.send(25.3);

// Subscriber (can be in a different node, process, or machine)
let sub_topic: Topic<f32> = Topic::new("sensor.temperature")?;
if let Some(temp) = sub_topic.recv() {
    hlog!(info, "Temperature: {}", temp);
}

Topics are the workhorse of robotics communication. Sensor data, motor commands, camera frames, diagnostic status, emergency stops — all flow through topics. They are fire-and-forget: the publisher sends and moves on. There is no acknowledgment, no reply, no waiting.

Latency: ~3 ns (same thread) to ~167 ns (cross-process), depending on topology. See Topics — Full Reference for the 10 automatic backend paths.

Use topics when:

  • Data is produced continuously (sensor readings, motor commands, video frames)
  • Multiple consumers might want the same data (logger + planner + display)
  • You care about the latest value, not every historical value
  • Speed matters more than guaranteed delivery

Services — Request/Response

A service is like a phone call. You dial, ask a question, wait for an answer, and hang up. It is synchronous — the caller blocks until the response arrives (or a timeout fires). There is exactly one server and one or more clients.

// simplified
use horus::prelude::*;

// Client asks for map data — blocks until response arrives
let mut client = ServiceClient::<GetMapRegion>::new()?;
let response = client.call(
    GetMapRegionRequest { x_min: 0.0, y_min: 0.0, x_max: 10.0, y_max: 10.0, resolution: 0.05 },
    Duration::from_secs(1),
)?;
println!("Map: {}x{} pixels", response.width, response.height);

Services are for operations that complete quickly (milliseconds) and where the caller cannot continue without the result. Parameter queries, configuration lookups, one-shot commands, joint limit checks — these are all service calls.

Use services when:

  • You need a response before you can continue
  • The operation finishes quickly (milliseconds, not seconds)
  • You need error reporting back to the caller
  • There is a clear question-and-answer pattern

The Services API is currently Rust-only. Python bindings are not yet available. Under the hood, services use two topics ({name}.request and {name}.response) for bidirectional communication — they inherit all of topics' shared-memory performance.

Actions — Long-Running Tasks

An action is like delegating a task to a colleague. You say "navigate to shelf B3," and they go do it. While they work, they send you progress updates: "12 meters remaining... 8 meters remaining... arrived." If plans change, you can say "cancel, new priority" and they stop what they are doing.

Action pattern: goal → progress feedback → cancel/complete

Actions follow the Goal / Feedback / Result pattern. The client sends a goal, the server sends periodic feedback, and eventually delivers a final result (succeeded, failed, or canceled). Actions support cancellation, preemption (a higher-priority goal interrupts the current one), and timeout.

Use actions when:

  • The task takes more than one tick to complete (navigation, arm motion, calibration)
  • You need progress updates while the task runs
  • You need to cancel or preempt in-flight tasks
  • You need to know whether the task succeeded, failed, or was interrupted

The Actions API is currently Rust-only. Python bindings are not yet available. Like services, actions use topics internally for goal submission, feedback streaming, and result delivery.

Choosing the Right Primitive

When you are not sure which primitive to use, walk through this decision flowchart:

Decision flowchart: continuous data goes to Topics, quick Q&A goes to Services, long tasks go to Actions

Decision Table

ScenarioUseWhy
Sensor data streaming (IMU, LiDAR, encoders)TopicContinuous, latest value matters, multiple consumers
Motor velocity commandsTopicHigh frequency, low latency, fire-and-forget
Camera frames at 30 FPSTopicStreaming, pool-backed zero-copy for large data
Emergency stop signalTopicMust be instantaneous, no handshake overhead
Diagnostics broadcastTopicMany publishers, flexible topology
Query joint limits before planningServiceNeed response before continuing, completes in ms
Fetch a map region for SLAMServiceNeed specific data, cannot proceed without it
Trigger a one-shot sensor calibrationServiceQuick operation, need success/failure result
Get current parameter valuesServiceDirect question, direct answer
Navigate to a waypointActionTakes seconds, needs progress updates, cancellable
Pick-and-place an objectActionMulti-phase, needs feedback per phase
Calibration routine (full sequence)ActionTakes time, reports progress, might fail
Dock at a charging stationActionLong-running, needs alignment feedback

Quick Rules of Thumb

  • If it is a stream of data flowing continuously, use a Topic.
  • If it is a question you need answered before you can continue, use a Service.
  • If it is a task you would start, monitor, and potentially cancel, use an Action.
  • When in doubt, start with a Topic. It is the simplest primitive and covers the vast majority of robotics communication.

How They Work Together

Real robot systems use all three primitives together. A navigation system subscribes to sensor topics (LiDAR, odometry), exposes an action server for goal-based navigation, and calls a service to fetch map data on demand:

// simplified
struct NavigationSystem {
    // Topics for streaming sensor data (pub/sub)
    lidar_sub: Topic<LaserScan>,
    odom_sub: Topic<Odometry>,

    // Topics for publishing commands (pub/sub)
    cmd_vel_pub: Topic<CmdVel>,
    status_pub: Topic<DiagnosticStatus>,

    // Action server for goal-based navigation (goal/feedback/result)
    nav_server: ActionServerNode<NavigateToGoal>,

    // Service client for on-demand map queries (request/response)
    map_client: ServiceClient<GetMapRegion>,
}

The navigation action server subscribes to sensor topics internally, calls the map service when it needs terrain data, computes a path, publishes velocity commands via topics, and reports progress back to the action client. All three primitives use the same underlying shared-memory infrastructure, so mixing them has zero additional overhead.

A typical navigation system combines Topics (sensor data, motor commands), Services (map queries), and Actions (goal-based navigation)

Design Decisions

Why three primitives and not just one? You could theoretically build everything with topics alone — request/response by publishing a request on one topic and listening for a response on another, long-running tasks by publishing goals and feedback on separate topics. ROS1 actually started with mostly topics and bolted on services and actions later. The problem is that reimplementing request correlation, timeout handling, cancellation, and progress tracking on top of raw pub/sub is error-prone and duplicated across every project. Three distinct primitives with clear semantics eliminate that boilerplate and make the developer's intent explicit in the code.

Why not more primitives? Some frameworks add parameter servers, shared blackboards, or distributed state stores as separate communication channels. HORUS keeps it to three because every additional primitive adds cognitive load — developers must learn when to use each one. Parameters can be modeled as services (GetParameter / SetParameter). Shared state can be modeled as topics with the latest-value semantic. Three covers the full space without redundancy.

Why are Services and Actions built on Topics internally? Services use two internal topics ({name}.request and {name}.response). Actions use topics for goal submission, feedback, and results. This is not an implementation shortcut — it is a deliberate design choice. Topics already handle all the hard problems: cross-process shared memory, automatic backend selection, live migration, zero-copy for large data. Building services and actions on top means they inherit all of that infrastructure for free. It also means a single debugging tool (horus topic list) shows all communication in the system, including service and action traffic.

Why poll-based services instead of callback-based? The service server polls for incoming requests on a configurable interval (default: 5 ms). An alternative would be to wake the server thread immediately when a request arrives (interrupt-driven). HORUS uses polling because it is simpler to reason about in a real-time context: the server thread wakes at predictable intervals, does bounded work, and sleeps. Interrupt-driven wakeups create unpredictable timing spikes. The 5 ms default poll interval means worst-case service latency is 5 ms — fast enough for the configuration-query use cases services are designed for. If you need sub-millisecond response, use a topic.

Trade-offs

GainCost
Three clear primitives — every communication pattern maps to exactly oneDevelopers must learn which primitive to use (this page helps)
Topics are fire-and-forget — zero overhead, no waiting for consumersNo delivery guarantee; slow subscribers lose messages
Services give guaranteed responses — caller blocks until answer arrivesBlocking means the caller's tick stalls if the server is slow or down
Actions support cancellation and feedback — full task lifecycle managementMore complex API surface than topics or services
All built on shared memory — same zero-copy infrastructure everywhereServices and actions inherit topic limitations (e.g., ring buffer overflow on extreme load)
Services are poll-based — predictable timing, no interrupt spikesWorst-case 5 ms latency on default poll interval (configurable)

See Also