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 Imu messages 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

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 timestamp
  • imu.is_valid() rejects NaN/infinite readings from faulty hardware
  • #[repr(C)] + Copy on Orientation enables zero-copy shared memory (~50 ns latency)
  • Gyro integration drifts over time — in production, use a Madgwick, Mahony, or complementary filter

Variations

Common Errors

SymptomCauseFix
Orientation drifts continuouslyPure gyro integration without correctionUse complementary or Madgwick filter
NaN in published dataHardware fault or I2C read errorCheck imu.is_valid() before publishing
Orientation jumps on startupInitial gyro bias not calibratedAverage first 100 readings as bias offset
Yaw rotates when stationaryGyro bias in Z-axisSubtract calibrated bias from angular_velocity[2]
Topic data stalePublisher rate too lowVerify .rate(100_u64.hz()) matches hardware capability

See Also