hive_btle/
config.rs

1//! Configuration types for HIVE-BTLE
2//!
3//! Provides configuration structures for BLE transport, discovery,
4//! GATT, mesh, power management, and security settings.
5
6use crate::NodeId;
7
8/// BLE Physical Layer (PHY) type
9///
10/// BLE 5.0+ supports multiple PHY options with different
11/// trade-offs between range, throughput, and power consumption.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum BlePhy {
14    /// LE 1M PHY - 1 Mbps, ~100m range (default, most compatible)
15    #[default]
16    Le1M,
17    /// LE 2M PHY - 2 Mbps, ~50m range (higher throughput)
18    Le2M,
19    /// LE Coded S=2 - 500 kbps, ~200m range
20    LeCodedS2,
21    /// LE Coded S=8 - 125 kbps, ~400m range (maximum range)
22    LeCodedS8,
23}
24
25impl BlePhy {
26    /// Get the theoretical bandwidth in bytes per second
27    pub fn bandwidth_bps(&self) -> u32 {
28        match self {
29            BlePhy::Le1M => 1_000_000,
30            BlePhy::Le2M => 2_000_000,
31            BlePhy::LeCodedS2 => 500_000,
32            BlePhy::LeCodedS8 => 125_000,
33        }
34    }
35
36    /// Get the typical range in meters
37    pub fn typical_range_meters(&self) -> u32 {
38        match self {
39            BlePhy::Le1M => 100,
40            BlePhy::Le2M => 50,
41            BlePhy::LeCodedS2 => 200,
42            BlePhy::LeCodedS8 => 400,
43        }
44    }
45
46    /// Check if this PHY requires BLE 5.0+
47    pub fn requires_ble5(&self) -> bool {
48        matches!(self, BlePhy::Le2M | BlePhy::LeCodedS2 | BlePhy::LeCodedS8)
49    }
50}
51
52/// Power management profile
53///
54/// Controls radio duty cycle and timing parameters to balance
55/// responsiveness against battery consumption.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum PowerProfile {
58    /// Aggressive - ~20% duty cycle, ~6 hour watch battery
59    /// Use for high-activity scenarios
60    Aggressive,
61
62    /// Balanced - ~10% duty cycle, ~12 hour watch battery
63    #[default]
64    Balanced,
65
66    /// Low Power - ~2% duty cycle, ~20+ hour watch battery
67    /// Recommended for HIVE-Lite nodes
68    LowPower,
69
70    /// Custom power profile with explicit parameters
71    Custom {
72        /// Scan interval in milliseconds
73        scan_interval_ms: u32,
74        /// Scan window in milliseconds
75        scan_window_ms: u32,
76        /// Advertisement interval in milliseconds
77        adv_interval_ms: u32,
78        /// Connection interval in milliseconds
79        conn_interval_ms: u32,
80    },
81}
82
83impl PowerProfile {
84    /// Get scan interval in milliseconds
85    pub fn scan_interval_ms(&self) -> u32 {
86        match self {
87            PowerProfile::Aggressive => 100,
88            PowerProfile::Balanced => 500,
89            PowerProfile::LowPower => 5000,
90            PowerProfile::Custom {
91                scan_interval_ms, ..
92            } => *scan_interval_ms,
93        }
94    }
95
96    /// Get scan window in milliseconds
97    pub fn scan_window_ms(&self) -> u32 {
98        match self {
99            PowerProfile::Aggressive => 50,
100            PowerProfile::Balanced => 50,
101            PowerProfile::LowPower => 100,
102            PowerProfile::Custom { scan_window_ms, .. } => *scan_window_ms,
103        }
104    }
105
106    /// Get advertisement interval in milliseconds
107    pub fn adv_interval_ms(&self) -> u32 {
108        match self {
109            PowerProfile::Aggressive => 100,
110            PowerProfile::Balanced => 500,
111            PowerProfile::LowPower => 2000,
112            PowerProfile::Custom {
113                adv_interval_ms, ..
114            } => *adv_interval_ms,
115        }
116    }
117
118    /// Get connection interval in milliseconds
119    pub fn conn_interval_ms(&self) -> u32 {
120        match self {
121            PowerProfile::Aggressive => 15,
122            PowerProfile::Balanced => 30,
123            PowerProfile::LowPower => 100,
124            PowerProfile::Custom {
125                conn_interval_ms, ..
126            } => *conn_interval_ms,
127        }
128    }
129
130    /// Estimated radio duty cycle as percentage
131    pub fn duty_cycle_percent(&self) -> u8 {
132        match self {
133            PowerProfile::Aggressive => 20,
134            PowerProfile::Balanced => 10,
135            PowerProfile::LowPower => 2,
136            PowerProfile::Custom {
137                scan_interval_ms,
138                scan_window_ms,
139                ..
140            } => {
141                if *scan_interval_ms == 0 {
142                    0
143                } else {
144                    ((scan_window_ms * 100) / scan_interval_ms) as u8
145                }
146            }
147        }
148    }
149}
150
151/// Discovery configuration
152#[derive(Debug, Clone)]
153pub struct DiscoveryConfig {
154    /// Scan interval in milliseconds
155    pub scan_interval_ms: u32,
156    /// Scan window in milliseconds (must be <= scan_interval_ms)
157    pub scan_window_ms: u32,
158    /// Advertisement interval in milliseconds
159    pub adv_interval_ms: u32,
160    /// Transmit power in dBm (-20 to +10 typical)
161    pub tx_power_dbm: i8,
162    /// PHY for advertising
163    pub adv_phy: BlePhy,
164    /// PHY for scanning
165    pub scan_phy: BlePhy,
166    /// Enable active scanning (requests scan response)
167    pub active_scan: bool,
168    /// Filter duplicates during scan
169    pub filter_duplicates: bool,
170}
171
172impl Default for DiscoveryConfig {
173    fn default() -> Self {
174        Self {
175            scan_interval_ms: 500,
176            scan_window_ms: 50,
177            adv_interval_ms: 500,
178            tx_power_dbm: 0,
179            adv_phy: BlePhy::Le1M,
180            scan_phy: BlePhy::Le1M,
181            active_scan: true,
182            filter_duplicates: true,
183        }
184    }
185}
186
187/// GATT configuration
188#[derive(Debug, Clone)]
189pub struct GattConfig {
190    /// Preferred MTU size (23-517 bytes)
191    pub preferred_mtu: u16,
192    /// Minimum acceptable MTU
193    pub min_mtu: u16,
194    /// Enable GATT server (peripheral) mode
195    pub enable_server: bool,
196    /// Enable GATT client (central) mode
197    pub enable_client: bool,
198}
199
200impl Default for GattConfig {
201    fn default() -> Self {
202        Self {
203            preferred_mtu: 251,
204            min_mtu: 23,
205            enable_server: true,
206            enable_client: true,
207        }
208    }
209}
210
211/// Default mesh ID for demos and testing
212pub const DEFAULT_MESH_ID: &str = "DEMO";
213
214/// Mesh configuration
215#[derive(Debug, Clone)]
216pub struct MeshConfig {
217    /// Mesh identifier - nodes only auto-connect to peers with matching mesh ID
218    ///
219    /// Format: 4-character alphanumeric (e.g., "DEMO", "ALFA", "TEST")
220    /// This maps to the `app_id` concept in hive-protocol.
221    pub mesh_id: String,
222    /// Maximum number of simultaneous connections
223    pub max_connections: u8,
224    /// Maximum children for this node (0 = leaf node)
225    pub max_children: u8,
226    /// Connection supervision timeout in milliseconds
227    pub supervision_timeout_ms: u16,
228    /// Slave latency (number of connection events to skip)
229    pub slave_latency: u16,
230    /// Minimum connection interval in milliseconds
231    pub conn_interval_min_ms: u16,
232    /// Maximum connection interval in milliseconds
233    pub conn_interval_max_ms: u16,
234}
235
236impl MeshConfig {
237    /// Create a new mesh config with the given mesh ID
238    pub fn new(mesh_id: impl Into<String>) -> Self {
239        Self {
240            mesh_id: mesh_id.into(),
241            ..Default::default()
242        }
243    }
244
245    /// Generate the BLE device name for this node
246    ///
247    /// Format: `HIVE_<MESH_ID>-<NODE_ID>` (e.g., "HIVE_DEMO-12345678")
248    pub fn device_name(&self, node_id: NodeId) -> String {
249        format!("HIVE_{}-{:08X}", self.mesh_id, node_id.as_u32())
250    }
251
252    /// Parse mesh ID and node ID from a device name
253    ///
254    /// Returns `Some((mesh_id, node_id))` for valid names, `None` otherwise.
255    ///
256    /// Supports both formats:
257    /// - New: `HIVE_<MESH_ID>-<NODE_ID>` (e.g., "HIVE_DEMO-12345678")
258    /// - Legacy: `HIVE-<NODE_ID>` (e.g., "HIVE-12345678") - returns None for mesh_id
259    pub fn parse_device_name(name: &str) -> Option<(Option<String>, NodeId)> {
260        if let Some(rest) = name.strip_prefix("HIVE_") {
261            // New format: HIVE_MESHID-NODEID
262            let (mesh_id, node_id_str) = rest.split_once('-')?;
263            let node_id = u32::from_str_radix(node_id_str, 16).ok()?;
264            Some((Some(mesh_id.to_string()), NodeId::new(node_id)))
265        } else if let Some(node_id_str) = name.strip_prefix("HIVE-") {
266            // Legacy format: HIVE-NODEID (no mesh ID)
267            let node_id = u32::from_str_radix(node_id_str, 16).ok()?;
268            Some((None, NodeId::new(node_id)))
269        } else {
270            None
271        }
272    }
273
274    /// Check if a discovered device matches this mesh
275    ///
276    /// Returns true if:
277    /// - The device has the same mesh ID, OR
278    /// - The device has no mesh ID (legacy format - backwards compatible)
279    pub fn matches_mesh(&self, device_mesh_id: Option<&str>) -> bool {
280        match device_mesh_id {
281            Some(id) => id == self.mesh_id,
282            None => true, // Legacy devices match any mesh
283        }
284    }
285}
286
287impl Default for MeshConfig {
288    fn default() -> Self {
289        Self {
290            mesh_id: DEFAULT_MESH_ID.to_string(),
291            max_connections: 7,
292            max_children: 3,
293            supervision_timeout_ms: 4000,
294            slave_latency: 0,
295            conn_interval_min_ms: 30,
296            conn_interval_max_ms: 50,
297        }
298    }
299}
300
301/// PHY selection strategy
302#[derive(Debug, Clone)]
303pub enum PhyStrategy {
304    /// Use a fixed PHY
305    Fixed(BlePhy),
306    /// Adaptive PHY selection based on RSSI
307    Adaptive {
308        /// RSSI threshold to switch to high-throughput PHY (dBm)
309        rssi_high_threshold: i8,
310        /// RSSI threshold to switch to long-range PHY (dBm)
311        rssi_low_threshold: i8,
312        /// Hysteresis to prevent oscillation (dB)
313        hysteresis_db: u8,
314    },
315    /// Always use maximum range (Coded S=8)
316    MaxRange,
317    /// Always use maximum throughput (2M)
318    MaxThroughput,
319}
320
321impl Default for PhyStrategy {
322    fn default() -> Self {
323        PhyStrategy::Fixed(BlePhy::Le1M)
324    }
325}
326
327/// PHY configuration
328#[derive(Debug, Clone, Default)]
329pub struct PhyConfig {
330    /// PHY selection strategy
331    pub strategy: PhyStrategy,
332    /// Preferred PHY for connections
333    pub preferred_phy: BlePhy,
334    /// Allow PHY upgrade after connection
335    pub allow_phy_update: bool,
336}
337
338/// Security configuration
339#[derive(Debug, Clone)]
340pub struct SecurityConfig {
341    /// Require pairing before data exchange
342    pub require_pairing: bool,
343    /// Require encrypted connections
344    pub require_encryption: bool,
345    /// Enable MITM protection
346    pub require_mitm_protection: bool,
347    /// Enable Secure Connections (BLE 4.2+)
348    pub require_secure_connections: bool,
349    /// Enable application-layer encryption (in addition to BLE)
350    pub app_layer_encryption: bool,
351}
352
353impl Default for SecurityConfig {
354    fn default() -> Self {
355        Self {
356            require_pairing: false,
357            require_encryption: true,
358            require_mitm_protection: false,
359            require_secure_connections: false,
360            app_layer_encryption: false,
361        }
362    }
363}
364
365/// Main BLE transport configuration
366#[derive(Debug, Clone)]
367pub struct BleConfig {
368    /// This node's identifier
369    pub node_id: NodeId,
370    /// Node capabilities flags
371    pub capabilities: u16,
372    /// Hierarchy level (0 = platform/leaf)
373    pub hierarchy_level: u8,
374    /// Geohash for location (24-bit, 6-char precision)
375    pub geohash: u32,
376    /// Discovery configuration
377    pub discovery: DiscoveryConfig,
378    /// GATT configuration
379    pub gatt: GattConfig,
380    /// Mesh configuration
381    pub mesh: MeshConfig,
382    /// Power profile
383    pub power_profile: PowerProfile,
384    /// PHY configuration
385    pub phy: PhyConfig,
386    /// Security configuration
387    pub security: SecurityConfig,
388}
389
390impl BleConfig {
391    /// Create a new configuration with the given node ID
392    pub fn new(node_id: NodeId) -> Self {
393        Self {
394            node_id,
395            capabilities: 0,
396            hierarchy_level: 0,
397            geohash: 0,
398            discovery: DiscoveryConfig::default(),
399            gatt: GattConfig::default(),
400            mesh: MeshConfig::default(),
401            power_profile: PowerProfile::default(),
402            phy: PhyConfig::default(),
403            security: SecurityConfig::default(),
404        }
405    }
406
407    /// Create a HIVE-Lite optimized configuration
408    ///
409    /// Optimized for battery efficiency with:
410    /// - Low power profile (~2% duty cycle)
411    /// - Leaf node (no children)
412    /// - Minimal scanning
413    pub fn hive_lite(node_id: NodeId) -> Self {
414        let mut config = Self::new(node_id);
415        config.power_profile = PowerProfile::LowPower;
416        config.mesh.max_children = 0; // Leaf node
417        config.discovery.scan_interval_ms = 5000;
418        config.discovery.scan_window_ms = 100;
419        config.discovery.adv_interval_ms = 2000;
420        config
421    }
422
423    /// Apply power profile settings to discovery config
424    pub fn apply_power_profile(&mut self) {
425        self.discovery.scan_interval_ms = self.power_profile.scan_interval_ms();
426        self.discovery.scan_window_ms = self.power_profile.scan_window_ms();
427        self.discovery.adv_interval_ms = self.power_profile.adv_interval_ms();
428        self.mesh.conn_interval_min_ms = self.power_profile.conn_interval_ms() as u16;
429        self.mesh.conn_interval_max_ms = self.power_profile.conn_interval_ms() as u16 + 20;
430    }
431}
432
433impl Default for BleConfig {
434    fn default() -> Self {
435        Self::new(NodeId::default())
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn test_phy_properties() {
445        assert_eq!(BlePhy::Le1M.bandwidth_bps(), 1_000_000);
446        assert_eq!(BlePhy::LeCodedS8.typical_range_meters(), 400);
447        assert!(!BlePhy::Le1M.requires_ble5());
448        assert!(BlePhy::Le2M.requires_ble5());
449    }
450
451    #[test]
452    fn test_power_profile_duty_cycle() {
453        assert_eq!(PowerProfile::Aggressive.duty_cycle_percent(), 20);
454        assert_eq!(PowerProfile::Balanced.duty_cycle_percent(), 10);
455        assert_eq!(PowerProfile::LowPower.duty_cycle_percent(), 2);
456    }
457
458    #[test]
459    fn test_hive_lite_config() {
460        let config = BleConfig::hive_lite(NodeId::new(0x12345678));
461        assert_eq!(config.mesh.max_children, 0);
462        assert_eq!(config.power_profile, PowerProfile::LowPower);
463        assert_eq!(config.discovery.scan_interval_ms, 5000);
464    }
465
466    #[test]
467    fn test_apply_power_profile() {
468        let mut config = BleConfig::new(NodeId::new(0x12345678));
469        config.power_profile = PowerProfile::LowPower;
470        config.apply_power_profile();
471        assert_eq!(config.discovery.scan_interval_ms, 5000);
472        assert_eq!(config.discovery.adv_interval_ms, 2000);
473    }
474
475    #[test]
476    fn test_mesh_config_default() {
477        let config = MeshConfig::default();
478        assert_eq!(config.mesh_id, DEFAULT_MESH_ID);
479        assert_eq!(config.mesh_id, "DEMO");
480    }
481
482    #[test]
483    fn test_mesh_config_new() {
484        let config = MeshConfig::new("ALFA");
485        assert_eq!(config.mesh_id, "ALFA");
486    }
487
488    #[test]
489    fn test_device_name_generation() {
490        let config = MeshConfig::new("DEMO");
491        let name = config.device_name(NodeId::new(0x12345678));
492        assert_eq!(name, "HIVE_DEMO-12345678");
493
494        let config = MeshConfig::new("ALFA");
495        let name = config.device_name(NodeId::new(0xDEADBEEF));
496        assert_eq!(name, "HIVE_ALFA-DEADBEEF");
497    }
498
499    #[test]
500    fn test_parse_device_name_new_format() {
501        // New format: HIVE_MESHID-NODEID
502        let result = MeshConfig::parse_device_name("HIVE_DEMO-12345678");
503        assert!(result.is_some());
504        let (mesh_id, node_id) = result.unwrap();
505        assert_eq!(mesh_id, Some("DEMO".to_string()));
506        assert_eq!(node_id.as_u32(), 0x12345678);
507
508        let result = MeshConfig::parse_device_name("HIVE_ALFA-DEADBEEF");
509        assert!(result.is_some());
510        let (mesh_id, node_id) = result.unwrap();
511        assert_eq!(mesh_id, Some("ALFA".to_string()));
512        assert_eq!(node_id.as_u32(), 0xDEADBEEF);
513    }
514
515    #[test]
516    fn test_parse_device_name_legacy_format() {
517        // Legacy format: HIVE-NODEID (no mesh ID)
518        let result = MeshConfig::parse_device_name("HIVE-12345678");
519        assert!(result.is_some());
520        let (mesh_id, node_id) = result.unwrap();
521        assert_eq!(mesh_id, None);
522        assert_eq!(node_id.as_u32(), 0x12345678);
523    }
524
525    #[test]
526    fn test_parse_device_name_invalid() {
527        assert!(MeshConfig::parse_device_name("NotHIVE").is_none());
528        assert!(MeshConfig::parse_device_name("HIVE_DEMO").is_none()); // Missing node ID
529        assert!(MeshConfig::parse_device_name("").is_none());
530    }
531
532    #[test]
533    fn test_matches_mesh() {
534        let config = MeshConfig::new("DEMO");
535
536        // Same mesh ID matches
537        assert!(config.matches_mesh(Some("DEMO")));
538
539        // Different mesh ID does not match
540        assert!(!config.matches_mesh(Some("ALFA")));
541
542        // Legacy devices (no mesh ID) match any mesh for backwards compatibility
543        assert!(config.matches_mesh(None));
544    }
545}