Skip to main content

moto_hses_mock/
lib.rs

1//! moto-hses-mock - Local mock HSES UDP server for testing
2
3#[macro_use]
4extern crate log;
5
6use moto_hses_proto as proto;
7use std::collections::HashMap;
8use std::net::SocketAddr;
9
10pub mod handlers;
11pub mod server;
12pub mod state;
13
14pub use handlers::CommandHandler;
15pub use server::MockServer;
16pub use state::{MockState, TypedVariables, VariableType};
17
18/// Mock server configuration
19#[derive(Debug, Clone)]
20pub struct MockConfig {
21    pub host: String,
22    pub robot_port: u16,
23    pub file_port: u16,
24    pub text_encoding: proto::TextEncoding,
25    pub default_status: proto::Status,
26    pub default_position: proto::Position,
27    pub registers: HashMap<u16, i16>,
28    pub variables: TypedVariables,
29    pub io_states: HashMap<u16, u8>,
30    pub alarms: Vec<proto::Alarm>,
31    pub alarm_history: Vec<proto::Alarm>,
32    pub executing_job: Option<proto::ExecutingJobInfo>,
33    pub cycle_mode: proto::CycleMode,
34}
35
36impl MockConfig {
37    /// Create a new `MockConfig` with specified host and ports
38    pub fn new(host: impl Into<String>, robot_port: u16, file_port: u16) -> Self {
39        let mut registers = HashMap::new();
40        registers.insert(0, 0);
41        registers.insert(1, 100);
42        registers.insert(2, 200);
43        registers.insert(3, 300);
44        registers.insert(4, 400);
45
46        let mut variables = TypedVariables::new();
47        variables.insert((VariableType::Double, 0), vec![0x01, 0x00, 0x00, 0x00]); // D000 = 1
48        variables.insert((VariableType::Double, 1), vec![0x64, 0x00, 0x00, 0x00]); // D001 = 100
49        variables.insert((VariableType::Double, 2), vec![0x00, 0x00, 0x20, 0x41]); // D002 = 10.0
50
51        let mut io_states = HashMap::new();
52        io_states.insert(1, 1); // Robot user input 1
53        io_states.insert(1001, 0); // Robot user output 1
54
55        Self {
56            host: host.into(),
57            robot_port,
58            file_port,
59            text_encoding: proto::TextEncoding::Utf8,
60            default_status: proto::Status::new(
61                proto::StatusData1 {
62                    step: false,
63                    one_cycle: false,
64                    continuous: true,
65                    running: false,
66                    speed_limited: false,
67                    teach: false,
68                    play: true,
69                    remote: false,
70                },
71                proto::StatusData2 {
72                    teach_pendant_hold: false,
73                    external_hold: false,
74                    command_hold: false,
75                    alarm: false,
76                    error: false,
77                    servo_on: true,
78                },
79            ),
80            default_position: proto::Position::Pulse(proto::PulsePosition::new(vec![
81                0, 0, 0, 0, 0, 0, 0, 0,
82            ])),
83            registers,
84            variables,
85            io_states,
86            alarms: Vec::new(),
87            alarm_history: Vec::new(),
88            executing_job: Some(proto::ExecutingJobInfo::new("TEST.JOB".to_string(), 2, 1, 100)),
89            cycle_mode: proto::CycleMode::Continuous,
90        }
91    }
92
93    /// Get robot control socket address
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if the address format is invalid
98    pub fn robot_addr(&self) -> Result<SocketAddr, std::net::AddrParseError> {
99        format!("{host}:{port}", host = self.host, port = self.robot_port).parse()
100    }
101
102    /// Get file control socket address
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the address format is invalid
107    pub fn file_addr(&self) -> Result<SocketAddr, std::net::AddrParseError> {
108        format!("{host}:{port}", host = self.host, port = self.file_port).parse()
109    }
110
111    /// Get robot control socket address as string
112    #[must_use]
113    pub fn robot_addr_string(&self) -> String {
114        format!("{host}:{port}", host = self.host, port = self.robot_port)
115    }
116
117    /// Get file control socket address as string
118    #[must_use]
119    pub fn file_addr_string(&self) -> String {
120        format!("{host}:{port}", host = self.host, port = self.file_port)
121    }
122}
123
124impl Default for MockConfig {
125    fn default() -> Self {
126        Self::new(
127            "127.0.0.1",
128            moto_hses_proto::ROBOT_CONTROL_PORT,
129            moto_hses_proto::FILE_CONTROL_PORT,
130        )
131    }
132}
133
134/// Test utilities for mock server
135pub mod test_utils {
136    use super::{MockConfig, MockServer, SocketAddr};
137    use tokio::time::{Duration, sleep};
138
139    /// Start a mock server for testing
140    /// # Errors
141    ///
142    /// Returns an error if server creation fails
143    pub async fn start_test_server()
144    -> Result<(SocketAddr, tokio::task::JoinHandle<()>), Box<dyn std::error::Error + Send + Sync>>
145    {
146        // Try to bind to a specific high port first
147        let mut port = 49152; // Start from dynamic port range
148        let mut server = None;
149
150        while port < 65535 && server.is_none() {
151            let test_config = MockConfig::new("127.0.0.1", port, port + 1);
152
153            match MockServer::new(test_config).await {
154                Ok(s) => server = Some(s),
155                Err(_) => port += 2, // Skip 2 ports since we need both robot and file ports
156            }
157        }
158
159        let server = server.ok_or("Could not find available port")?;
160        let addr = server.local_addr()?;
161
162        let handle = tokio::spawn(async move {
163            if let Err(e) = server.run().await {
164                error!("Mock server error: {e}");
165            }
166        });
167
168        // Give server time to start
169        sleep(Duration::from_millis(200)).await;
170
171        Ok((addr, handle))
172    }
173
174    /// Create a test client connected to mock server
175    /// # Errors
176    ///
177    /// Returns an error if client creation fails
178    pub fn create_test_client(
179        addr: SocketAddr,
180    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
181        // Note: This would require the client to be available
182        // For now, just return a placeholder
183        info!("Test client would connect to {addr}");
184        Ok(())
185    }
186}