1use crate::models::{Capability, CapabilityExt, HumanMachinePairExt, Operator};
9use crate::traits::Phase;
10use uuid::Uuid;
11
12pub use peat_schema::node::v1::{HealthStatus, NodeConfig, NodeState};
14
15pub trait NodeConfigExt {
17 fn new(platform_type: String) -> Self;
19
20 fn with_operator(
22 platform_type: String,
23 operator_binding: peat_schema::node::v1::HumanMachinePair,
24 ) -> Self;
25
26 fn add_capability(&mut self, capability: Capability);
28
29 fn has_capability_type(&self, capability_type: crate::models::CapabilityType) -> bool;
31
32 fn get_capabilities_by_type(
34 &self,
35 capability_type: crate::models::CapabilityType,
36 ) -> Vec<&Capability>;
37
38 fn has_operator(&self) -> bool;
40
41 fn is_human_operated(&self) -> bool;
43
44 fn get_primary_operator(&self) -> Option<&Operator>;
46
47 fn get_operator_binding(&self) -> Option<&peat_schema::node::v1::HumanMachinePair>;
49
50 fn set_operator_binding(&mut self, binding: Option<peat_schema::node::v1::HumanMachinePair>);
52
53 fn is_autonomous(&self) -> bool;
55}
56
57impl NodeConfigExt for NodeConfig {
58 fn new(platform_type: String) -> Self {
59 Self {
60 id: Uuid::new_v4().to_string(),
61 platform_type,
62 capabilities: Vec::new(),
63 comm_range_m: 1000.0,
64 max_speed_mps: 10.0,
65 operator_binding: None,
66 created_at: None,
67 }
68 }
69
70 fn with_operator(
71 platform_type: String,
72 operator_binding: peat_schema::node::v1::HumanMachinePair,
73 ) -> Self {
74 Self {
75 id: Uuid::new_v4().to_string(),
76 platform_type,
77 capabilities: Vec::new(),
78 comm_range_m: 1000.0,
79 max_speed_mps: 10.0,
80 operator_binding: Some(operator_binding),
81 created_at: None,
82 }
83 }
84
85 fn add_capability(&mut self, capability: Capability) {
86 if !self.capabilities.iter().any(|c| c.id == capability.id) {
88 self.capabilities.push(capability);
89 }
90 }
91
92 fn has_capability_type(&self, capability_type: crate::models::CapabilityType) -> bool {
93 self.capabilities
94 .iter()
95 .any(|c| c.get_capability_type() == capability_type)
96 }
97
98 fn get_capabilities_by_type(
99 &self,
100 capability_type: crate::models::CapabilityType,
101 ) -> Vec<&Capability> {
102 self.capabilities
103 .iter()
104 .filter(|c| c.get_capability_type() == capability_type)
105 .collect()
106 }
107
108 fn has_operator(&self) -> bool {
109 self.operator_binding.is_some()
110 }
111
112 fn is_human_operated(&self) -> bool {
113 self.operator_binding
114 .as_ref()
115 .map(|binding| !binding.operators.is_empty())
116 .unwrap_or(false)
117 }
118
119 fn get_primary_operator(&self) -> Option<&Operator> {
120 self.operator_binding
121 .as_ref()
122 .and_then(|binding| binding.primary_operator())
123 }
124
125 fn get_operator_binding(&self) -> Option<&peat_schema::node::v1::HumanMachinePair> {
126 self.operator_binding.as_ref()
127 }
128
129 fn set_operator_binding(&mut self, binding: Option<peat_schema::node::v1::HumanMachinePair>) {
130 self.operator_binding = binding;
131 }
132
133 fn is_autonomous(&self) -> bool {
134 !self.is_human_operated()
135 }
136}
137
138pub trait NodeStateExt {
140 fn new(position: (f64, f64, f64)) -> Self;
142
143 fn update_timestamp(&mut self);
145
146 fn get_position(&self) -> (f64, f64, f64);
148
149 fn update_position(&mut self, position: (f64, f64, f64));
151
152 fn get_health(&self) -> HealthStatus;
154
155 fn update_health(&mut self, health: HealthStatus);
157
158 fn get_phase(&self) -> Phase;
160
161 fn update_phase(&mut self, phase: Phase);
163
164 fn assign_cell(&mut self, cell_id: String);
166
167 fn leave_cell(&mut self);
169
170 fn assign_zone(&mut self, zone_id: String);
172
173 fn leave_zone(&mut self);
175
176 fn consume_fuel(&mut self, minutes: u32);
178
179 fn replenish_fuel(&mut self, minutes: u32);
181
182 fn is_operational(&self) -> bool;
184
185 fn needs_refuel(&self) -> bool;
187
188 fn merge(&mut self, other: &NodeState);
190}
191
192impl NodeStateExt for NodeState {
193 fn new(position: (f64, f64, f64)) -> Self {
194 use peat_schema::common::v1::Position;
195
196 Self {
197 position: Some(Position {
198 latitude: position.0,
199 longitude: position.1,
200 altitude: position.2,
201 }),
202 fuel_minutes: 120,
203 health: HealthStatus::Nominal as i32,
204 phase: peat_schema::node::v1::Phase::Discovery as i32,
205 cell_id: None,
206 zone_id: None,
207 timestamp: Some(peat_schema::common::v1::Timestamp {
208 seconds: std::time::SystemTime::now()
209 .duration_since(std::time::UNIX_EPOCH)
210 .unwrap()
211 .as_secs(),
212 nanos: 0,
213 }),
214 }
215 }
216
217 fn update_timestamp(&mut self) {
218 self.timestamp = Some(peat_schema::common::v1::Timestamp {
219 seconds: std::time::SystemTime::now()
220 .duration_since(std::time::UNIX_EPOCH)
221 .unwrap()
222 .as_secs(),
223 nanos: 0,
224 });
225 }
226
227 fn get_position(&self) -> (f64, f64, f64) {
228 if let Some(ref pos) = self.position {
229 (pos.latitude, pos.longitude, pos.altitude)
230 } else {
231 (0.0, 0.0, 0.0)
232 }
233 }
234
235 fn update_position(&mut self, position: (f64, f64, f64)) {
236 use peat_schema::common::v1::Position;
237 self.position = Some(Position {
238 latitude: position.0,
239 longitude: position.1,
240 altitude: position.2,
241 });
242 self.update_timestamp();
243 }
244
245 fn get_health(&self) -> HealthStatus {
246 HealthStatus::try_from(self.health).unwrap_or(HealthStatus::Unspecified)
247 }
248
249 fn update_health(&mut self, health: HealthStatus) {
250 self.health = health as i32;
251 self.update_timestamp();
252 }
253
254 fn get_phase(&self) -> Phase {
255 let proto_phase = peat_schema::node::v1::Phase::try_from(self.phase)
256 .unwrap_or(peat_schema::node::v1::Phase::Unspecified);
257 match proto_phase {
258 peat_schema::node::v1::Phase::Discovery => Phase::Discovery,
259 peat_schema::node::v1::Phase::Cell => Phase::Cell,
260 peat_schema::node::v1::Phase::Hierarchy => Phase::Hierarchy,
261 _ => Phase::Discovery,
262 }
263 }
264
265 fn update_phase(&mut self, phase: Phase) {
266 self.phase = phase as i32;
267 self.update_timestamp();
268 }
269
270 fn assign_cell(&mut self, cell_id: String) {
271 self.cell_id = Some(cell_id);
272 self.update_timestamp();
273 }
274
275 fn leave_cell(&mut self) {
276 self.cell_id = None;
277 self.update_timestamp();
278 }
279
280 fn assign_zone(&mut self, zone_id: String) {
281 self.zone_id = Some(zone_id);
282 self.update_timestamp();
283 }
284
285 fn leave_zone(&mut self) {
286 self.zone_id = None;
287 self.update_timestamp();
288 }
289
290 fn consume_fuel(&mut self, minutes: u32) {
291 self.fuel_minutes = self.fuel_minutes.saturating_sub(minutes);
292 self.update_timestamp();
293 }
294
295 fn replenish_fuel(&mut self, minutes: u32) {
296 self.fuel_minutes = self.fuel_minutes.saturating_add(minutes);
297 self.update_timestamp();
298 }
299
300 fn is_operational(&self) -> bool {
301 self.get_health() != HealthStatus::Failed && self.fuel_minutes > 0
302 }
303
304 fn needs_refuel(&self) -> bool {
305 self.fuel_minutes < 30 }
307
308 fn merge(&mut self, other: &NodeState) {
309 let self_ts = self.timestamp.as_ref().map(|t| t.seconds).unwrap_or(0);
310 let other_ts = other.timestamp.as_ref().map(|t| t.seconds).unwrap_or(0);
311
312 if other_ts > self_ts {
313 self.position = other.position;
314 self.health = other.health;
315 self.phase = other.phase;
316 self.cell_id = other.cell_id.clone();
317 self.zone_id = other.zone_id.clone();
318 self.fuel_minutes = other.fuel_minutes;
319 self.timestamp = other.timestamp;
320 }
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::models::{AuthorityLevel, BindingType, CapabilityType, HumanMachinePair};
328
329 #[test]
330 fn test_platform_config_add_capability() {
331 let mut config = NodeConfig::new("UAV".to_string());
332
333 let cap1 = Capability::new(
334 "camera_1".to_string(),
335 "HD Camera".to_string(),
336 CapabilityType::Sensor,
337 0.9,
338 );
339 let cap2 = Capability::new(
340 "gps_1".to_string(),
341 "GPS".to_string(),
342 CapabilityType::Sensor,
343 1.0,
344 );
345
346 config.add_capability(cap1.clone());
347 config.add_capability(cap2);
348
349 assert_eq!(config.capabilities.len(), 2);
350
351 config.add_capability(cap1);
353 assert_eq!(config.capabilities.len(), 2);
354 }
355
356 #[test]
357 fn test_platform_config_has_capability_type() {
358 let mut config = NodeConfig::new("UAV".to_string());
359 assert!(!config.has_capability_type(CapabilityType::Sensor));
360
361 config.add_capability(Capability::new(
362 "camera".to_string(),
363 "Camera".to_string(),
364 CapabilityType::Sensor,
365 0.9,
366 ));
367
368 assert!(config.has_capability_type(CapabilityType::Sensor));
369 assert!(!config.has_capability_type(CapabilityType::Compute));
370 }
371
372 #[test]
373 fn test_platform_state_lww_operations() {
374 let mut state = NodeState::new((37.7, -122.4, 100.0));
375 let initial_timestamp = state.timestamp.as_ref().map(|t| t.seconds).unwrap_or(0);
376
377 std::thread::sleep(std::time::Duration::from_secs(1));
379 state.update_position((37.8, -122.5, 150.0));
380 assert!(state.timestamp.as_ref().map(|t| t.seconds).unwrap_or(0) > initial_timestamp);
381 assert_eq!(state.get_position(), (37.8, -122.5, 150.0));
382
383 state.update_health(HealthStatus::Degraded);
385 assert_eq!(state.get_health(), HealthStatus::Degraded);
386
387 state.update_phase(Phase::Cell);
389 assert_eq!(state.get_phase(), Phase::Cell);
390
391 state.assign_cell("cell_1".to_string());
393 assert_eq!(state.cell_id, Some("cell_1".to_string()));
394
395 state.leave_cell();
396 assert_eq!(state.cell_id, None);
397
398 state.assign_zone("zone_1".to_string());
400 assert_eq!(state.zone_id, Some("zone_1".to_string()));
401
402 state.leave_zone();
403 assert_eq!(state.zone_id, None);
404 }
405
406 #[test]
407 fn test_platform_state_fuel_counter() {
408 let mut state = NodeState::new((0.0, 0.0, 0.0));
409 assert_eq!(state.fuel_minutes, 120);
410
411 state.consume_fuel(30);
413 assert_eq!(state.fuel_minutes, 90);
414
415 state.replenish_fuel(20);
417 assert_eq!(state.fuel_minutes, 110);
418
419 state.consume_fuel(200);
421 assert_eq!(state.fuel_minutes, 0);
422
423 state.replenish_fuel(150);
425 assert_eq!(state.fuel_minutes, 150);
426 }
427
428 #[test]
429 fn test_platform_state_operational_checks() {
430 let mut state = NodeState::new((0.0, 0.0, 0.0));
431 assert!(state.is_operational());
432
433 state.consume_fuel(120);
435 assert!(!state.is_operational());
436
437 state.replenish_fuel(50);
438 assert!(state.is_operational());
439
440 state.update_health(HealthStatus::Failed);
442 assert!(!state.is_operational());
443 }
444
445 #[test]
446 fn test_platform_state_needs_refuel() {
447 let mut state = NodeState::new((0.0, 0.0, 0.0));
448 assert!(!state.needs_refuel());
449
450 state.consume_fuel(100);
451 assert!(state.needs_refuel());
452 }
453
454 #[test]
455 fn test_platform_state_merge_lww() {
456 let mut state1 = NodeState::new((37.7, -122.4, 100.0));
457 let mut state2 = state1.clone();
458
459 std::thread::sleep(std::time::Duration::from_secs(1));
461 state2.update_position((37.8, -122.5, 150.0));
462 state2.update_health(HealthStatus::Degraded);
463
464 state1.merge(&state2);
466
467 assert_eq!(state1.get_position(), (37.8, -122.5, 150.0));
468 assert_eq!(state1.get_health(), HealthStatus::Degraded);
469 assert_eq!(
470 state1.timestamp.as_ref().map(|t| t.seconds),
471 state2.timestamp.as_ref().map(|t| t.seconds)
472 );
473 }
474
475 #[test]
476 fn test_platform_state_merge_older_ignored() {
477 let mut state1 = NodeState::new((37.7, -122.4, 100.0));
478 std::thread::sleep(std::time::Duration::from_secs(1));
479 state1.update_position((37.8, -122.5, 150.0));
480
481 let state2 = NodeState::new((38.0, -123.0, 200.0));
482
483 let original_pos = state1.get_position();
485 state1.merge(&state2);
486
487 assert_eq!(state1.get_position(), original_pos);
488 }
489
490 #[test]
491 fn test_platform_config_autonomous() {
492 let config = NodeConfig::new("UAV".to_string());
493
494 assert!(!config.has_operator());
495 assert!(!config.is_human_operated());
496 assert!(config.is_autonomous());
497 assert!(config.get_primary_operator().is_none());
498 assert!(config.get_operator_binding().is_none());
499 }
500
501 #[test]
502 fn test_platform_config_with_operator() {
503 use crate::models::{OperatorExt, OperatorRank};
504
505 let operator = Operator::new(
506 "op_1".to_string(),
507 "SSG Smith".to_string(),
508 OperatorRank::E6,
509 AuthorityLevel::Commander,
510 "11B".to_string(), );
512
513 let binding = HumanMachinePair::new(
514 vec![operator],
515 vec!["node_1".to_string()],
516 BindingType::OneToOne,
517 );
518
519 let config = NodeConfig::with_operator("Soldier System".to_string(), binding);
520
521 assert!(config.has_operator());
522 assert!(config.is_human_operated());
523 assert!(!config.is_autonomous());
524
525 let primary = config.get_primary_operator().unwrap();
526 assert_eq!(primary.rank, OperatorRank::E6 as i32);
527 assert_eq!(primary.name, "SSG Smith");
528 }
529
530 #[test]
531 fn test_platform_config_set_operator_binding() {
532 use crate::models::{OperatorExt, OperatorRank};
533
534 let mut config = NodeConfig::new("Robot".to_string());
535 assert!(config.is_autonomous());
536
537 let operator = Operator::new(
539 "op_1".to_string(),
540 "PFC Jones".to_string(),
541 OperatorRank::E3,
542 AuthorityLevel::Supervisor,
543 "11B".to_string(),
544 );
545
546 let binding = HumanMachinePair::new(
547 vec![operator],
548 vec![config.id.clone()],
549 BindingType::OneToOne,
550 );
551
552 config.set_operator_binding(Some(binding));
553 assert!(config.is_human_operated());
554 assert!(!config.is_autonomous());
555
556 config.set_operator_binding(None);
558 assert!(config.is_autonomous());
559 }
560
561 #[test]
562 fn test_platform_config_multiple_operators() {
563 use crate::models::{OperatorExt, OperatorRank};
564
565 let commander = Operator::new(
567 "op_1".to_string(),
568 "CPT Williams".to_string(),
569 OperatorRank::O3,
570 AuthorityLevel::Commander,
571 "11A".to_string(), );
573
574 let nco = Operator::new(
575 "op_2".to_string(),
576 "SFC Davis".to_string(),
577 OperatorRank::E7,
578 AuthorityLevel::Supervisor,
579 "11B".to_string(),
580 );
581
582 let rto = Operator::new(
583 "op_3".to_string(),
584 "SPC Brown".to_string(),
585 OperatorRank::E4,
586 AuthorityLevel::Advisor,
587 "25U".to_string(), );
589
590 let binding = HumanMachinePair::new(
591 vec![commander, nco, rto],
592 vec!["command_vehicle_1".to_string()],
593 BindingType::ManyToOne,
594 );
595
596 let config = NodeConfig::with_operator("Command Vehicle".to_string(), binding);
597
598 assert!(config.is_human_operated());
599
600 let primary = config.get_primary_operator().unwrap();
602 assert_eq!(primary.rank, OperatorRank::O3 as i32);
603 assert_eq!(primary.name, "CPT Williams");
604
605 let binding = config.get_operator_binding().unwrap();
606 assert_eq!(binding.operators.len(), 3);
607 assert_eq!(binding.binding_type, BindingType::ManyToOne as i32);
608 }
609
610 #[test]
611 fn test_platform_config_swarm_operator() {
612 use crate::models::{OperatorExt, OperatorRank};
613
614 let operator = Operator::new(
616 "op_1".to_string(),
617 "SSG Martinez".to_string(),
618 OperatorRank::E6,
619 AuthorityLevel::Supervisor,
620 "11B".to_string(),
621 );
622
623 let platform_ids = vec![
624 "robot_1".to_string(),
625 "robot_2".to_string(),
626 "robot_3".to_string(),
627 "robot_4".to_string(),
628 ];
629
630 let binding =
631 HumanMachinePair::new(vec![operator], platform_ids.clone(), BindingType::OneToMany);
632
633 let config = NodeConfig::with_operator("Swarm Control Station".to_string(), binding);
634
635 assert!(config.is_human_operated());
636
637 let binding = config.get_operator_binding().unwrap();
638 assert_eq!(binding.binding_type, BindingType::OneToMany as i32);
639 assert_eq!(binding.platform_ids.len(), 4);
640 assert_eq!(binding.operators.len(), 1);
641 }
642
643 #[test]
644 fn test_node_config_get_capabilities_by_type_multiple() {
645 let mut config = NodeConfig::new("Multi-sensor platform".to_string());
646
647 for i in 1..=3 {
649 config.add_capability(Capability::new(
650 format!("sensor_{}", i),
651 format!("Sensor {}", i),
652 CapabilityType::Sensor,
653 0.9,
654 ));
655 }
656
657 config.add_capability(Capability::new(
659 "compute_1".to_string(),
660 "Edge Compute".to_string(),
661 CapabilityType::Compute,
662 0.8,
663 ));
664
665 let sensors = config.get_capabilities_by_type(CapabilityType::Sensor);
666 assert_eq!(sensors.len(), 3);
667
668 let compute = config.get_capabilities_by_type(CapabilityType::Compute);
669 assert_eq!(compute.len(), 1);
670
671 let mobility = config.get_capabilities_by_type(CapabilityType::Mobility);
672 assert_eq!(mobility.len(), 0);
673 }
674
675 #[test]
676 fn test_node_state_health_transitions() {
677 let mut state = NodeState::new((0.0, 0.0, 0.0));
678
679 for health in [
681 HealthStatus::Nominal,
682 HealthStatus::Degraded,
683 HealthStatus::Critical,
684 HealthStatus::Failed,
685 ] {
686 state.update_health(health);
687 assert_eq!(state.get_health(), health);
688 }
689 }
690
691 #[test]
692 fn test_node_state_phase_transitions() {
693 use crate::traits::Phase;
694
695 let mut state = NodeState::new((0.0, 0.0, 0.0));
696
697 for phase in [Phase::Discovery, Phase::Cell, Phase::Hierarchy] {
699 state.update_phase(phase);
700 assert_eq!(state.get_phase(), phase);
701 }
702 }
703
704 #[test]
705 fn test_node_state_fuel_edge_cases() {
706 let mut state = NodeState::new((0.0, 0.0, 0.0));
707
708 state.replenish_fuel(u32::MAX);
710 assert!(state.fuel_minutes > 0);
711
712 state.consume_fuel(u32::MAX);
714 assert_eq!(state.fuel_minutes, 0);
715 assert!(!state.is_operational());
716 }
717
718 #[test]
719 fn test_node_state_position_updates() {
720 let mut state = NodeState::new((37.7, -122.4, 100.0));
721
722 let pos1 = state.get_position();
723 assert_eq!(pos1, (37.7, -122.4, 100.0));
724
725 state.update_position((38.0, -123.0, 200.0));
727 assert_eq!(state.get_position(), (38.0, -123.0, 200.0));
728
729 state.update_position((39.0, -124.0, 300.0));
730 assert_eq!(state.get_position(), (39.0, -124.0, 300.0));
731 }
732
733 #[test]
734 fn test_node_state_merge_with_equal_timestamps() {
735 let mut state1 = NodeState::new((37.7, -122.4, 100.0));
736 let state2 = state1.clone();
737
738 let original_pos = state1.get_position();
740 state1.merge(&state2);
741 assert_eq!(state1.get_position(), original_pos);
742 }
743
744 #[test]
745 fn test_node_state_cell_and_zone_assignments() {
746 let mut state = NodeState::new((0.0, 0.0, 0.0));
747
748 assert!(state.cell_id.is_none());
749 assert!(state.zone_id.is_none());
750
751 state.assign_cell("cell_1".to_string());
753 state.assign_zone("zone_1".to_string());
754
755 assert_eq!(state.cell_id, Some("cell_1".to_string()));
756 assert_eq!(state.zone_id, Some("zone_1".to_string()));
757
758 state.leave_cell();
760 assert!(state.cell_id.is_none());
761 assert_eq!(state.zone_id, Some("zone_1".to_string()));
762
763 state.leave_zone();
765 assert!(state.zone_id.is_none());
766 }
767
768 #[test]
769 fn test_node_state_needs_refuel_threshold() {
770 let mut state = NodeState::new((0.0, 0.0, 0.0));
771 assert_eq!(state.fuel_minutes, 120);
772 assert!(!state.needs_refuel());
773
774 state.consume_fuel(90);
776 assert_eq!(state.fuel_minutes, 30);
777 assert!(!state.needs_refuel());
778
779 state.consume_fuel(1);
781 assert_eq!(state.fuel_minutes, 29);
782 assert!(state.needs_refuel());
783 }
784
785 #[test]
786 fn test_node_config_empty_binding() {
787 let binding =
789 HumanMachinePair::new(vec![], vec!["node_1".to_string()], BindingType::Unspecified);
790
791 let config = NodeConfig::with_operator("Test".to_string(), binding);
792
793 assert!(config.has_operator());
795 assert!(!config.is_human_operated());
796 assert!(config.is_autonomous());
797 assert!(config.get_primary_operator().is_none());
798 }
799
800 #[test]
801 fn test_node_state_get_position_no_position() {
802 let mut state = NodeState::new((0.0, 0.0, 0.0));
803 state.position = None;
804
805 assert_eq!(state.get_position(), (0.0, 0.0, 0.0));
807 }
808
809 #[test]
810 fn test_node_state_invalid_health_defaults_to_unspecified() {
811 let mut state = NodeState::new((0.0, 0.0, 0.0));
812
813 state.health = 999;
815
816 assert_eq!(state.get_health(), HealthStatus::Unspecified);
817 }
818
819 #[test]
820 fn test_node_state_invalid_phase_defaults_to_discovery() {
821 let mut state = NodeState::new((0.0, 0.0, 0.0));
822
823 state.phase = 999;
825
826 assert_eq!(state.get_phase(), crate::traits::Phase::Discovery);
827 }
828}