Message Types
HORUS provides a comprehensive library of standard message types for robotics. All built-in messages are designed for shared memory efficiency — most use fixed-size structures with zero-copy POD semantics.
Message Requirements
Any type used with Topic<T> must satisfy these trait bounds:
T: Clone + Send + Sync + Serialize + DeserializeOwned + 'static
A minimal custom message:
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyMessage {
pub value: f32,
pub timestamp_ns: u64,
}
For zero-copy performance, make your type POD — see POD Types for details.
Typed Messages vs Generic Messages
Typed Messages (Recommended)
Strongly-typed Rust structs — all available via use horus::prelude::*:
use horus::prelude::*;
let topic: Topic<Pose2D> = Topic::new("robot.pose")?;
topic.send(Pose2D::new(1.0, 2.0, 0.5));
from horus import Node, Pose2D
node = Node(
name="controller",
pubs={"robot.pose": {"type": Pose2D}}
)
node.send("robot.pose", Pose2D(x=1.0, y=2.0, theta=0.5))
Benefits:
- Ultra-fast: ~50-85ns IPC latency (zero-copy shared memory for POD types)
- Type safety: Compile-time checks prevent type mismatches
- IDE support: Autocomplete, type hints, inline documentation
- Cross-language: Rust and Python see the same typed data
Generic Messages (Prototyping)
Dynamic data for arbitrary structures using GenericMessage:
# Python - No type specified
node = Node(name="sensor", pubs=["custom_data"])
node.send("custom_data", {
"value": 42,
"notes": "testing new algorithm",
"measurements": [1.2, 3.4, 5.6]
})
use horus::prelude::*;
let topic: Topic<GenericMessage> = Topic::new("custom_data")?;
let data = GenericMessage::from_value(&my_dynamic_data)?;
topic.send(data);
GenericMessage uses MessagePack serialization with a 4KB maximum payload. It has an inline buffer for small messages (≤256 bytes) and an overflow buffer for larger ones.
Tradeoffs:
- Flexible — any data structure, evolving schemas
- Slower IPC — serialization overhead vs zero-copy POD types
- No compile-time type safety
Use generic messages for quick prototypes, external JSON integrations, or truly dynamic schemas. Default to typed messages for production code.
Performance Comparison
| Feature | Typed Messages | Generic Messages |
|---|---|---|
| IPC Latency | ~50-85ns (POD) | Higher (serialization) |
| Type Safety | Compile-time | Runtime only |
| IDE Support | Full autocomplete | None |
| Best For | Production | Prototyping |
LogSummary Trait
The LogSummary trait provides human-readable summaries for logging. It is a formatting trait only — it is not required by Topic::new(), and Topic<T> has no logging builder.
pub trait LogSummary {
fn log_summary(&self) -> String;
}
When is LogSummary Used?
Topic::new("name")?in Rust never callslog_summary()— the send/recv path carries no logging overhead- You call it yourself inside
hlog!, e.g.hlog!(info, "{}", msg.log_summary()) - Python nodes call it automatically: when a
Topicis owned by aNode,horus_pylogslog_summary()once per send and once per recv
Those logs appear in the console, in the shared memory ring buffer at /dev/shm/horus_<namespace>/logs, and in horus monitor.
Deriving LogSummary
For most types, derive the trait to get Debug formatting automatically:
use horus::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, LogSummary)]
pub struct RobotState {
pub position: [f64; 3],
pub velocity: f64,
pub battery_level: f32,
}
// log_summary() outputs: RobotState { position: [1.0, 2.0, 0.0], velocity: 1.5, battery_level: 0.85 }
Custom Implementation for Large Types
For types where Debug output would be too large (images, point clouds, scans), implement LogSummary manually:
use horus::prelude::*;
impl LogSummary for MyLargeMessage {
fn log_summary(&self) -> String {
format!("MyMsg({} items, {:.2}MB)", self.count, self.size_mb())
}
}
Guidelines:
- Keep summaries concise — they appear inline in logs
- Include units (meters, rad/s, %) to make values unambiguous
- Log metadata about the message, not the full content
Built-in LogSummary Implementations
LogSummary is implemented for, among others:
- Primitive types:
f32,f64,i32,i64,u32,u64,usize,bool,String - Messages:
CmdVel,CompressedImage,CameraInfo,RegionOfInterest,StereoInfo,NavSatFix,GenericMessage,Detection,Detection3D,BoundingBox2D,BoundingBox3D,JointState,TrackedObject,TrackingHeader,Landmark,Landmark3D,LandmarkArray,AudioFrame,PlaneDetection,PlaneArray,PointField - Descriptors:
ImageDescriptor,PointCloudDescriptor,DepthImageDescriptor,Tensor - Any type that derives
#[derive(LogSummary)](usesDebugformatting)
The standard message catalogue
HORUS ships around a hundred standard messages. The per-field reference lives with each language, not here — this page is about what a message is, and the reference pages are about what each one contains.
If you are looking for a field list, follow the link for your language. Those
pages are the ones kept in step with the message definitions; a second copy here
would be a second thing to keep in step, and it was — BatteryState.charge read
"Remaining charge in Ah" on this page against "Charge in amp-hours (NaN if
unknown)" in the Rust reference. The reference is right: the field defaults to
f32::NAN, so an unknown charge is not a measurement of zero.
| Category | What is in it | Rust | Python | C++ |
|---|---|---|---|---|
| Geometry | Velocities, poses, transforms, vectors and quaternions | Geometry | Geometry | Geometry |
| Sensor | Lidar, IMU, odometry, GPS, range, battery | Sensor | Sensor | Sensor |
| Control | Motor, differential-drive, servo and joint commands, PID configuration, trajectory points | Control | Control | Control |
| Vision | Images, compressed images, camera and stereo calibration | Vision | Vision | — |
| Perception | Point clouds, depth images, detections, bounding boxes | Perception | Perception | Detection |
| Navigation | Goals, waypoints, paths, occupancy grids, cost maps | Navigation | Navigation | Navigation |
| Diagnostics | Heartbeats, diagnostic status, emergency stop, safety status, resource usage | Diagnostics | — | Diagnostics |
| Force and haptics | Wrenches, tactile arrays, impedance parameters, contact info | Force | Force/Tactile | Force |
| Tracking and landmarks | Tracked objects, keypoints, segmentation masks | — | — | Tracking |
| Tensors | Raw zero-copy numeric buffers for ML | Tensor, ML | ML utilities | — |
A dash means that language has no separate page for the category yet, not that
the messages are unavailable: every standard message crosses the language
boundary unchanged. Multi-language explains how, and
rust/api/messages is the fullest single list.
Two families, and why the difference is visible
Every message in the catalogue is one of two kinds, and which one it is determines what the runtime can do with it:
- POD — a fixed-size
#[repr(C)]value. It is written straight into shared memory and read back with no serialization at all, so publishing costs a memcpy and the cost does not depend on how busy the topic is. Most of the catalogue is POD. - Serialized — a variable-size value (anything holding a
Vec, aStringor a map). It is encoded on publish and decoded on receive.TactileArrayandPointCloud's owning forms are the common examples.
The distinction is the same in every language: a POD message in Rust is a POD message in Python and in C++, because all three bind the same memory layout — that is what makes zero-copy across languages possible. Each reference page marks which family a message belongs to.
Custom Messages
Defining your own message is a per-language task, and each language has its own
page for it: Custom Messages (Rust),
Custom Messages (Python), and the C++ side of
horus msg, which generates the C++ struct from
the same .hmsg definition. What follows is the shape of the idea, written in
Rust; the requirements above — a stable layout, or serialization — are what all
three have in common.
Basic Custom Message
use serde::{Serialize, Deserialize};
use horus::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RobotStatus {
pub battery_level: f32,
pub temperature: f32,
pub error_code: u32,
pub timestamp_ns: u64,
}
let topic: Topic<RobotStatus> = Topic::new("robot_status")?;
topic.send(RobotStatus {
battery_level: 75.0,
temperature: 42.0,
error_code: 0,
timestamp_ns: horus::time::now().as_nanos(),
});
POD Custom Message (Zero-Copy)
For maximum performance, make your type POD-compatible:
use horus::prelude::*;
use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct MotorFeedback {
pub timestamp_ns: u64,
pub motor_id: u32,
pub velocity: f32,
pub current_amps: f32,
pub temperature_c: f32,
}
unsafe impl Zeroable for MotorFeedback {}
unsafe impl Pod for MotorFeedback {}
Topic<T> detects POD layouts automatically (no destructor, and larger than one byte — bool and other single-byte types are deliberately excluded and take the serialized path) — there is no marker trait to implement in the prelude. The bytemuck impls above are optional: they are only needed if you also implement horus_core::communication::PodMessage for its as_bytes() / from_bytes() helpers.
See POD Types for full requirements.
Adding LogSummary
LogSummary is a Rust trait and a Rust derive macro; there is no Python or C++
equivalent to implement. Python and C++ nodes still benefit from it, because
the summary is what the logging pipeline records for a topic regardless of which
language published the message.
To give your message a compact one-line form for hlog! and the Python-side topic logging:
use horus::prelude::*;
// Option 1: Derive (uses Debug formatting)
#[derive(Debug, Clone, Serialize, Deserialize, LogSummary)]
pub struct SmallMessage { /* ... */ }
// Option 2: Manual (for large types)
impl LogSummary for LargeMessage {
fn log_summary(&self) -> String {
format!("LargeMsg({} items)", self.count)
}
}
Working with Messages in Nodes
Publishing
use horus::prelude::*;
struct LidarNode {
scan_pub: Topic<LaserScan>,
}
impl Node for LidarNode {
fn name(&self) -> &str { "LidarNode" }
fn tick(&mut self) {
let mut scan = LaserScan::new();
scan.ranges[0] = 5.2;
self.scan_pub.send(scan);
}
}
Subscribing
struct ObstacleDetector {
scan_sub: Topic<LaserScan>,
}
impl Node for ObstacleDetector {
fn name(&self) -> &str { "ObstacleDetector" }
fn tick(&mut self) {
if let Some(scan) = self.scan_sub.recv() {
if let Some(min_range) = scan.min_range() {
if min_range < 0.5 {
// Obstacle too close!
}
}
}
}
}
GenericMessage
GenericMessage is a dynamic, schema-less message type for situations where typed messages aren't practical — cross-language communication, prototyping, or flexible ML pipelines.
use horus::prelude::*;
// From any serializable value
let msg = GenericMessage::from_value(&serde_json::json!({
"detected": true,
"confidence": 0.95,
"label": "person",
}))?;
// Send via topic
let topic: Topic<GenericMessage> = Topic::new("detections")?;
topic.send(msg);
// Receive and deserialize
if let Some(msg) = topic.recv() {
let value: serde_json::Value = msg.to_value()?;
println!("Label: {}", value["label"]);
}
Key Methods
| Method | Description |
|---|---|
GenericMessage::new(data: Vec<u8>) | Create from raw bytes (max 4096 bytes) |
GenericMessage::from_value<T: Serialize>(value: &T) | Serialize any serde type |
GenericMessage::with_metadata(data, metadata) | Create with metadata string (max 255 bytes) |
msg.to_value<T: Deserialize>() | Deserialize to a typed value |
msg.data() | Get raw payload bytes |
msg.metadata() | Get metadata string if present |
Performance
- Small messages (≤256 bytes): ~4.0 µs (inline fast path)
- Large messages (>256 bytes): ~4.4 µs (overflow buffer)
- Maximum payload: 4096 bytes
- Uses zero-copy IPC for transport
When to Use
- Cross-language communication — Python and Rust nodes sharing untyped data
- Prototyping — Quick iteration before defining typed messages
- ML pipelines — Flexible model outputs with varying schemas
- Metadata tagging — Attach routing or context info via the metadata field
For production code with known schemas, prefer typed messages: they give compile-time safety, and a fixed-size typed message goes through the zero-copy path with no serialization at all, where a generic one is encoded to MessagePack on every send. (No benchmark quantifies the gap between the two, so no ratio is quoted here.)
See Also
- POD Types — Zero-serialization for maximum performance
- Tensor Messages — Tensor, Device, TensorDtype, and tensor domain types
- TensorPool API — Tensor memory management and auto-managed pools
- Topic — The unified communication API
- Basic Examples — Working examples with messages
- Architecture — How messages fit into HORUS