1use crate::NodeId;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum BlePhy {
14 #[default]
16 Le1M,
17 Le2M,
19 LeCodedS2,
21 LeCodedS8,
23}
24
25impl BlePhy {
26 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 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 pub fn requires_ble5(&self) -> bool {
48 matches!(self, BlePhy::Le2M | BlePhy::LeCodedS2 | BlePhy::LeCodedS8)
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub enum PowerProfile {
58 Aggressive,
61
62 #[default]
64 Balanced,
65
66 LowPower,
69
70 Custom {
72 scan_interval_ms: u32,
74 scan_window_ms: u32,
76 adv_interval_ms: u32,
78 conn_interval_ms: u32,
80 },
81}
82
83impl PowerProfile {
84 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 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 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 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 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#[derive(Debug, Clone)]
153pub struct DiscoveryConfig {
154 pub scan_interval_ms: u32,
156 pub scan_window_ms: u32,
158 pub adv_interval_ms: u32,
160 pub tx_power_dbm: i8,
162 pub adv_phy: BlePhy,
164 pub scan_phy: BlePhy,
166 pub active_scan: bool,
168 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#[derive(Debug, Clone)]
189pub struct GattConfig {
190 pub preferred_mtu: u16,
192 pub min_mtu: u16,
194 pub enable_server: bool,
196 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
211pub const DEFAULT_MESH_ID: &str = "DEMO";
213
214#[derive(Debug, Clone)]
216pub struct MeshConfig {
217 pub mesh_id: String,
222 pub max_connections: u8,
224 pub max_children: u8,
226 pub supervision_timeout_ms: u16,
228 pub slave_latency: u16,
230 pub conn_interval_min_ms: u16,
232 pub conn_interval_max_ms: u16,
234}
235
236impl MeshConfig {
237 pub fn new(mesh_id: impl Into<String>) -> Self {
239 Self {
240 mesh_id: mesh_id.into(),
241 ..Default::default()
242 }
243 }
244
245 pub fn device_name(&self, node_id: NodeId) -> String {
249 format!("HIVE_{}-{:08X}", self.mesh_id, node_id.as_u32())
250 }
251
252 pub fn parse_device_name(name: &str) -> Option<(Option<String>, NodeId)> {
260 if let Some(rest) = name.strip_prefix("HIVE_") {
261 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 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 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, }
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#[derive(Debug, Clone)]
303pub enum PhyStrategy {
304 Fixed(BlePhy),
306 Adaptive {
308 rssi_high_threshold: i8,
310 rssi_low_threshold: i8,
312 hysteresis_db: u8,
314 },
315 MaxRange,
317 MaxThroughput,
319}
320
321impl Default for PhyStrategy {
322 fn default() -> Self {
323 PhyStrategy::Fixed(BlePhy::Le1M)
324 }
325}
326
327#[derive(Debug, Clone, Default)]
329pub struct PhyConfig {
330 pub strategy: PhyStrategy,
332 pub preferred_phy: BlePhy,
334 pub allow_phy_update: bool,
336}
337
338#[derive(Debug, Clone)]
340pub struct SecurityConfig {
341 pub require_pairing: bool,
343 pub require_encryption: bool,
345 pub require_mitm_protection: bool,
347 pub require_secure_connections: bool,
349 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#[derive(Debug, Clone)]
367pub struct BleConfig {
368 pub node_id: NodeId,
370 pub capabilities: u16,
372 pub hierarchy_level: u8,
374 pub geohash: u32,
376 pub discovery: DiscoveryConfig,
378 pub gatt: GattConfig,
380 pub mesh: MeshConfig,
382 pub power_profile: PowerProfile,
384 pub phy: PhyConfig,
386 pub security: SecurityConfig,
388}
389
390impl BleConfig {
391 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 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; 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 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 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 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()); assert!(MeshConfig::parse_device_name("").is_none());
530 }
531
532 #[test]
533 fn test_matches_mesh() {
534 let config = MeshConfig::new("DEMO");
535
536 assert!(config.matches_mesh(Some("DEMO")));
538
539 assert!(!config.matches_mesh(Some("ALFA")));
541
542 assert!(config.matches_mesh(None));
544 }
545}