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 Topic, Pose2D

topic = Topic(Pose2D)
topic.send(Pose2D(x=1.0, y=2.0, theta=0.5))

Benefits:

  • Ultra-fast: ~50-167ns 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:

from horus import Topic

# Generic topic — pass a string name for untyped data
topic = Topic("custom_data")
topic.send({
    "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-167ns (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 not required for basic Topic::new() usage, but is required if you want logging via Topic::with_logging().

pub trait LogSummary {
    fn log_summary(&self) -> String;
}

When is LogSummary Used?

  • Topic::new("name")? — no LogSummary required, no logging overhead
  • Topic::new("name")?.with_logging() — requires T: LogSummary, enables logging on send/recv

When logging is active, log_summary() is called once per send/recv. Logs appear in the console 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:

  • Primitive types: f32, f64, i32, i64, u32, u64, usize, bool, String
  • Messages: CmdVel, CompressedImage, CameraInfo, RegionOfInterest, StereoInfo, NavSatFix, GenericMessage
  • Descriptors: ImageDescriptor, PointCloudDescriptor, DepthImageDescriptor, Tensor
  • Any type that derives #[derive(LogSummary)] (uses Debug formatting)

Geometry Messages

Spatial primitives for position, orientation, and motion. All are POD types.

CmdVel

Basic 2D velocity command:

use horus::prelude::*;

let cmd = CmdVel::new(1.0, 0.5);  // 1.0 m/s forward, 0.5 rad/s rotation
let stop = CmdVel::zero();
let cmd = CmdVel::with_timestamp(1.0, 0.5, 123456789);
FieldTypeDescription
stamp_nanosu64Timestamp in nanoseconds
linearf32Forward velocity in m/s
angularf32Rotation velocity in rad/s

Twist

3D velocity with linear and angular components:

use horus::prelude::*;

// 2D twist (common for mobile robots)
let cmd = Twist::new_2d(1.0, 0.5);  // 1.0 m/s forward, 0.5 rad/s rotation

// 3D twist
let cmd_3d = Twist::new(
    [1.0, 0.5, 0.0],      // Linear velocity [x, y, z] m/s
    [0.0, 0.0, 0.5]       // Angular velocity [roll, pitch, yaw] rad/s
);

let stop = Twist::stop();
assert!(cmd.is_valid());
FieldTypeDescription
linear[f64; 3]Linear velocity in m/s
angular[f64; 3]Angular velocity in rad/s
timestamp_nsu64Nanoseconds since epoch

Pose2D

2D position and orientation for planar robots:

use horus::prelude::*;

let pose = Pose2D::new(1.0, 2.0, 0.5);  // x=1m, y=2m, theta=0.5rad
let origin = Pose2D::origin();
let distance = pose.distance_to(&origin);

// Normalize angle to [-π, π]
let mut pose = Pose2D::new(1.0, 2.0, 3.5);
pose.normalize_angle();
FieldTypeDescription
xf64X position in meters
yf64Y position in meters
thetaf64Orientation in radians
timestamp_nsu64Nanoseconds since epoch

TransformStamped

3D transformation with translation and rotation:

use horus::prelude::*;

let identity = TransformStamped::identity();
let tf = TransformStamped::new(
    [1.0, 2.0, 3.0],           // Translation [x, y, z]
    [0.0, 0.0, 0.0, 1.0]       // Rotation quaternion [x, y, z, w]
);

// From 2D pose
let pose2d = Pose2D::new(1.0, 2.0, 0.5);
let tf = TransformStamped::from_pose_2d(&pose2d);

// Normalize quaternion
let mut tf = tf;
tf.normalize_rotation();
FieldTypeDescription
translation[f64; 3]Position in meters
rotation[f64; 4]Quaternion [x, y, z, w]
timestamp_nsu64Nanoseconds since epoch

Point3, Vector3, Quaternion

3D points, vectors, and rotations:

use horus::prelude::*;

// Point
let point = Point3::new(1.0, 2.0, 3.0);
let distance = point.distance_to(&Point3::origin());

// Vector with operations
let vec = Vector3::new(1.0, 0.0, 0.0);
let magnitude = vec.magnitude();
let dot = vec.dot(&Vector3::new(0.0, 1.0, 0.0));
let cross = vec.cross(&Vector3::new(0.0, 1.0, 0.0));

// Quaternion
let q = Quaternion::identity();
let q = Quaternion::from_euler(0.0, 0.0, std::f64::consts::PI / 2.0);

Sensor Messages

Standard sensor data formats. All are POD types.

LaserScan

2D lidar scan data (up to 360 points):

use horus::prelude::*;

let mut scan = LaserScan::new();
scan.ranges[0] = 5.2;
scan.angle_min = -std::f32::consts::PI;
scan.angle_max = std::f32::consts::PI;
scan.range_min = 0.1;
scan.range_max = 30.0;
scan.angle_increment = std::f32::consts::PI / 180.0;

let angle = scan.angle_at(45);
if scan.is_range_valid(0) {
    println!("Range: {}m", scan.ranges[0]);
}
let valid = scan.valid_count();
if let Some(min) = scan.min_range() {
    println!("Closest: {}m", min);
}
FieldTypeDescription
ranges[f32; 360]Range readings in meters (0 = invalid)
angle_min / angle_maxf32Scan angle range in radians
range_min / range_maxf32Valid range limits in meters
angle_incrementf32Angular resolution in radians
time_incrementf32Time between measurements
scan_timef32Time to complete scan in seconds
timestamp_nsu64Nanoseconds since epoch

Imu

Inertial Measurement Unit data:

use horus::prelude::*;

let mut imu = Imu::new();

imu.set_orientation_from_euler(0.1, 0.2, 1.5);  // roll, pitch, yaw
imu.angular_velocity = [0.1, 0.2, 0.3];          // rad/s
imu.linear_acceleration = [0.0, 0.0, 9.81];      // m/s²

if imu.has_orientation() {
    let quat = imu.orientation;
}
assert!(imu.is_valid());
FieldTypeDescription
orientation[f64; 4]Quaternion [x, y, z, w]
orientation_covariance[f64; 9]3x3 covariance matrix
angular_velocity[f64; 3]Gyroscope data in rad/s
angular_velocity_covariance[f64; 9]3x3 covariance matrix
linear_acceleration[f64; 3]Accelerometer data in m/s²
linear_acceleration_covariance[f64; 9]3x3 covariance matrix
timestamp_nsu64Nanoseconds since epoch

Odometry

Combined pose and velocity from wheel encoders or visual odometry:

use horus::prelude::*;

let mut odom = Odometry::new();
odom.set_frames("odom", "base_link");

let pose = Pose2D::new(1.0, 2.0, 0.5);
let twist = Twist::new_2d(0.5, 0.2);
odom.update(pose, twist);
FieldTypeDescription
posePose2DCurrent position and orientation
twistTwistCurrent velocity
pose_covariance[f64; 36]6x6 covariance matrix
twist_covariance[f64; 36]6x6 covariance matrix
frame_id[u8; 32]Reference frame (e.g., "odom")
child_frame_id[u8; 32]Child frame (e.g., "base_link")
timestamp_nsu64Nanoseconds since epoch

Range

Single-point distance sensor (ultrasonic, infrared):

use horus::prelude::*;

let range = Range::new(Range::ULTRASONIC, 1.5);

let mut range = Range::new(Range::INFRARED, 0.8);
range.min_range = 0.02;
range.max_range = 4.0;
range.field_of_view = 0.1;
FieldTypeDescription
sensor_typeu8Range::ULTRASONIC (0) or Range::INFRARED (1)
field_of_viewf32Sensor FOV in radians
min_range / max_rangef32Valid range limits in meters
rangef32Distance reading in meters
timestamp_nsu64Nanoseconds since epoch

BatteryState

Battery status and charge information:

use horus::prelude::*;

let mut battery = BatteryState::new(12.6, 75.0);  // 12.6V, 75% charge
battery.current = -2.5;
battery.temperature = 28.5;
battery.power_supply_status = BatteryState::STATUS_DISCHARGING;

if battery.is_low(20.0) {
    println!("Battery low!");
}
if battery.is_critical() {  // Below 10%
    println!("Battery critical!");
}
if let Some(time_left) = battery.time_remaining() {
    println!("Time remaining: {}s", time_left);
}
FieldTypeDescription
voltagef32Battery voltage in volts
currentf32Current in amperes (negative = discharging)
chargef32Remaining charge in Ah
capacityf32Total capacity in Ah
percentagef32Charge percentage (0-100)
power_supply_statusu8STATUS_UNKNOWN (0), STATUS_CHARGING (1), STATUS_DISCHARGING (2), STATUS_FULL (3)
temperaturef32Temperature in °C
cell_voltages[f32; 16]Individual cell voltages
cell_countu8Number of cells
timestamp_nsu64Nanoseconds since epoch

GPS position data:

FieldTypeDescription
latitude / longitude / altitudef64WGS84 coordinates
position_covariance[f64; 9]3x3 covariance matrix
statusu8Fix status
satellites_visibleu16Number of satellites
hdop / vdopf32Dilution of precision
speed / headingf32Speed (m/s) and heading (rad)
timestamp_nsu64Nanoseconds since epoch

Control Messages

Actuator commands and control parameters. All are POD types.

MotorCommand

Direct motor control:

FieldTypeDescription
motor_idu32Motor identifier
modef32Control mode
targetf32Target value
max_velocity / max_accelerationf32Limits
feed_forwardf32Feed-forward term
enableu8Enable flag
timestamp_nsu64Nanoseconds since epoch

DifferentialDriveCommand

Differential drive control (left/right wheels):

FieldTypeDescription
left_velocity / right_velocityf32Wheel velocities in m/s
max_accelerationf32Acceleration limit
enableu8Enable flag
timestamp_nsu64Nanoseconds since epoch

ServoCommand

Servo position/velocity control:

FieldTypeDescription
servo_idu32Servo identifier
position / speedf32Target position and speed
enableu8Enable flag
timestamp_nsu64Nanoseconds since epoch

JointCommand

Multi-joint position/velocity/effort (up to 16 joints):

FieldTypeDescription
joint_names[[u8; 32]; 16]Joint names
joint_countu8Number of active joints
positions / velocities / efforts[f64; 16]Joint commands
modes[u8; 16]Control mode per joint
timestamp_nsu64Nanoseconds since epoch

PidConfig

PID controller parameters:

FieldTypeDescription
controller_idu32Controller identifier
kp / ki / kdf64PID gains
integral_limit / output_limitf64Limits
anti_windupu8Anti-windup flag
timestamp_nsu64Nanoseconds since epoch

TrajectoryPoint

Single point in a trajectory:

FieldTypeDescription
position / velocity / acceleration[f64; 3]3D motion
orientation[f64; 4]Quaternion [x, y, z, w]
angular_velocity[f64; 3]Angular velocity
time_from_startf64Time offset in seconds

Vision Messages

Image and camera data types.

Image

RAII image type with zero-copy shared memory backing. Pixel data lives in a TensorPool — only a lightweight descriptor is transmitted through topics.

use horus::prelude::*;

// Note: args are (height, width, encoding)
let mut img = Image::new(480, 640, ImageEncoding::Rgb8)?;
img.set_pixel(100, 200, &[255, 0, 0]);  // Set pixel at (x=100, y=200)
let pixels: &[u8] = img.data();         // Zero-copy access

Accessor methods:

MethodReturnsDescription
width()u32Image width in pixels
height()u32Image height in pixels
encoding()ImageEncodingPixel format
channels()u32Number of channels
step()u32Row stride in bytes
data() / data_mut()&[u8] / &mut [u8]Zero-copy pixel data
pixel(x, y)Option<&[u8]>Get a single pixel
set_pixel(x, y, val)&mut SelfSet a single pixel
copy_from(buf)&mut SelfCopy pixel data from buffer
fill(val)&mut SelfFill entire image
roi(x, y, w, h)Option<Vec<u8>>Extract region of interest
frame_id()&strCamera frame
timestamp_ns()u64Nanoseconds since epoch

Supported encodings: Mono8, Mono16, Rgb8, Bgr8, Rgba8, Bgra8, Yuv422, Mono32F, Rgb32F, BayerRggb8, Depth16

CompressedImage

JPEG/PNG compressed images (variable-size, not POD):

FieldTypeDescription
format[u8; 8]Compression format string
dataVec<u8>Compressed image data
width / heightu32Image dimensions
frame_id[u8; 32]Camera frame
timestamp_nsu64Nanoseconds since epoch

CameraInfo

Camera calibration parameters (POD type):

FieldTypeDescription
width / heightu32Image dimensions
distortion_model[u8; 16]Distortion model name
distortion_coefficients[f64; 8]Distortion coefficients
camera_matrix[f64; 9]3x3 intrinsic matrix
rectification_matrix[f64; 9]3x3 rectification matrix
projection_matrix[f64; 12]3x4 projection matrix
frame_id[u8; 32]Camera frame
timestamp_nsu64Nanoseconds since epoch

StereoInfo

Stereo camera parameters (POD type):

FieldTypeDescription
left_camera / right_cameraCameraInfoPer-camera calibration
baselinef64Baseline distance in meters
depth_scalef64Depth scaling factor

Methods: depth_from_disparity(), disparity_from_depth()


Tensor (Raw Zero-Copy)

For low-level tensor transport (ML inference, custom pipelines), Topic<Tensor> provides direct access to the zero-copy shared memory path. Only a lightweight descriptor is transmitted through topics while the actual data stays in a shared-memory TensorPool.

use horus::prelude::*;

let topic: Topic<Tensor> = Topic::new("camera.rgb")?;
let handle = topic.alloc_tensor(&[1080, 1920, 3], TensorDtype::U8, Device::cpu())?;
// ... fill pixels via handle.data_slice_mut() ...
topic.send_handle(&handle);

// Receiver gets a TensorHandle with raw shape/dtype access
if let Some(handle) = topic.recv_handle() {
    let tensor = handle.tensor();
    println!("Shape: {:?}, dtype: {:?}", tensor.shape(), tensor.dtype());
}

For most use cases, prefer the high-level domain types (Image, PointCloud, DepthImage) which use the same zero-copy tensor transport internally but provide domain-specific convenience methods like pixel(), point_at(), and get_depth().


Detection Messages

Object detection results. All are POD types.

BoundingBox2D / BoundingBox3D

2D and 3D bounding boxes:

TypeFields
BoundingBox2Dx, y, width, height (all f32)
BoundingBox3Dcx, cy, cz (center), length, width, height, roll, pitch, yaw (all f32)

Detection / Detection3D

2D and 3D object detections:

FieldTypeDescription
bboxBoundingBox2D or BoundingBox3DBounding box
confidencef32Detection confidence
class_idu32Class identifier
class_name[u8; 32]Class name string
instance_idu32Instance identifier

Detection3D also includes velocity: [f32; 3].


Perception Messages

3D perception data types.

PointCloud

RAII point cloud type with zero-copy shared memory backing. Point data lives in a TensorPool — only a lightweight descriptor is transmitted through topics.

use horus::prelude::*;

// Create XYZ cloud: (num_points, fields_per_point, dtype)
let cloud = PointCloud::new(1000, 3, TensorDtype::F32)?;  // 1000 XYZ points
let cloud = PointCloud::new(1000, 6, TensorDtype::F32)?;  // 1000 XYZRGB points

Accessor methods:

MethodReturnsDescription
point_count()u64Number of points
fields_per_point()u32Floats per point (3=XYZ, 4=XYZI, 6=XYZRGB)
dtype()TensorDtypeData type of components
is_xyz()boolWhether this is a plain XYZ cloud
has_intensity()boolWhether cloud has intensity
has_color()boolWhether cloud has color
data() / data_mut()&[u8] / &mut [u8]Zero-copy point data
point_at(idx)Option<&[u8]>Get the i-th point as bytes
extract_xyz()Option<Vec<[f32; 3]>>Extract all XYZ coordinates
copy_from(buf)&mut SelfCopy point data from buffer
frame_id()&strReference frame
timestamp_ns()u64Nanoseconds since epoch

Fixed-Size Point Types (POD)

For zero-copy point cloud processing:

TypeFieldsDescription
PointXYZx, y, z (f32)3D point
PointXYZRGBx, y, z (f32), r, g, b, a (u8)Colored point
PointXYZIx, y, z, intensity (f32)Point with intensity

DepthImage

RAII depth image type with zero-copy shared memory backing. Supports both F32 (meters) and U16 (millimeters) formats.

use horus::prelude::*;

let mut depth = DepthImage::new(480, 640, TensorDtype::F32)?;  // F32 meters
depth.set_depth(100, 200, 1.5);  // Set depth at (x=100, y=200)

Accessor methods:

MethodReturnsDescription
width() / height()u32Image dimensions
dtype()TensorDtypeData type (F32 or U16)
is_meters()boolWhether F32 depth in meters
is_millimeters()boolWhether U16 depth in mm
depth_scale()f32Depth scale factor
data() / data_mut()&[u8] / &mut [u8]Zero-copy depth data
get_depth(x, y)Option<f32>Get depth at pixel (meters)
set_depth(x, y, val)&mut SelfSet depth at pixel
get_depth_u16(x, y)Option<u16>Get raw U16 depth
depth_statistics()(f32, f32, f32)(min, max, mean) in meters
frame_id()&strReference frame
timestamp_ns()u64Nanoseconds since epoch

Path planning and navigation types.

Goal / GoalResult

Navigation goal and result (both POD):

use horus::prelude::*;

let goal = Goal::new(Pose2D::new(5.0, 3.0, 0.0), 0.1, 0.05);  // pose, position_tol, angle_tol
let goal = goal.with_timeout(30.0).with_priority(1);

if goal.is_reached(&current_pose) {
    println!("Goal reached!");
}

Goal fields: target_pose (Pose2D), tolerance_position, tolerance_angle, timeout_seconds (f64), priority (u8), goal_id (u32), timestamp_ns (u64)

GoalResult fields: goal_id (u32), status (u8), distance_to_goal (f64), eta_seconds (f64), progress (f32), error_message ([u8; 64]), timestamp_ns (u64)

GoalStatus values: Pending, Active, Succeeded, Aborted, Cancelled, Preempted, TimedOut

Waypoint / Path

Waypoints and paths (both POD):

use horus::prelude::*;

let wp = Waypoint::new(Pose2D::new(1.0, 2.0, 0.0));
let wp = wp.with_velocity(Twist::new_2d(0.5, 0.0)).with_stop();

let mut path = Path::new();
path.add_waypoint(wp);

Path holds up to 256 waypoints with fields for total_length, duration_seconds, frame_id, algorithm, and timestamp_ns.

PathPlan

Compact planned path (POD, fixed-size):

FieldTypeDescription
waypoint_data[f32; 768]256 waypoints × 3 floats (x, y, theta)
goal_pose[f32; 3]Target pose
waypoint_countu16Number of waypoints
timestamp_nsu64Nanoseconds since epoch

OccupancyGrid

2D occupancy map (variable-size, not POD — uses serialization):

let mut grid = OccupancyGrid::new(200, 200, 0.05, Pose2D::origin());  // 200x200, 5cm resolution
if let Some((gx, gy)) = grid.world_to_grid(1.0, 2.0) {
    grid.set_occupancy(gx, gy, 100);  // Mark occupied (grid coords)
}

// is_free/is_occupied take world coordinates directly
if grid.is_free(1.0, 2.0) { /* navigable */ }
if grid.is_occupied(1.0, 2.0) { /* blocked */ }

Values: -1 = unknown, 0 = free, 100 = occupied.

CostMap

Cost map for path planning (variable-size, not POD):

let costmap = CostMap::from_occupancy_grid(grid, 0.55);  // 55cm inflation radius
let cost = costmap.cost(1.0, 2.0);  // Get cost at world coordinates

VelocityObstacle / VelocityObstacles

For velocity obstacle-based collision avoidance (both POD). VelocityObstacles holds up to 32 velocity obstacles.


Diagnostics Messages

Health monitoring and safety. All are POD types.

Heartbeat

Node liveness signal:

FieldTypeDescription
node_name[u8; 32]Node name
node_idu32Node identifier
sequenceu64Sequence number
aliveu8Alive flag
uptimef64Uptime in seconds
timestamp_nsu64Nanoseconds since epoch

NodeHeartbeat

Detailed per-node health status:

FieldTypeDescription
state / healthu8Node state and health
tick_countu32Total ticks executed
target_rate / actual_ratef32Expected vs actual tick rate
error_countu32Error counter
last_tick_timestamp / heartbeat_timestampu64Timestamps

Status

General status report:

FieldTypeDescription
levelu8Severity level
codeu32Status code
message[u8; 128]Status message
component[u8; 32]Component name
timestamp_nsu64Nanoseconds since epoch

EmergencyStop

Emergency stop signal:

FieldTypeDescription
engagedu8E-stop engaged flag
reason[u8; 64]Reason string
source[u8; 32]Source of e-stop
auto_resetu8Auto-reset flag
timestamp_nsu64Nanoseconds since epoch

SafetyStatus

Safety system state:

FieldTypeDescription
enabled, estop_engaged, watchdog_ok, limits_ok, comms_oku8Status flags
modeu8Safety mode
fault_codeu32Fault code
timestamp_nsu64Nanoseconds since epoch

ResourceUsage

CPU/memory monitoring:

FieldTypeDescription
cpu_percent / memory_percent / disk_percentf32Usage percentages
memory_bytes / disk_bytesu64Usage in bytes
network_tx_bytes / network_rx_bytesu64Network traffic
temperaturef32Temperature in °C
thread_countu32Thread count
timestamp_nsu64Nanoseconds since epoch

DiagnosticValue / DiagnosticReport

Key-value diagnostics: DiagnosticValue holds a single key-value pair. DiagnosticReport groups up to 16 DiagnosticValue entries with a component name and severity level.


Force/Haptics Messages

Force sensing and haptic feedback. All are POD types.

MessageDescription
WrenchStampedForce/torque measurement with point of application
ImpedanceParametersImpedance control parameters (stiffness, damping, inertia)
ForceCommandForce control command with target force/torque
ContactInfoContact detection state, force, normal, and point
HapticFeedbackHaptic output command (vibration, force feedback)

Input Messages

Human input devices. All are POD types.

MessageDescription
JoystickInputGamepad/joystick state (buttons, axes, hats)
KeyboardInputKeyboard key events with modifier flags

Segmentation, Landmark, and Tracking Messages

Computer vision pipeline types. All are POD types.

MessageDescription
SegmentationMaskSemantic/instance/panoptic segmentation mask descriptor
Landmark / Landmark3D2D/3D keypoints with visibility
LandmarkArraySet of landmarks (supports COCO, MediaPipe Pose/Hand/Face presets)
TrackedObjectTracked object with bbox, velocity, age, and state
TrackingHeaderTracking frame header with active track count

Custom Messages

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: timestamp_now(),
});

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 {}
unsafe impl PodMessage for MotorFeedback {}

See POD Types for full requirements.

Adding LogSummary

To enable logging with Topic::with_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 for compile-time safety and ~50x lower serialization overhead.


See Also