LiDAR Obstacle Avoidance
Reads LaserScan data from a 2D LiDAR, identifies obstacles in three zones (left, center, right), and publishes reactive CmdVel commands to avoid collisions. Stops if an obstacle is too close.
horus.toml
[package]
name = "lidar-avoidance"
version = "0.1.0"
description = "Reactive obstacle avoidance from LaserScan"
Complete Code
use horus::prelude::*;
/// Safety zones (meters)
const STOP_DISTANCE: f32 = 0.3; // emergency stop
const SLOW_DISTANCE: f32 = 0.8; // reduce speed
const CRUISE_SPEED: f32 = 0.5; // m/s forward
const TURN_SPEED: f32 = 0.8; // rad/s turning
// ── Avoidance Node ──────────────────────────────────────────
struct AvoidanceNode {
scan_sub: Topic<LaserScan>,
cmd_pub: Topic<CmdVel>,
}
impl AvoidanceNode {
fn new() -> Result<Self> {
Ok(Self {
scan_sub: Topic::new("lidar.scan")?,
cmd_pub: Topic::new("cmd_vel")?,
})
}
/// Find minimum range in a slice of the scan
fn min_range(ranges: &[f32], start: usize, end: usize) -> f32 {
ranges[start..end]
.iter()
.copied()
.filter(|r| r.is_finite() && *r > 0.01)
.fold(f32::MAX, f32::min)
}
}
impl Node for AvoidanceNode {
fn name(&self) -> &str { "Avoidance" }
fn tick(&mut self) {
// IMPORTANT: always recv() every tick to drain the buffer
let scan = match self.scan_sub.recv() {
Some(s) => s,
None => return, // no data yet — skip this tick
};
let n = scan.ranges.len();
if n == 0 { return; }
// Split scan into three zones: left (0..n/3), center (n/3..2n/3), right (2n/3..n)
let third = n / 3;
let left_min = Self::min_range(&scan.ranges, 0, third);
let center_min = Self::min_range(&scan.ranges, third, 2 * third);
let right_min = Self::min_range(&scan.ranges, 2 * third, n);
// Reactive behavior
let cmd = if center_min < STOP_DISTANCE {
// WARNING: obstacle dead ahead — emergency stop
CmdVel { linear: 0.0, angular: 0.0 }
} else if center_min < SLOW_DISTANCE {
// Obstacle ahead — turn toward the more open side
let angular = if left_min > right_min { TURN_SPEED } else { -TURN_SPEED };
CmdVel { linear: 0.1, angular }
} else if left_min < SLOW_DISTANCE {
// Obstacle on left — veer right
CmdVel { linear: CRUISE_SPEED * 0.7, angular: -TURN_SPEED * 0.5 }
} else if right_min < SLOW_DISTANCE {
// Obstacle on right — veer left
CmdVel { linear: CRUISE_SPEED * 0.7, angular: TURN_SPEED * 0.5 }
} else {
// Clear — cruise forward
CmdVel { linear: CRUISE_SPEED, angular: 0.0 }
};
self.cmd_pub.send(cmd);
}
fn shutdown(&mut self) -> Result<()> {
// SAFETY: stop the robot on exit
self.cmd_pub.send(CmdVel { linear: 0.0, angular: 0.0 });
Ok(())
}
}
fn main() -> Result<()> {
let mut scheduler = Scheduler::new();
// Execution order: reads lidar scan, publishes velocity
scheduler.add(AvoidanceNode::new()?)
.order(0)
.rate(20_u64.hz()) // 20Hz — match typical LiDAR rate
.on_miss(Miss::Warn)
.build()?;
scheduler.run()
}
Expected Output
[HORUS] Scheduler running — tick_rate: 20 Hz
[HORUS] Node "Avoidance" started (Rt, 20 Hz, budget: 40.0ms, deadline: 47.5ms)
^C
[HORUS] Shutting down...
[HORUS] Node "Avoidance" shutdown complete
Key Points
- Three-zone split (left/center/right) is the simplest reactive architecture — extend to N zones for smoother behavior
min_range()filters invalid readings (NaN,Inf, near-zero) before comparisonSTOP_DISTANCEis the hard safety limit — tune to your robot's stopping distance at cruise speedshutdown()sends zero velocity — robot stops even if killed mid-avoidance-maneuver- 20Hz matches most 2D LiDARs (RPLiDAR A1/A2, Hokuyo URG) — no benefit running faster than sensor
- Pair with differential-drive recipe — this publishes
cmd_vel, the drive recipe subscribes to it