mecha10-core 0.6.2

Mecha10 core runtime and foundational types
Documentation
// Shared Message Types
//
// This module provides common message types used across the Mecha10 framework.
// These are the standard "vocabulary" for robot nodes to communicate with each other.
//
// Message types are organized by category:
// - sensor: Raw sensor data (Image, LaserScan, Odometry, IMU, GPS)
// - actuator: Actuator commands (Twist, JointState, Gripper)
// - perception: High-level perception (Detection, Pose, Tracking)
// - navigation: Navigation primitives (Goal, Path, Waypoint)
// - system: System-level messages (Health, Status, Command)

pub mod actuator;
pub mod navigation;
pub mod perception;
pub mod sensor;
pub mod system;

// Re-export commonly used types
pub use actuator::{GripperCommand, JointState, Twist};
pub use navigation::{NavigationGoal, NavigationStatus, Path, Waypoint};
pub use perception::{BoundingBox, Detection, Pose};
pub use sensor::{GpsData, Image, Imu, LaserScan, Odometry};
pub use system::{HealthLevel, HealthStatus, NodeStatus, SystemCommand};

/// Message trait marker
///
/// All types that can be sent through the pub/sub system must implement this trait.
/// This is typically done via `#[derive(Message)]` which also implements:
/// - Debug
/// - Clone
/// - Serialize
/// - Deserialize
pub trait Message: Send + Sync + 'static {}

/// Trait for message types that carry a capture/creation timestamp
///
/// Provides a default `age_micros` implementation so individual message types
/// don't need to hand-roll their own "how old is this" method.
pub trait Timestamped {
    /// The message's capture/creation timestamp, in microseconds since UNIX_EPOCH.
    fn timestamp(&self) -> u64;

    /// How long ago this message was captured/created, in microseconds.
    fn age_micros(&self) -> u64 {
        crate::prelude::now_micros().saturating_sub(self.timestamp())
    }
}

/// Timestamped message wrapper
///
/// Wraps any message payload with metadata (timestamp, publisher).
/// This is commonly used for sensor data and motor commands that need timing information.
///
/// # Example
/// ```
/// use mecha10_core::messages::{RedisMessage, Twist};
/// use mecha10_core::prelude::now_micros;
///
/// let twist = Twist { linear: 0.5, angular: 0.3 };
/// let msg = RedisMessage::new(twist, "teleop", now_micros());
/// ```
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RedisMessage<T> {
    /// Message payload
    pub payload: T,
    /// Publisher node ID (optional for backwards compatibility)
    #[serde(default)]
    pub publisher: String,
    /// Unix timestamp in microseconds
    pub timestamp: u64,
}

impl<T> RedisMessage<T> {
    /// Create a new timestamped message
    pub fn new(payload: T, publisher: impl Into<String>, timestamp: u64) -> Self {
        Self {
            payload,
            publisher: publisher.into(),
            timestamp,
        }
    }
}

// Implement Message for all standard types
impl Message for Image {}
impl Message for LaserScan {}
impl Message for Odometry {}
impl Message for Imu {}
impl Message for GpsData {}
impl Message for Twist {}
impl Message for JointState {}
impl Message for GripperCommand {}
impl Message for Detection {}
impl Message for Pose {}
impl Message for NavigationGoal {}
impl Message for NavigationStatus {}
impl Message for Path {}
impl Message for Waypoint {}
impl Message for HealthStatus {}
impl Message for NodeStatus {}
impl Message for SystemCommand {}

// Implement Message for primitive types
impl Message for bool {}
impl Message for f32 {}
impl Message for f64 {}
impl Message for i32 {}
impl Message for i64 {}
impl Message for u32 {}
impl Message for u64 {}
impl Message for String {}
// Vec<T> types covered by generic Vec<T> implementation below
impl Message for [f32; 2] {}
impl Message for [f32; 3] {}
impl Message for [f32; 6] {}
// Vec<[f32; N]> covered by generic Vec<T>
// Vec<Vec<u8>> covered by generic Vec<T>
// Vec<(T1, T2)> covered by generic Vec<T>

// Implement Message for types module duplicates
impl Message for crate::types::Image {}
impl Message for crate::types::LaserScan {}
impl Message for crate::types::IMU {}
// MotorStatus already has Message impl in actuator.rs

