horus_macros
Procedural macros for reducing boilerplate in HORUS applications.
use horus::prelude::*; // Includes all macros
node!
Declarative macro for creating HORUS nodes with minimal boilerplate.
Syntax
node! {
NodeName {
name: "custom_name", // Custom node name (optional)
rate 100.0 // Tick rate in Hz (optional)
pub { ... } // Publishers (optional)
sub { ... } // Subscribers (optional)
data { ... } // Internal state (optional)
tick { ... } // Main loop (required)
init { ... } // Initialization (optional)
shutdown { ... } // Cleanup (optional)
impl { ... } // Custom methods (optional)
}
}
Only the node name and tick are required. Everything else is optional.
Sections
pub - Publishers
Define topics this node publishes to.
pub {
// Syntax: name: Type -> "topic_name"
velocity: f32 -> "robot.velocity",
status: String -> "robot.status",
pose: Pose2D -> "robot.pose"
}
Generated code:
Topic<Type>field for each publisher- Automatic initialization in
new()
sub - Subscribers
Define topics this node subscribes to.
sub {
// Syntax: name: Type -> "topic_name"
commands: String -> "user.commands",
sensors: f32 -> "sensors.temperature"
}
Generated code:
Topic<Type>field for each subscriber- Automatic initialization in
new()
data - Internal State
Define internal fields with default values.
data {
counter: u32 = 0,
buffer: Vec<f32> = Vec::new(),
last_time: Instant = Instant::now(),
config: MyConfig = MyConfig::default()
}
tick - Main Loop
Required. Called every scheduler cycle (~100 Hz by default).
tick {
// Read from subscribers
if let Some(cmd) = self.commands.recv() {
// Process
}
// Write to publishers
self.velocity.send(1.0);
// Access internal state
self.counter += 1;
}
init - Initialization
Called once before the first tick. The block must return Ok(()) on success (it generates fn init(&mut self) -> Result<()>).
init {
hlog!(info, "Node starting");
self.buffer.reserve(1000);
Ok(())
}
shutdown - Cleanup
Called once when the scheduler stops. Must return Ok(()) on success (generates fn shutdown(&mut self) -> Result<()>).
shutdown {
hlog!(info, "Node stopping");
// Close files, save state, etc.
Ok(())
}
impl - Custom Methods
Add helper methods to the node.
impl {
fn calculate(&self, x: f32) -> f32 {
x * 2.0 + self.offset
}
fn reset(&mut self) {
self.counter = 0;
}
}
Generated Code
The macro generates:
pub struct NodeNamewithTopic<T>fields for publishers/subscribers and your data fieldsimpl NodeName { pub fn new() -> Self }constructor that creates all Topicsimpl Node for NodeNamewithname(),tick(), optionalinit(),shutdown(),publishers(),subscribers(), andrate()impl Default for NodeNamethat callsSelf::new()impl NodeName { ... }for any methods from theimplsection
// This macro call:
node! {
SensorNode {
pub { data: f32 -> "sensor" }
data { count: u32 = 0 }
tick { self.count += 1; }
}
}
// Generates approximately:
pub struct SensorNode {
data: Topic<f32>,
count: u32,
}
impl SensorNode {
pub fn new() -> Self {
Self {
data: Topic::new("sensor").expect("Failed to create publisher 'sensor'"),
count: 0,
}
}
}
impl Node for SensorNode {
fn name(&self) -> &str { "sensor_node" } // Auto snake_case
fn tick(&mut self) {
self.count += 1;
}
}
impl Default for SensorNode {
fn default() -> Self {
Self::new()
}
}
The struct name is converted to snake_case for the node name (e.g., SensorNode becomes "sensor_node"), unless overridden with name:.
Examples
Minimal Node
node! {
MinimalNode {
tick {
// Called every tick
}
}
}
Publisher Only
node! {
HeartbeatNode {
pub { alive: bool -> "system.heartbeat" }
data { count: u64 = 0 }
tick {
self.alive.send(true);
self.count += 1;
}
}
}
Subscriber Only
node! {
LoggerNode {
sub { messages: String -> "logs" }
tick {
while let Some(msg) = self.messages.recv() {
hlog!(info, "{}", msg);
}
}
}
}
Full Pipeline
node! {
ProcessorNode {
sub { input: f32 -> "raw_data" }
pub { output: f32 -> "processed_data" }
data {
scale: f32 = 2.0,
offset: f32 = 10.0
}
tick {
if let Some(value) = self.input.recv() {
let result = value * self.scale + self.offset;
self.output.send(result);
}
}
impl {
fn set_scale(&mut self, scale: f32) {
self.scale = scale;
}
}
}
}
With Lifecycle
node! {
StatefulNode {
pub { status: String -> "status" }
data {
initialized: bool = false,
tick_count: u64 = 0
}
init {
hlog!(info, "Initializing...");
self.initialized = true;
Ok(())
}
tick {
self.tick_count += 1;
let msg = format!("Tick {}", self.tick_count);
self.status.send(msg);
}
shutdown {
hlog!(info, "Total ticks: {}", self.tick_count);
Ok(())
}
}
}
Usage
use horus::prelude::*;
node! {
MyNode {
pub { output: f32 -> "data" }
tick {
self.output.send(42.0);
}
}
}
fn main() -> Result<()> {
let mut scheduler = Scheduler::new();
scheduler.add(MyNode::new()).order(0).build()?;
scheduler.run()
}
#[derive(LogSummary)]
Derive macro for implementing the LogSummary trait with default Debug formatting.
When to Use
Use #[derive(LogSummary)] when you need Topic::with_logging() on a custom message type. LogSummary is not required for basic Topic::new() — only for the opt-in introspection mode.
The derive requires Debug on the type since it generates a Debug-based implementation.
use horus::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, LogSummary)]
pub struct MyStatus {
pub temperature: f32,
pub voltage: f32,
}
// Now you can use with_logging()
let topic: Topic<MyStatus> = Topic::new("status")?.with_logging();
The derive generates:
impl LogSummary for MyStatus {
fn log_summary(&self) -> String {
format!("{:?}", self)
}
}
Custom LogSummary
For large types (images, point clouds) where Debug output would be too verbose, implement LogSummary manually instead of deriving:
use horus::prelude::*;
impl LogSummary for MyLargeData {
fn log_summary(&self) -> String {
format!("MyLargeData({}x{}, {} bytes)", self.width, self.height, self.data.len())
}
}
Best Practices
Keep tick Fast
// Good - non-blocking
tick {
if let Some(x) = self.input.recv() {
self.output.send(x * 2.0);
}
}
// Bad - blocking operation
tick {
std::thread::sleep(Duration::from_secs(1)); // Blocks scheduler!
}
Pre-allocate in init
init {
self.buffer.reserve(1000); // Do once
Ok(())
}
tick {
// Don't allocate here - runs every tick
}
Use Descriptive Names
// Good
pub { motor_velocity: f32 -> "motors.velocity" }
// Bad
pub { x: f32 -> "data" }
Handle Errors Gracefully
tick {
// send() is infallible — always succeeds
self.status.send("ok".to_string());
// No error handling needed — ring buffer overwrites oldest on full
self.critical.send(data);
}
Troubleshooting
"Cannot find type in scope"
Import message types:
use horus::prelude::*;
node! {
MyNode {
pub { cmd: CmdVel -> "cmd_vel" }
tick { }
}
}
"Expected ,, found {"
Check arrow syntax:
// Wrong
pub { cmd: f32 "topic" }
// Correct
pub { cmd: f32 -> "topic" }
"Node name must be CamelCase"
// Wrong
node! { my_node { ... } }
// Correct
node! { MyNode { ... } }
Use hlog! for logging
tick {
// Use hlog! macro for logging
hlog!(info, "test");
hlog!(debug, "value = {}", some_value);
hlog!(warn, "potential issue");
hlog!(error, "something went wrong");
}
Logging Macros
hlog!
Node-aware logging that publishes to the shared memory log buffer (visible in monitor) and emits to stderr with ANSI colors.
hlog!(info, "Sensor initialized");
hlog!(debug, "Value: {}", some_value);
hlog!(warn, "Battery low: {}%", battery_pct);
hlog!(error, "Failed to read sensor: {}", err);
Levels: trace, debug, info, warn, error
The scheduler automatically sets the current node context, so log messages include the node name:
[INFO] [SensorNode] Sensor initialized
hlog_once!
Log a message once per callsite. Subsequent calls from the same source location are silently ignored. Uses a per-callsite AtomicBool — zero overhead after the first call.
fn tick(&mut self) {
// Log when sensor first produces valid data
hlog_once!(info, "Sensor online — first reading: {:.2}", value);
// Warn about a condition the first time it's detected
if self.error_count > 0 {
hlog_once!(warn, "Sensor errors detected — check wiring");
}
}
Common uses: first-connection notifications, one-time calibration messages, feature availability checks at startup.
hlog_every!
Throttled logging — emits at most once per interval_ms milliseconds. Uses a per-callsite AtomicU64 timestamp — zero overhead when the interval hasn't elapsed. Essential for nodes running at high frequencies (100Hz+) where per-tick logging would flood the system.
fn tick(&mut self) {
// Status heartbeat every 5 seconds
hlog_every!(5000, info, "Motor controller OK — speed: {:.1} rad/s", self.velocity);
// Battery warnings every second (not every tick at 1kHz)
if self.battery_pct < 20.0 {
hlog_every!(1000, warn, "Battery low: {:.0}%", self.battery_pct);
}
// Periodic performance stats every 10 seconds
hlog_every!(10_000, debug, "Avg latency: {:.1}us, ticks: {}", self.avg_latency_us, self.tick_count);
}
message!
Declarative macro for defining custom message types for use with Topic<T>. Auto-derives all required traits so messages work with zero configuration.
Syntax
use horus::prelude::*;
message! {
/// Motor command sent to actuators
MotorCommand {
velocity: f32,
torque: f32,
}
}
// Ready to use with Topic
let topic: Topic<MotorCommand> = Topic::new("motor.cmd")?;
topic.send(MotorCommand { velocity: 1.0, torque: 0.5 });
Multiple Messages
Define multiple message types in a single block:
message! {
/// Velocity command
CmdVel {
linear_x: f64,
angular_z: f64,
}
/// Battery status
BatteryStatus {
voltage: f32,
current: f32,
percentage: f32,
}
}
Generated Code
For message! { Foo { x: f32, y: f32 } }, the macro generates:
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Foo {
pub x: f32,
pub y: f32,
}
impl LogSummary for Foo {
fn log_summary(&self) -> String {
format!("{:?}", self)
}
}
The struct automatically satisfies TopicMessage via the blanket impl — no additional trait implementation is needed.
When to Use
Use message! when HORUS's standard message types don't cover your domain. For standard robotics types (Twist, Pose2D, Imu, etc.), use the prelude types instead.
action! and service! Macros
See Actions and Services for the action! and service! macros that generate typed communication patterns.
See Also
- node! Macro Guide - Detailed tutorial
- API Reference - Core types reference
- Actions - action! macro reference
- Services - service! macro reference