hive_btle/discovery/
advertiser.rs1#[cfg(not(feature = "std"))]
6use alloc::{string::String, vec::Vec};
7
8use crate::config::DiscoveryConfig;
9use crate::{HierarchyLevel, NodeId, HIVE_SERVICE_UUID_16BIT};
10
11use super::beacon::{HiveBeacon, BEACON_COMPACT_SIZE};
12
13const LEGACY_ADV_MAX: usize = 31;
15
16#[allow(dead_code)]
18const EXTENDED_ADV_MAX: usize = 254;
19
20const AD_TYPE_FLAGS: u8 = 0x01;
22
23const AD_TYPE_SERVICE_UUID_16: u8 = 0x03;
25
26const AD_TYPE_SERVICE_DATA_16: u8 = 0x16;
28
29const AD_TYPE_LOCAL_NAME: u8 = 0x09;
31
32const AD_TYPE_SHORT_NAME: u8 = 0x08;
34
35const AD_TYPE_TX_POWER: u8 = 0x0A;
37
38const FLAGS_VALUE: u8 = 0x06;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum AdvertiserState {
44 Idle,
46 Advertising,
48 Paused,
50}
51
52#[derive(Debug, Clone)]
54pub struct AdvertisingPacket {
55 pub adv_data: Vec<u8>,
57 pub scan_rsp: Option<Vec<u8>>,
59 pub extended: bool,
61}
62
63impl AdvertisingPacket {
64 pub fn fits_legacy(&self) -> bool {
66 self.adv_data.len() <= LEGACY_ADV_MAX
67 && self
68 .scan_rsp
69 .as_ref()
70 .is_none_or(|sr| sr.len() <= LEGACY_ADV_MAX)
71 }
72
73 pub fn total_size(&self) -> usize {
75 self.adv_data.len() + self.scan_rsp.as_ref().map_or(0, |sr| sr.len())
76 }
77}
78
79pub struct Advertiser {
83 #[allow(dead_code)]
85 config: DiscoveryConfig,
86 beacon: HiveBeacon,
88 state: AdvertiserState,
90 started_at_ms: Option<u64>,
92 current_time_ms: u64,
94 tx_power: Option<i8>,
96 device_name: Option<String>,
98 use_extended: bool,
100 cached_packet: Option<AdvertisingPacket>,
102 cache_dirty: bool,
104}
105
106impl Advertiser {
107 pub fn new(config: DiscoveryConfig, node_id: NodeId) -> Self {
109 let beacon = HiveBeacon::new(node_id);
110 Self {
111 config,
112 beacon,
113 state: AdvertiserState::Idle,
114 started_at_ms: None,
115 current_time_ms: 0,
116 tx_power: None,
117 device_name: None,
118 use_extended: false,
119 cached_packet: None,
120 cache_dirty: true,
121 }
122 }
123
124 pub fn hive_lite(config: DiscoveryConfig, node_id: NodeId) -> Self {
126 let beacon = HiveBeacon::hive_lite(node_id);
127 Self {
128 config,
129 beacon,
130 state: AdvertiserState::Idle,
131 started_at_ms: None,
132 current_time_ms: 0,
133 tx_power: None,
134 device_name: None,
135 use_extended: false,
136 cached_packet: None,
137 cache_dirty: true,
138 }
139 }
140
141 pub fn set_time_ms(&mut self, time_ms: u64) {
143 self.current_time_ms = time_ms;
144 }
145
146 pub fn with_tx_power(mut self, tx_power: i8) -> Self {
148 self.tx_power = Some(tx_power);
149 self.cache_dirty = true;
150 self
151 }
152
153 pub fn with_name(mut self, name: String) -> Self {
155 self.device_name = Some(name);
156 self.cache_dirty = true;
157 self
158 }
159
160 pub fn with_extended_advertising(mut self, enabled: bool) -> Self {
162 self.use_extended = enabled;
163 self.cache_dirty = true;
164 self
165 }
166
167 pub fn state(&self) -> AdvertiserState {
169 self.state
170 }
171
172 pub fn beacon(&self) -> &HiveBeacon {
174 &self.beacon
175 }
176
177 pub fn beacon_mut(&mut self) -> &mut HiveBeacon {
179 self.cache_dirty = true;
180 &mut self.beacon
181 }
182
183 pub fn set_hierarchy_level(&mut self, level: HierarchyLevel) {
185 self.beacon.hierarchy_level = level;
186 self.cache_dirty = true;
187 }
188
189 pub fn set_capabilities(&mut self, caps: u16) {
191 self.beacon.capabilities = caps;
192 self.cache_dirty = true;
193 }
194
195 pub fn set_battery(&mut self, percent: u8) {
197 self.beacon.battery_percent = percent.min(100);
198 self.cache_dirty = true;
199 }
200
201 pub fn set_geohash(&mut self, geohash: u32) {
203 self.beacon.geohash = geohash & 0x00FFFFFF;
204 self.cache_dirty = true;
205 }
206
207 pub fn start(&mut self) {
209 self.state = AdvertiserState::Advertising;
210 self.started_at_ms = Some(self.current_time_ms);
211 }
212
213 pub fn pause(&mut self) {
215 self.state = AdvertiserState::Paused;
216 }
217
218 pub fn resume(&mut self) {
220 if self.state == AdvertiserState::Paused {
221 self.state = AdvertiserState::Advertising;
222 }
223 }
224
225 pub fn stop(&mut self) {
227 self.state = AdvertiserState::Idle;
228 self.started_at_ms = None;
229 }
230
231 pub fn advertising_duration_ms(&self) -> Option<u64> {
233 self.started_at_ms
234 .map(|t| self.current_time_ms.saturating_sub(t))
235 }
236
237 pub fn increment_sequence(&mut self) {
239 self.beacon.increment_seq();
240 self.cache_dirty = true;
241 }
242
243 pub fn build_packet(&mut self) -> &AdvertisingPacket {
247 if self.cache_dirty || self.cached_packet.is_none() {
248 let packet = self.build_packet_inner();
249 self.cached_packet = Some(packet);
250 self.cache_dirty = false;
251 }
252 self.cached_packet.as_ref().unwrap()
253 }
254
255 pub fn rebuild_packet(&mut self) -> &AdvertisingPacket {
257 self.cache_dirty = true;
258 self.build_packet()
259 }
260
261 fn build_packet_inner(&self) -> AdvertisingPacket {
263 let mut adv_data = Vec::with_capacity(31);
264 let mut scan_rsp = Vec::with_capacity(31);
265
266 adv_data.push(2); adv_data.push(AD_TYPE_FLAGS);
269 adv_data.push(FLAGS_VALUE);
270
271 adv_data.push(3); adv_data.push(AD_TYPE_SERVICE_UUID_16);
274 adv_data.push((HIVE_SERVICE_UUID_16BIT & 0xFF) as u8);
275 adv_data.push((HIVE_SERVICE_UUID_16BIT >> 8) as u8);
276
277 let beacon_data = self.beacon.encode_compact();
279 adv_data.push((2 + BEACON_COMPACT_SIZE) as u8); adv_data.push(AD_TYPE_SERVICE_DATA_16);
281 adv_data.push((HIVE_SERVICE_UUID_16BIT & 0xFF) as u8);
282 adv_data.push((HIVE_SERVICE_UUID_16BIT >> 8) as u8);
283 adv_data.extend_from_slice(&beacon_data);
284
285 if let Some(tx_power) = self.tx_power {
287 if adv_data.len() + 3 <= LEGACY_ADV_MAX {
288 adv_data.push(2); adv_data.push(AD_TYPE_TX_POWER);
290 adv_data.push(tx_power as u8);
291 } else {
292 scan_rsp.push(2);
294 scan_rsp.push(AD_TYPE_TX_POWER);
295 scan_rsp.push(tx_power as u8);
296 }
297 }
298
299 if let Some(ref name) = self.device_name {
301 let name_bytes = name.as_bytes();
302 let max_name_len = LEGACY_ADV_MAX - 2; if name_bytes.len() <= max_name_len {
305 scan_rsp.push(name_bytes.len() as u8 + 1);
307 scan_rsp.push(AD_TYPE_LOCAL_NAME);
308 scan_rsp.extend_from_slice(name_bytes);
309 } else {
310 let short_name = &name_bytes[..max_name_len.min(name_bytes.len())];
312 scan_rsp.push(short_name.len() as u8 + 1);
313 scan_rsp.push(AD_TYPE_SHORT_NAME);
314 scan_rsp.extend_from_slice(short_name);
315 }
316 }
317
318 let extended =
319 self.use_extended || adv_data.len() > LEGACY_ADV_MAX || scan_rsp.len() > LEGACY_ADV_MAX;
320
321 AdvertisingPacket {
322 adv_data,
323 scan_rsp: if scan_rsp.is_empty() {
324 None
325 } else {
326 Some(scan_rsp)
327 },
328 extended,
329 }
330 }
331
332 pub fn advertising_data(&mut self) -> Vec<u8> {
334 self.build_packet().adv_data.clone()
335 }
336
337 pub fn scan_response_data(&mut self) -> Option<Vec<u8>> {
339 self.build_packet().scan_rsp.clone()
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::capabilities;
347
348 #[test]
349 fn test_advertiser_new() {
350 let config = DiscoveryConfig::default();
351 let node_id = NodeId::new(0x12345678);
352 let advertiser = Advertiser::new(config, node_id);
353
354 assert_eq!(advertiser.state(), AdvertiserState::Idle);
355 assert_eq!(advertiser.beacon().node_id, node_id);
356 }
357
358 #[test]
359 fn test_advertiser_hive_lite() {
360 let config = DiscoveryConfig::default();
361 let node_id = NodeId::new(0xCAFEBABE);
362 let advertiser = Advertiser::hive_lite(config, node_id);
363
364 assert!(advertiser.beacon().is_lite_node());
365 }
366
367 #[test]
368 fn test_advertiser_state_transitions() {
369 let config = DiscoveryConfig::default();
370 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
371
372 assert_eq!(advertiser.state(), AdvertiserState::Idle);
373
374 advertiser.set_time_ms(1000);
375 advertiser.start();
376 assert_eq!(advertiser.state(), AdvertiserState::Advertising);
377 advertiser.set_time_ms(2000);
378 assert_eq!(advertiser.advertising_duration_ms(), Some(1000));
379
380 advertiser.pause();
381 assert_eq!(advertiser.state(), AdvertiserState::Paused);
382
383 advertiser.resume();
384 assert_eq!(advertiser.state(), AdvertiserState::Advertising);
385
386 advertiser.stop();
387 assert_eq!(advertiser.state(), AdvertiserState::Idle);
388 assert!(advertiser.advertising_duration_ms().is_none());
389 }
390
391 #[test]
392 fn test_build_packet_fits_legacy() {
393 let config = DiscoveryConfig::default();
394 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
395
396 let packet = advertiser.build_packet();
397 assert!(packet.fits_legacy());
398 assert!(!packet.extended);
399
400 assert!(packet.adv_data.len() <= LEGACY_ADV_MAX);
402 }
403
404 #[test]
405 fn test_build_packet_with_name() {
406 let config = DiscoveryConfig::default();
407 let mut advertiser =
408 Advertiser::new(config, NodeId::new(0x12345678)).with_name("HIVE-12345678".to_string());
409
410 let packet = advertiser.build_packet();
411 assert!(packet.scan_rsp.is_some());
412
413 let scan_rsp = packet.scan_rsp.as_ref().unwrap();
414 assert!(scan_rsp.contains(&AD_TYPE_LOCAL_NAME));
416 }
417
418 #[test]
419 fn test_build_packet_with_tx_power() {
420 let config = DiscoveryConfig::default();
421 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678)).with_tx_power(0);
422
423 let packet = advertiser.build_packet();
424
425 assert!(packet.adv_data.contains(&AD_TYPE_TX_POWER));
427 }
428
429 #[test]
430 fn test_packet_caching() {
431 let config = DiscoveryConfig::default();
432 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
433
434 let packet1 = advertiser.build_packet();
436 let data1 = packet1.adv_data.clone();
437
438 let packet2 = advertiser.build_packet();
440 assert_eq!(data1, packet2.adv_data);
441
442 advertiser.set_battery(50);
444 let packet3 = advertiser.build_packet();
445 assert_ne!(data1, packet3.adv_data);
447 }
448
449 #[test]
450 fn test_sequence_increment() {
451 let config = DiscoveryConfig::default();
452 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
453
454 let seq1 = advertiser.beacon().seq_num;
455 advertiser.increment_sequence();
456 let seq2 = advertiser.beacon().seq_num;
457
458 assert_eq!(seq2, seq1 + 1);
459 }
460
461 #[test]
462 fn test_update_beacon_fields() {
463 let config = DiscoveryConfig::default();
464 let mut advertiser = Advertiser::new(config, NodeId::new(0x12345678));
465
466 advertiser.set_hierarchy_level(HierarchyLevel::Squad);
467 assert_eq!(advertiser.beacon().hierarchy_level, HierarchyLevel::Squad);
468
469 advertiser.set_capabilities(capabilities::CAN_RELAY);
470 assert!(advertiser.beacon().can_relay());
471
472 advertiser.set_battery(75);
473 assert_eq!(advertiser.beacon().battery_percent, 75);
474
475 advertiser.set_geohash(0x123456);
476 assert_eq!(advertiser.beacon().geohash, 0x123456);
477 }
478}