Sensor Messages

HORUS provides standard sensor data formats for common robotics sensors including lidar, IMU, GPS, and battery monitoring. All sensor messages are fixed-size types with #[repr(C)] layout, enabling zero-copy shared memory transport at ~50ns latency.

LaserScan

Laser scan data from a 2D lidar sensor.

Fixed-size array (360 readings) for shared memory safety. Supports up to 360-degree scanning with 1-degree resolution.

use horus::prelude::*;

// Create a new laser scan
let mut scan = LaserScan::new();

// Set scan parameters
scan.angle_min = -std::f32::consts::PI;  // -180 degrees
scan.angle_max = std::f32::consts::PI;   // +180 degrees
scan.range_min = 0.1;                     // 10cm minimum
scan.range_max = 30.0;                    // 30m maximum

// Fill in range data (360 readings)
for i in 0..360 {
    scan.ranges[i] = 2.5;  // 2.5m reading at all angles
}

// Get angle for a specific reading
let angle = scan.angle_at(90);  // 90th reading
println!("Angle at index 90: {:.2} rad", angle);

// Check if a reading is valid
if scan.is_range_valid(45) {
    println!("Reading at 45 is valid: {:.2}m", scan.ranges[45]);
}

// Get statistics
let valid = scan.valid_count();
let min_dist = scan.min_range();
println!("Valid readings: {}, Min distance: {:?}", valid, min_dist);

Fields:

FieldTypeDescription
ranges[f32; 360]Range measurements in meters (0 = invalid)
angle_minf32Start angle in radians
angle_maxf32End angle in radians
range_minf32Minimum valid range in meters
range_maxf32Maximum valid range in meters
angle_incrementf32Angular resolution in radians
time_incrementf32Time between measurements (seconds)
scan_timef32Time to complete full scan (seconds)
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
new()Create with default parameters
angle_at(index)Get angle for a specific range index
is_range_valid(index)Check if a reading is valid
valid_count()Count valid range readings
min_range()Get minimum valid range reading

Imu

IMU (Inertial Measurement Unit) sensor data.

Provides orientation, angular velocity, and linear acceleration measurements.

use horus::prelude::*;

// Create new IMU message
let mut imu = Imu::new();

// Set orientation from Euler angles (roll, pitch, yaw)
imu.set_orientation_from_euler(0.0, 0.05, 1.57);  // Slight pitch, 90° yaw

// Set angular velocity [x, y, z] in rad/s
imu.angular_velocity = [0.0, 0.0, 0.5];  // Rotating around Z-axis

// Set linear acceleration [x, y, z] in m/s²
imu.linear_acceleration = [0.0, 0.0, 9.81];  // Gravity pointing up

// Check data availability
if imu.has_orientation() {
    println!("Orientation: {:?}", imu.orientation);
}

// Get as Vector3 for calculations
let angular_vel = imu.angular_velocity_vec();
let linear_acc = imu.linear_acceleration_vec();
println!("Angular velocity magnitude: {:.2} rad/s", angular_vel.magnitude());

// Validate data
assert!(imu.is_valid());

Fields:

FieldTypeDescription
orientation[f64; 4]Quaternion [x, y, z, w]
orientation_covariance[f64; 9]Orientation covariance (3x3, -1 = no data)
angular_velocity[f64; 3]Angular velocity [x, y, z] in rad/s
angular_velocity_covariance[f64; 9]Angular velocity covariance (3x3)
linear_acceleration[f64; 3]Linear acceleration [x, y, z] in m/s²
linear_acceleration_covariance[f64; 9]Acceleration covariance (3x3)
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
new()Create new IMU message
set_orientation_from_euler(roll, pitch, yaw)Set orientation from Euler angles
has_orientation()Check if orientation data available
is_valid()Check if all values are finite
angular_velocity_vec()Get angular velocity as Vector3
linear_acceleration_vec()Get linear acceleration as Vector3

Odometry

Odometry data combining pose and velocity.

Typically computed from wheel encoders or visual odometry, provides the robot's estimated position and velocity.

use horus::prelude::*;

// Create odometry message
let mut odom = Odometry::new();

// Set coordinate frames
odom.set_frames("odom", "base_link");

// Update with current pose and velocity
let pose = Pose2D::new(5.0, 3.0, 0.785);  // x, y, theta
let twist = Twist::new_2d(0.5, 0.1);       // linear, angular
odom.update(pose, twist);

