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
454pub fn embedded_encryption_secret() -> Option<[u8; 32]> {
491 option_env!("HIVE_ENCRYPTION_SECRET").and_then(parse_hex_secret)
493}
494
495pub fn embedded_mesh_id() -> Option<&'static str> {
506 option_env!("HIVE_MESH_ID")
507}
508
509pub fn has_embedded_encryption_secret() -> bool {
511 option_env!("HIVE_ENCRYPTION_SECRET").is_some()
512}
513
514fn parse_hex_secret(hex: &str) -> Option<[u8; 32]> {
516 if hex.len() != 64 {
517 return None;
518 }
519
520 let mut result = [0u8; 32];
521 for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
522 if i >= 32 {
523 return None;
524 }
525 let high = hex_digit(chunk[0])?;
526 let low = hex_digit(chunk[1])?;
527 result[i] = (high << 4) | low;
528 }
529 Some(result)
530}
531
532fn hex_digit(c: u8) -> Option<u8> {
534 match c {
535 b'0'..=b'9' => Some(c - b'0'),
536 b'a'..=b'f' => Some(c - b'a' + 10),
537 b'A'..=b'F' => Some(c - b'A' + 10),
538 _ => None,
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_phy_properties() {
548 assert_eq!(BlePhy::Le1M.bandwidth_bps(), 1_000_000);
549 assert_eq!(BlePhy::LeCodedS8.typical_range_meters(), 400);
550 assert!(!BlePhy::Le1M.requires_ble5());
551 assert!(BlePhy::Le2M.requires_ble5());
552 }
553
554 #[test]
555 fn test_power_profile_duty_cycle() {
556 assert_eq!(PowerProfile::Aggressive.duty_cycle_percent(), 20);
557 assert_eq!(PowerProfile::Balanced.duty_cycle_percent(), 10);
558 assert_eq!(PowerProfile::LowPower.duty_cycle_percent(), 2);
559 }
560
561 #[test]
562 fn test_hive_lite_config() {
563 let config = BleConfig::hive_lite(NodeId::new(0x12345678));
564 assert_eq!(config.mesh.max_children, 0);
565 assert_eq!(config.power_profile, PowerProfile::LowPower);
566 assert_eq!(config.discovery.scan_interval_ms, 5000);
567 }
568
569 #[test]
570 fn test_apply_power_profile() {
571 let mut config = BleConfig::new(NodeId::new(0x12345678));
572 config.power_profile = PowerProfile::LowPower;
573 config.apply_power_profile();
574 assert_eq!(config.discovery.scan_interval_ms, 5000);
575 assert_eq!(config.discovery.adv_interval_ms, 2000);
576 }
577
578 #[test]
579 fn test_mesh_config_default() {
580 let config = MeshConfig::default();
581 assert_eq!(config.mesh_id, DEFAULT_MESH_ID);
582 assert_eq!(config.mesh_id, "DEMO");
583 }
584
585 #[test]
586 fn test_mesh_config_new() {
587 let config = MeshConfig::new("ALFA");
588 assert_eq!(config.mesh_id, "ALFA");
589 }
590
591 #[test]
592 fn test_device_name_generation() {
593 let config = MeshConfig::new("DEMO");
594 let name = config.device_name(NodeId::new(0x12345678));
595 assert_eq!(name, "HIVE_DEMO-12345678");
596
597 let config = MeshConfig::new("ALFA");
598 let name = config.device_name(NodeId::new(0xDEADBEEF));
599 assert_eq!(name, "HIVE_ALFA-DEADBEEF");
600 }
601
602 #[test]
603 fn test_parse_device_name_new_format() {
604 let result = MeshConfig::parse_device_name("HIVE_DEMO-12345678");
606 assert!(result.is_some());
607 let (mesh_id, node_id) = result.unwrap();
608 assert_eq!(mesh_id, Some("DEMO".to_string()));
609 assert_eq!(node_id.as_u32(), 0x12345678);
610
611 let result = MeshConfig::parse_device_name("HIVE_ALFA-DEADBEEF");
612 assert!(result.is_some());
613 let (mesh_id, node_id) = result.unwrap();
614 assert_eq!(mesh_id, Some("ALFA".to_string()));
615 assert_eq!(node_id.as_u32(), 0xDEADBEEF);
616 }
617
618 #[test]
619 fn test_parse_device_name_legacy_format() {
620 let result = MeshConfig::parse_device_name("HIVE-12345678");
622 assert!(result.is_some());
623 let (mesh_id, node_id) = result.unwrap();
624 assert_eq!(mesh_id, None);
625 assert_eq!(node_id.as_u32(), 0x12345678);
626 }
627
628 #[test]
629 fn test_parse_device_name_invalid() {
630 assert!(MeshConfig::parse_device_name("NotHIVE").is_none());
631 assert!(MeshConfig::parse_device_name("HIVE_DEMO").is_none()); assert!(MeshConfig::parse_device_name("").is_none());
633 }
634
635 #[test]
636 fn test_matches_mesh() {
637 let config = MeshConfig::new("DEMO");
638
639 assert!(config.matches_mesh(Some("DEMO")));
641
642 assert!(!config.matches_mesh(Some("ALFA")));
644
645 assert!(config.matches_mesh(None));
647 }
648
649 #[test]
650 fn test_parse_hex_secret() {
651 let hex = "0102030405060708091011121314151617181920212223242526272829303132";
653 let result = parse_hex_secret(hex);
654 assert!(result.is_some());
655 let secret = result.unwrap();
656 assert_eq!(secret[0], 0x01);
657 assert_eq!(secret[1], 0x02);
658 assert_eq!(secret[31], 0x32);
659
660 let hex = "AABBCCDD01020304050607080910111213141516171819202122232425262728";
662 let result = parse_hex_secret(hex);
663 assert!(result.is_some());
664 let secret = result.unwrap();
665 assert_eq!(secret[0], 0xAA);
666 assert_eq!(secret[1], 0xBB);
667 }
668
669 #[test]
670 fn test_parse_hex_secret_invalid() {
671 assert!(parse_hex_secret("0102030405").is_none());
673
674 assert!(parse_hex_secret(
676 "01020304050607080910111213141516171819202122232425262728293031323334"
677 )
678 .is_none());
679
680 assert!(
682 parse_hex_secret("GGHHIIJJ0102030405060708091011121314151617181920212223242526")
683 .is_none()
684 );
685
686 assert!(parse_hex_secret("").is_none());
688 }
689
690 #[test]
691 fn test_embedded_functions_exist() {
692 let _ = embedded_encryption_secret();
695 let _ = embedded_mesh_id();
696 let _ = has_embedded_encryption_secret();
697 }
698}