phytrace_sdk/models/domains/
operational.rs

1//! Operational Domain - Mode, state, tasks, and errors.
2//!
3//! Contains operational mode, state, task information, and error tracking.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::models::enums::{ErrorSeverity, OperationalMode, OperationalState};
9
10/// Operational domain containing mode, state, and task information.
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct OperationalDomain {
13    // === Mode and State ===
14    /// Current operational mode
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub mode: Option<OperationalMode>,
17
18    /// Current operational state
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub state: Option<OperationalState>,
21
22    /// Previous state (for transition tracking)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub previous_state: Option<OperationalState>,
25
26    /// Time in current state (seconds)
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub time_in_state_sec: Option<f64>,
29
30    // === Task Information ===
31    /// Current task details
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub task: Option<Task>,
34
35    /// Task queue
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub queue: Option<TaskQueue>,
38
39    // === Errors ===
40    /// Active errors
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub errors: Option<Vec<OperationalError>>,
43
44    /// Error count
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub error_count: Option<u32>,
47
48    // === Availability ===
49    /// Whether robot is available for new tasks
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub is_available: Option<bool>,
52
53    /// Reason for unavailability
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub unavailable_reason: Option<String>,
56
57    // === Uptime ===
58    /// System uptime in seconds
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub uptime_sec: Option<f64>,
61
62    /// Time since last reboot
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub last_boot: Option<DateTime<Utc>>,
65
66    // === Mission/Session ===
67    /// Current mission ID
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub mission_id: Option<String>,
70
71    /// Current session ID
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub session_id: Option<String>,
74}
75
76/// Current task information.
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78pub struct Task {
79    /// Task identifier
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub task_id: Option<String>,
82
83    /// Task type (e.g., "pick", "transport", "inspect")
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub task_type: Option<String>,
86
87    /// Task name or description
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub name: Option<String>,
90
91    /// Task status
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub status: Option<TaskStatus>,
94
95    /// Task priority (higher = more urgent)
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub priority: Option<i32>,
98
99    /// Progress percentage (0-100)
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub progress_pct: Option<f64>,
102
103    /// Current step in task
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub current_step: Option<String>,
106
107    /// Total steps in task
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub total_steps: Option<u32>,
110
111    /// Completed steps
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub completed_steps: Option<u32>,
114
115    /// Task start time
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub started_at: Option<DateTime<Utc>>,
118
119    /// Expected completion time
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub eta: Option<DateTime<Utc>>,
122
123    /// Elapsed time in seconds
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub elapsed_sec: Option<f64>,
126
127    /// Source/origin location
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub source_location: Option<String>,
130
131    /// Destination location
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub destination_location: Option<String>,
134
135    /// Associated payload/item IDs
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub payload_ids: Option<Vec<String>>,
138
139    /// Requesting system or user
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub requester: Option<String>,
142}
143
144/// Task status.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
146#[serde(rename_all = "snake_case")]
147pub enum TaskStatus {
148    /// Task queued
149    #[default]
150    Queued,
151    /// Task assigned to robot
152    Assigned,
153    /// Task in progress
154    InProgress,
155    /// Task paused
156    Paused,
157    /// Task completed successfully
158    Completed,
159    /// Task failed
160    Failed,
161    /// Task cancelled
162    Cancelled,
163    /// Task blocked (waiting for something)
164    Blocked,
165}
166
167/// Task queue information.
168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub struct TaskQueue {
170    /// Number of tasks in queue
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub length: Option<u32>,
173
174    /// Queue capacity
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub capacity: Option<u32>,
177
178    /// Task IDs in queue (in order)
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub task_ids: Option<Vec<String>>,
181
182    /// Estimated time to complete queue
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub estimated_completion_min: Option<f64>,
185}
186
187/// Operational error.
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct OperationalError {
190    /// Error code
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub code: Option<String>,
193
194    /// Error message
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub message: Option<String>,
197
198    /// Error severity
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub severity: Option<ErrorSeverity>,
201
202    /// Error category/component
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub category: Option<String>,
205
206    /// Time error occurred
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub timestamp: Option<DateTime<Utc>>,
209
210    /// Whether error is recoverable
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub recoverable: Option<bool>,
213
214    /// Recovery action taken or suggested
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub recovery_action: Option<String>,
217
218    /// Whether error is currently active
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub is_active: Option<bool>,
221}
222
223impl OperationalDomain {
224    /// Create with mode and state.
225    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    /// Create an idle state.
234    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    /// Builder to add task.
244    pub fn with_task(mut self, task: Task) -> Self {
245        self.task = Some(task);
246        self
247    }
248
249    /// Builder to add error.
250    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    /// Builder to set availability.
258    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    /// Builder to add uptime.
265    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    /// Create a new task.
273    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    /// Builder to set status.
283    pub fn with_status(mut self, status: TaskStatus) -> Self {
284        self.status = Some(status);
285        self
286    }
287
288    /// Builder to set progress.
289    pub fn with_progress(mut self, pct: f64) -> Self {
290        self.progress_pct = Some(pct);
291        self
292    }
293
294    /// Builder to set priority.
295    pub fn with_priority(mut self, priority: i32) -> Self {
296        self.priority = Some(priority);
297        self
298    }
299
300    /// Builder to set destination.
301    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    /// Create a new error.
309    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    /// Mark as recoverable.
325    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}