// Access pose and velocity
println!("Position: ({:.2}, {:.2})", odom.pose.x, odom.pose.y);
println!("Velocity: {:.2} m/s", odom.twist.linear[0]);

// Validate
assert!(odom.is_valid());

Fields:

FieldTypeDescription
posePose2DCurrent pose estimate
twistTwistCurrent velocity estimate
pose_covariance[f64; 36]Pose covariance (6x6 row-major)
twist_covariance[f64; 36]Twist covariance (6x6 row-major)
frame_id[u8; 32]Pose reference frame (e.g., "odom")
child_frame_id[u8; 32]Body frame (e.g., "base_link")
timestamp_nsu64Nanoseconds since epoch

Methods:

MethodDescription
new()Create new odometry message
set_frames(frame, child_frame)Set coordinate frame names
update(pose, twist)Update pose and velocity with timestamp
is_valid()Check if pose and twist are valid

RangeSensor

Single-point range sensor data (ultrasonic, infrared).

use horus::prelude::*;

// Create ultrasonic range reading
let ultrasonic = RangeSensor::new(RangeSensor::ULTRASONIC, 1.5);  // 1.5m reading

// Create infrared range reading
let ir = RangeSensor::new(RangeSensor::INFRARED, 0.3);  // 30cm reading

// Check if reading is valid (within sensor limits)
if ultrasonic.is_valid() {
    println!("Distance: {:.2}m", ultrasonic.range);
}

// Access sensor parameters
println!("FOV: {:.2} rad", ultrasonic.field_of_view);
println!("Range: {:.2} - {:.2}m", ultrasonic.min_range, ultrasonic.max_range);

Fields:

FieldTypeDescription
sensor_typeu80 = ultrasonic, 1 = infrared
field_of_viewf32Field of view in radians
min_rangef32Minimum valid range (meters)
max_rangef32Maximum valid range (meters)
rangef32Range reading (meters)
timestamp_nsu64Nanoseconds since epoch

Constants:

ConstantValueDescription
RangeSensor::ULTRASONIC0Ultrasonic sensor
RangeSensor::INFRARED1Infrared sensor

Methods:

MethodDescription
new(sensor_type, range)Create with sensor type and reading
is_valid()Check if reading is within sensor limits

GPS/GNSS position data.

Standard GNSS position data from GPS, GLONASS, Galileo, or other satellite navigation systems.

use horus::prelude::*;

// Create GPS fix from coordinates
let fix = NavSatFix::from_coordinates(
    37.7749,    // Latitude (positive = North)
    -122.4194,  // Longitude (positive = East)
    10.5        // Altitude in meters
);

// Check fix status
if fix.has_fix() {
    println!("GPS Fix acquired!");
    println!("Position: {:.6}°N, {:.6}°E", fix.latitude, fix.longitude);
    println!("Altitude: {:.1}m", fix.altitude);
    println!("Satellites: {}", fix.satellites_visible);
}

// Get accuracy estimate
let accuracy = fix.horizontal_accuracy();
println!("Horizontal accuracy: ±{:.1}m", accuracy);

// Calculate distance to another position
let destination = NavSatFix::from_coordinates(37.8044, -122.2712, 0.0);
let distance = fix.distance_to(&destination);
println!("Distance to destination: {:.0}m", distance);

// Validate coordinates
assert!(fix.is_valid());

Fields:

FieldTypeDescription
latitudef64Latitude in degrees (+ = North)
longitudef64Longitude in degrees (+ = East)
altitudef64Altitude in meters (WGS84 ellipsoid)
position_covariance[f64; 9]Position covariance (3x3)
position_covariance_typeu8Covariance type (see constants)
statusu8Fix status (see constants)
satellites_visibleu16Number of satellites visible
hdopf32Horizontal dilution of precision
vdopf32Vertical dilution of precision
speedf32Ground speed in m/s
headingf32Course/heading in degrees
timestamp_nsu64Nanoseconds since epoch

Status Constants:

ConstantValueDescription
STATUS_NO_FIX0No GPS fix
STATUS_FIX1Standard GPS fix
STATUS_SBAS_FIX2SBAS-augmented fix
STATUS_GBAS_FIX3GBAS-augmented fix

Covariance Type Constants:

