1#[cfg(not(feature = "std"))]
22use alloc::{string::String, vec::Vec};
23
24use crate::NodeId;
25
26#[derive(Debug, Clone)]
31pub struct PeatPeer {
32 pub node_id: NodeId,
34
35 pub identifier: String,
40
41 pub mesh_id: Option<String>,
43
44 pub name: Option<String>,
46
47 pub rssi: i8,
49
50 pub is_connected: bool,
52
53 pub last_seen_ms: u64,
55}
56
57impl PeatPeer {
58 pub fn new(
60 node_id: NodeId,
61 identifier: String,
62 mesh_id: Option<String>,
63 name: Option<String>,
64 rssi: i8,
65 ) -> Self {
66 Self {
67 node_id,
68 identifier,
69 mesh_id,
70 name,
71 rssi,
72 is_connected: false,
73 last_seen_ms: 0,
74 }
75 }
76
77 pub fn touch(&mut self, now_ms: u64) {
79 self.last_seen_ms = now_ms;
80 }
81
82 pub fn is_stale(&self, now_ms: u64, timeout_ms: u64) -> bool {
84 if self.last_seen_ms == 0 {
85 return false; }
87 now_ms.saturating_sub(self.last_seen_ms) > timeout_ms
88 }
89
90 pub fn display_name(&self) -> &str {
92 self.name.as_deref().unwrap_or(self.identifier.as_str())
93 }
94
95 pub fn signal_strength(&self) -> SignalStrength {
97 match self.rssi {
98 r if r >= -50 => SignalStrength::Excellent,
99 r if r >= -70 => SignalStrength::Good,
100 r if r >= -85 => SignalStrength::Fair,
101 _ => SignalStrength::Weak,
102 }
103 }
104}
105
106impl Default for PeatPeer {
107 fn default() -> Self {
108 Self {
109 node_id: NodeId::default(),
110 identifier: String::new(),
111 mesh_id: None,
112 name: None,
113 rssi: -100,
114 is_connected: false,
115 last_seen_ms: 0,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum SignalStrength {
123 Excellent,
125 Good,
127 Fair,
129 Weak,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138pub enum ConnectionState {
139 #[default]
141 Discovered,
142 Connecting,
144 Connected,
146 Degraded,
148 Disconnecting,
150 Disconnected,
152 Lost,
154}
155
156impl ConnectionState {
157 pub fn is_connected(&self) -> bool {
159 matches!(self, Self::Connected | Self::Degraded)
160 }
161
162 pub fn was_connected(&self) -> bool {
164 matches!(
165 self,
166 Self::Connected
167 | Self::Degraded
168 | Self::Disconnecting
169 | Self::Disconnected
170 | Self::Lost
171 )
172 }
173
174 pub fn is_degraded_or_worse(&self) -> bool {
176 matches!(
177 self,
178 Self::Degraded | Self::Disconnecting | Self::Disconnected | Self::Lost
179 )
180 }
181}
182
183pub use crate::platform::DisconnectReason;
185
186#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct BlePeerLinkInfo {
202 pub state: ConnectionState,
204 pub last_rssi: Option<i8>,
206}
207
208#[derive(Debug, Clone)]
215pub struct PeerConnectionState {
216 pub node_id: NodeId,
218
219 pub identifier: String,
221
222 pub state: ConnectionState,
224
225 pub discovered_at: u64,
227
228 pub connected_at: Option<u64>,
230
231 pub disconnected_at: Option<u64>,
233
234 pub disconnect_reason: Option<DisconnectReason>,
236
237 pub last_rssi: Option<i8>,
239
240 pub connection_count: u32,
242
243 pub documents_synced: u32,
245
246 pub bytes_received: u64,
248
249 pub bytes_sent: u64,
251
252 pub last_seen_ms: u64,
254
255 pub name: Option<String>,
257
258 pub mesh_id: Option<String>,
260}
261
262impl PeerConnectionState {
263 pub fn new_discovered(node_id: NodeId, identifier: String, now_ms: u64) -> Self {
265 Self {
266 node_id,
267 identifier,
268 state: ConnectionState::Discovered,
269 discovered_at: now_ms,
270 connected_at: None,
271 disconnected_at: None,
272 disconnect_reason: None,
273 last_rssi: None,
274 connection_count: 0,
275 documents_synced: 0,
276 bytes_received: 0,
277 bytes_sent: 0,
278 last_seen_ms: now_ms,
279 name: None,
280 mesh_id: None,
281 }
282 }
283
284 pub fn from_peer(peer: &PeatPeer, now_ms: u64) -> Self {
286 let state = if peer.is_connected {
287 ConnectionState::Connected
288 } else {
289 ConnectionState::Discovered
290 };
291
292 Self {
293 node_id: peer.node_id,
294 identifier: peer.identifier.clone(),
295 state,
296 discovered_at: now_ms,
297 connected_at: if peer.is_connected {
298 Some(now_ms)
299 } else {
300 None
301 },
302 disconnected_at: None,
303 disconnect_reason: None,
304 last_rssi: Some(peer.rssi),
305 connection_count: if peer.is_connected { 1 } else { 0 },
306 documents_synced: 0,
307 bytes_received: 0,
308 bytes_sent: 0,
309 last_seen_ms: peer.last_seen_ms,
310 name: peer.name.clone(),
311 mesh_id: peer.mesh_id.clone(),
312 }
313 }
314
315 pub fn set_connecting(&mut self, now_ms: u64) {
317 self.state = ConnectionState::Connecting;
318 self.last_seen_ms = now_ms;
319 }
320
321 pub fn set_connected(&mut self, now_ms: u64) {
323 self.state = ConnectionState::Connected;
324 self.connected_at = Some(now_ms);
325 self.connection_count += 1;
326 self.last_seen_ms = now_ms;
327 self.disconnect_reason = None;
328 }
329
330 pub fn set_degraded(&mut self, now_ms: u64) {
332 if self.state == ConnectionState::Connected {
333 self.state = ConnectionState::Degraded;
334 self.last_seen_ms = now_ms;
335 }
336 }
337
338 pub fn set_disconnected(&mut self, now_ms: u64, reason: DisconnectReason) {
340 self.state = ConnectionState::Disconnected;
341 self.disconnected_at = Some(now_ms);
342 self.disconnect_reason = Some(reason);
343 self.last_seen_ms = now_ms;
344 }
345
346 pub fn set_lost(&mut self, now_ms: u64) {
348 if self.state == ConnectionState::Disconnected {
349 self.state = ConnectionState::Lost;
350 self.last_seen_ms = now_ms;
351 }
352 }
353
354 pub fn update_rssi(&mut self, rssi: i8, now_ms: u64, degraded_threshold: i8) -> bool {
358 self.last_rssi = Some(rssi);
359 self.last_seen_ms = now_ms;
360
361 if self.state == ConnectionState::Connected && rssi < degraded_threshold {
362 self.state = ConnectionState::Degraded;
363 return true;
364 } else if self.state == ConnectionState::Degraded && rssi >= degraded_threshold {
365 self.state = ConnectionState::Connected;
366 }
367 false
368 }
369
370 pub fn record_transfer(&mut self, bytes_received: u64, bytes_sent: u64) {
372 self.bytes_received += bytes_received;
373 self.bytes_sent += bytes_sent;
374 }
375
376 pub fn record_sync(&mut self) {
378 self.documents_synced += 1;
379 }
380
381 pub fn time_since_connected(&self, now_ms: u64) -> Option<u64> {
383 self.connected_at.map(|t| now_ms.saturating_sub(t))
384 }
385
386 pub fn time_since_disconnected(&self, now_ms: u64) -> Option<u64> {
388 self.disconnected_at.map(|t| now_ms.saturating_sub(t))
389 }
390
391 pub fn connection_duration(&self, now_ms: u64) -> Option<u64> {
393 if self.state.is_connected() {
394 self.connected_at.map(|t| now_ms.saturating_sub(t))
395 } else {
396 None
397 }
398 }
399
400 pub fn signal_strength(&self) -> Option<SignalStrength> {
402 self.last_rssi.map(|rssi| match rssi {
403 r if r >= -50 => SignalStrength::Excellent,
404 r if r >= -70 => SignalStrength::Good,
405 r if r >= -85 => SignalStrength::Fair,
406 _ => SignalStrength::Weak,
407 })
408 }
409}
410
411#[cfg(feature = "std")]
412use std::collections::BTreeMap;
413
414#[cfg(not(feature = "std"))]
415use alloc::collections::BTreeMap;
416
417#[derive(Debug, Clone, Default)]
444pub struct ConnectionStateGraph {
445 peers: BTreeMap<NodeId, PeerConnectionState>,
447
448 indirect_peers: BTreeMap<NodeId, IndirectPeer>,
450
451 rssi_degraded_threshold: i8,
453
454 lost_timeout_ms: u64,
456
457 indirect_peer_timeout_ms: u64,
459}
460
461impl ConnectionStateGraph {
462 pub fn new() -> Self {
464 Self {
465 peers: BTreeMap::new(),
466 indirect_peers: BTreeMap::new(),
467 rssi_degraded_threshold: -80,
468 lost_timeout_ms: 30_000,
469 indirect_peer_timeout_ms: 120_000, }
471 }
472
473 pub fn with_config(rssi_degraded_threshold: i8, lost_timeout_ms: u64) -> Self {
475 Self {
476 peers: BTreeMap::new(),
477 indirect_peers: BTreeMap::new(),
478 rssi_degraded_threshold,
479 lost_timeout_ms,
480 indirect_peer_timeout_ms: 120_000,
481 }
482 }
483
484 pub fn get_all(&self) -> Vec<&PeerConnectionState> {
486 self.peers.values().collect()
487 }
488
489 pub fn get_all_owned(&self) -> Vec<PeerConnectionState> {
491 self.peers.values().cloned().collect()
492 }
493
494 pub fn get_peer(&self, node_id: NodeId) -> Option<&PeerConnectionState> {
496 self.peers.get(&node_id)
497 }
498
499 pub fn get_peer_mut(&mut self, node_id: NodeId) -> Option<&mut PeerConnectionState> {
501 self.peers.get_mut(&node_id)
502 }
503
504 pub fn get_connected(&self) -> Vec<&PeerConnectionState> {
506 self.peers
507 .values()
508 .filter(|p| p.state.is_connected())
509 .collect()
510 }
511
512 pub fn get_degraded(&self) -> Vec<&PeerConnectionState> {
514 self.peers
515 .values()
516 .filter(|p| p.state == ConnectionState::Degraded)
517 .collect()
518 }
519
520 pub fn get_recently_disconnected(
522 &self,
523 within_ms: u64,
524 now_ms: u64,
525 ) -> Vec<&PeerConnectionState> {
526 self.peers
527 .values()
528 .filter(|p| {
529 p.state == ConnectionState::Disconnected
530 && p.disconnected_at
531 .map(|t| now_ms.saturating_sub(t) <= within_ms)
532 .unwrap_or(false)
533 })
534 .collect()
535 }
536
537 pub fn get_lost(&self) -> Vec<&PeerConnectionState> {
539 self.peers
540 .values()
541 .filter(|p| p.state == ConnectionState::Lost)
542 .collect()
543 }
544
545 pub fn get_with_history(&self) -> Vec<&PeerConnectionState> {
547 self.peers
548 .values()
549 .filter(|p| p.state.was_connected())
550 .collect()
551 }
552
553 pub fn state_counts(&self) -> StateCountSummary {
555 let mut summary = StateCountSummary::default();
556 for peer in self.peers.values() {
557 match peer.state {
558 ConnectionState::Discovered => summary.discovered += 1,
559 ConnectionState::Connecting => summary.connecting += 1,
560 ConnectionState::Connected => summary.connected += 1,
561 ConnectionState::Degraded => summary.degraded += 1,
562 ConnectionState::Disconnecting => summary.disconnecting += 1,
563 ConnectionState::Disconnected => summary.disconnected += 1,
564 ConnectionState::Lost => summary.lost += 1,
565 }
566 }
567 summary
568 }
569
570 pub fn len(&self) -> usize {
572 self.peers.len()
573 }
574
575 pub fn is_empty(&self) -> bool {
577 self.peers.is_empty()
578 }
579
580 pub fn on_discovered(
582 &mut self,
583 node_id: NodeId,
584 identifier: String,
585 name: Option<String>,
586 mesh_id: Option<String>,
587 rssi: i8,
588 now_ms: u64,
589 ) -> &PeerConnectionState {
590 let entry = self.peers.entry(node_id).or_insert_with(|| {
591 PeerConnectionState::new_discovered(node_id, identifier.clone(), now_ms)
592 });
593
594 entry.last_rssi = Some(rssi);
596 entry.last_seen_ms = now_ms;
597 if name.is_some() {
598 entry.name = name;
599 }
600 if mesh_id.is_some() {
601 entry.mesh_id = mesh_id;
602 }
603
604 if entry.state == ConnectionState::Lost {
606 entry.state = ConnectionState::Disconnected;
607 }
608
609 entry
610 }
611
612 pub fn on_connecting(&mut self, node_id: NodeId, now_ms: u64) {
614 if let Some(peer) = self.peers.get_mut(&node_id) {
615 peer.set_connecting(now_ms);
616 }
617 }
618
619 pub fn on_connected(&mut self, node_id: NodeId, now_ms: u64) {
621 if let Some(peer) = self.peers.get_mut(&node_id) {
622 peer.set_connected(now_ms);
623 }
624 }
625
626 pub fn on_disconnected(&mut self, node_id: NodeId, reason: DisconnectReason, now_ms: u64) {
628 if let Some(peer) = self.peers.get_mut(&node_id) {
629 peer.set_disconnected(now_ms, reason);
630 }
631 }
632
633 pub fn update_rssi(&mut self, node_id: NodeId, rssi: i8, now_ms: u64) -> bool {
637 if let Some(peer) = self.peers.get_mut(&node_id) {
638 return peer.update_rssi(rssi, now_ms, self.rssi_degraded_threshold);
639 }
640 false
641 }
642
643 pub fn record_transfer(&mut self, node_id: NodeId, bytes_received: u64, bytes_sent: u64) {
645 if let Some(peer) = self.peers.get_mut(&node_id) {
646 peer.record_transfer(bytes_received, bytes_sent);
647 }
648 }
649
650 pub fn record_sync(&mut self, node_id: NodeId) {
652 if let Some(peer) = self.peers.get_mut(&node_id) {
653 peer.record_sync();
654 }
655 }
656
657 pub fn tick(&mut self, now_ms: u64) -> Vec<NodeId> {
661 let mut newly_lost = Vec::new();
662
663 for (node_id, peer) in self.peers.iter_mut() {
664 if peer.state == ConnectionState::Disconnected {
665 if let Some(disconnected_at) = peer.disconnected_at {
666 if now_ms.saturating_sub(disconnected_at) > self.lost_timeout_ms {
667 peer.set_lost(now_ms);
668 newly_lost.push(*node_id);
669 }
670 }
671 }
672 }
673
674 newly_lost
675 }
676
677 pub fn cleanup_lost(&mut self, older_than_ms: u64, now_ms: u64) -> Vec<NodeId> {
679 let to_remove: Vec<NodeId> = self
680 .peers
681 .iter()
682 .filter(|(_, p)| {
683 p.state == ConnectionState::Lost
684 && now_ms.saturating_sub(p.last_seen_ms) > older_than_ms
685 })
686 .map(|(id, _)| *id)
687 .collect();
688
689 for id in &to_remove {
690 self.peers.remove(id);
691 }
692
693 to_remove
694 }
695
696 pub fn import_peer(&mut self, peer: &PeatPeer, now_ms: u64) {
698 let state = PeerConnectionState::from_peer(peer, now_ms);
699 self.peers.insert(peer.node_id, state);
700 }
701
702 pub fn on_relay_received(
718 &mut self,
719 source_peer: NodeId,
720 origin_node: NodeId,
721 hop_count: u8,
722 now_ms: u64,
723 ) -> bool {
724 if hop_count > MAX_TRACKED_DEGREE {
726 return false;
727 }
728
729 if self.peers.contains_key(&origin_node) {
731 return false;
733 }
734
735 if let Some(existing) = self.indirect_peers.get_mut(&origin_node) {
737 existing.update_path(source_peer, hop_count, now_ms);
738 false
739 } else {
740 self.indirect_peers.insert(
741 origin_node,
742 IndirectPeer::new(origin_node, source_peer, hop_count, now_ms),
743 );
744 true
745 }
746 }
747
748 pub fn get_indirect_peers(&self) -> Vec<&IndirectPeer> {
750 self.indirect_peers.values().collect()
751 }
752
753 pub fn get_indirect_peers_owned(&self) -> Vec<IndirectPeer> {
755 self.indirect_peers.values().cloned().collect()
756 }
757
758 pub fn get_indirect_peer(&self, node_id: NodeId) -> Option<&IndirectPeer> {
760 self.indirect_peers.get(&node_id)
761 }
762
763 pub fn get_peers_by_degree(&self, degree: PeerDegree) -> Vec<NodeId> {
765 match degree {
766 PeerDegree::Direct => self.peers.keys().copied().collect(),
767 _ => self
768 .indirect_peers
769 .iter()
770 .filter(|(_, p)| p.degree() == Some(degree))
771 .map(|(id, _)| *id)
772 .collect(),
773 }
774 }
775
776 pub fn peer_degree(&self, node_id: NodeId) -> Option<PeerDegree> {
778 if self.peers.contains_key(&node_id) {
779 Some(PeerDegree::Direct)
780 } else {
781 self.indirect_peers.get(&node_id).and_then(|p| p.degree())
782 }
783 }
784
785 pub fn get_paths_to(&self, node_id: NodeId) -> Vec<(NodeId, u8)> {
789 self.indirect_peers
790 .get(&node_id)
791 .map(|p| p.paths())
792 .unwrap_or_default()
793 }
794
795 pub fn is_known(&self, node_id: NodeId) -> bool {
797 self.peers.contains_key(&node_id) || self.indirect_peers.contains_key(&node_id)
798 }
799
800 pub fn cleanup_indirect(&mut self, now_ms: u64) -> Vec<NodeId> {
804 let to_remove: Vec<NodeId> = self
805 .indirect_peers
806 .iter()
807 .filter(|(_, p)| p.is_stale(now_ms, self.indirect_peer_timeout_ms))
808 .map(|(id, _)| *id)
809 .collect();
810
811 for id in &to_remove {
812 self.indirect_peers.remove(id);
813 }
814
815 to_remove
816 }
817
818 pub fn remove_via_peer(&mut self, via_peer: NodeId) {
823 let mut to_remove = Vec::new();
824
825 for (node_id, indirect) in self.indirect_peers.iter_mut() {
826 indirect.via_peers.remove(&via_peer);
827
828 if indirect.via_peers.is_empty() {
830 to_remove.push(*node_id);
831 } else {
832 indirect.min_hops = indirect.via_peers.values().copied().min().unwrap_or(255);
833 }
834 }
835
836 for id in to_remove {
838 self.indirect_peers.remove(&id);
839 }
840 }
841
842 pub fn full_state_counts(&self) -> FullStateCountSummary {
844 let direct = self.state_counts();
845
846 let mut one_hop = 0;
847 let mut two_hop = 0;
848 let mut three_hop = 0;
849
850 for peer in self.indirect_peers.values() {
851 match peer.min_hops {
852 1 => one_hop += 1,
853 2 => two_hop += 1,
854 3 => three_hop += 1,
855 _ => {}
856 }
857 }
858
859 FullStateCountSummary {
860 direct,
861 one_hop,
862 two_hop,
863 three_hop,
864 }
865 }
866
867 pub fn indirect_peer_count(&self) -> usize {
869 self.indirect_peers.len()
870 }
871
872 pub fn set_indirect_callsign(&mut self, node_id: NodeId, callsign: String) {
874 if let Some(peer) = self.indirect_peers.get_mut(&node_id) {
875 peer.callsign = Some(callsign);
876 }
877 }
878}
879
880#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
882pub struct StateCountSummary {
883 pub discovered: usize,
885 pub connecting: usize,
887 pub connected: usize,
889 pub degraded: usize,
891 pub disconnecting: usize,
893 pub disconnected: usize,
895 pub lost: usize,
897}
898
899impl StateCountSummary {
900 pub fn active_connections(&self) -> usize {
902 self.connected + self.degraded
903 }
904
905 pub fn total(&self) -> usize {
907 self.discovered
908 + self.connecting
909 + self.connected
910 + self.degraded
911 + self.disconnecting
912 + self.disconnected
913 + self.lost
914 }
915}
916
917pub const MAX_TRACKED_DEGREE: u8 = 3;
919
920#[derive(Debug, Clone, Copy, PartialEq, Eq)]
922pub enum PeerDegree {
923 Direct,
925 OneHop,
927 TwoHop,
929 ThreeHop,
931}
932
933impl PeerDegree {
934 pub fn from_hops(hops: u8) -> Option<Self> {
936 match hops {
937 0 => Some(Self::Direct),
938 1 => Some(Self::OneHop),
939 2 => Some(Self::TwoHop),
940 3 => Some(Self::ThreeHop),
941 _ => None, }
943 }
944
945 pub fn hops(&self) -> u8 {
947 match self {
948 Self::Direct => 0,
949 Self::OneHop => 1,
950 Self::TwoHop => 2,
951 Self::ThreeHop => 3,
952 }
953 }
954}
955
956#[derive(Debug, Clone)]
961pub struct IndirectPeer {
962 pub node_id: NodeId,
964
965 pub min_hops: u8,
967
968 pub via_peers: BTreeMap<NodeId, u8>,
971
972 pub discovered_at: u64,
974
975 pub last_seen_ms: u64,
977
978 pub messages_received: u32,
980
981 pub callsign: Option<String>,
983}
984
985impl IndirectPeer {
986 pub fn new(node_id: NodeId, via_peer: NodeId, hop_count: u8, now_ms: u64) -> Self {
988 let mut via_peers = BTreeMap::new();
989 via_peers.insert(via_peer, hop_count);
990
991 Self {
992 node_id,
993 min_hops: hop_count,
994 via_peers,
995 discovered_at: now_ms,
996 last_seen_ms: now_ms,
997 messages_received: 1,
998 callsign: None,
999 }
1000 }
1001
1002 pub fn update_path(&mut self, via_peer: NodeId, hop_count: u8, now_ms: u64) -> bool {
1006 self.last_seen_ms = now_ms;
1007 self.messages_received += 1;
1008
1009 let is_better = hop_count < self.min_hops;
1010
1011 self.via_peers.insert(via_peer, hop_count);
1013
1014 if is_better {
1016 self.min_hops = hop_count;
1017 } else {
1018 self.min_hops = self.via_peers.values().copied().min().unwrap_or(hop_count);
1020 }
1021
1022 is_better
1023 }
1024
1025 pub fn degree(&self) -> Option<PeerDegree> {
1027 PeerDegree::from_hops(self.min_hops)
1028 }
1029
1030 pub fn is_stale(&self, now_ms: u64, timeout_ms: u64) -> bool {
1032 now_ms.saturating_sub(self.last_seen_ms) > timeout_ms
1033 }
1034
1035 pub fn paths(&self) -> Vec<(NodeId, u8)> {
1037 self.via_peers.iter().map(|(&k, &v)| (k, v)).collect()
1038 }
1039}
1040
1041#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1043pub struct FullStateCountSummary {
1044 pub direct: StateCountSummary,
1046 pub one_hop: usize,
1048 pub two_hop: usize,
1050 pub three_hop: usize,
1052}
1053
1054impl FullStateCountSummary {
1055 pub fn total(&self) -> usize {
1057 self.direct.total() + self.one_hop + self.two_hop + self.three_hop
1058 }
1059
1060 pub fn total_indirect(&self) -> usize {
1062 self.one_hop + self.two_hop + self.three_hop
1063 }
1064}
1065
1066#[derive(Debug, Clone)]
1071pub struct PeerManagerConfig {
1072 pub peer_timeout_ms: u64,
1074
1075 pub cleanup_interval_ms: u64,
1077
1078 pub sync_interval_ms: u64,
1080
1081 pub sync_cooldown_ms: u64,
1084
1085 pub auto_connect: bool,
1087
1088 pub mesh_id: String,
1090
1091 pub max_peers: usize,
1093
1094 pub rssi_degraded_threshold: i8,
1096
1097 pub lost_timeout_ms: u64,
1099}
1100
1101impl Default for PeerManagerConfig {
1102 fn default() -> Self {
1103 Self {
1104 peer_timeout_ms: 45_000, cleanup_interval_ms: 10_000, sync_interval_ms: 5_000, sync_cooldown_ms: 30_000, auto_connect: true,
1109 mesh_id: String::from("DEMO"),
1110 max_peers: 8,
1111 rssi_degraded_threshold: -80, lost_timeout_ms: 30_000, }
1114 }
1115}
1116
1117impl PeerManagerConfig {
1118 pub fn with_mesh_id(mesh_id: impl Into<String>) -> Self {
1120 Self {
1121 mesh_id: mesh_id.into(),
1122 ..Default::default()
1123 }
1124 }
1125
1126 pub fn peer_timeout(mut self, timeout_ms: u64) -> Self {
1128 self.peer_timeout_ms = timeout_ms;
1129 self
1130 }
1131
1132 pub fn sync_interval(mut self, interval_ms: u64) -> Self {
1134 self.sync_interval_ms = interval_ms;
1135 self
1136 }
1137
1138 pub fn auto_connect(mut self, enabled: bool) -> Self {
1140 self.auto_connect = enabled;
1141 self
1142 }
1143
1144 pub fn max_peers(mut self, max: usize) -> Self {
1146 self.max_peers = max;
1147 self
1148 }
1149
1150 pub fn matches_mesh(&self, device_mesh_id: Option<&str>) -> bool {
1156 match device_mesh_id {
1157 Some(id) => id == self.mesh_id,
1158 None => true, }
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166
1167 #[test]
1168 fn test_peer_stale_detection() {
1169 let mut peer = PeatPeer::new(
1170 NodeId::new(0x12345678),
1171 "test-id".into(),
1172 Some("DEMO".into()),
1173 Some("PEAT_DEMO-12345678".into()),
1174 -70,
1175 );
1176
1177 peer.touch(1000);
1179 assert!(!peer.is_stale(2000, 45_000));
1180
1181 assert!(peer.is_stale(50_000, 45_000));
1183 }
1184
1185 #[test]
1186 fn test_signal_strength() {
1187 let peer_excellent = PeatPeer {
1188 rssi: -45,
1189 ..Default::default()
1190 };
1191 assert_eq!(peer_excellent.signal_strength(), SignalStrength::Excellent);
1192
1193 let peer_good = PeatPeer {
1194 rssi: -65,
1195 ..Default::default()
1196 };
1197 assert_eq!(peer_good.signal_strength(), SignalStrength::Good);
1198
1199 let peer_fair = PeatPeer {
1200 rssi: -80,
1201 ..Default::default()
1202 };
1203 assert_eq!(peer_fair.signal_strength(), SignalStrength::Fair);
1204
1205 let peer_weak = PeatPeer {
1206 rssi: -95,
1207 ..Default::default()
1208 };
1209 assert_eq!(peer_weak.signal_strength(), SignalStrength::Weak);
1210 }
1211
1212 #[test]
1213 fn test_mesh_matching() {
1214 let config = PeerManagerConfig::with_mesh_id("ALPHA");
1215
1216 assert!(config.matches_mesh(Some("ALPHA")));
1218
1219 assert!(!config.matches_mesh(Some("BETA")));
1221
1222 assert!(config.matches_mesh(None));
1224 }
1225}