1#[cfg(not(feature = "std"))]
21use alloc::{string::String, vec::Vec};
22
23use crate::config::DiscoveryConfig;
24use crate::{HierarchyLevel, NodeId, HIVE_SERVICE_UUID_16BIT};
25
26use super::beacon::{HiveBeacon, BEACON_COMPACT_SIZE};
27use super::encrypted_beacon::{
28 BeaconKey, EncryptedBeacon, ENCRYPTED_BEACON_SIZE, ENCRYPTED_DEVICE_NAME,
29};
30
31const LEGACY_ADV_MAX: usize = 31;
33
34#[allow(dead_code)]
36const EXTENDED_ADV_MAX: usize = 254;
37
38const AD_TYPE_FLAGS: u8 = 0x01;
40
41const AD_TYPE_SERVICE_UUID_16: u8 = 0x03;
43
44const AD_TYPE_SERVICE_DATA_16: u8 = 0x16;
46
47const AD_TYPE_LOCAL_NAME: u8 = 0x09;
49
50const AD_TYPE_SHORT_NAME: u8 = 0x08;
52
53const AD_TYPE_TX_POWER: u8 = 0x0A;
55
56const FLAGS_VALUE: u8 = 0x06;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum AdvertiserState {
62 Idle,
64 Advertising,
66 Paused,
68}
69
70#[derive(Debug, Clone)]
72pub struct AdvertisingPacket {
73 pub adv_data: Vec<u8>,
75 pub scan_rsp: Option<Vec<u8>>,
77 pub extended: bool,
79}
80
81impl AdvertisingPacket {
82 pub fn fits_legacy(&self) -> bool {
84 self.adv_data.len() <= LEGACY_ADV_MAX
85 && self
86 .scan_rsp
87 .as_ref()
88 .is_none_or(|sr| sr.len() <= LEGACY_ADV_MAX)
89 }
90
91 pub fn total_size(&self) -> usize {
93 self.adv_data.len() + self.scan_rsp.as_ref().map_or(0, |sr| sr.len())
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
99pub enum AdvertisingMode {
100 #[default]
102 Plaintext,
103 Encrypted,
105}
106
107pub struct Advertiser {
111 #[allow(dead_code)]
113 config: DiscoveryConfig,
114 beacon: HiveBeacon,
116 state: AdvertiserState,
118 started_at_ms: Option<u64>,
120 current_time_ms: u64,
122 tx_power: Option<i8>,
124 device_name: Option<String>,
126 use_extended: bool,
128 cached_packet: Option<AdvertisingPacket>,
130 cache_dirty: bool,
132 mode: AdvertisingMode,
134 beacon_key: Option<BeaconKey>,
136 mesh_id_bytes: Option<[u8; 4]>,
138}
139
140impl Advertiser {
141 pub fn new(config: DiscoveryConfig, node_id: NodeId) -> Self {
143 let beacon = HiveBeacon::new(node_id);
144 Self {
145 config,
146 beacon,
147 state: AdvertiserState::Idle,
148 started_at_ms: None,
149 current_time_ms: 0,
150 tx_power: None,
151 device_name: None,
152 use_extended: false,
153 cached_packet: None,
154 cache_dirty: true,
155 mode: AdvertisingMode::Plaintext,
156 beacon_key: None,
157 mesh_id_bytes: None,
158 }
159 }
160
161 pub fn hive_lite(config: DiscoveryConfig, node_id: NodeId) -> Self {
163 let beacon = HiveBeacon::hive_lite(node_id);
164 Self {
165 config,
166 beacon,
167 state: AdvertiserState::Idle,
168 started_at_ms: None,
169 current_time_ms: 0,
170 tx_power: None,
171 device_name: None,
172 use_extended: false,
173 cached_packet: None,
174 cache_dirty: true,
175 mode: AdvertisingMode::Plaintext,
176 beacon_key: None,
177 mesh_id_bytes: None,
178 }
179 }
180
181 pub fn set_time_ms(&mut self, time_ms: u64) {
183 self.current_time_ms = time_ms;
184 }
185
186 pub fn with_tx_power(mut self, tx_power: i8) -> Self {
188 self.tx_power = Some(tx_power);
189 self.cache_dirty = true;
190 self
191 }
192
193 pub fn with_name(mut self, name: String) -> Self {
195 self.device_name = Some(name);
196 self.cache_dirty = true;
197 self
198 }
199
200 pub fn with_extended_advertising(mut self, enabled: bool) -> Self {
202 self.use_extended = enabled;
203 self.cache_dirty = true;
204 self
205 }
206
207 pub fn with_encryption(mut self, beacon_key: BeaconKey, mesh_id_bytes: [u8; 4]) -> Self {
218 self.mode = AdvertisingMode::Encrypted;
219 self.beacon_key = Some(beacon_key);
220 self.mesh_id_bytes = Some(mesh_id_bytes);
221 self.device_name = Some(ENCRYPTED_DEVICE_NAME.into());
223 self.cache_dirty = true;
224 self
225 }
226
227 pub fn set_mode(&mut self, mode: AdvertisingMode) {
229 self.mode = mode;
230 self.cache_dirty = true;
231 }
232
233 pub fn mode(&self) -> AdvertisingMode {
235 self.mode
236 }
237
238 pub fn set_beacon_key(&mut self, key: BeaconKey) {
240 self.beacon_key = Some(key);
241 self.cache_dirty = true;
242 }
243
244 pub fn state(&self) -> AdvertiserState {
246 self.state
247 }
248
249 pub fn beacon(&self) -> &HiveBeacon {
251 &self.beacon
252 }
253
254 pub fn beacon_mut(&mut self) -> &mut HiveBeacon {
256 self.cache_dirty = true;
257 &mut self.beacon
258 }
259
260 pub fn set_hierarchy_level(&mut self, level: HierarchyLevel) {
262 self.beacon.hierarchy_level = level;
263 self.cache_dirty = true;
264 }
265
266 pub fn set_capabilities(&mut self, caps: u16) {
268 self.beacon.capabilities = caps;
269 self.cache_dirty = true;
270 }
271
272 pub fn set_battery(&mut self, percent: u8) {
274 self.beacon.battery_percent = percent.min(100);
275 self.cache_dirty = true;
276 }
277
278 pub fn set_geohash(&mut self, geohash: u32) {
280 self.beacon.geohash = geohash & 0x00FFFFFF;
281 self.cache_dirty = true;
282 }
283
284 pub fn start(&mut self) {
286 self.state = AdvertiserState::Advertising;
287 self.started_at_ms = Some(self.current_time_ms);
288 }
289
290 pub fn pause(&mut self) {
292 self.state = AdvertiserState::Paused;
293 }
294
295 pub fn resume(&mut self) {
297 if self.state == AdvertiserState::Paused {
298 self.state = AdvertiserState::Advertising;
299 }
300 }
301
302 pub fn stop(&mut self) {
304 self.state = AdvertiserState::Idle;
305 self.started_at_ms = None;
306 }
307
308 pub fn advertising_duration_ms(&self) -> Option<u64> {
310 self.started_at_ms
311 .map(|t| self.current_time_ms.saturating_sub(t))
312 }
313
314 pub fn increment_sequence(&mut self) {
316 self.beacon.increment_seq();
317 self.cache_dirty = true;
318 }
319
320 pub fn build_packet(&mut self) -> &AdvertisingPacket {
324 if self.cache_dirty || self.cached_packet.is_none() {
325 let packet = self.build_packet_inner();
326 self.cached_packet = Some(packet);
327 self.cache_dirty = false;
328 }
329 self.cached_packet.as_ref().unwrap()
330 }
331
332 pub fn rebuild_packet(&mut self) -> &AdvertisingPacket {
334 self.cache_dirty = true;
335 self.build_packet()
336 }
337
338 fn build_packet_inner(&self) -> AdvertisingPacket {
340 let mut adv_data = Vec::with_capacity(31);
341 let mut scan_rsp = Vec::with_capacity(31);
342
343 adv_data.push(2); adv_data.push(AD_TYPE_FLAGS);
346 adv_data.push(FLAGS_VALUE);
347
348 adv_data.push(3); adv_data.push(AD_TYPE_SERVICE_UUID_16);
351 adv_data.push((HIVE_SERVICE_UUID_16BIT & 0xFF) as u8);
352 adv_data.push((HIVE_SERVICE_UUID_16BIT >> 8) as u8);
353
354 match self.mode {
356 AdvertisingMode::Plaintext => {
357 let beacon_data = self.beacon.encode_compact();
359 adv_data.push((2 + BEACON_COMPACT_SIZE) as u8); adv_data.push(AD_TYPE_SERVICE_DATA_16);
361 adv_data.push((HIVE_SERVICE_UUID_16BIT & 0xFF) as u8);
362 adv_data.push((HIVE_SERVICE_UUID_16BIT >> 8) as u8);
363 adv_data.extend_from_slice(&beacon_data);
364 }
365 AdvertisingMode::Encrypted => {
366 if let (Some(key), Some(mesh_id_bytes)) = (&self.beacon_key, &self.mesh_id_bytes) {
368 let encrypted_beacon = EncryptedBeacon::new(
369 self.beacon.node_id,
370 self.beacon.capabilities,
371 u8::from(self.beacon.hierarchy_level),
372 self.beacon.battery_percent,
373 );
374 let beacon_data = encrypted_beacon.encrypt(key, mesh_id_bytes);
375 adv_data.push((2 + ENCRYPTED_BEACON_SIZE) as u8); adv_data.push(AD_TYPE_SERVICE_DATA_16);
377 adv_data.push((HIVE_SERVICE_UUID_16BIT & 0xFF) as u8);
378 adv_data.push((HIVE_SERVICE_UUID_16BIT >> 8) as u8);
379 adv_data.extend_from_slice(&beacon_data);
380 } else {
381 let beacon_data = self.beacon.encode_compact();
383 adv_data.push((2 + BEACON_COMPACT_SIZE) as u8);
384 adv_data.push(AD_TYPE_SERVICE_DATA_16);
385 adv_data.push((HIVE_SERVICE_UUID_16BIT & 0xFF) as u8);
386 adv_data.push((HIVE_SERVICE_UUID_16BIT >> 8) as u8);
387 adv_data.extend_from_slice(&beacon_data);
388 }
389 }
390 }
391
392 if let Some(tx_power) = self.tx_power {
394 if adv_data.len() + 3 <= LEGACY_ADV_MAX {
395 adv_data.push(2); adv_data.push(AD_TYPE_TX_POWER);
397 adv_data.push(tx_power as u8);
398 } else {
399 scan_rsp.push(2);
401 scan_rsp.push(AD_TYPE_TX_POWER);
402 scan_rsp.push(tx_power as u8);
403 }
404 }
405
406 if let Some(ref name) = self.device_name {
408 let name_bytes = name.as_bytes();
409 let max_name_len = LEGACY_ADV_MAX - 2; if name_bytes.len() <= max_name_len {
412 scan_rsp.push(name_bytes.len() as u8 + 1);
414 scan_rsp.push(AD_TYPE_LOCAL_NAME);
415 scan_rsp.extend_from_slice(name_bytes);
416 } else {
417 let short_name = &name_bytes[..max_name_len.min(name_bytes.len())];
419 scan_rsp.push(short_name.len() as u8 + 1);
420 scan_rsp.push(AD_TYPE_SHORT_NAME);
421 scan_rsp.extend_from_slice(short_name);
422 }
423 }
424
425 let extended =
426 self.use_extended || adv_data.len() > LEGACY_ADV_MAX || scan_rsp.len() > LEGACY_ADV_MAX;
427
428 AdvertisingPacket {
429 adv_data,
430 scan_rsp: if scan_rsp.is_empty() {
431 None
432 } else {
433 Some(scan_rsp)
434 },
435 extended,
436 }
437 }
438
439 pub fn advertising_data(&mut self) -> Vec<u8> {
441 self.build_packet().adv_data.clone()
442 }
443
444 pub fn scan_response_data(&mut self) -> Option<Vec<u8>> {
446 self.build_packet().scan_rsp.clone()
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use crate::capabilities;
454
455 #[test]
456 fn test_advertiser_new() {
457 let config = DiscoveryConfig::default();
458 let node_id = NodeId::new(0x12345678);
459 let advertiser = Advertiser::new(config, node_id);
460
461 assert_eq!(advertiser.state(), AdvertiserState::Idle);
462 assert_eq!(advertiser.beacon().node_id, node_id);
463 }
464
465 #[test]
466 fn test_advertiser_hive_lite() {
467 let config = DiscoveryConfig::default();
468 let node_id = NodeId::new(0xCAFEBABE);
469 let advertiser = Advertiser::hive_lite(config, node_id);
470
471 assert!(advertiser.beacon().is_lite_node());
472 }
473
474 #[test]
475 fn test_advertiser_state_transitions() {
476 let config = DiscoveryConfig::default();
477 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
478
479 assert_eq!(advertiser.state(), AdvertiserState::Idle);
480
481 advertiser.set_time_ms(1000);
482 advertiser.start();
483 assert_eq!(advertiser.state(), AdvertiserState::Advertising);
484 advertiser.set_time_ms(2000);
485 assert_eq!(advertiser.advertising_duration_ms(), Some(1000));
486
487 advertiser.pause();
488 assert_eq!(advertiser.state(), AdvertiserState::Paused);
489
490 advertiser.resume();
491 assert_eq!(advertiser.state(), AdvertiserState::Advertising);
492
493 advertiser.stop();
494 assert_eq!(advertiser.state(), AdvertiserState::Idle);
495 assert!(advertiser.advertising_duration_ms().is_none());
496 }
497
498 #[test]
499 fn test_build_packet_fits_legacy() {
500 let config = DiscoveryConfig::default();
501 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
502
503 let packet = advertiser.build_packet();
504 assert!(packet.fits_legacy());
505 assert!(!packet.extended);
506
507 assert!(packet.adv_data.len() <= LEGACY_ADV_MAX);
509 }
510
511 #[test]
512 fn test_build_packet_with_name() {
513 let config = DiscoveryConfig::default();
514 let mut advertiser =
515 Advertiser::new(config, NodeId::new(0x12345678)).with_name("HIVE-12345678".to_string());
516
517 let packet = advertiser.build_packet();
518 assert!(packet.scan_rsp.is_some());
519
520 let scan_rsp = packet.scan_rsp.as_ref().unwrap();
521 assert!(scan_rsp.contains(&AD_TYPE_LOCAL_NAME));
523 }
524
525 #[test]
526 fn test_build_packet_with_tx_power() {
527 let config = DiscoveryConfig::default();
528 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678)).with_tx_power(0);
529
530 let packet = advertiser.build_packet();
531
532 assert!(packet.adv_data.contains(&AD_TYPE_TX_POWER));
534 }
535
536 #[test]
537 fn test_packet_caching() {
538 let config = DiscoveryConfig::default();
539 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
540
541 let packet1 = advertiser.build_packet();
543 let data1 = packet1.adv_data.clone();
544
545 let packet2 = advertiser.build_packet();
547 assert_eq!(data1, packet2.adv_data);
548
549 advertiser.set_battery(50);
551 let packet3 = advertiser.build_packet();
552 assert_ne!(data1, packet3.adv_data);
554 }
555
556 #[test]
557 fn test_sequence_increment() {
558 let config = DiscoveryConfig::default();
559 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
560
561 let seq1 = advertiser.beacon().seq_num;
562 advertiser.increment_sequence();
563 let seq2 = advertiser.beacon().seq_num;
564
565 assert_eq!(seq2, seq1 + 1);
566 }
567
568 #[test]
569 fn test_update_beacon_fields() {
570 let config = DiscoveryConfig::default();
571 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
572
573 advertiser.set_hierarchy_level(HierarchyLevel::Squad);
574 assert_eq!(advertiser.beacon().hierarchy_level, HierarchyLevel::Squad);
575
576 advertiser.set_capabilities(capabilities::CAN_RELAY);
577 assert!(advertiser.beacon().can_relay());
578
579 advertiser.set_battery(75);
580 assert_eq!(advertiser.beacon().battery_percent, 75);
581
582 advertiser.set_geohash(0x123456);
583 assert_eq!(advertiser.beacon().geohash, 0x123456);
584 }
585
586 #[test]
587 fn test_encrypted_advertising() {
588 use crate::discovery::mesh_id_to_bytes;
589
590 let config = DiscoveryConfig::default();
591 let beacon_key = BeaconKey::from_base(&[0x42; 32]);
592 let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
593
594 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678))
595 .with_encryption(beacon_key.clone(), mesh_id_bytes);
596
597 assert_eq!(advertiser.mode(), AdvertisingMode::Encrypted);
598
599 let packet = advertiser.build_packet();
600
601 assert!(packet.extended || packet.adv_data.len() > LEGACY_ADV_MAX);
604
605 let scan_rsp = packet.scan_rsp.as_ref().unwrap();
607 assert!(scan_rsp.windows(4).any(|w| w == b"HIVE"));
608 }
609
610 #[test]
611 fn test_encrypted_beacon_decrypts() {
612 use crate::discovery::mesh_id_to_bytes;
613
614 let config = DiscoveryConfig::default();
615 let beacon_key = BeaconKey::from_base(&[0x42; 32]);
616 let mesh_id_bytes = mesh_id_to_bytes("TEST-MESH");
617 let node_id = NodeId::new(0x12345678);
618
619 let mut advertiser =
620 Advertiser::new(config, node_id).with_encryption(beacon_key.clone(), mesh_id_bytes);
621
622 advertiser.set_hierarchy_level(HierarchyLevel::Squad);
623 advertiser.set_battery(85);
624 advertiser.set_capabilities(0x0F00);
625
626 let packet = advertiser.build_packet();
627
628 let mut offset = 0;
631 let mut found_beacon = false;
632
633 while offset < packet.adv_data.len() {
634 let len = packet.adv_data[offset] as usize;
635 if offset + 1 + len > packet.adv_data.len() {
636 break;
637 }
638
639 let ad_type = packet.adv_data[offset + 1];
640 if ad_type == AD_TYPE_SERVICE_DATA_16 && len >= 2 + ENCRYPTED_BEACON_SIZE {
641 let beacon_data = &packet.adv_data[offset + 4..offset + 4 + ENCRYPTED_BEACON_SIZE];
643
644 if let Some((decrypted, decrypted_mesh_id)) =
646 EncryptedBeacon::decrypt(beacon_data, &beacon_key)
647 {
648 assert_eq!(decrypted.node_id, node_id);
649 assert_eq!(decrypted.capabilities, 0x0F00);
650 assert_eq!(decrypted.hierarchy_level, u8::from(HierarchyLevel::Squad));
651 assert_eq!(decrypted.battery_percent, 85);
652 assert_eq!(decrypted_mesh_id, mesh_id_bytes);
653 found_beacon = true;
654 }
655 }
656 offset += 1 + len;
657 }
658
659 assert!(
660 found_beacon,
661 "Encrypted beacon not found in advertising data"
662 );
663 }
664}