1#[cfg(not(feature = "std"))]
22use alloc::{string::String, vec::Vec};
23
24use crate::NodeId;
25
26#[derive(Debug, Clone)]
31pub struct HivePeer {
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 HivePeer {
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 HivePeer {
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)]
193pub struct PeerConnectionState {
194 pub node_id: NodeId,
196
197 pub identifier: String,
199
200 pub state: ConnectionState,
202
203 pub discovered_at: u64,
205
206 pub connected_at: Option<u64>,
208
209 pub disconnected_at: Option<u64>,
211
212 pub disconnect_reason: Option<DisconnectReason>,
214
215 pub last_rssi: Option<i8>,
217
218 pub connection_count: u32,
220
221 pub documents_synced: u32,
223
224 pub bytes_received: u64,
226
227 pub bytes_sent: u64,
229
230 pub last_seen_ms: u64,
232
233 pub name: Option<String>,
235
236 pub mesh_id: Option<String>,
238}
239
240impl PeerConnectionState {
241 pub fn new_discovered(node_id: NodeId, identifier: String, now_ms: u64) -> Self {
243 Self {
244 node_id,
245 identifier,
246 state: ConnectionState::Discovered,
247 discovered_at: now_ms,
248 connected_at: None,
249 disconnected_at: None,
250 disconnect_reason: None,
251 last_rssi: None,
252 connection_count: 0,
253 documents_synced: 0,
254 bytes_received: 0,
255 bytes_sent: 0,
256 last_seen_ms: now_ms,
257 name: None,
258 mesh_id: None,
259 }
260 }
261
262 pub fn from_peer(peer: &HivePeer, now_ms: u64) -> Self {
264 let state = if peer.is_connected {
265 ConnectionState::Connected
266 } else {
267 ConnectionState::Discovered
268 };
269
270 Self {
271 node_id: peer.node_id,
272 identifier: peer.identifier.clone(),
273 state,
274 discovered_at: now_ms,
275 connected_at: if peer.is_connected {
276 Some(now_ms)
277 } else {
278 None
279 },
280 disconnected_at: None,
281 disconnect_reason: None,
282 last_rssi: Some(peer.rssi),
283 connection_count: if peer.is_connected { 1 } else { 0 },
284 documents_synced: 0,
285 bytes_received: 0,
286 bytes_sent: 0,
287 last_seen_ms: peer.last_seen_ms,
288 name: peer.name.clone(),
289 mesh_id: peer.mesh_id.clone(),
290 }
291 }
292
293 pub fn set_connecting(&mut self, now_ms: u64) {
295 self.state = ConnectionState::Connecting;
296 self.last_seen_ms = now_ms;
297 }
298
299 pub fn set_connected(&mut self, now_ms: u64) {
301 self.state = ConnectionState::Connected;
302 self.connected_at = Some(now_ms);
303 self.connection_count += 1;
304 self.last_seen_ms = now_ms;
305 self.disconnect_reason = None;
306 }
307
308 pub fn set_degraded(&mut self, now_ms: u64) {
310 if self.state == ConnectionState::Connected {
311 self.state = ConnectionState::Degraded;
312 self.last_seen_ms = now_ms;
313 }
314 }
315
316 pub fn set_disconnected(&mut self, now_ms: u64, reason: DisconnectReason) {
318 self.state = ConnectionState::Disconnected;
319 self.disconnected_at = Some(now_ms);
320 self.disconnect_reason = Some(reason);
321 self.last_seen_ms = now_ms;
322 }
323
324 pub fn set_lost(&mut self, now_ms: u64) {
326 if self.state == ConnectionState::Disconnected {
327 self.state = ConnectionState::Lost;
328 self.last_seen_ms = now_ms;
329 }
330 }
331
332 pub fn update_rssi(&mut self, rssi: i8, now_ms: u64, degraded_threshold: i8) -> bool {
336 self.last_rssi = Some(rssi);
337 self.last_seen_ms = now_ms;
338
339 if self.state == ConnectionState::Connected && rssi < degraded_threshold {
340 self.state = ConnectionState::Degraded;
341 return true;
342 } else if self.state == ConnectionState::Degraded && rssi >= degraded_threshold {
343 self.state = ConnectionState::Connected;
344 }
345 false
346 }
347
348 pub fn record_transfer(&mut self, bytes_received: u64, bytes_sent: u64) {
350 self.bytes_received += bytes_received;
351 self.bytes_sent += bytes_sent;
352 }
353
354 pub fn record_sync(&mut self) {
356 self.documents_synced += 1;
357 }
358
359 pub fn time_since_connected(&self, now_ms: u64) -> Option<u64> {
361 self.connected_at.map(|t| now_ms.saturating_sub(t))
362 }
363
364 pub fn time_since_disconnected(&self, now_ms: u64) -> Option<u64> {
366 self.disconnected_at.map(|t| now_ms.saturating_sub(t))
367 }
368
369 pub fn connection_duration(&self, now_ms: u64) -> Option<u64> {
371 if self.state.is_connected() {
372 self.connected_at.map(|t| now_ms.saturating_sub(t))
373 } else {
374 None
375 }
376 }
377
378 pub fn signal_strength(&self) -> Option<SignalStrength> {
380 self.last_rssi.map(|rssi| match rssi {
381 r if r >= -50 => SignalStrength::Excellent,
382 r if r >= -70 => SignalStrength::Good,
383 r if r >= -85 => SignalStrength::Fair,
384 _ => SignalStrength::Weak,
385 })
386 }
387}
388
389#[cfg(feature = "std")]
390use std::collections::BTreeMap;
391
392#[cfg(not(feature = "std"))]
393use alloc::collections::BTreeMap;
394
395#[derive(Debug, Clone, Default)]
422pub struct ConnectionStateGraph {
423 peers: BTreeMap<NodeId, PeerConnectionState>,
425
426 indirect_peers: BTreeMap<NodeId, IndirectPeer>,
428
429 rssi_degraded_threshold: i8,
431
432 lost_timeout_ms: u64,
434
435 indirect_peer_timeout_ms: u64,
437}
438
439impl ConnectionStateGraph {
440 pub fn new() -> Self {
442 Self {
443 peers: BTreeMap::new(),
444 indirect_peers: BTreeMap::new(),
445 rssi_degraded_threshold: -80,
446 lost_timeout_ms: 30_000,
447 indirect_peer_timeout_ms: 120_000, }
449 }
450
451 pub fn with_config(rssi_degraded_threshold: i8, lost_timeout_ms: u64) -> Self {
453 Self {
454 peers: BTreeMap::new(),
455 indirect_peers: BTreeMap::new(),
456 rssi_degraded_threshold,
457 lost_timeout_ms,
458 indirect_peer_timeout_ms: 120_000,
459 }
460 }
461
462 pub fn get_all(&self) -> Vec<&PeerConnectionState> {
464 self.peers.values().collect()
465 }
466
467 pub fn get_all_owned(&self) -> Vec<PeerConnectionState> {
469 self.peers.values().cloned().collect()
470 }
471
472 pub fn get_peer(&self, node_id: NodeId) -> Option<&PeerConnectionState> {
474 self.peers.get(&node_id)
475 }
476
477 pub fn get_peer_mut(&mut self, node_id: NodeId) -> Option<&mut PeerConnectionState> {
479 self.peers.get_mut(&node_id)
480 }
481
482 pub fn get_connected(&self) -> Vec<&PeerConnectionState> {
484 self.peers
485 .values()
486 .filter(|p| p.state.is_connected())
487 .collect()
488 }
489
490 pub fn get_degraded(&self) -> Vec<&PeerConnectionState> {
492 self.peers
493 .values()
494 .filter(|p| p.state == ConnectionState::Degraded)
495 .collect()
496 }
497
498 pub fn get_recently_disconnected(
500 &self,
501 within_ms: u64,
502 now_ms: u64,
503 ) -> Vec<&PeerConnectionState> {
504 self.peers
505 .values()
506 .filter(|p| {
507 p.state == ConnectionState::Disconnected
508 && p.disconnected_at
509 .map(|t| now_ms.saturating_sub(t) <= within_ms)
510 .unwrap_or(false)
511 })
512 .collect()
513 }
514
515 pub fn get_lost(&self) -> Vec<&PeerConnectionState> {
517 self.peers
518 .values()
519 .filter(|p| p.state == ConnectionState::Lost)
520 .collect()
521 }
522
523 pub fn get_with_history(&self) -> Vec<&PeerConnectionState> {
525 self.peers
526 .values()
527 .filter(|p| p.state.was_connected())
528 .collect()
529 }
530
531 pub fn state_counts(&self) -> StateCountSummary {
533 let mut summary = StateCountSummary::default();
534 for peer in self.peers.values() {
535 match peer.state {
536 ConnectionState::Discovered => summary.discovered += 1,
537 ConnectionState::Connecting => summary.connecting += 1,
538 ConnectionState::Connected => summary.connected += 1,
539 ConnectionState::Degraded => summary.degraded += 1,
540 ConnectionState::Disconnecting => summary.disconnecting += 1,
541 ConnectionState::Disconnected => summary.disconnected += 1,
542 ConnectionState::Lost => summary.lost += 1,
543 }
544 }
545 summary
546 }
547
548 pub fn len(&self) -> usize {
550 self.peers.len()
551 }
552
553 pub fn is_empty(&self) -> bool {
555 self.peers.is_empty()
556 }
557
558 pub fn on_discovered(
560 &mut self,
561 node_id: NodeId,
562 identifier: String,
563 name: Option<String>,
564 mesh_id: Option<String>,
565 rssi: i8,
566 now_ms: u64,
567 ) -> &PeerConnectionState {
568 let entry = self.peers.entry(node_id).or_insert_with(|| {
569 PeerConnectionState::new_discovered(node_id, identifier.clone(), now_ms)
570 });
571
572 entry.last_rssi = Some(rssi);
574 entry.last_seen_ms = now_ms;
575 if name.is_some() {
576 entry.name = name;
577 }
578 if mesh_id.is_some() {
579 entry.mesh_id = mesh_id;
580 }
581
582 if entry.state == ConnectionState::Lost {
584 entry.state = ConnectionState::Disconnected;
585 }
586
587 entry
588 }
589
590 pub fn on_connecting(&mut self, node_id: NodeId, now_ms: u64) {
592 if let Some(peer) = self.peers.get_mut(&node_id) {
593 peer.set_connecting(now_ms);
594 }
595 }
596
597 pub fn on_connected(&mut self, node_id: NodeId, now_ms: u64) {
599 if let Some(peer) = self.peers.get_mut(&node_id) {
600 peer.set_connected(now_ms);
601 }
602 }
603
604 pub fn on_disconnected(&mut self, node_id: NodeId, reason: DisconnectReason, now_ms: u64) {
606 if let Some(peer) = self.peers.get_mut(&node_id) {
607 peer.set_disconnected(now_ms, reason);
608 }
609 }
610
611 pub fn update_rssi(&mut self, node_id: NodeId, rssi: i8, now_ms: u64) -> bool {
615 if let Some(peer) = self.peers.get_mut(&node_id) {
616 return peer.update_rssi(rssi, now_ms, self.rssi_degraded_threshold);
617 }
618 false
619 }
620
621 pub fn record_transfer(&mut self, node_id: NodeId, bytes_received: u64, bytes_sent: u64) {
623 if let Some(peer) = self.peers.get_mut(&node_id) {
624 peer.record_transfer(bytes_received, bytes_sent);
625 }
626 }
627
628 pub fn record_sync(&mut self, node_id: NodeId) {
630 if let Some(peer) = self.peers.get_mut(&node_id) {
631 peer.record_sync();
632 }
633 }
634
635 pub fn tick(&mut self, now_ms: u64) -> Vec<NodeId> {
639 let mut newly_lost = Vec::new();
640
641 for (node_id, peer) in self.peers.iter_mut() {
642 if peer.state == ConnectionState::Disconnected {
643 if let Some(disconnected_at) = peer.disconnected_at {
644 if now_ms.saturating_sub(disconnected_at) > self.lost_timeout_ms {
645 peer.set_lost(now_ms);
646 newly_lost.push(*node_id);
647 }
648 }
649 }
650 }
651
652 newly_lost
653 }
654
655 pub fn cleanup_lost(&mut self, older_than_ms: u64, now_ms: u64) -> Vec<NodeId> {
657 let to_remove: Vec<NodeId> = self
658 .peers
659 .iter()
660 .filter(|(_, p)| {
661 p.state == ConnectionState::Lost
662 && now_ms.saturating_sub(p.last_seen_ms) > older_than_ms
663 })
664 .map(|(id, _)| *id)
665 .collect();
666
667 for id in &to_remove {
668 self.peers.remove(id);
669 }
670
671 to_remove
672 }
673
674 pub fn import_peer(&mut self, peer: &HivePeer, now_ms: u64) {
676 let state = PeerConnectionState::from_peer(peer, now_ms);
677 self.peers.insert(peer.node_id, state);
678 }
679
680 pub fn on_relay_received(
696 &mut self,
697 source_peer: NodeId,
698 origin_node: NodeId,
699 hop_count: u8,
700 now_ms: u64,
701 ) -> bool {
702 if hop_count > MAX_TRACKED_DEGREE {
704 return false;
705 }
706
707 if self.peers.contains_key(&origin_node) {
709 return false;
711 }
712
713 if let Some(existing) = self.indirect_peers.get_mut(&origin_node) {
715 existing.update_path(source_peer, hop_count, now_ms);
716 false
717 } else {
718 self.indirect_peers.insert(
719 origin_node,
720 IndirectPeer::new(origin_node, source_peer, hop_count, now_ms),
721 );
722 true
723 }
724 }
725
726 pub fn get_indirect_peers(&self) -> Vec<&IndirectPeer> {
728 self.indirect_peers.values().collect()
729 }
730
731 pub fn get_indirect_peers_owned(&self) -> Vec<IndirectPeer> {
733 self.indirect_peers.values().cloned().collect()
734 }
735
736 pub fn get_indirect_peer(&self, node_id: NodeId) -> Option<&IndirectPeer> {
738 self.indirect_peers.get(&node_id)
739 }
740
741 pub fn get_peers_by_degree(&self, degree: PeerDegree) -> Vec<NodeId> {
743 match degree {
744 PeerDegree::Direct => self.peers.keys().copied().collect(),
745 _ => self
746 .indirect_peers
747 .iter()
748 .filter(|(_, p)| p.degree() == Some(degree))
749 .map(|(id, _)| *id)
750 .collect(),
751 }
752 }
753
754 pub fn peer_degree(&self, node_id: NodeId) -> Option<PeerDegree> {
756 if self.peers.contains_key(&node_id) {
757 Some(PeerDegree::Direct)
758 } else {
759 self.indirect_peers.get(&node_id).and_then(|p| p.degree())
760 }
761 }
762
763 pub fn get_paths_to(&self, node_id: NodeId) -> Vec<(NodeId, u8)> {
767 self.indirect_peers
768 .get(&node_id)
769 .map(|p| p.paths())
770 .unwrap_or_default()
771 }
772
773 pub fn is_known(&self, node_id: NodeId) -> bool {
775 self.peers.contains_key(&node_id) || self.indirect_peers.contains_key(&node_id)
776 }
777
778 pub fn cleanup_indirect(&mut self, now_ms: u64) -> Vec<NodeId> {
782 let to_remove: Vec<NodeId> = self
783 .indirect_peers
784 .iter()
785 .filter(|(_, p)| p.is_stale(now_ms, self.indirect_peer_timeout_ms))
786 .map(|(id, _)| *id)
787 .collect();
788
789 for id in &to_remove {
790 self.indirect_peers.remove(id);
791 }
792
793 to_remove
794 }
795
796 pub fn remove_via_peer(&mut self, via_peer: NodeId) {
801 let mut to_remove = Vec::new();
802
803 for (node_id, indirect) in self.indirect_peers.iter_mut() {
804 indirect.via_peers.remove(&via_peer);
805
806 if indirect.via_peers.is_empty() {
808 to_remove.push(*node_id);
809 } else {
810 indirect.min_hops = indirect.via_peers.values().copied().min().unwrap_or(255);
811 }
812 }
813
814 for id in to_remove {
816 self.indirect_peers.remove(&id);
817 }
818 }
819
820 pub fn full_state_counts(&self) -> FullStateCountSummary {
822 let direct = self.state_counts();
823
824 let mut one_hop = 0;
825 let mut two_hop = 0;
826 let mut three_hop = 0;
827
828 for peer in self.indirect_peers.values() {
829 match peer.min_hops {
830 1 => one_hop += 1,
831 2 => two_hop += 1,
832 3 => three_hop += 1,
833 _ => {}
834 }
835 }
836
837 FullStateCountSummary {
838 direct,
839 one_hop,
840 two_hop,
841 three_hop,
842 }
843 }
844
845 pub fn indirect_peer_count(&self) -> usize {
847 self.indirect_peers.len()
848 }
849
850 pub fn set_indirect_callsign(&mut self, node_id: NodeId, callsign: String) {
852 if let Some(peer) = self.indirect_peers.get_mut(&node_id) {
853 peer.callsign = Some(callsign);
854 }
855 }
856}
857
858#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
860pub struct StateCountSummary {
861 pub discovered: usize,
863 pub connecting: usize,
865 pub connected: usize,
867 pub degraded: usize,
869 pub disconnecting: usize,
871 pub disconnected: usize,
873 pub lost: usize,
875}
876
877impl StateCountSummary {
878 pub fn active_connections(&self) -> usize {
880 self.connected + self.degraded
881 }
882
883 pub fn total(&self) -> usize {
885 self.discovered
886 + self.connecting
887 + self.connected
888 + self.degraded
889 + self.disconnecting
890 + self.disconnected
891 + self.lost
892 }
893}
894
895pub const MAX_TRACKED_DEGREE: u8 = 3;
897
898#[derive(Debug, Clone, Copy, PartialEq, Eq)]
900pub enum PeerDegree {
901 Direct,
903 OneHop,
905 TwoHop,
907 ThreeHop,
909}
910
911impl PeerDegree {
912 pub fn from_hops(hops: u8) -> Option<Self> {
914 match hops {
915 0 => Some(Self::Direct),
916 1 => Some(Self::OneHop),
917 2 => Some(Self::TwoHop),
918 3 => Some(Self::ThreeHop),
919 _ => None, }
921 }
922
923 pub fn hops(&self) -> u8 {
925 match self {
926 Self::Direct => 0,
927 Self::OneHop => 1,
928 Self::TwoHop => 2,
929 Self::ThreeHop => 3,
930 }
931 }
932}
933
934#[derive(Debug, Clone)]
939pub struct IndirectPeer {
940 pub node_id: NodeId,
942
943 pub min_hops: u8,
945
946 pub via_peers: BTreeMap<NodeId, u8>,
949
950 pub discovered_at: u64,
952
953 pub last_seen_ms: u64,
955
956 pub messages_received: u32,
958
959 pub callsign: Option<String>,
961}
962
963impl IndirectPeer {
964 pub fn new(node_id: NodeId, via_peer: NodeId, hop_count: u8, now_ms: u64) -> Self {
966 let mut via_peers = BTreeMap::new();
967 via_peers.insert(via_peer, hop_count);
968
969 Self {
970 node_id,
971 min_hops: hop_count,
972 via_peers,
973 discovered_at: now_ms,
974 last_seen_ms: now_ms,
975 messages_received: 1,
976 callsign: None,
977 }
978 }
979
980 pub fn update_path(&mut self, via_peer: NodeId, hop_count: u8, now_ms: u64) -> bool {
984 self.last_seen_ms = now_ms;
985 self.messages_received += 1;
986
987 let is_better = hop_count < self.min_hops;
988
989 self.via_peers.insert(via_peer, hop_count);
991
992 if is_better {
994 self.min_hops = hop_count;
995 } else {
996 self.min_hops = self.via_peers.values().copied().min().unwrap_or(hop_count);
998 }
999
1000 is_better
1001 }
1002
1003 pub fn degree(&self) -> Option<PeerDegree> {
1005 PeerDegree::from_hops(self.min_hops)
1006 }
1007
1008 pub fn is_stale(&self, now_ms: u64, timeout_ms: u64) -> bool {
1010 now_ms.saturating_sub(self.last_seen_ms) > timeout_ms
1011 }
1012
1013 pub fn paths(&self) -> Vec<(NodeId, u8)> {
1015 self.via_peers.iter().map(|(&k, &v)| (k, v)).collect()
1016 }
1017}
1018
1019#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1021pub struct FullStateCountSummary {
1022 pub direct: StateCountSummary,
1024 pub one_hop: usize,
1026 pub two_hop: usize,
1028 pub three_hop: usize,
1030}
1031
1032impl FullStateCountSummary {
1033 pub fn total(&self) -> usize {
1035 self.direct.total() + self.one_hop + self.two_hop + self.three_hop
1036 }
1037
1038 pub fn total_indirect(&self) -> usize {
1040 self.one_hop + self.two_hop + self.three_hop
1041 }
1042}
1043
1044#[derive(Debug, Clone)]
1049pub struct PeerManagerConfig {
1050 pub peer_timeout_ms: u64,
1052
1053 pub cleanup_interval_ms: u64,
1055
1056 pub sync_interval_ms: u64,
1058
1059 pub sync_cooldown_ms: u64,
1062
1063 pub auto_connect: bool,
1065
1066 pub mesh_id: String,
1068
1069 pub max_peers: usize,
1071
1072 pub rssi_degraded_threshold: i8,
1074
1075 pub lost_timeout_ms: u64,
1077}
1078
1079impl Default for PeerManagerConfig {
1080 fn default() -> Self {
1081 Self {
1082 peer_timeout_ms: 45_000, cleanup_interval_ms: 10_000, sync_interval_ms: 5_000, sync_cooldown_ms: 30_000, auto_connect: true,
1087 mesh_id: String::from("DEMO"),
1088 max_peers: 8,
1089 rssi_degraded_threshold: -80, lost_timeout_ms: 30_000, }
1092 }
1093}
1094
1095impl PeerManagerConfig {
1096 pub fn with_mesh_id(mesh_id: impl Into<String>) -> Self {
1098 Self {
1099 mesh_id: mesh_id.into(),
1100 ..Default::default()
1101 }
1102 }
1103
1104 pub fn peer_timeout(mut self, timeout_ms: u64) -> Self {
1106 self.peer_timeout_ms = timeout_ms;
1107 self
1108 }
1109
1110 pub fn sync_interval(mut self, interval_ms: u64) -> Self {
1112 self.sync_interval_ms = interval_ms;
1113 self
1114 }
1115
1116 pub fn auto_connect(mut self, enabled: bool) -> Self {
1118 self.auto_connect = enabled;
1119 self
1120 }
1121
1122 pub fn max_peers(mut self, max: usize) -> Self {
1124 self.max_peers = max;
1125 self
1126 }
1127
1128 pub fn matches_mesh(&self, device_mesh_id: Option<&str>) -> bool {
1134 match device_mesh_id {
1135 Some(id) => id == self.mesh_id,
1136 None => true, }
1138 }
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144
1145 #[test]
1146 fn test_peer_stale_detection() {
1147 let mut peer = HivePeer::new(
1148 NodeId::new(0x12345678),
1149 "test-id".into(),
1150 Some("DEMO".into()),
1151 Some("HIVE_DEMO-12345678".into()),
1152 -70,
1153 );
1154
1155 peer.touch(1000);
1157 assert!(!peer.is_stale(2000, 45_000));
1158
1159 assert!(peer.is_stale(50_000, 45_000));
1161 }
1162
1163 #[test]
1164 fn test_signal_strength() {
1165 let peer_excellent = HivePeer {
1166 rssi: -45,
1167 ..Default::default()
1168 };
1169 assert_eq!(peer_excellent.signal_strength(), SignalStrength::Excellent);
1170
1171 let peer_good = HivePeer {
1172 rssi: -65,
1173 ..Default::default()
1174 };
1175 assert_eq!(peer_good.signal_strength(), SignalStrength::Good);
1176
1177 let peer_fair = HivePeer {
1178 rssi: -80,
1179 ..Default::default()
1180 };
1181 assert_eq!(peer_fair.signal_strength(), SignalStrength::Fair);
1182
1183 let peer_weak = HivePeer {
1184 rssi: -95,
1185 ..Default::default()
1186 };
1187 assert_eq!(peer_weak.signal_strength(), SignalStrength::Weak);
1188 }
1189
1190 #[test]
1191 fn test_mesh_matching() {
1192 let config = PeerManagerConfig::with_mesh_id("ALPHA");
1193
1194 assert!(config.matches_mesh(Some("ALPHA")));
1196
1197 assert!(!config.matches_mesh(Some("BETA")));
1199
1200 assert!(config.matches_mesh(None));
1202 }
1203}