hive_btle/discovery/
advertiser.rs1#[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};
27
28const LEGACY_ADV_MAX: usize = 31;
30
31#[allow(dead_code)]
33const EXTENDED_ADV_MAX: usize = 254;
34
35const AD_TYPE_FLAGS: u8 = 0x01;
37
38const AD_TYPE_SERVICE_UUID_16: u8 = 0x03;
40
41const AD_TYPE_SERVICE_DATA_16: u8 = 0x16;
43
44const AD_TYPE_LOCAL_NAME: u8 = 0x09;
46
47const AD_TYPE_SHORT_NAME: u8 = 0x08;
49
50const AD_TYPE_TX_POWER: u8 = 0x0A;
52
53const FLAGS_VALUE: u8 = 0x06;
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum AdvertiserState {
59 Idle,
61 Advertising,
63 Paused,
65}
66
67#[derive(Debug, Clone)]
69pub struct AdvertisingPacket {
70 pub adv_data: Vec<u8>,
72 pub scan_rsp: Option<Vec<u8>>,
74 pub extended: bool,
76}
77
78impl AdvertisingPacket {
79 pub fn fits_legacy(&self) -> bool {
81 self.adv_data.len() <= LEGACY_ADV_MAX
82 && self
83 .scan_rsp
84 .as_ref()
85 .is_none_or(|sr| sr.len() <= LEGACY_ADV_MAX)
86 }
87
88 pub fn total_size(&self) -> usize {
90 self.adv_data.len() + self.scan_rsp.as_ref().map_or(0, |sr| sr.len())
91 }
92}
93
94pub struct Advertiser {
98 #[allow(dead_code)]
100 config: DiscoveryConfig,
101 beacon: HiveBeacon,
103 state: AdvertiserState,
105 started_at_ms: Option<u64>,
107 current_time_ms: u64,
109 tx_power: Option<i8>,
111 device_name: Option<String>,
113 use_extended: bool,
115 cached_packet: Option<AdvertisingPacket>,
117 cache_dirty: bool,
119}
120
121impl Advertiser {
122 pub fn new(config: DiscoveryConfig, node_id: NodeId) -> Self {
124 let beacon = HiveBeacon::new(node_id);
125 Self {
126 config,
127 beacon,
128 state: AdvertiserState::Idle,
129 started_at_ms: None,
130 current_time_ms: 0,
131 tx_power: None,
132 device_name: None,
133 use_extended: false,
134 cached_packet: None,
135 cache_dirty: true,
136 }
137 }
138
139 pub fn hive_lite(config: DiscoveryConfig, node_id: NodeId) -> Self {
141 let beacon = HiveBeacon::hive_lite(node_id);
142 Self {
143 config,
144 beacon,
145 state: AdvertiserState::Idle,
146 started_at_ms: None,
147 current_time_ms: 0,
148 tx_power: None,
149 device_name: None,
150 use_extended: false,
151 cached_packet: None,
152 cache_dirty: true,
153 }
154 }
155
156 pub fn set_time_ms(&mut self, time_ms: u64) {
158 self.current_time_ms = time_ms;
159 }
160
161 pub fn with_tx_power(mut self, tx_power: i8) -> Self {
163 self.tx_power = Some(tx_power);
164 self.cache_dirty = true;
165 self
166 }
167
168 pub fn with_name(mut self, name: String) -> Self {
170 self.device_name = Some(name);
171 self.cache_dirty = true;
172 self
173 }
174
175 pub fn with_extended_advertising(mut self, enabled: bool) -> Self {
177 self.use_extended = enabled;
178 self.cache_dirty = true;
179 self
180 }
181
182 pub fn state(&self) -> AdvertiserState {
184 self.state
185 }
186
187 pub fn beacon(&self) -> &HiveBeacon {
189 &self.beacon
190 }
191
192 pub fn beacon_mut(&mut self) -> &mut HiveBeacon {
194 self.cache_dirty = true;
195 &mut self.beacon
196 }
197
198 pub fn set_hierarchy_level(&mut self, level: HierarchyLevel) {
200 self.beacon.hierarchy_level = level;
201 self.cache_dirty = true;
202 }
203
204 pub fn set_capabilities(&mut self, caps: u16) {
206 self.beacon.capabilities = caps;
207 self.cache_dirty = true;
208 }
209
210 pub fn set_battery(&mut self, percent: u8) {
212 self.beacon.battery_percent = percent.min(100);
213 self.cache_dirty = true;
214 }
215
216 pub fn set_geohash(&mut self, geohash: u32) {
218 self.beacon.geohash = geohash & 0x00FFFFFF;
219 self.cache_dirty = true;
220 }
221
222 pub fn start(&mut self) {
224 self.state = AdvertiserState::Advertising;
225 self.started_at_ms = Some(self.current_time_ms);
226 }
227
228 pub fn pause(&mut self) {
230 self.state = AdvertiserState::Paused;
231 }
232
233 pub fn resume(&mut self) {
235 if self.state == AdvertiserState::Paused {
236 self.state = AdvertiserState::Advertising;
237 }
238 }
239
240 pub fn stop(&mut self) {
242 self.state = AdvertiserState::Idle;
243 self.started_at_ms = None;
244 }
245
246 pub fn advertising_duration_ms(&self) -> Option<u64> {
248 self.started_at_ms
249 .map(|t| self.current_time_ms.saturating_sub(t))
250 }
251
252 pub fn increment_sequence(&mut self) {
254 self.beacon.increment_seq();
255 self.cache_dirty = true;
256 }
257
258 pub fn build_packet(&mut self) -> &AdvertisingPacket {
262 if self.cache_dirty || self.cached_packet.is_none() {
263 let packet = self.build_packet_inner();
264 self.cached_packet = Some(packet);
265 self.cache_dirty = false;
266 }
267 self.cached_packet.as_ref().unwrap()
268 }
269
270 pub fn rebuild_packet(&mut self) -> &AdvertisingPacket {
272 self.cache_dirty = true;
273 self.build_packet()
274 }
275
276 fn build_packet_inner(&self) -> AdvertisingPacket {
278 let mut adv_data = Vec::with_capacity(31);
279 let mut scan_rsp = Vec::with_capacity(31);
280
281 adv_data.push(2); adv_data.push(AD_TYPE_FLAGS);
284 adv_data.push(FLAGS_VALUE);
285
286 adv_data.push(3); adv_data.push(AD_TYPE_SERVICE_UUID_16);
289 adv_data.push((HIVE_SERVICE_UUID_16BIT & 0xFF) as u8);
290 adv_data.push((HIVE_SERVICE_UUID_16BIT >> 8) as u8);
291
292 let beacon_data = self.beacon.encode_compact();
294 adv_data.push((2 + BEACON_COMPACT_SIZE) as u8); adv_data.push(AD_TYPE_SERVICE_DATA_16);
296 adv_data.push((HIVE_SERVICE_UUID_16BIT & 0xFF) as u8);
297 adv_data.push((HIVE_SERVICE_UUID_16BIT >> 8) as u8);
298 adv_data.extend_from_slice(&beacon_data);
299
300 if let Some(tx_power) = self.tx_power {
302 if adv_data.len() + 3 <= LEGACY_ADV_MAX {
303 adv_data.push(2); adv_data.push(AD_TYPE_TX_POWER);
305 adv_data.push(tx_power as u8);
306 } else {
307 scan_rsp.push(2);
309 scan_rsp.push(AD_TYPE_TX_POWER);
310 scan_rsp.push(tx_power as u8);
311 }
312 }
313
314 if let Some(ref name) = self.device_name {
316 let name_bytes = name.as_bytes();
317 let max_name_len = LEGACY_ADV_MAX - 2; if name_bytes.len() <= max_name_len {
320 scan_rsp.push(name_bytes.len() as u8 + 1);
322 scan_rsp.push(AD_TYPE_LOCAL_NAME);
323 scan_rsp.extend_from_slice(name_bytes);
324 } else {
325 let short_name = &name_bytes[..max_name_len.min(name_bytes.len())];
327 scan_rsp.push(short_name.len() as u8 + 1);
328 scan_rsp.push(AD_TYPE_SHORT_NAME);
329 scan_rsp.extend_from_slice(short_name);
330 }
331 }
332
333 let extended =
334 self.use_extended || adv_data.len() > LEGACY_ADV_MAX || scan_rsp.len() > LEGACY_ADV_MAX;
335
336 AdvertisingPacket {
337 adv_data,
338 scan_rsp: if scan_rsp.is_empty() {
339 None
340 } else {
341 Some(scan_rsp)
342 },
343 extended,
344 }
345 }
346
347 pub fn advertising_data(&mut self) -> Vec<u8> {
349 self.build_packet().adv_data.clone()
350 }
351
352 pub fn scan_response_data(&mut self) -> Option<Vec<u8>> {
354 self.build_packet().scan_rsp.clone()
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use crate::capabilities;
362
363 #[test]
364 fn test_advertiser_new() {
365 let config = DiscoveryConfig::default();
366 let node_id = NodeId::new(0x12345678);
367 let advertiser = Advertiser::new(config, node_id);
368
369 assert_eq!(advertiser.state(), AdvertiserState::Idle);
370 assert_eq!(advertiser.beacon().node_id, node_id);
371 }
372
373 #[test]
374 fn test_advertiser_hive_lite() {
375 let config = DiscoveryConfig::default();
376 let node_id = NodeId::new(0xCAFEBABE);
377 let advertiser = Advertiser::hive_lite(config, node_id);
378
379 assert!(advertiser.beacon().is_lite_node());
380 }
381
382 #[test]
383 fn test_advertiser_state_transitions() {
384 let config = DiscoveryConfig::default();
385 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
386
387 assert_eq!(advertiser.state(), AdvertiserState::Idle);
388
389 advertiser.set_time_ms(1000);
390 advertiser.start();
391 assert_eq!(advertiser.state(), AdvertiserState::Advertising);
392 advertiser.set_time_ms(2000);
393 assert_eq!(advertiser.advertising_duration_ms(), Some(1000));
394
395 advertiser.pause();
396 assert_eq!(advertiser.state(), AdvertiserState::Paused);
397
398 advertiser.resume();
399 assert_eq!(advertiser.state(), AdvertiserState::Advertising);
400
401 advertiser.stop();
402 assert_eq!(advertiser.state(), AdvertiserState::Idle);
403 assert!(advertiser.advertising_duration_ms().is_none());
404 }
405
406 #[test]
407 fn test_build_packet_fits_legacy() {
408 let config = DiscoveryConfig::default();
409 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
410
411 let packet = advertiser.build_packet();
412 assert!(packet.fits_legacy());
413 assert!(!packet.extended);
414
415 assert!(packet.adv_data.len() <= LEGACY_ADV_MAX);
417 }
418
419 #[test]
420 fn test_build_packet_with_name() {
421 let config = DiscoveryConfig::default();
422 let mut advertiser =
423 Advertiser::new(config, NodeId::new(0x12345678)).with_name("HIVE-12345678".to_string());
424
425 let packet = advertiser.build_packet();
426 assert!(packet.scan_rsp.is_some());
427
428 let scan_rsp = packet.scan_rsp.as_ref().unwrap();
429 assert!(scan_rsp.contains(&AD_TYPE_LOCAL_NAME));
431 }
432
433 #[test]
434 fn test_build_packet_with_tx_power() {
435 let config = DiscoveryConfig::default();
436 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678)).with_tx_power(0);
437
438 let packet = advertiser.build_packet();
439
440 assert!(packet.adv_data.contains(&AD_TYPE_TX_POWER));
442 }
443
444 #[test]
445 fn test_packet_caching() {
446 let config = DiscoveryConfig::default();
447 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
448
449 let packet1 = advertiser.build_packet();
451 let data1 = packet1.adv_data.clone();
452
453 let packet2 = advertiser.build_packet();
455 assert_eq!(data1, packet2.adv_data);
456
457 advertiser.set_battery(50);
459 let packet3 = advertiser.build_packet();
460 assert_ne!(data1, packet3.adv_data);
462 }
463
464 #[test]
465 fn test_sequence_increment() {
466 let config = DiscoveryConfig::default();
467 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
468
469 let seq1 = advertiser.beacon().seq_num;
470 advertiser.increment_sequence();
471 let seq2 = advertiser.beacon().seq_num;
472
473 assert_eq!(seq2, seq1 + 1);
474 }
475
476 #[test]
477 fn test_update_beacon_fields() {
478 let config = DiscoveryConfig::default();
479 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
480
481 advertiser.set_hierarchy_level(HierarchyLevel::Squad);
482 assert_eq!(advertiser.beacon().hierarchy_level, HierarchyLevel::Squad);
483
484 advertiser.set_capabilities(capabilities::CAN_RELAY);
485 assert!(advertiser.beacon().can_relay());
486
487 advertiser.set_battery(75);
488 assert_eq!(advertiser.beacon().battery_percent, 75);
489
490 advertiser.set_geohash(0x123456);
491 assert_eq!(advertiser.beacon().geohash, 0x123456);
492 }
493}