ConstantValueDescription
COVARIANCE_TYPE_UNKNOWN0Unknown covariance
COVARIANCE_TYPE_APPROXIMATED1Approximated covariance
COVARIANCE_TYPE_DIAGONAL_KNOWN2Diagonal elements known
COVARIANCE_TYPE_KNOWN3Full covariance matrix known

Methods:

MethodDescription
new()Create empty fix
from_coordinates(lat, lon, alt)Create from coordinates
has_fix()Check if valid GPS fix
is_valid()Check if coordinates are valid
horizontal_accuracy()Estimate accuracy from HDOP
distance_to(&other)Calculate distance using Haversine formula

BatteryState

Battery status monitoring.

use horus::prelude::*;

// Create battery state
let mut battery = BatteryState::new(12.6, 85.0);  // 12.6V, 85%

// Set additional fields
battery.current = -2.5;  // Discharging at 2.5A
battery.temperature = 28.0;
battery.power_supply_status = BatteryState::STATUS_DISCHARGING;

// Check battery level
if battery.is_low(20.0) {
    println!("Battery low!");
}
if battery.is_critical() {
    println!("Battery critical (below 10%)!");
}

// Estimate remaining time
if let Some(remaining) = battery.time_remaining() {
    println!("Estimated time remaining: {:.0} seconds", remaining);
}

println!("Voltage: {:.2}V", battery.voltage);
println!("Charge: {:.0}%", battery.percentage);
println!("Temperature: {:.1}°C", battery.temperature);

Fields:

FieldTypeDescription
voltagef32Voltage in volts
currentf32Current in amperes (- = discharging)
chargef32Charge in amp-hours (NaN if unknown)
capacityf32Capacity in amp-hours (NaN if unknown)
percentagef32Charge percentage (0-100)
power_supply_statusu8Status (see constants)
temperaturef32Temperature in Celsius
cell_voltages[f32; 16]Individual cell voltages
cell_countu8Number of valid cell readings
timestamp_nsu64Nanoseconds since epoch

Status Constants:

ConstantValueDescription
STATUS_UNKNOWN0Unknown status
STATUS_CHARGING1Charging
STATUS_DISCHARGING2Discharging
STATUS_FULL3Fully charged

Methods:

MethodDescription
new(voltage, percentage)Create new battery state
is_low(threshold)Check if below threshold %
is_critical()Check if below 10%
time_remaining()Estimate remaining time (seconds)

Sensor Fusion Example

use horus::prelude::*;

struct SensorFusionNode {
    imu_sub: Topic<Imu>,
    odom_sub: Topic<Odometry>,
    gps_sub: Topic<NavSatFix>,
    fused_pose_pub: Topic<Pose2D>,
    // Extended Kalman Filter state
    ekf_state: [f64; 6],  // [x, y, theta, vx, vy, omega]
}

impl Node for SensorFusionNode {
    fn name(&self) -> &str { "SensorFusion" }

    fn tick(&mut self) {
        // Process IMU at highest rate
        if let Some(imu) = self.imu_sub.recv() {
            if imu.is_valid() {
                // Use angular velocity for heading prediction
                let omega = imu.angular_velocity[2];
                self.predict_state(omega);
            }
        }

        // Process odometry
        if let Some(odom) = self.odom_sub.recv() {
            if odom.is_valid() {
                // Update with wheel odometry
                self.update_odometry(&odom);
            }
        }

        // Process GPS (lower rate, absolute position)
        if let Some(gps) = self.gps_sub.recv() {
            if gps.has_fix() && gps.is_valid() {
                // Update with GPS (when available)
                self.update_gps(&gps);
            }
        }

        // Publish fused pose
        let pose = Pose2D::new(
            self.ekf_state[0],
            self.ekf_state[1],
            self.ekf_state[2]
        );
        self.fused_pose_pub.send(pose);
    }
}

impl SensorFusionNode {
    fn predict_state(&mut self, omega: f64) {
        // EKF prediction step using IMU
        let dt = 0.01;  // 100Hz
        self.ekf_state[2] += omega * dt;
    }

    fn update_odometry(&mut self, odom: &Odometry) {
        // EKF update with odometry measurement
        // ... implementation
    }

    fn update_gps(&mut self, gps: &NavSatFix) {
        // EKF update with GPS measurement
        // ... implementation
    }
}

See Also