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

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

FeatureTyped MessagesGeneric Messages
IPC Latency~50-85ns (POD)Higher (serialization)
Type SafetyCompile-timeRuntime only
IDE SupportFull autocompleteNone
Best ForProductionPrototyping

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 calls log_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 Topic is owned by a Node, horus_py logs log_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)] (uses Debug formatting)

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.

CategoryWhat is in itRustPythonC++
GeometryVelocities, poses, transforms, vectors and quaternionsGeometryGeometryGeometry
SensorLidar, IMU, odometry, GPS, range, batterySensorSensorSensor
ControlMotor, differential-drive, servo and joint commands, PID configuration, trajectory pointsControlControlControl
VisionImages, compressed images, camera and stereo calibrationVisionVision
PerceptionPoint clouds, depth images, detections, bounding boxesPerceptionPerceptionDetection
NavigationGoals, waypoints, paths, occupancy grids, cost mapsNavigationNavigationNavigation
DiagnosticsHeartbeats, diagnostic status, emergency stop, safety status, resource usageDiagnosticsDiagnostics
Force and hapticsWrenches, tactile arrays, impedance parameters, contact infoForceForce/TactileForce
Tracking and landmarksTracked objects, keypoints, segmentation masksTracking
TensorsRaw zero-copy numeric buffers for MLTensor, MLML 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, a String or a map). It is encoded on publish and decoded on receive. TactileArray and PointCloud'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

MethodDescription
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