mechutil 0.8.0

Utility structures and functions for mechatronics applications.
Documentation
//
// Copyright (C) 2024 - 2025 Automated Design Corp. All Rights Reserved.
//

//! Wire format definitions for IPC message framing.
//!
//! Messages are transmitted over TCP with a simple framing protocol:
//! - 4 bytes: message length (u32, little-endian)
//! - 1 byte: message type (MessageType)
//! - N bytes: payload (JSON-serialized CommandMessage)
//!
//! The message type byte in the header is redundant with CommandMessage.message_type
//! but is kept for efficient routing without deserializing the full payload.

use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};

use super::command_message::{CommandMessage, MessageType};

/// Control message subtypes for initialization, finalization, etc.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr, FromPrimitive, ToPrimitive)]
#[repr(u8)]
pub enum ControlType {
    /// No operation
    NoOp = 0,
    /// Initialize the module
    Initialize = 1,
    /// Finalize/shutdown the module
    Finalize = 2,
    /// Module registration handshake
    Register = 3,
    /// Module registration acknowledgement
    RegisterAck = 4,
    /// Configuration update
    Configure = 5,
    /// Status query
    Status = 6,
}

impl Default for ControlType {
    fn default() -> Self {
        ControlType::NoOp
    }
}

/// Header for IPC messages, used for framing on the wire.
///
/// The header precedes every message and allows the receiver to:
/// 1. Know how many bytes to read for the payload
/// 2. Quickly determine message type without full deserialization
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct IpcMessageHeader {
    /// Total length of the payload (not including header)
    pub length: u32,
    /// Type of message (mirrors CommandMessage.message_type for quick routing)
    pub message_type: MessageType,
}

impl IpcMessageHeader {
    /// Size of the header in bytes (4 bytes length + 1 byte type)
    pub const SIZE: usize = 5;

    /// Create a new header
    pub fn new(message_type: MessageType, payload_length: u32) -> Self {
        Self {
            length: payload_length,
            message_type,
        }
    }

    /// Create a header from a CommandMessage (extracts type and computes length)
    pub fn from_message(msg: &CommandMessage) -> Result<Self, serde_json::Error> {
        let payload = serde_json::to_vec(msg)?;
        Ok(Self {
            length: payload.len() as u32,
            message_type: msg.message_type,
        })
    }

    /// Serialize header to bytes (little-endian)
    pub fn to_bytes(&self) -> [u8; Self::SIZE] {
        let mut bytes = [0u8; Self::SIZE];
        bytes[0..4].copy_from_slice(&self.length.to_le_bytes());
        bytes[4] = self.message_type as u8;
        bytes
    }

    /// Deserialize header from bytes
    pub fn from_bytes(bytes: &[u8; Self::SIZE]) -> Self {
        let length = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
        let message_type = MessageType::from(bytes[4]);
        Self {
            length,
            message_type,
        }
    }
}

/// Registration message sent by module when connecting to server.
///
/// This is sent as the data payload of a Control message with ControlType::Register.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleRegistration {
    /// Unique name/identifier for this module (becomes its domain)
    pub name: String,
    /// Module version string
    pub version: String,
    /// List of topics/capabilities this module provides
    pub capabilities: Vec<String>,
}

impl ModuleRegistration {
    pub fn new(name: &str, version: &str) -> Self {
        Self {
            name: name.to_string(),
            version: version.to_string(),
            capabilities: Vec::new(),
        }
    }

    pub fn with_capabilities(mut self, caps: Vec<String>) -> Self {
        self.capabilities = caps;
        self
    }

    /// Convert to a CommandMessage for sending over IPC
    pub fn to_command_message(&self) -> CommandMessage {
        CommandMessage::control(
            "register",
            serde_json::to_value(self).unwrap_or(serde_json::Value::Null),
        )
    }

    /// Parse from a CommandMessage's data field
    pub fn from_command_message(msg: &CommandMessage) -> Result<Self, serde_json::Error> {
        serde_json::from_value(msg.data.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_header_serialization() {
        let header = IpcMessageHeader::new(MessageType::Request, 1024);
        let bytes = header.to_bytes();
        let decoded = IpcMessageHeader::from_bytes(&bytes);

        assert_eq!(decoded.length, 1024);
        assert_eq!(decoded.message_type, MessageType::Request);
    }

    #[test]
    fn test_header_from_message() {
        let msg = CommandMessage::read("test.topic");
        let header = IpcMessageHeader::from_message(&msg).unwrap();

        assert_eq!(header.message_type, MessageType::Read);
        assert!(header.length > 0);
    }

    #[test]
    fn test_module_registration() {
        let reg = ModuleRegistration::new("test_module", "1.0.0")
            .with_capabilities(vec!["read".to_string(), "write".to_string()]);

        let msg = reg.to_command_message();
        assert_eq!(msg.message_type, MessageType::Control);
        assert!(msg.topic.contains("register"));

        let parsed = ModuleRegistration::from_command_message(&msg).unwrap();
        assert_eq!(parsed.name, "test_module");
        assert_eq!(parsed.version, "1.0.0");
        assert_eq!(parsed.capabilities.len(), 2);
    }
}