phytrace_sdk/models/domains/
environment_interaction.rs

1//! Environment Interaction Domain - Doors, elevators, charging stations.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use validator::Validate;
6
7use super::common::Position2D;
8use crate::models::enums::{ChargingStationState, DoorState, ElevatorState};
9
10/// Environment interaction domain.
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct EnvironmentInteractionDomain {
13    /// Door interactions
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub doors: Option<Vec<DoorInteraction>>,
16
17    /// Elevator interactions
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub elevators: Option<Vec<ElevatorInteraction>>,
20
21    /// Charging station interactions
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub charging_stations: Option<Vec<ChargingStationInteraction>>,
24
25    /// Surface detection
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub surface: Option<SurfaceInfo>,
28}
29
30/// Door interaction.
31#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
32pub struct DoorInteraction {
33    /// Door ID
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub door_id: Option<String>,
36
37    /// Door name
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub name: Option<String>,
40
41    /// Door state
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub state: Option<DoorState>,
44
45    /// Door position
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub position: Option<Position2D>,
48
49    /// Distance to door (m)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    #[validate(range(min = 0.0))]
52    pub distance_m: Option<f64>,
53
54    /// Whether waiting for door
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub is_waiting: Option<bool>,
57
58    /// Whether robot requested open
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub open_requested: Option<bool>,
61
62    /// Wait time (seconds)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub wait_time_sec: Option<f64>,
65
66    /// Last state change
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub last_change: Option<DateTime<Utc>>,
69}
70
71/// Elevator interaction.
72#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
73pub struct ElevatorInteraction {
74    /// Elevator ID
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub elevator_id: Option<String>,
77
78    /// Elevator name
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub name: Option<String>,
81
82    /// Interaction state
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub state: Option<ElevatorState>,
85
86    /// Current floor
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub current_floor: Option<i32>,
89
90    /// Target floor
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub target_floor: Option<i32>,
93
94    /// Robot's floor
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub robot_floor: Option<i32>,
97
98    /// Whether robot is in elevator
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub robot_inside: Option<bool>,
101
102    /// Whether elevator is reserved
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub is_reserved: Option<bool>,
105
106    /// ETA (seconds)
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub eta_sec: Option<f64>,
109
110    /// Wait time (seconds)
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub wait_time_sec: Option<f64>,
113}
114
115/// Charging station interaction.
116#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
117pub struct ChargingStationInteraction {
118    /// Station ID
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub station_id: Option<String>,
121
122    /// Station name
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub name: Option<String>,
125
126    /// Station state
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub state: Option<ChargingStationState>,
129
130    /// Station position
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub position: Option<Position2D>,
133
134    /// Distance to station (m)
135    #[serde(skip_serializing_if = "Option::is_none")]
136    #[validate(range(min = 0.0))]
137    pub distance_m: Option<f64>,
138
139    /// Whether docked
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub is_docked: Option<bool>,
142
143    /// Whether approaching
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub is_approaching: Option<bool>,
146
147    /// Dock alignment score (0-1)
148    #[serde(skip_serializing_if = "Option::is_none")]
149    #[validate(range(min = 0.0, max = 1.0))]
150    pub alignment_score: Option<f64>,
151
152    /// Charger type
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub charger_type: Option<String>,
155
156    /// Max power (W)
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub max_power_w: Option<f64>,
159}
160
161/// Surface detection information.
162#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
163pub struct SurfaceInfo {
164    /// Detected surface type
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub surface_type: Option<String>,
167
168    /// Surface friction estimate (0-1)
169    #[serde(skip_serializing_if = "Option::is_none")]
170    #[validate(range(min = 0.0, max = 1.0))]
171    pub friction: Option<f64>,
172
173    /// Incline (degrees)
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub incline_deg: Option<f64>,
176
177    /// Whether wet
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub is_wet: Option<bool>,
180
181    /// Whether slippery
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub is_slippery: Option<bool>,
184
185    /// Roughness estimate
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub roughness: Option<f64>,
188
189    /// Confidence (0-1)
190    #[serde(skip_serializing_if = "Option::is_none")]
191    #[validate(range(min = 0.0, max = 1.0))]
192    pub confidence: Option<f64>,
193}
194
195impl EnvironmentInteractionDomain {
196    /// Add a door interaction.
197    pub fn with_door(mut self, door: DoorInteraction) -> Self {
198        let doors = self.doors.get_or_insert_with(Vec::new);
199        doors.push(door);
200        self
201    }
202
203    /// Add an elevator interaction.
204    pub fn with_elevator(mut self, elevator: ElevatorInteraction) -> Self {
205        let elevators = self.elevators.get_or_insert_with(Vec::new);
206        elevators.push(elevator);
207        self
208    }
209
210    /// Add a charging station interaction.
211    pub fn with_charging_station(mut self, station: ChargingStationInteraction) -> Self {
212        let stations = self.charging_stations.get_or_insert_with(Vec::new);
213        stations.push(station);
214        self
215    }
216
217    /// Set surface info.
218    pub fn with_surface(mut self, surface: SurfaceInfo) -> Self {
219        self.surface = Some(surface);
220        self
221    }
222}
223
224impl DoorInteraction {
225    /// Create a door interaction.
226    pub fn new(door_id: impl Into<String>, state: DoorState) -> Self {
227        Self {
228            door_id: Some(door_id.into()),
229            state: Some(state),
230            last_change: Some(Utc::now()),
231            ..Default::default()
232        }
233    }
234
235    /// Mark as waiting.
236    pub fn waiting(mut self, wait_time_sec: f64) -> Self {
237        self.is_waiting = Some(true);
238        self.wait_time_sec = Some(wait_time_sec);
239        self
240    }
241}
242
243impl ChargingStationInteraction {
244    /// Create a docked charging station.
245    pub fn docked(station_id: impl Into<String>) -> Self {
246        Self {
247            station_id: Some(station_id.into()),
248            state: Some(ChargingStationState::Occupied),
249            is_docked: Some(true),
250            ..Default::default()
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_door_interaction() {
261        let door = DoorInteraction::new("door-001", DoorState::Closed).waiting(5.0);
262
263        assert_eq!(door.is_waiting, Some(true));
264        assert_eq!(door.wait_time_sec, Some(5.0));
265    }
266
267    #[test]
268    fn test_environment_domain() {
269        let env = EnvironmentInteractionDomain::default()
270            .with_door(DoorInteraction::new("door-001", DoorState::Open))
271            .with_charging_station(ChargingStationInteraction::docked("station-001"));
272
273        assert_eq!(env.doors.unwrap().len(), 1);
274        assert_eq!(env.charging_stations.unwrap().len(), 1);
275    }
276}