phytrace_sdk/models/domains/
actuators.rs

1//! Actuators Domain - Motors, joints, grippers, and lifts.
2//!
3//! Contains drive motor, joint, gripper, and lift actuator information.
4
5use serde::{Deserialize, Serialize};
6use validator::Validate;
7
8use crate::models::enums::{GripperState, LiftState, MotorStatus};
9
10/// Actuators domain containing motor and actuator information.
11#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
12pub struct ActuatorsDomain {
13    // === Drive Motors ===
14    /// Drive motor information
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub drive_motors: Option<Vec<DriveMotor>>,
17
18    // === Joints ===
19    /// Joint/arm information
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub joints: Option<Vec<Joint>>,
22
23    // === Grippers ===
24    /// Gripper information
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub grippers: Option<Vec<Gripper>>,
27
28    // === Lifts ===
29    /// Lift/elevator mechanism
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub lifts: Option<Vec<Lift>>,
32
33    // === Steering ===
34    /// Steering actuators
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub steering: Option<Vec<SteeringActuator>>,
37
38    // === Hydraulics ===
39    /// Hydraulic system status
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub hydraulics: Option<HydraulicSystem>,
42
43    // === Pneumatics ===
44    /// Pneumatic system status
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub pneumatics: Option<PneumaticSystem>,
47}
48
49/// Drive motor information.
50#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
51pub struct DriveMotor {
52    /// Motor ID
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub motor_id: Option<String>,
55
56    /// Motor name/location (e.g., "left_wheel", "right_wheel")
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub name: Option<String>,
59
60    /// Motor status
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub status: Option<MotorStatus>,
63
64    /// Current velocity in RPM
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub velocity_rpm: Option<f64>,
67
68    /// Commanded velocity in RPM
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub commanded_rpm: Option<f64>,
71
72    /// Current in amps
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub current_a: Option<f64>,
75
76    /// Voltage in volts
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub voltage_v: Option<f64>,
79
80    /// Power consumption in watts
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub power_w: Option<f64>,
83
84    /// Temperature in Celsius
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub temperature_c: Option<f64>,
87
88    /// Torque in Newton-meters
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub torque_nm: Option<f64>,
91
92    /// Encoder position (ticks or radians)
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub encoder_position: Option<f64>,
95
96    /// Whether motor is enabled
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub is_enabled: Option<bool>,
99
100    /// Error code if any
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub error_code: Option<String>,
103}
104
105/// Joint information (for arms/manipulators).
106#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
107pub struct Joint {
108    /// Joint ID
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub joint_id: Option<String>,
111
112    /// Joint name (e.g., "shoulder", "elbow", "wrist")
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub name: Option<String>,
115
116    /// Joint type (revolute, prismatic)
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub joint_type: Option<JointType>,
119
120    /// Current position (degrees for revolute, meters for prismatic)
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub position: Option<f64>,
123
124    /// Commanded position
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub commanded_position: Option<f64>,
127
128    /// Current velocity (deg/s or m/s)
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub velocity: Option<f64>,
131
132    /// Current effort/torque
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub effort: Option<f64>,
135
136    /// Temperature in Celsius
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub temperature_c: Option<f64>,
139
140    /// Minimum position limit
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub min_position: Option<f64>,
143
144    /// Maximum position limit
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub max_position: Option<f64>,
147
148    /// Motor status
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub status: Option<MotorStatus>,
151
152    /// Whether at limit
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub at_limit: Option<bool>,
155}
156
157/// Joint type.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
159#[serde(rename_all = "snake_case")]
160pub enum JointType {
161    /// Revolute (rotational) joint
162    #[default]
163    Revolute,
164    /// Prismatic (linear) joint
165    Prismatic,
166    /// Continuous (unlimited rotation) joint
167    Continuous,
168    /// Fixed joint
169    Fixed,
170}
171
172/// Gripper information.
173#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
174pub struct Gripper {
175    /// Gripper ID
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub gripper_id: Option<String>,
178
179    /// Gripper name
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub name: Option<String>,
182
183    /// Gripper type (parallel, vacuum, magnetic, etc.)
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub gripper_type: Option<String>,
186
187    /// Current state
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub state: Option<GripperState>,
190
191    /// Position/opening (0-100%)
192    #[serde(skip_serializing_if = "Option::is_none")]
193    #[validate(range(min = 0.0, max = 100.0))]
194    pub position_pct: Option<f64>,
195
196    /// Grip force in Newtons
197    #[serde(skip_serializing_if = "Option::is_none")]
198    #[validate(range(min = 0.0))]
199    pub force_n: Option<f64>,
200
201    /// Whether object is detected/held
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub object_detected: Option<bool>,
204
205    /// Vacuum pressure (for vacuum grippers) in kPa
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub vacuum_kpa: Option<f64>,
208
209    /// Whether grip is secure
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub grip_secure: Option<bool>,
212
213    /// Motor status
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub status: Option<MotorStatus>,
216}
217
218/// Lift/elevator mechanism.
219#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
220pub struct Lift {
221    /// Lift ID
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub lift_id: Option<String>,
224
225    /// Lift name
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub name: Option<String>,
228
229    /// Current state
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub state: Option<LiftState>,
232
233    /// Current height in meters
234    #[serde(skip_serializing_if = "Option::is_none")]
235    #[validate(range(min = 0.0))]
236    pub height_m: Option<f64>,
237
238    /// Target height in meters
239    #[serde(skip_serializing_if = "Option::is_none")]
240    #[validate(range(min = 0.0))]
241    pub target_height_m: Option<f64>,
242
243    /// Minimum height in meters
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub min_height_m: Option<f64>,
246
247    /// Maximum height in meters
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub max_height_m: Option<f64>,
250
251    /// Current load in kg
252    #[serde(skip_serializing_if = "Option::is_none")]
253    #[validate(range(min = 0.0))]
254    pub load_kg: Option<f64>,
255
256    /// Maximum load capacity in kg
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub max_load_kg: Option<f64>,
259
260    /// Velocity in m/s
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub velocity_mps: Option<f64>,
263
264    /// Motor status
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub status: Option<MotorStatus>,
267
268    /// Whether at top limit
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub at_top: Option<bool>,
271
272    /// Whether at bottom limit
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub at_bottom: Option<bool>,
275}
276
277/// Steering actuator.
278#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
279pub struct SteeringActuator {
280    /// Actuator ID
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub actuator_id: Option<String>,
283
284    /// Name (e.g., "front_steer")
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub name: Option<String>,
287
288    /// Current angle in degrees
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub angle_deg: Option<f64>,
291
292    /// Commanded angle in degrees
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub commanded_deg: Option<f64>,
295
296    /// Steering rate in deg/s
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub rate_dps: Option<f64>,
299
300    /// Minimum angle
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub min_angle_deg: Option<f64>,
303
304    /// Maximum angle
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub max_angle_deg: Option<f64>,
307
308    /// Status
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub status: Option<MotorStatus>,
311}
312
313/// Hydraulic system status.
314#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
315pub struct HydraulicSystem {
316    /// System pressure in bar
317    #[serde(skip_serializing_if = "Option::is_none")]
318    #[validate(range(min = 0.0))]
319    pub pressure_bar: Option<f64>,
320
321    /// Target pressure in bar
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub target_pressure_bar: Option<f64>,
324
325    /// Fluid temperature in Celsius
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub temperature_c: Option<f64>,
328
329    /// Fluid level percentage
330    #[serde(skip_serializing_if = "Option::is_none")]
331    #[validate(range(min = 0.0, max = 100.0))]
332    pub fluid_level_pct: Option<f64>,
333
334    /// Pump status
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub pump_status: Option<MotorStatus>,
337
338    /// Whether system is pressurized
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub is_pressurized: Option<bool>,
341}
342
343/// Pneumatic system status.
344#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
345pub struct PneumaticSystem {
346    /// System pressure in bar
347    #[serde(skip_serializing_if = "Option::is_none")]
348    #[validate(range(min = 0.0))]
349    pub pressure_bar: Option<f64>,
350
351    /// Tank pressure in bar
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub tank_pressure_bar: Option<f64>,
354
355    /// Compressor status
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub compressor_status: Option<MotorStatus>,
358
359    /// Whether compressor is running
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub compressor_running: Option<bool>,
362
363    /// Air flow rate in L/min
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub flow_rate_lpm: Option<f64>,
366}
367
368impl ActuatorsDomain {
369    /// Create with drive motors.
370    pub fn with_drive_motors(motors: Vec<DriveMotor>) -> Self {
371        Self {
372            drive_motors: Some(motors),
373            ..Default::default()
374        }
375    }
376
377    /// Add a joint.
378    pub fn with_joint(mut self, joint: Joint) -> Self {
379        let joints = self.joints.get_or_insert_with(Vec::new);
380        joints.push(joint);
381        self
382    }
383
384    /// Add a gripper.
385    pub fn with_gripper(mut self, gripper: Gripper) -> Self {
386        let grippers = self.grippers.get_or_insert_with(Vec::new);
387        grippers.push(gripper);
388        self
389    }
390
391    /// Add a lift.
392    pub fn with_lift(mut self, lift: Lift) -> Self {
393        let lifts = self.lifts.get_or_insert_with(Vec::new);
394        lifts.push(lift);
395        self
396    }
397}
398
399impl DriveMotor {
400    /// Create a drive motor.
401    pub fn new(name: impl Into<String>) -> Self {
402        Self {
403            name: Some(name.into()),
404            status: Some(MotorStatus::Ok),
405            is_enabled: Some(true),
406            ..Default::default()
407        }
408    }
409
410    /// Set velocity.
411    pub fn with_velocity(mut self, rpm: f64) -> Self {
412        self.velocity_rpm = Some(rpm);
413        self
414    }
415
416    /// Set current.
417    pub fn with_current(mut self, amps: f64) -> Self {
418        self.current_a = Some(amps);
419        self
420    }
421
422    /// Set temperature.
423    pub fn with_temperature(mut self, temp_c: f64) -> Self {
424        self.temperature_c = Some(temp_c);
425        self
426    }
427}
428
429impl Gripper {
430    /// Create an open gripper.
431    pub fn open(name: impl Into<String>) -> Self {
432        Self {
433            name: Some(name.into()),
434            state: Some(GripperState::Open),
435            position_pct: Some(100.0),
436            object_detected: Some(false),
437            status: Some(MotorStatus::Ok),
438            ..Default::default()
439        }
440    }
441
442    /// Create a gripper holding an object.
443    pub fn holding(name: impl Into<String>, force_n: f64) -> Self {
444        Self {
445            name: Some(name.into()),
446            state: Some(GripperState::Holding),
447            object_detected: Some(true),
448            grip_secure: Some(true),
449            force_n: Some(force_n),
450            status: Some(MotorStatus::Ok),
451            ..Default::default()
452        }
453    }
454}
455
456impl Lift {
457    /// Create a lift at a given height.
458    pub fn at_height(name: impl Into<String>, height_m: f64) -> Self {
459        Self {
460            name: Some(name.into()),
461            height_m: Some(height_m),
462            state: Some(if height_m < 0.1 {
463                LiftState::Lowered
464            } else {
465                LiftState::Intermediate
466            }),
467            status: Some(MotorStatus::Ok),
468            ..Default::default()
469        }
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_drive_motor() {
479        let motor = DriveMotor::new("left_wheel")
480            .with_velocity(100.0)
481            .with_current(2.5);
482
483        assert_eq!(motor.name, Some("left_wheel".to_string()));
484        assert_eq!(motor.velocity_rpm, Some(100.0));
485    }
486
487    #[test]
488    fn test_gripper_holding() {
489        let gripper = Gripper::holding("main_gripper", 15.0);
490        assert_eq!(gripper.state, Some(GripperState::Holding));
491        assert_eq!(gripper.grip_secure, Some(true));
492    }
493
494    #[test]
495    fn test_actuators_domain() {
496        let actuators = ActuatorsDomain::with_drive_motors(vec![
497            DriveMotor::new("left"),
498            DriveMotor::new("right"),
499        ])
500        .with_gripper(Gripper::open("gripper"));
501
502        assert_eq!(actuators.drive_motors.as_ref().unwrap().len(), 2);
503        assert_eq!(actuators.grippers.as_ref().unwrap().len(), 1);
504    }
505}