phytrace_sdk/models/domains/
operational.rs1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::models::enums::{ErrorSeverity, OperationalMode, OperationalState};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct OperationalDomain {
13 #[serde(skip_serializing_if = "Option::is_none")]
16 pub mode: Option<OperationalMode>,
17
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub state: Option<OperationalState>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub previous_state: Option<OperationalState>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub time_in_state_sec: Option<f64>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
33 pub task: Option<Task>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub queue: Option<TaskQueue>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
42 pub errors: Option<Vec<OperationalError>>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub error_count: Option<u32>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
51 pub is_available: Option<bool>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub unavailable_reason: Option<String>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
60 pub uptime_sec: Option<f64>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub last_boot: Option<DateTime<Utc>>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
69 pub mission_id: Option<String>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub session_id: Option<String>,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct Task {
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub task_id: Option<String>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub task_type: Option<String>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub name: Option<String>,
90
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub status: Option<TaskStatus>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub priority: Option<i32>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub progress_pct: Option<f64>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub current_step: Option<String>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub total_steps: Option<u32>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub completed_steps: Option<u32>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub started_at: Option<DateTime<Utc>>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub eta: Option<DateTime<Utc>>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub elapsed_sec: Option<f64>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub source_location: Option<String>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub destination_location: Option<String>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub payload_ids: Option<Vec<String>>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub requester: Option<String>,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
146#[serde(rename_all = "snake_case")]
147pub enum TaskStatus {
148 #[default]
150 Queued,
151 Assigned,
153 InProgress,
155 Paused,
157 Completed,
159 Failed,
161 Cancelled,
163 Blocked,
165}
166
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub struct TaskQueue {
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub length: Option<u32>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub capacity: Option<u32>,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub task_ids: Option<Vec<String>>,
181
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub estimated_completion_min: Option<f64>,
185}
186
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct OperationalError {
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub code: Option<String>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub message: Option<String>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub severity: Option<ErrorSeverity>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub category: Option<String>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub timestamp: Option<DateTime<Utc>>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub recoverable: Option<bool>,
213
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub recovery_action: Option<String>,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub is_active: Option<bool>,
221}
222
223impl OperationalDomain {
224 pub fn new(mode: OperationalMode, state: OperationalState) -> Self {
226 Self {
227 mode: Some(mode),
228 state: Some(state),
229 ..Default::default()
230 }
231 }
232
233 pub fn idle() -> Self {
235 Self {
236 mode: Some(OperationalMode::Idle),
237 state: Some(OperationalState::Ready),
238 is_available: Some(true),
239 ..Default::default()
240 }
241 }
242
243 pub fn with_task(mut self, task: Task) -> Self {
245 self.task = Some(task);
246 self
247 }
248
249 pub fn with_error(mut self, error: OperationalError) -> Self {
251 let errors = self.errors.get_or_insert_with(Vec::new);
252 errors.push(error);
253 self.error_count = Some(errors.len() as u32);
254 self
255 }
256
257 pub fn with_availability(mut self, available: bool, reason: Option<String>) -> Self {
259 self.is_available = Some(available);
260 self.unavailable_reason = reason;
261 self
262 }
263
264 pub fn with_uptime(mut self, uptime_sec: f64) -> Self {
266 self.uptime_sec = Some(uptime_sec);
267 self
268 }
269}
270
271impl Task {
272 pub fn new(task_id: impl Into<String>, task_type: impl Into<String>) -> Self {
274 Self {
275 task_id: Some(task_id.into()),
276 task_type: Some(task_type.into()),
277 status: Some(TaskStatus::Queued),
278 ..Default::default()
279 }
280 }
281
282 pub fn with_status(mut self, status: TaskStatus) -> Self {
284 self.status = Some(status);
285 self
286 }
287
288 pub fn with_progress(mut self, pct: f64) -> Self {
290 self.progress_pct = Some(pct);
291 self
292 }
293
294 pub fn with_priority(mut self, priority: i32) -> Self {
296 self.priority = Some(priority);
297 self
298 }
299
300 pub fn with_destination(mut self, dest: impl Into<String>) -> Self {
302 self.destination_location = Some(dest.into());
303 self
304 }
305}
306
307impl OperationalError {
308 pub fn new(
310 code: impl Into<String>,
311 message: impl Into<String>,
312 severity: ErrorSeverity,
313 ) -> Self {
314 Self {
315 code: Some(code.into()),
316 message: Some(message.into()),
317 severity: Some(severity),
318 timestamp: Some(Utc::now()),
319 is_active: Some(true),
320 ..Default::default()
321 }
322 }
323
324 pub fn recoverable(mut self) -> Self {
326 self.recoverable = Some(true);
327 self
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_operational_domain() {
337 let op =
338 OperationalDomain::new(OperationalMode::Autonomous, OperationalState::ExecutingTask)
339 .with_task(Task::new("task-001", "transport").with_progress(50.0));
340
341 assert_eq!(op.mode, Some(OperationalMode::Autonomous));
342 assert_eq!(op.task.as_ref().unwrap().progress_pct, Some(50.0));
343 }
344
345 #[test]
346 fn test_task_creation() {
347 let task = Task::new("t-123", "pick")
348 .with_status(TaskStatus::InProgress)
349 .with_priority(5)
350 .with_destination("zone-b");
351
352 assert_eq!(task.task_id, Some("t-123".to_string()));
353 assert_eq!(task.status, Some(TaskStatus::InProgress));
354 }
355
356 #[test]
357 fn test_operational_serialization() {
358 let op = OperationalDomain::idle();
359 let json = serde_json::to_string(&op).unwrap();
360 assert!(json.contains("idle"));
361 assert!(json.contains("ready"));
362 }
363}