phytrace_sdk/models/domains/
communication.rs

1//! Communication Domain - Network, fleet, and integration status.
2//!
3//! Contains network connectivity, cellular, fleet communication, and integration status.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9use crate::models::enums::{CellularGeneration, ConnectionStatus, NetworkType};
10
11/// Communication domain containing network and connectivity information.
12#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
13pub struct CommunicationDomain {
14    // === Network ===
15    /// Primary network connection
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub network: Option<NetworkInfo>,
18
19    // === Cellular ===
20    /// Cellular connection (if available)
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub cellular: Option<CellularInfo>,
23
24    // === Fleet Communication ===
25    /// Fleet/robot-to-robot communication
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub fleet: Option<FleetCommunication>,
28
29    // === Integrations ===
30    /// External system integrations
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub integrations: Option<Vec<IntegrationStatus>>,
33
34    // === PhyTrace ===
35    /// PhyTrace connection status
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub phytrace: Option<PhyTraceConnection>,
38}
39
40/// Network connection information.
41#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
42pub struct NetworkInfo {
43    /// Network type
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub network_type: Option<NetworkType>,
46
47    /// Connection status
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub status: Option<ConnectionStatus>,
50
51    /// Whether connected
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub is_connected: Option<bool>,
54
55    /// SSID (for WiFi)
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub ssid: Option<String>,
58
59    /// Signal strength (dBm)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub signal_strength_dbm: Option<i32>,
62
63    /// Signal quality percentage (0-100)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    #[validate(range(min = 0.0, max = 100.0))]
66    pub signal_quality_pct: Option<f64>,
67
68    /// IP address
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub ip_address: Option<String>,
71
72    /// MAC address
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub mac_address: Option<String>,
75
76    /// Gateway address
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub gateway: Option<String>,
79
80    /// DNS servers
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub dns_servers: Option<Vec<String>>,
83
84    /// Link speed in Mbps
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub link_speed_mbps: Option<f64>,
87
88    /// Bytes transmitted
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub bytes_tx: Option<u64>,
91
92    /// Bytes received
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub bytes_rx: Option<u64>,
95
96    /// Latency to gateway (ms)
97    #[serde(skip_serializing_if = "Option::is_none")]
98    #[validate(range(min = 0.0))]
99    pub latency_ms: Option<f64>,
100
101    /// Packet loss percentage
102    #[serde(skip_serializing_if = "Option::is_none")]
103    #[validate(range(min = 0.0, max = 100.0))]
104    pub packet_loss_pct: Option<f64>,
105}
106
107/// Cellular connection information.
108#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
109pub struct CellularInfo {
110    /// Connection status
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub status: Option<ConnectionStatus>,
113
114    /// Cellular generation (3G, 4G, 5G)
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub generation: Option<CellularGeneration>,
117
118    /// Carrier name
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub carrier: Option<String>,
121
122    /// Signal strength (dBm)
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub signal_strength_dbm: Option<i32>,
125
126    /// Signal bars (0-5)
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub signal_bars: Option<u8>,
129
130    /// Whether roaming
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub is_roaming: Option<bool>,
133
134    /// Data usage (bytes)
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub data_usage_bytes: Option<u64>,
137
138    /// SIM status
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub sim_status: Option<String>,
141
142    /// IMEI
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub imei: Option<String>,
145}
146
147/// Fleet communication status.
148#[derive(Debug, Clone, Default, Serialize, Deserialize)]
149pub struct FleetCommunication {
150    /// Fleet management system connection
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub fms_connected: Option<bool>,
153
154    /// FMS server address
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub fms_server: Option<String>,
157
158    /// Last FMS heartbeat
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub last_fms_heartbeat: Option<DateTime<Utc>>,
161
162    /// Robot-to-robot communication enabled
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub r2r_enabled: Option<bool>,
165
166    /// Number of robots in communication range
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub robots_in_range: Option<u32>,
169
170    /// Peer robot IDs
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub peer_robot_ids: Option<Vec<String>>,
173
174    /// Messages sent
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub messages_sent: Option<u64>,
177
178    /// Messages received
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub messages_received: Option<u64>,
181}
182
183/// External integration status.
184#[derive(Debug, Clone, Default, Serialize, Deserialize)]
185pub struct IntegrationStatus {
186    /// Integration name (e.g., "wms", "erp", "mes")
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub name: Option<String>,
189
190    /// Integration type
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub integration_type: Option<String>,
193
194    /// Connection status
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub status: Option<ConnectionStatus>,
197
198    /// API endpoint
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub endpoint: Option<String>,
201
202    /// Last successful communication
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub last_success: Option<DateTime<Utc>>,
205
206    /// Last error (if any)
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub last_error: Option<String>,
209
210    /// Request count
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub request_count: Option<u64>,
213
214    /// Error count
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub error_count: Option<u64>,
217
218    /// Average latency (ms)
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub avg_latency_ms: Option<f64>,
221}
222
223/// PhyTrace connection status.
224#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
225pub struct PhyTraceConnection {
226    /// Connection status
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub status: Option<ConnectionStatus>,
229
230    /// PhyCloud endpoint
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub endpoint: Option<String>,
233
234    /// Last event sent timestamp
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub last_event_sent: Option<DateTime<Utc>>,
237
238    /// Events sent (session)
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub events_sent: Option<u64>,
241
242    /// Events buffered (pending)
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub events_buffered: Option<u64>,
245
246    /// Buffer size (bytes)
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub buffer_size_bytes: Option<u64>,
249
250    /// Average latency (ms)
251    #[serde(skip_serializing_if = "Option::is_none")]
252    #[validate(range(min = 0.0))]
253    pub avg_latency_ms: Option<f64>,
254
255    /// Send errors (session)
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub send_errors: Option<u64>,
258
259    /// Agent version
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub agent_version: Option<String>,
262}
263
264impl CommunicationDomain {
265    /// Create with network info.
266    pub fn with_network(network: NetworkInfo) -> Self {
267        Self {
268            network: Some(network),
269            ..Default::default()
270        }
271    }
272
273    /// Builder to add cellular.
274    pub fn with_cellular(mut self, cellular: CellularInfo) -> Self {
275        self.cellular = Some(cellular);
276        self
277    }
278
279    /// Builder to add fleet communication.
280    pub fn with_fleet(mut self, fleet: FleetCommunication) -> Self {
281        self.fleet = Some(fleet);
282        self
283    }
284
285    /// Builder to add integration.
286    pub fn with_integration(mut self, integration: IntegrationStatus) -> Self {
287        let integrations = self.integrations.get_or_insert_with(Vec::new);
288        integrations.push(integration);
289        self
290    }
291
292    /// Builder to add PhyTrace status.
293    pub fn with_phytrace(mut self, phytrace: PhyTraceConnection) -> Self {
294        self.phytrace = Some(phytrace);
295        self
296    }
297}
298
299impl NetworkInfo {
300    /// Create WiFi connection info.
301    pub fn wifi(ssid: impl Into<String>, signal_dbm: i32) -> Self {
302        Self {
303            network_type: Some(NetworkType::Wifi),
304            status: Some(ConnectionStatus::Connected),
305            is_connected: Some(true),
306            ssid: Some(ssid.into()),
307            signal_strength_dbm: Some(signal_dbm),
308            signal_quality_pct: Some(Self::dbm_to_quality(signal_dbm)),
309            ..Default::default()
310        }
311    }
312
313    /// Create ethernet connection info.
314    pub fn ethernet(ip: impl Into<String>) -> Self {
315        Self {
316            network_type: Some(NetworkType::Ethernet),
317            status: Some(ConnectionStatus::Connected),
318            is_connected: Some(true),
319            ip_address: Some(ip.into()),
320            signal_quality_pct: Some(100.0),
321            ..Default::default()
322        }
323    }
324
325    /// Convert dBm to quality percentage.
326    fn dbm_to_quality(dbm: i32) -> f64 {
327        // Approximate conversion: -30 dBm = 100%, -90 dBm = 0%
328        ((dbm + 90) as f64 / 60.0 * 100.0).clamp(0.0, 100.0)
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn test_wifi_network() {
338        let network = NetworkInfo::wifi("RobotNet", -65);
339        assert_eq!(network.network_type, Some(NetworkType::Wifi));
340        assert_eq!(network.ssid, Some("RobotNet".to_string()));
341        assert!(network.signal_quality_pct.unwrap() > 0.0);
342    }
343
344    #[test]
345    fn test_communication_domain() {
346        let comm = CommunicationDomain::with_network(NetworkInfo::ethernet("192.168.1.100"))
347            .with_phytrace(PhyTraceConnection {
348                status: Some(ConnectionStatus::Connected),
349                events_sent: Some(1000),
350                ..Default::default()
351            });
352
353        assert!(comm.network.is_some());
354        assert_eq!(comm.phytrace.as_ref().unwrap().events_sent, Some(1000));
355    }
356}