// Implement Message for external types
impl Message for serde_json::Value {}

// Generic implementations for Vec and tuples
impl<T: Send + Sync + 'static> Message for Vec<T> {}
// Note: References cannot implement Message since they don't satisfy 'static bound
impl<T1: Send + Sync + 'static, T2: Send + Sync + 'static> Message for (T1, T2) {}

// Implement Message for TracedMessage (requires messaging feature)
#[cfg(feature = "messaging")]
impl<T: Send + Sync + 'static> Message for crate::tracing_otel::TracedMessage<T> {}

// Implement Message for RedisMessage
impl<T: Send + Sync + 'static> Message for RedisMessage<T> {}

// Implement Timestamped for message types with a `timestamp: u64` field
impl Timestamped for Detection {
    fn timestamp(&self) -> u64 {
        self.timestamp
    }
}
impl Timestamped for Image {
    fn timestamp(&self) -> u64 {
        self.timestamp
    }
}
impl Timestamped for LaserScan {
    fn timestamp(&self) -> u64 {
        self.timestamp
    }
}
impl Timestamped for Imu {
    fn timestamp(&self) -> u64 {
        self.timestamp
    }
}
impl Timestamped for GpsData {
    fn timestamp(&self) -> u64 {
        self.timestamp
    }
}

#[cfg(test)]
mod timestamped_tests {
    use super::*;
    use crate::messages::perception::BoundingBox;
    use crate::prelude::now_micros;

    #[test]
    fn timestamp_returns_the_underlying_field() {
        let detection = Detection::new(1, "cup".to_string(), 0.9, BoundingBox::new(0.0, 0.0, 1.0, 1.0));
        assert_eq!(detection.timestamp(), detection.timestamp);
    }

    #[test]
    fn age_micros_reflects_elapsed_time_since_timestamp() {
        let past = now_micros().saturating_sub(1_000);
        let detection = Detection {
            timestamp: past,
            class_id: 0,
            class_name: "test".to_string(),
            confidence: 1.0,
            bbox: BoundingBox::new(0.0, 0.0, 1.0, 1.0),
            track_id: None,
            position_3d: None,
        };

        assert!(detection.age_micros() >= 1_000);
    }

    #[test]
    fn age_micros_saturates_instead_of_underflowing_for_future_timestamps() {
        let future = now_micros() + 1_000_000_000;
        let detection = Detection {
            timestamp: future,
            class_id: 0,
            class_name: "test".to_string(),
            confidence: 1.0,
            bbox: BoundingBox::new(0.0, 0.0, 1.0, 1.0),
            track_id: None,
            position_3d: None,
        };

        assert_eq!(detection.age_micros(), 0);
    }

    #[test]
    fn all_five_message_types_implement_timestamped() {
        fn assert_timestamped<T: Timestamped>(value: &T) -> u64 {
            value.age_micros()
        }

        let image = Image {
            timestamp: now_micros(),
            width: 1,
            height: 1,
            encoding: "rgb8".to_string(),
            data: vec![0, 0, 0],
            frame_id: None,
        };
        let laser_scan = LaserScan {
            timestamp: now_micros(),
            angle_min: 0.0,
            angle_max: 0.0,
            angle_increment: 0.0,
            time_increment: 0.0,
            scan_time: 0.0,
            range_min: 0.0,
            range_max: 0.0,
            ranges: vec![],
            intensities: None,
            frame_id: None,
        };
        let imu = Imu {
            timestamp: now_micros(),
            orientation: [0.0, 0.0, 0.0, 1.0],
            orientation_covariance: None,
            angular_velocity: [0.0, 0.0, 0.0],
            angular_velocity_covariance: None,
            linear_acceleration: [0.0, 0.0, 0.0],
            linear_acceleration_covariance: None,
            frame_id: None,
        };
        let gps = GpsData {
            timestamp: now_micros(),
            latitude: 0.0,
            longitude: 0.0,
            altitude: 0.0,
            horizontal_accuracy: None,
            vertical_accuracy: None,
            satellites: None,
            fix_type: None,
        };

        assert_timestamped(&image);
        assert_timestamped(&laser_scan);
        assert_timestamped(&imu);
        assert_timestamped(&gps);
    }
}