phytrace_sdk/models/domains/
perception.rs

1//! Perception Domain - Sensors and detections.
2//!
3//! Contains LiDAR, camera, IMU, GPS, and detection information.
4
5use serde::{Deserialize, Serialize};
6use validator::Validate;
7
8use super::common::{BoundingBox, BoundingBox2D, ObjectRef, Position3D};
9use crate::models::enums::{DetectionConfidence, GpsFixType, SensorStatus};
10
11/// Perception domain containing sensor and detection data.
12#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
13pub struct PerceptionDomain {
14    // === LiDAR ===
15    /// LiDAR sensor data
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub lidar: Option<Vec<LidarSensor>>,
18
19    // === Cameras ===
20    /// Camera sensor data
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub cameras: Option<Vec<CameraSensor>>,
23
24    // === IMU ===
25    /// IMU sensor data
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub imu: Option<ImuSensor>,
28
29    // === GPS ===
30    /// GPS sensor data
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub gps: Option<GpsSensor>,
33
34    // === UWB ===
35    /// Ultra-wideband sensor data
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub uwb: Option<UwbSensor>,
38
39    // === Detections ===
40    /// Object detections
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub detections: Option<Vec<Detection>>,
43
44    /// Detection count
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub detection_count: Option<u32>,
47
48    // === Environment ===
49    /// Environment perception data
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub environment: Option<EnvironmentPerception>,
52
53    // === Other Sensors ===
54    /// Ultrasonic sensors
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub ultrasonics: Option<Vec<UltrasonicSensor>>,
57
58    /// Bumper/contact sensors
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub bumpers: Option<Vec<BumperSensor>>,
61
62    /// Cliff sensors
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub cliff_sensors: Option<Vec<CliffSensor>>,
65}
66
67/// LiDAR sensor information.
68#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
69pub struct LidarSensor {
70    /// Sensor ID
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub sensor_id: Option<String>,
73
74    /// Sensor name
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub name: Option<String>,
77
78    /// Sensor status
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub status: Option<SensorStatus>,
81
82    /// Sensor type/model
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub sensor_type: Option<String>,
85
86    /// Number of points in current scan
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub point_count: Option<u32>,
89
90    /// Scan frequency in Hz
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub scan_frequency_hz: Option<f64>,
93
94    /// Minimum range in meters
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub min_range_m: Option<f64>,
97
98    /// Maximum range in meters
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub max_range_m: Option<f64>,
101
102    /// Minimum angle in degrees
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub min_angle_deg: Option<f64>,
105
106    /// Maximum angle in degrees
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub max_angle_deg: Option<f64>,
109
110    /// Angular resolution in degrees
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub angular_resolution_deg: Option<f64>,
113
114    /// Whether sensor is 3D
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub is_3d: Option<bool>,
117
118    /// Closest detected range in meters
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub closest_range_m: Option<f64>,
121
122    /// Angle of closest detection in degrees
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub closest_angle_deg: Option<f64>,
125}
126
127/// Camera sensor information.
128#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
129pub struct CameraSensor {
130    /// Sensor ID
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub sensor_id: Option<String>,
133
134    /// Camera name
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub name: Option<String>,
137
138    /// Sensor status
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub status: Option<SensorStatus>,
141
142    /// Camera type (rgb, depth, stereo, thermal)
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub camera_type: Option<String>,
145
146    /// Image width in pixels
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub width: Option<u32>,
149
150    /// Image height in pixels
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub height: Option<u32>,
153
154    /// Frame rate in fps
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub frame_rate_fps: Option<f64>,
157
158    /// Field of view in degrees
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub fov_deg: Option<f64>,
161
162    /// Whether depth is available
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub has_depth: Option<bool>,
165
166    /// Exposure time in ms
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub exposure_ms: Option<f64>,
169
170    /// Gain level
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub gain: Option<f64>,
173}
174
175/// IMU sensor information.
176#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
177pub struct ImuSensor {
178    /// Sensor status
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub status: Option<SensorStatus>,
181
182    /// Linear acceleration X (m/s²)
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub accel_x_mps2: Option<f64>,
185
186    /// Linear acceleration Y (m/s²)
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub accel_y_mps2: Option<f64>,
189
190    /// Linear acceleration Z (m/s²)
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub accel_z_mps2: Option<f64>,
193
194    /// Angular velocity X (deg/s)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub gyro_x_dps: Option<f64>,
197
198    /// Angular velocity Y (deg/s)
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub gyro_y_dps: Option<f64>,
201
202    /// Angular velocity Z (deg/s)
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub gyro_z_dps: Option<f64>,
205
206    /// Magnetometer X (µT)
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub mag_x_ut: Option<f64>,
209
210    /// Magnetometer Y (µT)
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub mag_y_ut: Option<f64>,
213
214    /// Magnetometer Z (µT)
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub mag_z_ut: Option<f64>,
217
218    /// Temperature in Celsius
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub temperature_c: Option<f64>,
221
222    /// Update rate in Hz
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub update_rate_hz: Option<f64>,
225}
226
227/// GPS sensor information.
228#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
229pub struct GpsSensor {
230    /// Sensor status
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub status: Option<SensorStatus>,
233
234    /// Fix type
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub fix_type: Option<GpsFixType>,
237
238    /// Number of satellites
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub satellites: Option<u32>,
241
242    /// HDOP (horizontal dilution of precision)
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub hdop: Option<f64>,
245
246    /// VDOP (vertical dilution of precision)
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub vdop: Option<f64>,
249
250    /// PDOP (position dilution of precision)
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub pdop: Option<f64>,
253
254    /// Horizontal accuracy in meters
255    #[serde(skip_serializing_if = "Option::is_none")]
256    #[validate(range(min = 0.0))]
257    pub horizontal_accuracy_m: Option<f64>,
258
259    /// Vertical accuracy in meters
260    #[serde(skip_serializing_if = "Option::is_none")]
261    #[validate(range(min = 0.0))]
262    pub vertical_accuracy_m: Option<f64>,
263
264    /// Speed over ground in m/s
265    #[serde(skip_serializing_if = "Option::is_none")]
266    #[validate(range(min = 0.0))]
267    pub speed_mps: Option<f64>,
268
269    /// Course over ground in degrees
270    #[serde(skip_serializing_if = "Option::is_none")]
271    #[validate(range(min = 0.0, max = 360.0))]
272    pub course_deg: Option<f64>,
273}
274
275/// UWB (Ultra-Wideband) sensor information.
276#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
277pub struct UwbSensor {
278    /// Sensor status
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub status: Option<SensorStatus>,
281
282    /// Number of anchors in range
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub anchors_in_range: Option<u32>,
285
286    /// Position accuracy in meters
287    #[serde(skip_serializing_if = "Option::is_none")]
288    #[validate(range(min = 0.0))]
289    pub position_accuracy_m: Option<f64>,
290
291    /// Anchor measurements
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub anchors: Option<Vec<UwbAnchor>>,
294}
295
296/// UWB anchor measurement.
297#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
298pub struct UwbAnchor {
299    /// Anchor ID
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub anchor_id: Option<String>,
302
303    /// Distance to anchor in meters
304    #[serde(skip_serializing_if = "Option::is_none")]
305    #[validate(range(min = 0.0))]
306    pub distance_m: Option<f64>,
307
308    /// Signal strength (RSSI)
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub rssi_dbm: Option<f64>,
311}
312
313/// Object detection.
314#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
315pub struct Detection {
316    /// Detection ID
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub detection_id: Option<String>,
319
320    /// Object reference (for tracking)
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub object: Option<ObjectRef>,
323
324    /// Object class/label
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub class_label: Option<String>,
327
328    /// Confidence level
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub confidence: Option<DetectionConfidence>,
331
332    /// Confidence score (0-1)
333    #[serde(skip_serializing_if = "Option::is_none")]
334    #[validate(range(min = 0.0, max = 1.0))]
335    pub confidence_score: Option<f64>,
336
337    /// 3D bounding box
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub bounding_box_3d: Option<BoundingBox>,
340
341    /// 2D bounding box (in image)
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub bounding_box_2d: Option<BoundingBox2D>,
344
345    /// Position relative to robot
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub position: Option<Position3D>,
348
349    /// Distance to detection in meters
350    #[serde(skip_serializing_if = "Option::is_none")]
351    #[validate(range(min = 0.0))]
352    pub distance_m: Option<f64>,
353
354    /// Detection source sensor
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub source_sensor: Option<String>,
357
358    /// Whether this is a human detection
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub is_human: Option<bool>,
361
362    /// Velocity if tracked (m/s)
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub velocity_mps: Option<f64>,
365
366    /// Heading if tracked (degrees)
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub heading_deg: Option<f64>,
369}
370
371/// Environment perception data.
372#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
373pub struct EnvironmentPerception {
374    /// Ambient light level (lux)
375    #[serde(skip_serializing_if = "Option::is_none")]
376    #[validate(range(min = 0.0))]
377    pub ambient_light_lux: Option<f64>,
378
379    /// Surface type detected
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub surface_type: Option<String>,
382
383    /// Surface friction estimate (0-1)
384    #[serde(skip_serializing_if = "Option::is_none")]
385    #[validate(range(min = 0.0, max = 1.0))]
386    pub surface_friction: Option<f64>,
387
388    /// Detected incline in degrees
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub incline_deg: Option<f64>,
391
392    /// Visibility estimate (meters)
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub visibility_m: Option<f64>,
395
396    /// Whether environment is cluttered
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub is_cluttered: Option<bool>,
399}
400
401/// Ultrasonic sensor reading.
402#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
403pub struct UltrasonicSensor {
404    /// Sensor ID
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub sensor_id: Option<String>,
407
408    /// Status
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub status: Option<SensorStatus>,
411
412    /// Range reading in meters
413    #[serde(skip_serializing_if = "Option::is_none")]
414    #[validate(range(min = 0.0))]
415    pub range_m: Option<f64>,
416
417    /// Minimum range capability
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub min_range_m: Option<f64>,
420
421    /// Maximum range capability
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub max_range_m: Option<f64>,
424}
425
426/// Bumper/contact sensor.
427#[derive(Debug, Clone, Default, Serialize, Deserialize)]
428pub struct BumperSensor {
429    /// Sensor ID
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub sensor_id: Option<String>,
432
433    /// Whether bumper is triggered
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub is_triggered: Option<bool>,
436
437    /// Location (front, rear, left, right)
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub location: Option<String>,
440}
441
442/// Cliff sensor.
443#[derive(Debug, Clone, Default, Serialize, Deserialize)]
444pub struct CliffSensor {
445    /// Sensor ID
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub sensor_id: Option<String>,
448
449    /// Whether cliff is detected
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub cliff_detected: Option<bool>,
452
453    /// Location (front_left, front_right, etc.)
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub location: Option<String>,
456}
457
458impl PerceptionDomain {
459    /// Create with a LiDAR sensor.
460    pub fn with_lidar(sensor: LidarSensor) -> Self {
461        Self {
462            lidar: Some(vec![sensor]),
463            ..Default::default()
464        }
465    }
466
467    /// Add a detection.
468    pub fn with_detection(mut self, detection: Detection) -> Self {
469        let detections = self.detections.get_or_insert_with(Vec::new);
470        detections.push(detection);
471        self.detection_count = Some(detections.len() as u32);
472        self
473    }
474
475    /// Add IMU data.
476    pub fn with_imu(mut self, imu: ImuSensor) -> Self {
477        self.imu = Some(imu);
478        self
479    }
480
481    /// Add GPS data.
482    pub fn with_gps(mut self, gps: GpsSensor) -> Self {
483        self.gps = Some(gps);
484        self
485    }
486}
487
488impl Detection {
489    /// Create a human detection.
490    pub fn human(distance_m: f64, confidence: f64) -> Self {
491        Self {
492            class_label: Some("person".to_string()),
493            is_human: Some(true),
494            distance_m: Some(distance_m),
495            confidence_score: Some(confidence),
496            ..Default::default()
497        }
498    }
499
500    /// Create a generic detection.
501    pub fn new(class_label: impl Into<String>, distance_m: f64, confidence: f64) -> Self {
502        Self {
503            class_label: Some(class_label.into()),
504            distance_m: Some(distance_m),
505            confidence_score: Some(confidence),
506            ..Default::default()
507        }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514
515    #[test]
516    fn test_detection_human() {
517        let det = Detection::human(2.5, 0.95);
518        assert_eq!(det.is_human, Some(true));
519        assert_eq!(det.distance_m, Some(2.5));
520    }
521
522    #[test]
523    fn test_perception_domain() {
524        let perception = PerceptionDomain::default()
525            .with_detection(Detection::human(3.0, 0.9))
526            .with_detection(Detection::new("forklift", 5.0, 0.85));
527
528        assert_eq!(perception.detection_count, Some(2));
529    }
530}