1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9use super::common::{Polygon2D, Position2D};
10use crate::models::enums::{
11 EStopType, SafetyState, SafetyZoneType, ViolationSeverity, ViolationType,
12};
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
16pub struct SafetyDomain {
17 #[serde(skip_serializing_if = "Option::is_none")]
20 pub safety_state: Option<SafetyState>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub is_safe: Option<bool>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
29 pub e_stop: Option<EStopInfo>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
34 pub protective_stop_active: Option<bool>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub protective_stop_reason: Option<String>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
43 pub zones: Option<Vec<SafetyZone>>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub current_zone_ids: Option<Vec<String>>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
52 pub proximity: Option<ProximityInfo>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
57 pub violations: Option<Vec<SafetyViolation>>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub violation_count: Option<u32>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
66 pub collaborative_operation: Option<CollaborativeOperation>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
71 #[validate(range(min = 0.0))]
72 pub speed_limit_mps: Option<f64>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub speed_limit_reason: Option<String>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
81 pub safety_system_ok: Option<bool>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub safety_plc_status: Option<String>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub last_safety_check: Option<DateTime<Utc>>,
90}
91
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
94pub struct EStopInfo {
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub is_active: Option<bool>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub e_stop_type: Option<EStopType>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub source: Option<String>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub reason: Option<String>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub triggered_at: Option<DateTime<Utc>>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub can_remote_reset: Option<bool>,
118}
119
120impl EStopInfo {
121 pub fn active(e_stop_type: EStopType, reason: impl Into<String>) -> Self {
123 Self {
124 is_active: Some(true),
125 e_stop_type: Some(e_stop_type),
126 reason: Some(reason.into()),
127 triggered_at: Some(Utc::now()),
128 ..Default::default()
129 }
130 }
131
132 pub fn inactive() -> Self {
134 Self {
135 is_active: Some(false),
136 ..Default::default()
137 }
138 }
139}
140
141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143pub struct SafetyZone {
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub zone_id: Option<String>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub name: Option<String>,
151
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub zone_type: Option<SafetyZoneType>,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub is_active: Option<bool>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub robot_inside: Option<bool>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub boundary: Option<Polygon2D>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub speed_limit_mps: Option<f64>,
171
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub required_action: Option<String>,
175}
176
177#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
179pub struct ProximityInfo {
180 #[serde(skip_serializing_if = "Option::is_none")]
182 #[validate(range(min = 0.0))]
183 pub closest_distance_m: Option<f64>,
184
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub closest_bearing_deg: Option<f64>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
191 #[validate(range(min = 0.0))]
192 pub closest_human_m: Option<f64>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub humans_nearby: Option<u32>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub warning_distance_m: Option<f64>,
201
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub stop_distance_m: Option<f64>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub warning_active: Option<bool>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub stop_active: Option<bool>,
213}
214
215#[derive(Debug, Clone, Default, Serialize, Deserialize)]
217pub struct SafetyViolation {
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub violation_id: Option<String>,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub violation_type: Option<ViolationType>,
225
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub severity: Option<ViolationSeverity>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub description: Option<String>,
233
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub timestamp: Option<DateTime<Utc>>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub location: Option<Position2D>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub zone_id: Option<String>,
245
246 #[serde(skip_serializing_if = "Option::is_none")]
248 pub is_active: Option<bool>,
249
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub resolution: Option<String>,
253
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub resolved_at: Option<DateTime<Utc>>,
257
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub measured_value: Option<f64>,
261
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub threshold_value: Option<f64>,
265}
266
267impl SafetyViolation {
268 pub fn new(
270 violation_type: ViolationType,
271 severity: ViolationSeverity,
272 description: impl Into<String>,
273 ) -> Self {
274 Self {
275 violation_id: Some(uuid::Uuid::new_v4().to_string()),
276 violation_type: Some(violation_type),
277 severity: Some(severity),
278 description: Some(description.into()),
279 timestamp: Some(Utc::now()),
280 is_active: Some(true),
281 ..Default::default()
282 }
283 }
284
285 pub fn speed_exceeded(actual_mps: f64, limit_mps: f64) -> Self {
287 Self::new(
288 ViolationType::SpeedExceeded,
289 if actual_mps > limit_mps * 1.5 {
290 ViolationSeverity::High
291 } else {
292 ViolationSeverity::Medium
293 },
294 format!(
295 "Speed {:.2} m/s exceeded limit of {:.2} m/s",
296 actual_mps, limit_mps
297 ),
298 )
299 .with_values(actual_mps, limit_mps)
300 }
301
302 pub fn proximity_violation(distance_m: f64, threshold_m: f64) -> Self {
304 Self::new(
305 ViolationType::ProximityViolation,
306 ViolationSeverity::High,
307 format!(
308 "Distance {:.2}m below threshold {:.2}m",
309 distance_m, threshold_m
310 ),
311 )
312 .with_values(distance_m, threshold_m)
313 }
314
315 pub fn with_values(mut self, measured: f64, threshold: f64) -> Self {
317 self.measured_value = Some(measured);
318 self.threshold_value = Some(threshold);
319 self
320 }
321
322 pub fn with_location(mut self, x: f64, y: f64) -> Self {
324 self.location = Some(Position2D::new(x, y));
325 self
326 }
327}
328
329#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
331pub struct CollaborativeOperation {
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub enabled: Option<bool>,
335
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub method: Option<CollaborativeMethod>,
339
340 #[serde(skip_serializing_if = "Option::is_none")]
342 #[validate(range(min = 0.0))]
343 pub max_speed_mps: Option<f64>,
344
345 #[serde(skip_serializing_if = "Option::is_none")]
347 #[validate(range(min = 0.0))]
348 pub max_force_n: Option<f64>,
349
350 #[serde(skip_serializing_if = "Option::is_none")]
352 #[validate(range(min = 0.0))]
353 pub max_pressure_n_cm2: Option<f64>,
354
355 #[serde(skip_serializing_if = "Option::is_none")]
357 #[validate(range(min = 0.0))]
358 pub separation_distance_m: Option<f64>,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub human_in_space: Option<bool>,
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
367#[serde(rename_all = "snake_case")]
368pub enum CollaborativeMethod {
369 #[default]
371 SafetyRatedMonitoredStop,
372 HandGuiding,
374 SpeedAndSeparationMonitoring,
376 PowerAndForceLimiting,
378}
379
380impl SafetyDomain {
381 pub fn new(state: SafetyState) -> Self {
383 Self {
384 safety_state: Some(state),
385 is_safe: Some(state == SafetyState::Normal),
386 ..Default::default()
387 }
388 }
389
390 pub fn normal() -> Self {
392 Self::new(SafetyState::Normal)
393 }
394
395 pub fn e_stopped(e_stop: EStopInfo) -> Self {
397 Self {
398 safety_state: Some(SafetyState::EmergencyStop),
399 is_safe: Some(false),
400 e_stop: Some(e_stop),
401 ..Default::default()
402 }
403 }
404
405 pub fn with_violation(mut self, violation: SafetyViolation) -> Self {
407 let violations = self.violations.get_or_insert_with(Vec::new);
408 violations.push(violation);
409 self.violation_count = Some(violations.len() as u32);
410 self
411 }
412
413 pub fn with_proximity(mut self, proximity: ProximityInfo) -> Self {
415 self.proximity = Some(proximity);
416 self
417 }
418
419 pub fn with_speed_limit(mut self, limit_mps: f64, reason: impl Into<String>) -> Self {
421 self.speed_limit_mps = Some(limit_mps);
422 self.speed_limit_reason = Some(reason.into());
423 self
424 }
425
426 pub fn with_collaborative(mut self, collab: CollaborativeOperation) -> Self {
428 self.collaborative_operation = Some(collab);
429 self
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_safety_normal() {
439 let safety = SafetyDomain::normal();
440 assert_eq!(safety.safety_state, Some(SafetyState::Normal));
441 assert_eq!(safety.is_safe, Some(true));
442 }
443
444 #[test]
445 fn test_safety_violation() {
446 let violation = SafetyViolation::speed_exceeded(2.5, 1.5);
447 assert_eq!(violation.violation_type, Some(ViolationType::SpeedExceeded));
448 assert_eq!(violation.measured_value, Some(2.5));
449 assert_eq!(violation.threshold_value, Some(1.5));
450 }
451
452 #[test]
453 fn test_e_stop() {
454 let e_stop = EStopInfo::active(EStopType::Hardware, "Button pressed");
455 let safety = SafetyDomain::e_stopped(e_stop);
456 assert_eq!(safety.safety_state, Some(SafetyState::EmergencyStop));
457 assert_eq!(safety.is_safe, Some(false));
458 }
459
460 #[test]
465 fn test_safety_domain_default() {
466 let safety = SafetyDomain::default();
467 assert!(safety.safety_state.is_none());
468 assert!(safety.is_safe.is_none());
469 assert!(safety.e_stop.is_none());
470 assert!(safety.protective_stop_active.is_none());
471 assert!(safety.zones.is_none());
472 assert!(safety.proximity.is_none());
473 assert!(safety.violations.is_none());
474 }
475
476 #[test]
477 fn test_safety_domain_new() {
478 let safety = SafetyDomain::new(SafetyState::ReducedSpeed);
479 assert_eq!(safety.safety_state, Some(SafetyState::ReducedSpeed));
480 assert_eq!(safety.is_safe, Some(false)); }
482
483 #[test]
484 fn test_safety_domain_with_violation() {
485 let violation = SafetyViolation::speed_exceeded(3.0, 2.0);
486 let safety = SafetyDomain::normal().with_violation(violation);
487
488 assert!(safety.violations.is_some());
489 assert_eq!(safety.violation_count, Some(1));
490 }
491
492 #[test]
493 fn test_safety_domain_with_multiple_violations() {
494 let v1 = SafetyViolation::speed_exceeded(3.0, 2.0);
495 let v2 = SafetyViolation {
496 violation_type: Some(ViolationType::ZoneViolation),
497 ..Default::default()
498 };
499
500 let safety = SafetyDomain::normal().with_violation(v1).with_violation(v2);
501
502 assert_eq!(safety.violation_count, Some(2));
503 }
504
505 #[test]
506 fn test_safety_domain_with_proximity() {
507 let proximity = ProximityInfo {
508 closest_distance_m: Some(1.5),
509 closest_human_m: Some(2.0),
510 humans_nearby: Some(3),
511 ..Default::default()
512 };
513
514 let safety = SafetyDomain::normal().with_proximity(proximity);
515 assert!(safety.proximity.is_some());
516 assert_eq!(
517 safety.proximity.as_ref().unwrap().closest_human_m,
518 Some(2.0)
519 );
520 }
521
522 #[test]
523 fn test_safety_domain_with_speed_limit() {
524 let safety = SafetyDomain::normal().with_speed_limit(0.5, "Zone restriction");
525
526 assert_eq!(safety.speed_limit_mps, Some(0.5));
527 assert_eq!(
528 safety.speed_limit_reason,
529 Some("Zone restriction".to_string())
530 );
531 }
532
533 #[test]
534 fn test_safety_domain_with_collaborative() {
535 let collab = CollaborativeOperation {
536 enabled: Some(true),
537 method: Some(CollaborativeMethod::PowerAndForceLimiting),
538 ..Default::default()
539 };
540
541 let safety = SafetyDomain::normal().with_collaborative(collab);
542 assert!(safety.collaborative_operation.is_some());
543 }
544
545 #[test]
546 fn test_safety_domain_chained_builders() {
547 let safety = SafetyDomain::normal()
548 .with_violation(SafetyViolation::speed_exceeded(2.0, 1.0))
549 .with_proximity(ProximityInfo::default())
550 .with_speed_limit(1.0, "Safety zone")
551 .with_collaborative(CollaborativeOperation::default());
552
553 assert!(safety.violations.is_some());
554 assert!(safety.proximity.is_some());
555 assert!(safety.speed_limit_mps.is_some());
556 assert!(safety.collaborative_operation.is_some());
557 }
558
559 #[test]
564 fn test_estop_info_active() {
565 let estop = EStopInfo::active(EStopType::Software, "Safety violation");
566
567 assert_eq!(estop.is_active, Some(true));
568 assert_eq!(estop.e_stop_type, Some(EStopType::Software));
569 assert_eq!(estop.reason, Some("Safety violation".to_string()));
570 assert!(estop.triggered_at.is_some());
571 }
572
573 #[test]
574 fn test_estop_info_inactive() {
575 let estop = EStopInfo::inactive();
576 assert_eq!(estop.is_active, Some(false));
577 }
578
579 #[test]
580 fn test_estop_info_default() {
581 let estop = EStopInfo::default();
582 assert!(estop.is_active.is_none());
583 assert!(estop.e_stop_type.is_none());
584 assert!(estop.source.is_none());
585 assert!(estop.reason.is_none());
586 assert!(estop.triggered_at.is_none());
587 assert!(estop.can_remote_reset.is_none());
588 }
589
590 #[test]
591 fn test_estop_info_full() {
592 let estop = EStopInfo {
593 is_active: Some(true),
594 e_stop_type: Some(EStopType::Remote),
595 source: Some("Fleet Manager".to_string()),
596 reason: Some("Maintenance required".to_string()),
597 triggered_at: Some(Utc::now()),
598 can_remote_reset: Some(true),
599 };
600
601 assert_eq!(estop.source, Some("Fleet Manager".to_string()));
602 assert_eq!(estop.can_remote_reset, Some(true));
603 }
604
605 #[test]
610 fn test_safety_zone_default() {
611 let zone = SafetyZone::default();
612 assert!(zone.zone_id.is_none());
613 assert!(zone.name.is_none());
614 assert!(zone.zone_type.is_none());
615 assert!(zone.is_active.is_none());
616 assert!(zone.robot_inside.is_none());
617 assert!(zone.boundary.is_none());
618 assert!(zone.speed_limit_mps.is_none());
619 assert!(zone.required_action.is_none());
620 }
621
622 #[test]
623 fn test_safety_zone_full() {
624 let zone = SafetyZone {
625 zone_id: Some("zone-001".to_string()),
626 name: Some("Restricted Area A".to_string()),
627 zone_type: Some(SafetyZoneType::Restricted),
628 is_active: Some(true),
629 robot_inside: Some(false),
630 boundary: Some(Polygon2D {
631 vertices: vec![],
632 closed: true,
633 }),
634 speed_limit_mps: Some(0.5),
635 required_action: Some("Slow down".to_string()),
636 };
637
638 assert_eq!(zone.zone_type, Some(SafetyZoneType::Restricted));
639 assert_eq!(zone.speed_limit_mps, Some(0.5));
640 }
641
642 #[test]
647 fn test_proximity_info_default() {
648 let prox = ProximityInfo::default();
649 assert!(prox.closest_distance_m.is_none());
650 assert!(prox.closest_bearing_deg.is_none());
651 assert!(prox.closest_human_m.is_none());
652 assert!(prox.humans_nearby.is_none());
653 assert!(prox.warning_distance_m.is_none());
654 }
655
656 #[test]
657 fn test_proximity_info_full() {
658 let prox = ProximityInfo {
659 closest_distance_m: Some(0.8),
660 closest_bearing_deg: Some(45.0),
661 closest_human_m: Some(1.2),
662 humans_nearby: Some(2),
663 warning_distance_m: Some(2.0),
664 stop_distance_m: Some(0.5),
665 warning_active: Some(true),
666 stop_active: Some(false),
667 };
668
669 assert_eq!(prox.closest_distance_m, Some(0.8));
670 assert_eq!(prox.humans_nearby, Some(2));
671 }
672
673 #[test]
678 fn test_safety_violation_speed_exceeded() {
679 let viol = SafetyViolation::speed_exceeded(4.0, 2.0);
681
682 assert!(viol.violation_id.is_some());
683 assert_eq!(viol.violation_type, Some(ViolationType::SpeedExceeded));
684 assert_eq!(viol.severity, Some(ViolationSeverity::High));
685 assert_eq!(viol.measured_value, Some(4.0));
686 assert_eq!(viol.threshold_value, Some(2.0));
687 assert!(viol.timestamp.is_some());
688 }
689
690 #[test]
691 fn test_safety_violation_default() {
692 let viol = SafetyViolation::default();
693 assert!(viol.violation_id.is_none());
694 assert!(viol.violation_type.is_none());
695 assert!(viol.severity.is_none());
696 assert!(viol.description.is_none());
697 assert!(viol.measured_value.is_none());
698 assert!(viol.threshold_value.is_none());
699 assert!(viol.timestamp.is_none());
700 assert!(viol.is_active.is_none());
701 }
702
703 #[test]
708 fn test_collaborative_operation_default() {
709 let collab = CollaborativeOperation::default();
710 assert!(collab.enabled.is_none());
711 assert!(collab.method.is_none());
712 }
713
714 #[test]
715 fn test_collaborative_operation_full() {
716 let collab = CollaborativeOperation {
717 enabled: Some(true),
718 method: Some(CollaborativeMethod::PowerAndForceLimiting),
719 max_speed_mps: Some(0.25),
720 max_force_n: Some(150.0),
721 max_pressure_n_cm2: Some(10.0),
722 separation_distance_m: Some(0.5),
723 human_in_space: Some(true),
724 };
725
726 assert_eq!(collab.max_speed_mps, Some(0.25));
727 assert_eq!(collab.human_in_space, Some(true));
728 }
729
730 #[test]
735 fn test_safety_domain_serialization_roundtrip() {
736 let safety = SafetyDomain::normal()
737 .with_violation(SafetyViolation::speed_exceeded(2.5, 1.5))
738 .with_speed_limit(1.0, "Safety zone");
739
740 let json = serde_json::to_string(&safety).unwrap();
741 let deserialized: SafetyDomain = serde_json::from_str(&json).unwrap();
742
743 assert_eq!(deserialized.safety_state, Some(SafetyState::Normal));
744 assert!(deserialized.violations.is_some());
745 assert_eq!(deserialized.speed_limit_mps, Some(1.0));
746 }
747}