Topics: How Nodes Talk
For the full reference with capacity tuning, performance optimization, and multi-process details, see Topics — Full Reference.
What is a Topic?
A topic is a named channel that carries one type of data between nodes. Think of it like a mailbox with a specific name:
"temperature"carriesf32temperature readings"camera.image"carries image frames"motor.command"carries velocity commands
Any node can publish (send) to a topic. Any node can subscribe (receive) from it. The topic handles all the memory management automatically.
Basic Usage
Publishing Data
use horus::prelude::*;
struct Thermometer {
publisher: Topic<f32>,
}
impl Node for Thermometer {
fn tick(&mut self) {
let temp = read_sensor(); // Your sensor code
self.publisher.send(temp); // Send to topic
}
}
Receiving Data
struct Display {
subscriber: Topic<f32>,
}
impl Node for Display {
fn tick(&mut self) {
if let Some(temp) = self.subscriber.recv() {
println!("Temperature: {:.1}", temp);
}
}
}
Both nodes create a Topic with the same name — HORUS connects them automatically.
In Python:
import horus
def sensor_tick(node):
node.send("temperature", 25.0)
def display_tick(node):
temp = node.recv("temperature") # Returns value or None
if temp is not None:
print(f"Temperature: {temp:.1f}")
sensor = horus.Node(name="Sensor", tick=sensor_tick, pubs=["temperature"])
display = horus.Node(name="Display", tick=display_tick, subs=["temperature"])
horus.run(sensor, display)
How It Works
Key facts:
- Latency: ~3ns (same thread) to ~167ns (cross-process)
- Zero-copy: Large data (images, point clouds) is shared, not copied
- Automatic: HORUS picks the fastest path based on where your nodes run
- One-to-many: Multiple subscribers can receive from the same topic
Creating Topics
Topics are created in your node's init() or constructor:
impl SensorNode {
fn new() -> Result<Self> {
Ok(Self {
// Same constructor for publishing AND subscribing
topic: Topic::new("sensor.data")?,
})
}
}
Topic names are simple strings. Convention: use dots for namespacing ("robot.sensors.imu"). Do not use slashes — they cause errors on macOS.
What Happens When There's No Subscriber?
send() always succeeds — even if nobody is listening. Data goes into shared memory and waits. When a subscriber connects later, it receives the latest value.
recv() returns None if no data has been published yet.
fn tick(&mut self) {
// Always safe — never blocks, never panics
self.publisher.send(42.0);
// Returns None if no data yet
match self.subscriber.recv() {
Some(value) => println!("Got: {}", value),
None => {} // Nothing published yet, that's fine
}
}
Key Takeaways
- A topic = named channel for one data type
send()to publish,recv()to subscribe- Same topic name = automatic connection
- Latency is ~87ns on average — fast enough for any control loop
send()never fails,recv()returnsNoneif no data
Next Steps
- Nodes: The Building Blocks — what nodes are and how they work
- Scheduler: Running Your Nodes — execution order and timing
- Quick Start — build a working example with topics
- Topics — Full Reference — capacity, performance, multi-process details