IMU Reader
You need to read an IMU sensor (accelerometer + gyroscope) at a fixed rate and publish the data for downstream consumers. Here's a production-ready pattern with raw data publishing and orientation estimation.
When To Use This
- Reading a 6-axis or 9-axis IMU over I2C/SPI
- Publishing raw
Imumessages for sensor fusion or logging - Estimating orientation from gyroscope integration (simple) or a complementary filter (production)
Use the built-in Imu message type — don't define custom IMU structs. Downstream nodes (fusion, SLAM, display) expect Imu.
Prerequisites
- Quick Start completed
- Familiarity with Imu message type and Topic
Solution
horus.toml
[package]
name = "imu-reader"
version = "0.1.0"
description = "100 Hz IMU sensor with orientation publishing"
src/main.rs (or src/main.py)
// simplified
use horus::prelude::*;
/// Simplified orientation from integrated gyroscope
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, LogSummary)]
#[repr(C)]
struct Orientation {
roll: f32,
pitch: f32,
yaw: f32,
timestamp_ns: u64,
}
struct ImuNode {
imu_pub: Topic<Imu>,
orientation_pub: Topic<Orientation>,
roll: f32,
pitch: f32,
yaw: f32,
tick_count: u64,
}
impl ImuNode {
fn new() -> Result<Self> {
Ok(Self {
imu_pub: Topic::new("imu.raw")?,
orientation_pub: Topic::new("imu.orientation")?,
roll: 0.0, pitch: 0.0, yaw: 0.0,
tick_count: 0,
})
}
/// Read IMU hardware — replace with your I2C/SPI driver
fn read_hardware(&self) -> Imu {
let mut imu = Imu::new();
imu.linear_acceleration = [0.0, 0.0, 9.81]; // gravity
imu.angular_velocity = [0.01, 0.0, 0.05]; // slow yaw
imu
}
}
impl Node for ImuNode {
fn name(&self) -> &str { "ImuReader" }
fn tick(&mut self) {
let imu = self.read_hardware();
// Validate before publishing — hardware faults produce NaN
if !imu.is_valid() {
return; // skip corrupted readings
}
// Publish raw IMU for any subscriber
self.imu_pub.send(imu);
// Simple gyro integration (replace with Madgwick/Mahony in production)
let dt = 1.0 / 100.0_f32; // 100 Hz tick rate
self.roll += imu.angular_velocity[0] as f32 * dt;
self.pitch += imu.angular_velocity[1] as f32 * dt;
self.yaw += imu.angular_velocity[2] as f32 * dt;
self.tick_count += 1;
self.orientation_pub.send(Orientation {
roll: self.roll,
pitch: self.pitch,
yaw: self.yaw,
timestamp_ns: self.tick_count * 10_000_000, // 10 ms per tick
});
}
}
fn main() -> Result<()> {
let mut scheduler = Scheduler::new();
scheduler.add(ImuNode::new()?)
.order(0)
.rate(100_u64.hz()) // 100 Hz sensor rate — auto-enables RT
.build()?;
scheduler.run()
}
Understanding the Code
read_hardware() is a placeholder. For a complete working example with a real MPU6050 over I2C using i2cdev (Rust) or smbus2 (Python), see the Real Hardware Recipe.
Imu::new()creates a message with identity quaternion, zero acceleration/velocity, and current timestampimu.is_valid()rejects NaN/infinite readings from faulty hardware#[repr(C)]+CopyonOrientationenables zero-copy shared memory (~50 ns latency)- Gyro integration drifts over time — in production, use a Madgwick, Mahony, or complementary filter
Variations
Common Errors
| Symptom | Cause | Fix |
|---|---|---|
| Orientation drifts continuously | Pure gyro integration without correction | Use complementary or Madgwick filter |
NaN in published data | Hardware fault or I2C read error | Check imu.is_valid() before publishing |
| Orientation jumps on startup | Initial gyro bias not calibrated | Average first 100 readings as bias offset |
| Yaw rotates when stationary | Gyro bias in Z-axis | Subtract calibrated bias from angular_velocity[2] |
| Topic data stale | Publisher rate too low | Verify .rate(100_u64.hz()) matches hardware capability |
See Also
- Real Hardware Recipe — Complete MPU6050 I2C example (no stubs)
- Imu — IMU message type reference
- Sensor Node Tutorial — Step-by-step sensor tutorial
- Multi-Sensor Fusion Recipe — Combine IMU with odometry
- Sensor Messages (Rust) — All sensor message types