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 rssi_degraded_threshold: i8,
428
429 lost_timeout_ms: u64,
431}
432
433impl ConnectionStateGraph {
434 pub fn new() -> Self {
436 Self {
437 peers: BTreeMap::new(),
438 rssi_degraded_threshold: -80,
439 lost_timeout_ms: 30_000,
440 }
441 }
442
443 pub fn with_config(rssi_degraded_threshold: i8, lost_timeout_ms: u64) -> Self {
445 Self {
446 peers: BTreeMap::new(),
447 rssi_degraded_threshold,
448 lost_timeout_ms,
449 }
450 }
451
452 pub fn get_all(&self) -> Vec<&PeerConnectionState> {
454 self.peers.values().collect()
455 }
456
457 pub fn get_all_owned(&self) -> Vec<PeerConnectionState> {
459 self.peers.values().cloned().collect()
460 }
461
462 pub fn get_peer(&self, node_id: NodeId) -> Option<&PeerConnectionState> {
464 self.peers.get(&node_id)
465 }
466
467 pub fn get_peer_mut(&mut self, node_id: NodeId) -> Option<&mut PeerConnectionState> {
469 self.peers.get_mut(&node_id)
470 }
471
472 pub fn get_connected(&self) -> Vec<&PeerConnectionState> {
474 self.peers
475 .values()
476 .filter(|p| p.state.is_connected())
477 .collect()
478 }
479
480 pub fn get_degraded(&self) -> Vec<&PeerConnectionState> {
482 self.peers
483 .values()
484 .filter(|p| p.state == ConnectionState::Degraded)
485 .collect()
486 }
487
488 pub fn get_recently_disconnected(
490 &self,
491 within_ms: u64,
492 now_ms: u64,
493 ) -> Vec<&PeerConnectionState> {
494 self.peers
495 .values()
496 .filter(|p| {
497 p.state == ConnectionState::Disconnected
498 && p.disconnected_at
499 .map(|t| now_ms.saturating_sub(t) <= within_ms)
500 .unwrap_or(false)
501 })
502 .collect()
503 }
504
505 pub fn get_lost(&self) -> Vec<&PeerConnectionState> {
507 self.peers
508 .values()
509 .filter(|p| p.state == ConnectionState::Lost)
510 .collect()
511 }
512
513 pub fn get_with_history(&self) -> Vec<&PeerConnectionState> {
515 self.peers
516 .values()
517 .filter(|p| p.state.was_connected())
518 .collect()
519 }
520
521 pub fn state_counts(&self) -> StateCountSummary {
523 let mut summary = StateCountSummary::default();
524 for peer in self.peers.values() {
525 match peer.state {
526 ConnectionState::Discovered => summary.discovered += 1,
527 ConnectionState::Connecting => summary.connecting += 1,
528 ConnectionState::Connected => summary.connected += 1,
529 ConnectionState::Degraded => summary.degraded += 1,
530 ConnectionState::Disconnecting => summary.disconnecting += 1,
531 ConnectionState::Disconnected => summary.disconnected += 1,
532 ConnectionState::Lost => summary.lost += 1,
533 }
534 }
535 summary
536 }
537
538 pub fn len(&self) -> usize {
540 self.peers.len()
541 }
542
543 pub fn is_empty(&self) -> bool {
545 self.peers.is_empty()
546 }
547
548 pub fn on_discovered(
550 &mut self,
551 node_id: NodeId,
552 identifier: String,
553 name: Option<String>,
554 mesh_id: Option<String>,
555 rssi: i8,
556 now_ms: u64,
557 ) -> &PeerConnectionState {
558 let entry = self.peers.entry(node_id).or_insert_with(|| {
559 PeerConnectionState::new_discovered(node_id, identifier.clone(), now_ms)
560 });
561
562 entry.last_rssi = Some(rssi);
564 entry.last_seen_ms = now_ms;
565 if name.is_some() {
566 entry.name = name;
567 }
568 if mesh_id.is_some() {
569 entry.mesh_id = mesh_id;
570 }
571
572 if entry.state == ConnectionState::Lost {
574 entry.state = ConnectionState::Disconnected;
575 }
576
577 entry
578 }
579
580 pub fn on_connecting(&mut self, node_id: NodeId, now_ms: u64) {
582 if let Some(peer) = self.peers.get_mut(&node_id) {
583 peer.set_connecting(now_ms);
584 }
585 }
586
587 pub fn on_connected(&mut self, node_id: NodeId, now_ms: u64) {
589 if let Some(peer) = self.peers.get_mut(&node_id) {
590 peer.set_connected(now_ms);
591 }
592 }
593
594 pub fn on_disconnected(&mut self, node_id: NodeId, reason: DisconnectReason, now_ms: u64) {
596 if let Some(peer) = self.peers.get_mut(&node_id) {
597 peer.set_disconnected(now_ms, reason);
598 }
599 }
600
601 pub fn update_rssi(&mut self, node_id: NodeId, rssi: i8, now_ms: u64) -> bool {
605 if let Some(peer) = self.peers.get_mut(&node_id) {
606 return peer.update_rssi(rssi, now_ms, self.rssi_degraded_threshold);
607 }
608 false
609 }
610
611 pub fn record_transfer(&mut self, node_id: NodeId, bytes_received: u64, bytes_sent: u64) {
613 if let Some(peer) = self.peers.get_mut(&node_id) {
614 peer.record_transfer(bytes_received, bytes_sent);
615 }
616 }
617
618 pub fn record_sync(&mut self, node_id: NodeId) {
620 if let Some(peer) = self.peers.get_mut(&node_id) {
621 peer.record_sync();
622 }
623 }
624
625 pub fn tick(&mut self, now_ms: u64) -> Vec<NodeId> {
629 let mut newly_lost = Vec::new();
630
631 for (node_id, peer) in self.peers.iter_mut() {
632 if peer.state == ConnectionState::Disconnected {
633 if let Some(disconnected_at) = peer.disconnected_at {
634 if now_ms.saturating_sub(disconnected_at) > self.lost_timeout_ms {
635 peer.set_lost(now_ms);
636 newly_lost.push(*node_id);
637 }
638 }
639 }
640 }
641
642 newly_lost
643 }
644
645 pub fn cleanup_lost(&mut self, older_than_ms: u64, now_ms: u64) -> Vec<NodeId> {
647 let to_remove: Vec<NodeId> = self
648 .peers
649 .iter()
650 .filter(|(_, p)| {
651 p.state == ConnectionState::Lost
652 && now_ms.saturating_sub(p.last_seen_ms) > older_than_ms
653 })
654 .map(|(id, _)| *id)
655 .collect();
656
657 for id in &to_remove {
658 self.peers.remove(id);
659 }
660
661 to_remove
662 }
663
664 pub fn import_peer(&mut self, peer: &HivePeer, now_ms: u64) {
666 let state = PeerConnectionState::from_peer(peer, now_ms);
667 self.peers.insert(peer.node_id, state);
668 }
669}
670
671#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
673pub struct StateCountSummary {
674 pub discovered: usize,
676 pub connecting: usize,
678 pub connected: usize,
680 pub degraded: usize,
682 pub disconnecting: usize,
684 pub disconnected: usize,
686 pub lost: usize,
688}
689
690impl StateCountSummary {
691 pub fn active_connections(&self) -> usize {
693 self.connected + self.degraded
694 }
695
696 pub fn total(&self) -> usize {
698 self.discovered
699 + self.connecting
700 + self.connected
701 + self.degraded
702 + self.disconnecting
703 + self.disconnected
704 + self.lost
705 }
706}
707
708#[derive(Debug, Clone)]
713pub struct PeerManagerConfig {
714 pub peer_timeout_ms: u64,
716
717 pub cleanup_interval_ms: u64,
719
720 pub sync_interval_ms: u64,
722
723 pub sync_cooldown_ms: u64,
726
727 pub auto_connect: bool,
729
730 pub mesh_id: String,
732
733 pub max_peers: usize,
735
736 pub rssi_degraded_threshold: i8,
738
739 pub lost_timeout_ms: u64,
741}
742
743impl Default for PeerManagerConfig {
744 fn default() -> Self {
745 Self {
746 peer_timeout_ms: 45_000, cleanup_interval_ms: 10_000, sync_interval_ms: 5_000, sync_cooldown_ms: 30_000, auto_connect: true,
751 mesh_id: String::from("DEMO"),
752 max_peers: 8,
753 rssi_degraded_threshold: -80, lost_timeout_ms: 30_000, }
756 }
757}
758
759impl PeerManagerConfig {
760 pub fn with_mesh_id(mesh_id: impl Into<String>) -> Self {
762 Self {
763 mesh_id: mesh_id.into(),
764 ..Default::default()
765 }
766 }
767
768 pub fn peer_timeout(mut self, timeout_ms: u64) -> Self {
770 self.peer_timeout_ms = timeout_ms;
771 self
772 }
773
774 pub fn sync_interval(mut self, interval_ms: u64) -> Self {
776 self.sync_interval_ms = interval_ms;
777 self
778 }
779
780 pub fn auto_connect(mut self, enabled: bool) -> Self {
782 self.auto_connect = enabled;
783 self
784 }
785
786 pub fn max_peers(mut self, max: usize) -> Self {
788 self.max_peers = max;
789 self
790 }
791
792 pub fn matches_mesh(&self, device_mesh_id: Option<&str>) -> bool {
798 match device_mesh_id {
799 Some(id) => id == self.mesh_id,
800 None => true, }
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808
809 #[test]
810 fn test_peer_stale_detection() {
811 let mut peer = HivePeer::new(
812 NodeId::new(0x12345678),
813 "test-id".into(),
814 Some("DEMO".into()),
815 Some("HIVE_DEMO-12345678".into()),
816 -70,
817 );
818
819 peer.touch(1000);
821 assert!(!peer.is_stale(2000, 45_000));
822
823 assert!(peer.is_stale(50_000, 45_000));
825 }
826
827 #[test]
828 fn test_signal_strength() {
829 let peer_excellent = HivePeer {
830 rssi: -45,
831 ..Default::default()
832 };
833 assert_eq!(peer_excellent.signal_strength(), SignalStrength::Excellent);
834
835 let peer_good = HivePeer {
836 rssi: -65,
837 ..Default::default()
838 };
839 assert_eq!(peer_good.signal_strength(), SignalStrength::Good);
840
841 let peer_fair = HivePeer {
842 rssi: -80,
843 ..Default::default()
844 };
845 assert_eq!(peer_fair.signal_strength(), SignalStrength::Fair);
846
847 let peer_weak = HivePeer {
848 rssi: -95,
849 ..Default::default()
850 };
851 assert_eq!(peer_weak.signal_strength(), SignalStrength::Weak);
852 }
853
854 #[test]
855 fn test_mesh_matching() {
856 let config = PeerManagerConfig::with_mesh_id("ALPHA");
857
858 assert!(config.matches_mesh(Some("ALPHA")));
860
861 assert!(!config.matches_mesh(Some("BETA")));
863
864 assert!(config.matches_mesh(None));
866 }
867}