hive_btle/
peer.rs

1//! Peer management types for HIVE BLE mesh
2//!
3//! This module provides the core peer representation and configuration
4//! for centralized peer management across all platforms (iOS, Android, ESP32).
5
6#[cfg(not(feature = "std"))]
7use alloc::{string::String, vec::Vec};
8
9use crate::NodeId;
10
11/// Unified peer representation across all platforms
12///
13/// Represents a discovered or connected HIVE mesh peer with all
14/// relevant metadata for mesh operations.
15#[derive(Debug, Clone)]
16pub struct HivePeer {
17    /// HIVE node identifier (32-bit)
18    pub node_id: NodeId,
19
20    /// Platform-specific BLE identifier
21    /// - iOS: CBPeripheral UUID string
22    /// - Android: MAC address string
23    /// - ESP32: MAC address or NimBLE handle
24    pub identifier: String,
25
26    /// Mesh ID this peer belongs to (e.g., "DEMO")
27    pub mesh_id: Option<String>,
28
29    /// Advertised device name (e.g., "HIVE_DEMO-12345678")
30    pub name: Option<String>,
31
32    /// Last known signal strength (RSSI in dBm)
33    pub rssi: i8,
34
35    /// Whether we have an active BLE connection to this peer
36    pub is_connected: bool,
37
38    /// Timestamp when this peer was last seen (milliseconds since epoch/boot)
39    pub last_seen_ms: u64,
40}
41
42impl HivePeer {
43    /// Create a new peer from discovery data
44    pub fn new(
45        node_id: NodeId,
46        identifier: String,
47        mesh_id: Option<String>,
48        name: Option<String>,
49        rssi: i8,
50    ) -> Self {
51        Self {
52            node_id,
53            identifier,
54            mesh_id,
55            name,
56            rssi,
57            is_connected: false,
58            last_seen_ms: 0,
59        }
60    }
61
62    /// Update the peer's last seen timestamp
63    pub fn touch(&mut self, now_ms: u64) {
64        self.last_seen_ms = now_ms;
65    }
66
67    /// Check if this peer is stale (not seen within timeout)
68    pub fn is_stale(&self, now_ms: u64, timeout_ms: u64) -> bool {
69        if self.last_seen_ms == 0 {
70            return false; // Never seen, don't consider stale
71        }
72        now_ms.saturating_sub(self.last_seen_ms) > timeout_ms
73    }
74
75    /// Get display name for this peer
76    pub fn display_name(&self) -> &str {
77        self.name.as_deref().unwrap_or(self.identifier.as_str())
78    }
79
80    /// Get signal strength category
81    pub fn signal_strength(&self) -> SignalStrength {
82        match self.rssi {
83            r if r >= -50 => SignalStrength::Excellent,
84            r if r >= -70 => SignalStrength::Good,
85            r if r >= -85 => SignalStrength::Fair,
86            _ => SignalStrength::Weak,
87        }
88    }
89}
90
91impl Default for HivePeer {
92    fn default() -> Self {
93        Self {
94            node_id: NodeId::default(),
95            identifier: String::new(),
96            mesh_id: None,
97            name: None,
98            rssi: -100,
99            is_connected: false,
100            last_seen_ms: 0,
101        }
102    }
103}
104
105/// Signal strength categories for display
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum SignalStrength {
108    /// RSSI >= -50 dBm
109    Excellent,
110    /// RSSI >= -70 dBm
111    Good,
112    /// RSSI >= -85 dBm
113    Fair,
114    /// RSSI < -85 dBm
115    Weak,
116}
117
118/// Configuration for the PeerManager
119///
120/// Provides configurable timeouts and behaviors for peer management.
121/// All time values are in milliseconds.
122#[derive(Debug, Clone)]
123pub struct PeerManagerConfig {
124    /// Time after which a peer is considered stale and removed (default: 45000ms)
125    pub peer_timeout_ms: u64,
126
127    /// How often to run cleanup of stale peers (default: 10000ms)
128    pub cleanup_interval_ms: u64,
129
130    /// How often to sync documents with peers (default: 5000ms)
131    pub sync_interval_ms: u64,
132
133    /// Minimum time between syncs to the same peer (default: 30000ms)
134    /// Prevents "thrashing" when peers keep reconnecting
135    pub sync_cooldown_ms: u64,
136
137    /// Whether to automatically connect to discovered peers (default: true)
138    pub auto_connect: bool,
139
140    /// Local mesh ID for filtering peers (e.g., "DEMO")
141    pub mesh_id: String,
142
143    /// Maximum number of tracked peers (for no_std/embedded, default: 8)
144    pub max_peers: usize,
145}
146
147impl Default for PeerManagerConfig {
148    fn default() -> Self {
149        Self {
150            peer_timeout_ms: 45_000,     // 45 seconds
151            cleanup_interval_ms: 10_000, // 10 seconds
152            sync_interval_ms: 5_000,     // 5 seconds
153            sync_cooldown_ms: 30_000,    // 30 seconds
154            auto_connect: true,
155            mesh_id: String::from("DEMO"),
156            max_peers: 8,
157        }
158    }
159}
160
161impl PeerManagerConfig {
162    /// Create a new config with the specified mesh ID
163    pub fn with_mesh_id(mesh_id: impl Into<String>) -> Self {
164        Self {
165            mesh_id: mesh_id.into(),
166            ..Default::default()
167        }
168    }
169
170    /// Set peer timeout
171    pub fn peer_timeout(mut self, timeout_ms: u64) -> Self {
172        self.peer_timeout_ms = timeout_ms;
173        self
174    }
175
176    /// Set sync interval
177    pub fn sync_interval(mut self, interval_ms: u64) -> Self {
178        self.sync_interval_ms = interval_ms;
179        self
180    }
181
182    /// Set auto-connect behavior
183    pub fn auto_connect(mut self, enabled: bool) -> Self {
184        self.auto_connect = enabled;
185        self
186    }
187
188    /// Set max peers (for embedded systems)
189    pub fn max_peers(mut self, max: usize) -> Self {
190        self.max_peers = max;
191        self
192    }
193
194    /// Check if a device mesh ID matches our mesh
195    ///
196    /// Returns true if:
197    /// - Device mesh ID matches our mesh ID exactly, OR
198    /// - Device mesh ID is None (legacy device, matches any mesh)
199    pub fn matches_mesh(&self, device_mesh_id: Option<&str>) -> bool {
200        match device_mesh_id {
201            Some(id) => id == self.mesh_id,
202            None => true, // Legacy devices match any mesh
203        }
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_peer_stale_detection() {
213        let mut peer = HivePeer::new(
214            NodeId::new(0x12345678),
215            "test-id".into(),
216            Some("DEMO".into()),
217            Some("HIVE_DEMO-12345678".into()),
218            -70,
219        );
220
221        // Fresh peer is not stale
222        peer.touch(1000);
223        assert!(!peer.is_stale(2000, 45_000));
224
225        // Peer becomes stale after timeout
226        assert!(peer.is_stale(50_000, 45_000));
227    }
228
229    #[test]
230    fn test_signal_strength() {
231        let peer_excellent = HivePeer {
232            rssi: -45,
233            ..Default::default()
234        };
235        assert_eq!(peer_excellent.signal_strength(), SignalStrength::Excellent);
236
237        let peer_good = HivePeer {
238            rssi: -65,
239            ..Default::default()
240        };
241        assert_eq!(peer_good.signal_strength(), SignalStrength::Good);
242
243        let peer_fair = HivePeer {
244            rssi: -80,
245            ..Default::default()
246        };
247        assert_eq!(peer_fair.signal_strength(), SignalStrength::Fair);
248
249        let peer_weak = HivePeer {
250            rssi: -95,
251            ..Default::default()
252        };
253        assert_eq!(peer_weak.signal_strength(), SignalStrength::Weak);
254    }
255
256    #[test]
257    fn test_mesh_matching() {
258        let config = PeerManagerConfig::with_mesh_id("ALPHA");
259
260        // Exact match
261        assert!(config.matches_mesh(Some("ALPHA")));
262
263        // No match
264        assert!(!config.matches_mesh(Some("BETA")));
265
266        // Legacy device (no mesh ID) matches any
267        assert!(config.matches_mesh(None));
268    }
269}