1use crate::NodeId;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum BlePhy {
29 #[default]
31 Le1M,
32 Le2M,
34 LeCodedS2,
36 LeCodedS8,
38}
39
40impl BlePhy {
41 pub fn bandwidth_bps(&self) -> u32 {
43 match self {
44 BlePhy::Le1M => 1_000_000,
45 BlePhy::Le2M => 2_000_000,
46 BlePhy::LeCodedS2 => 500_000,
47 BlePhy::LeCodedS8 => 125_000,
48 }
49 }
50
51 pub fn typical_range_meters(&self) -> u32 {
53 match self {
54 BlePhy::Le1M => 100,
55 BlePhy::Le2M => 50,
56 BlePhy::LeCodedS2 => 200,
57 BlePhy::LeCodedS8 => 400,
58 }
59 }
60
61 pub fn requires_ble5(&self) -> bool {
63 matches!(self, BlePhy::Le2M | BlePhy::LeCodedS2 | BlePhy::LeCodedS8)
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum PowerProfile {
73 Aggressive,
76
77 #[default]
79 Balanced,
80
81 LowPower,
84
85 Custom {
87 scan_interval_ms: u32,
89 scan_window_ms: u32,
91 adv_interval_ms: u32,
93 conn_interval_ms: u32,
95 },
96}
97
98impl PowerProfile {
99 pub fn scan_interval_ms(&self) -> u32 {
101 match self {
102 PowerProfile::Aggressive => 100,
103 PowerProfile::Balanced => 500,
104 PowerProfile::LowPower => 5000,
105 PowerProfile::Custom {
106 scan_interval_ms, ..
107 } => *scan_interval_ms,
108 }
109 }
110
111 pub fn scan_window_ms(&self) -> u32 {
113 match self {
114 PowerProfile::Aggressive => 50,
115 PowerProfile::Balanced => 50,
116 PowerProfile::LowPower => 100,
117 PowerProfile::Custom { scan_window_ms, .. } => *scan_window_ms,
118 }
119 }
120
121 pub fn adv_interval_ms(&self) -> u32 {
123 match self {
124 PowerProfile::Aggressive => 100,
125 PowerProfile::Balanced => 500,
126 PowerProfile::LowPower => 2000,
127 PowerProfile::Custom {
128 adv_interval_ms, ..
129 } => *adv_interval_ms,
130 }
131 }
132
133 pub fn conn_interval_ms(&self) -> u32 {
135 match self {
136 PowerProfile::Aggressive => 15,
137 PowerProfile::Balanced => 30,
138 PowerProfile::LowPower => 100,
139 PowerProfile::Custom {
140 conn_interval_ms, ..
141 } => *conn_interval_ms,
142 }
143 }
144
145 pub fn duty_cycle_percent(&self) -> u8 {
147 match self {
148 PowerProfile::Aggressive => 20,
149 PowerProfile::Balanced => 10,
150 PowerProfile::LowPower => 2,
151 PowerProfile::Custom {
152 scan_interval_ms,
153 scan_window_ms,
154 ..
155 } => {
156 if *scan_interval_ms == 0 {
157 0
158 } else {
159 ((scan_window_ms * 100) / scan_interval_ms) as u8
160 }
161 }
162 }
163 }
164}
165
166#[derive(Debug, Clone)]
168pub struct DiscoveryConfig {
169 pub scan_interval_ms: u32,
171 pub scan_window_ms: u32,
173 pub adv_interval_ms: u32,
175 pub tx_power_dbm: i8,
177 pub adv_phy: BlePhy,
179 pub scan_phy: BlePhy,
181 pub active_scan: bool,
183 pub filter_duplicates: bool,
185}
186
187impl Default for DiscoveryConfig {
188 fn default() -> Self {
189 Self {
190 scan_interval_ms: 500,
191 scan_window_ms: 50,
192 adv_interval_ms: 500,
193 tx_power_dbm: 0,
194 adv_phy: BlePhy::Le1M,
195 scan_phy: BlePhy::Le1M,
196 active_scan: true,
197 filter_duplicates: true,
198 }
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct GattConfig {
205 pub preferred_mtu: u16,
207 pub min_mtu: u16,
209 pub enable_server: bool,
211 pub enable_client: bool,
213}
214
215impl Default for GattConfig {
216 fn default() -> Self {
217 Self {
218 preferred_mtu: 251,
219 min_mtu: 23,
220 enable_server: true,
221 enable_client: true,
222 }
223 }
224}
225
226pub const DEFAULT_MESH_ID: &str = "DEMO";
228
229#[derive(Debug, Clone)]
231pub struct MeshConfig {
232 pub mesh_id: String,
237 pub max_connections: u8,
239 pub max_children: u8,
241 pub supervision_timeout_ms: u16,
243 pub slave_latency: u16,
245 pub conn_interval_min_ms: u16,
247 pub conn_interval_max_ms: u16,
249}
250
251impl MeshConfig {
252 pub fn new(mesh_id: impl Into<String>) -> Self {
254 Self {
255 mesh_id: mesh_id.into(),
256 ..Default::default()
257 }
258 }
259
260 pub fn device_name(&self, node_id: NodeId) -> String {
264 format!("HIVE_{}-{:08X}", self.mesh_id, node_id.as_u32())
265 }
266
267 pub fn parse_device_name(name: &str) -> Option<(Option<String>, NodeId)> {
275 if let Some(rest) = name.strip_prefix("HIVE_") {
276 let (mesh_id, node_id_str) = rest.split_once('-')?;
278 let node_id = u32::from_str_radix(node_id_str, 16).ok()?;
279 Some((Some(mesh_id.to_string()), NodeId::new(node_id)))
280 } else if let Some(node_id_str) = name.strip_prefix("HIVE-") {
281 let node_id = u32::from_str_radix(node_id_str, 16).ok()?;
283 Some((None, NodeId::new(node_id)))
284 } else {
285 None
286 }
287 }
288
289 pub fn matches_mesh(&self, device_mesh_id: Option<&str>) -> bool {
295 match device_mesh_id {
296 Some(id) => id == self.mesh_id,
297 None => true, }
299 }
300}
301
302impl Default for MeshConfig {
303 fn default() -> Self {
304 Self {
305 mesh_id: DEFAULT_MESH_ID.to_string(),
306 max_connections: 7,
307 max_children: 3,
308 supervision_timeout_ms: 4000,
309 slave_latency: 0,
310 conn_interval_min_ms: 30,
311 conn_interval_max_ms: 50,
312 }
313 }
314}
315
316#[derive(Debug, Clone)]
318pub enum PhyStrategy {
319 Fixed(BlePhy),
321 Adaptive {
323 rssi_high_threshold: i8,
325 rssi_low_threshold: i8,
327 hysteresis_db: u8,
329 },
330 MaxRange,
332 MaxThroughput,
334}
335
336impl Default for PhyStrategy {
337 fn default() -> Self {
338 PhyStrategy::Fixed(BlePhy::Le1M)
339 }
340}
341
342#[derive(Debug, Clone, Default)]
344pub struct PhyConfig {
345 pub strategy: PhyStrategy,
347 pub preferred_phy: BlePhy,
349 pub allow_phy_update: bool,
351}
352
353#[derive(Debug, Clone)]
355pub struct SecurityConfig {
356 pub require_pairing: bool,
358 pub require_encryption: bool,
360 pub require_mitm_protection: bool,
362 pub require_secure_connections: bool,
364 pub app_layer_encryption: bool,
366}
367
368impl Default for SecurityConfig {
369 fn default() -> Self {
370 Self {
371 require_pairing: false,
372 require_encryption: true,
373 require_mitm_protection: false,
374 require_secure_connections: false,
375 app_layer_encryption: false,
376 }
377 }
378}
379
380#[derive(Debug, Clone)]
382pub struct BleConfig {
383 pub node_id: NodeId,
385 pub capabilities: u16,
387 pub hierarchy_level: u8,
389 pub geohash: u32,
391 pub discovery: DiscoveryConfig,
393 pub gatt: GattConfig,
395 pub mesh: MeshConfig,
397 pub power_profile: PowerProfile,
399 pub phy: PhyConfig,
401 pub security: SecurityConfig,
403}
404
405impl BleConfig {
406 pub fn new(node_id: NodeId) -> Self {
408 Self {
409 node_id,
410 capabilities: 0,
411 hierarchy_level: 0,
412 geohash: 0,
413 discovery: DiscoveryConfig::default(),
414 gatt: GattConfig::default(),
415 mesh: MeshConfig::default(),
416 power_profile: PowerProfile::default(),
417 phy: PhyConfig::default(),
418 security: SecurityConfig::default(),
419 }
420 }
421
422 pub fn hive_lite(node_id: NodeId) -> Self {
429 let mut config = Self::new(node_id);
430 config.power_profile = PowerProfile::LowPower;
431 config.mesh.max_children = 0; config.discovery.scan_interval_ms = 5000;
433 config.discovery.scan_window_ms = 100;
434 config.discovery.adv_interval_ms = 2000;
435 config
436 }
437
438 pub fn apply_power_profile(&mut self) {
440 self.discovery.scan_interval_ms = self.power_profile.scan_interval_ms();
441 self.discovery.scan_window_ms = self.power_profile.scan_window_ms();
442 self.discovery.adv_interval_ms = self.power_profile.adv_interval_ms();
443 self.mesh.conn_interval_min_ms = self.power_profile.conn_interval_ms() as u16;
444 self.mesh.conn_interval_max_ms = self.power_profile.conn_interval_ms() as u16 + 20;
445 }
446}
447
448impl Default for BleConfig {
449 fn default() -> Self {
450 Self::new(NodeId::default())
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_phy_properties() {
460 assert_eq!(BlePhy::Le1M.bandwidth_bps(), 1_000_000);
461 assert_eq!(BlePhy::LeCodedS8.typical_range_meters(), 400);
462 assert!(!BlePhy::Le1M.requires_ble5());
463 assert!(BlePhy::Le2M.requires_ble5());
464 }
465
466 #[test]
467 fn test_power_profile_duty_cycle() {
468 assert_eq!(PowerProfile::Aggressive.duty_cycle_percent(), 20);
469 assert_eq!(PowerProfile::Balanced.duty_cycle_percent(), 10);
470 assert_eq!(PowerProfile::LowPower.duty_cycle_percent(), 2);
471 }
472
473 #[test]
474 fn test_hive_lite_config() {
475 let config = BleConfig::hive_lite(NodeId::new(0x12345678));
476 assert_eq!(config.mesh.max_children, 0);
477 assert_eq!(config.power_profile, PowerProfile::LowPower);
478 assert_eq!(config.discovery.scan_interval_ms, 5000);
479 }
480
481 #[test]
482 fn test_apply_power_profile() {
483 let mut config = BleConfig::new(NodeId::new(0x12345678));
484 config.power_profile = PowerProfile::LowPower;
485 config.apply_power_profile();
486 assert_eq!(config.discovery.scan_interval_ms, 5000);
487 assert_eq!(config.discovery.adv_interval_ms, 2000);
488 }
489
490 #[test]
491 fn test_mesh_config_default() {
492 let config = MeshConfig::default();
493 assert_eq!(config.mesh_id, DEFAULT_MESH_ID);
494 assert_eq!(config.mesh_id, "DEMO");
495 }
496
497 #[test]
498 fn test_mesh_config_new() {
499 let config = MeshConfig::new("ALFA");
500 assert_eq!(config.mesh_id, "ALFA");
501 }
502
503 #[test]
504 fn test_device_name_generation() {
505 let config = MeshConfig::new("DEMO");
506 let name = config.device_name(NodeId::new(0x12345678));
507 assert_eq!(name, "HIVE_DEMO-12345678");
508
509 let config = MeshConfig::new("ALFA");
510 let name = config.device_name(NodeId::new(0xDEADBEEF));
511 assert_eq!(name, "HIVE_ALFA-DEADBEEF");
512 }
513
514 #[test]
515 fn test_parse_device_name_new_format() {
516 let result = MeshConfig::parse_device_name("HIVE_DEMO-12345678");
518 assert!(result.is_some());
519 let (mesh_id, node_id) = result.unwrap();
520 assert_eq!(mesh_id, Some("DEMO".to_string()));
521 assert_eq!(node_id.as_u32(), 0x12345678);
522
523 let result = MeshConfig::parse_device_name("HIVE_ALFA-DEADBEEF");
524 assert!(result.is_some());
525 let (mesh_id, node_id) = result.unwrap();
526 assert_eq!(mesh_id, Some("ALFA".to_string()));
527 assert_eq!(node_id.as_u32(), 0xDEADBEEF);
528 }
529
530 #[test]
531 fn test_parse_device_name_legacy_format() {
532 let result = MeshConfig::parse_device_name("HIVE-12345678");
534 assert!(result.is_some());
535 let (mesh_id, node_id) = result.unwrap();
536 assert_eq!(mesh_id, None);
537 assert_eq!(node_id.as_u32(), 0x12345678);
538 }
539
540 #[test]
541 fn test_parse_device_name_invalid() {
542 assert!(MeshConfig::parse_device_name("NotHIVE").is_none());
543 assert!(MeshConfig::parse_device_name("HIVE_DEMO").is_none()); assert!(MeshConfig::parse_device_name("").is_none());
545 }
546
547 #[test]
548 fn test_matches_mesh() {
549 let config = MeshConfig::new("DEMO");
550
551 assert!(config.matches_mesh(Some("DEMO")));
553
554 assert!(!config.matches_mesh(Some("ALFA")));
556
557 assert!(config.matches_mesh(None));
559 }
560}