The node! Macro
The problem: Writing HORUS nodes manually requires lots of boilerplate code.
The solution: The node! macro generates all the boilerplate for you!
Why Use It?
Without the macro (47 lines):
struct SensorNode {
publisher: Hub<f32>,
counter: u32,
}
impl SensorNode {
fn new() -> HorusResult<Self> {
Ok(Self {
publisher: Hub::new("temperature")?,
counter: 0,
})
}
}
impl Node for SensorNode {
fn name(&self) -> &'static str {
"SensorNode"
}
fn tick(&mut self, ctx: Option<&mut NodeInfo>) {
let temp = 20.0 + (self.counter as f32 * 0.1);
self.publisher.send(temp, ctx).ok();
self.counter += 1;
}
}
impl Default for SensorNode {
fn default() -> Self {
Self::new().expect("Failed to create SensorNode")
}
}
With the macro (13 lines):
node! {
SensorNode {
pub { temperature: f32 -> "temperature" }
data { counter: u32 = 0 }
tick(ctx) {
let temp = 20.0 + (self.counter as f32 * 0.1);
self.temperature.send(temp, ctx).ok();
self.counter += 1;
}
}
}
73% less code! And it's easier to read.
Basic Syntax
node! {
NodeName {
pub { ... } // Publishers (optional)
sub { ... } // Subscribers (optional)
data { ... } // Internal state (optional)
tick(ctx) { ... } // Main loop (required)
init(ctx) { ... } // Startup (optional)
shutdown(ctx) { ... } // Cleanup (optional)
impl { ... } // Custom methods (optional)
}
}
Only the node name and tick are required! Everything else is optional.
Sections Explained
pub - Send Messages
Define what this node sends:
pub {
// Syntax: name: Type -> "topic"
velocity: f32 -> "robot/velocity",
status: String -> "robot/status"
}
This creates:
- A
Hub<f32>field calledvelocity - A
Hub<String>field calledstatus - Both connected to their respective topics
sub - Receive Messages
Define what this node receives:
sub {
// Syntax: name: Type <- "topic"
commands: String <- "user/commands",
sensors: f32 <- "sensors/temperature"
}
This creates:
- A
Hub<String>field calledcommands - A
Hub<f32>field calledsensors - Both listening to their respective topics
data - Internal State
Store data inside your node:
data {
counter: u32 = 0,
buffer: Vec<f32> = Vec::new(),
last_time: Instant = Instant::now()
}
Access these as self.counter, self.buffer, etc.
tick(ctx) - Main Loop
This runs repeatedly (about 60 times per second):
tick(ctx) {
// Read inputs
if let Some(cmd) = self.commands.recv(ctx) {
// Process
let result = process(cmd);
// Send outputs
self.status.send(result, ctx).ok();
}
// Update state
self.counter += 1;
}
Keep this fast! It runs every frame.
init(ctx) - Startup (Optional)
Runs once when your node starts:
init(ctx) {
ctx.log_info("Starting up");
self.buffer.reserve(1000); // Pre-allocate
}
Use this for:
- Opening files/connections
- Pre-allocating memory
- One-time setup
shutdown(ctx) - Cleanup (Optional)
Runs once when your node stops:
shutdown(ctx) {
ctx.log_info(&format!("Processed {} messages", self.counter));
// Save state, close files, etc.
}
impl - Custom Methods (Optional)
Add helper functions:
impl {
fn calculate(&self, x: f32) -> f32 {
x * 2.0 + self.counter as f32
}
fn reset(&mut self) {
self.counter = 0;
}
}
Complete Examples
Simple Publisher
node! {
HeartbeatNode {
pub { alive: bool -> "system/heartbeat" }
tick(ctx) {
self.alive.send(true, ctx).ok();
}
}
}
Simple Subscriber
node! {
LoggerNode {
sub { messages: String <- "logs" }
tick(ctx) {
if let Some(msg) = self.messages.recv(ctx) {
println!("[LOG] {}", msg);
}
}
}
}
Pipeline (Sub + Pub)
node! {
DoubleNode {
sub { input: f32 <- "numbers" }
pub { output: f32 -> "doubled" }
tick(ctx) {
if let Some(num) = self.input.recv(ctx) {
self.output.send(num * 2.0, ctx).ok();
}
}
}
}
With State
node! {
AverageNode {
sub { input: f32 <- "values" }
pub { output: f32 -> "average" }
data {
buffer: Vec<f32> = Vec::new(),
max_size: usize = 10
}
tick(ctx) {
if let Some(value) = self.input.recv(ctx) {
self.buffer.push(value);
// Keep only last 10 values
if self.buffer.len() > self.max_size {
self.buffer.remove(0);
}
// Calculate average
let avg: f32 = self.buffer.iter().sum::<f32>() / self.buffer.len() as f32;
self.output.send(avg, ctx).ok();
}
}
}
}
With Lifecycle
node! {
FileLoggerNode {
sub { data: String <- "logs" }
data { file: Option<std::fs::File> = None }
init(ctx) {
use std::fs::OpenOptions;
self.file = OpenOptions::new()
.create(true)
.append(true)
.open("log.txt")
.ok();
ctx.log_info("File opened");
}
tick(ctx) {
if let Some(msg) = self.data.recv(ctx) {
if let Some(file) = &mut self.file {
use std::io::Write;
writeln!(file, "{}", msg).ok();
}
}
}
shutdown(ctx) {
ctx.log_info("Closing file");
self.file = None; // Closes the file
}
}
}
Tips and Tricks
Use Descriptive Names
// Good
pub { motor_speed: f32 -> "motors/speed" }
// Bad
pub { x: f32 -> "data" }
Keep tick() Fast
// Good - quick operation
tick(ctx) {
if let Some(x) = self.input.recv(ctx) {
let y = x * 2.0;
self.output.send(y, ctx).ok();
}
}
// Bad - slow operation
tick(ctx) {
std::thread::sleep(Duration::from_secs(1)); // Blocks everything!
}
Pre-allocate in init()
init(ctx) {
self.buffer.reserve(1000); // Do this once
}
tick(ctx) {
// Not here - would allocate every tick!
}
Common Questions
Do I need to import anything?
Yes, import the prelude:
use horus::prelude::*;
node! {
MyNode { ... }
}
Can I have multiple publishers?
Yes!
pub {
speed: f32 -> "speed",
direction: f32 -> "direction",
status: String -> "status"
}
Can I skip sections I don't need?
Yes! Only NodeName and tick are required:
node! {
MinimalNode {
tick(ctx) {
println!("Hello!");
}
}
}
How do I use the node?
Just create it and register it:
let mut scheduler = Scheduler::new();
scheduler.register(Box::new(MyNode::new()?), 0, Some(true));
scheduler.tick_all()?;
The macro generates a new() method automatically.
Troubleshooting
"Cannot find type in scope"
Import your message types:
use horus::prelude::*;
use horus_library::messages::CmdVel; // If using library types
node! {
MyNode {
pub { cmd: CmdVel -> "cmd_vel" }
...
}
}
"Expected ,, found {"
Check your syntax:
// Wrong
pub { cmd: f32 "topic" }
// Right
pub { cmd: f32 -> "topic" }
Node name must be CamelCase
// Wrong
node! { my_node { ... } }
// Right
node! { MyNode { ... } }
Next Steps
Now that you can write nodes quickly:
- Try some examples - See real applications
- Learn about Messages - Work with complex data
- Master the Hub - Understand communication
The macro makes HORUS fast to write. Shared memory makes it fast to run!