hive_btle/discovery/
beacon.rs1#[cfg(not(feature = "std"))]
52use alloc::{string::String, vec::Vec};
53
54use crate::{capabilities, HierarchyLevel, NodeId};
55
56pub const BEACON_VERSION: u8 = 1;
58
59pub const BEACON_SIZE: usize = 16;
61
62pub const BEACON_COMPACT_SIZE: usize = 10;
64
65#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct HiveBeacon {
70 pub version: u8,
72 pub capabilities: u16,
74 pub node_id: NodeId,
76 pub hierarchy_level: HierarchyLevel,
78 pub geohash: u32,
80 pub battery_percent: u8,
82 pub seq_num: u16,
84}
85
86impl HiveBeacon {
87 pub fn new(node_id: NodeId) -> Self {
89 Self {
90 version: BEACON_VERSION,
91 capabilities: 0,
92 node_id,
93 hierarchy_level: HierarchyLevel::Platform,
94 geohash: 0,
95 battery_percent: 255, seq_num: 0,
97 }
98 }
99
100 pub fn hive_lite(node_id: NodeId) -> Self {
102 Self {
103 version: BEACON_VERSION,
104 capabilities: capabilities::LITE_NODE,
105 node_id,
106 hierarchy_level: HierarchyLevel::Platform,
107 geohash: 0,
108 battery_percent: 255,
109 seq_num: 0,
110 }
111 }
112
113 pub fn with_capabilities(mut self, capabilities: u16) -> Self {
115 self.capabilities = capabilities;
116 self
117 }
118
119 pub fn with_hierarchy_level(mut self, level: HierarchyLevel) -> Self {
121 self.hierarchy_level = level;
122 self
123 }
124
125 pub fn with_geohash(mut self, geohash: u32) -> Self {
127 self.geohash = geohash & 0x00FFFFFF; self
129 }
130
131 pub fn with_battery(mut self, percent: u8) -> Self {
133 self.battery_percent = percent.min(100);
134 self
135 }
136
137 pub fn increment_seq(&mut self) {
139 self.seq_num = self.seq_num.wrapping_add(1);
140 }
141
142 pub fn encode(&self) -> [u8; BEACON_SIZE] {
144 let mut buf = [0u8; BEACON_SIZE];
145
146 buf[0] = ((self.version & 0x0F) << 4) | ((self.capabilities >> 8) as u8 & 0x0F);
148
149 buf[1] = (self.capabilities & 0xFF) as u8;
151
152 let node_id = self.node_id.as_u32();
154 buf[2] = (node_id >> 24) as u8;
155 buf[3] = (node_id >> 16) as u8;
156 buf[4] = (node_id >> 8) as u8;
157 buf[5] = node_id as u8;
158
159 buf[6] = self.hierarchy_level.into();
161
162 buf[7] = (self.geohash >> 16) as u8;
164 buf[8] = (self.geohash >> 8) as u8;
165 buf[9] = self.geohash as u8;
166
167 buf[10] = self.battery_percent;
169
170 buf[11] = (self.seq_num >> 8) as u8;
172 buf[12] = self.seq_num as u8;
173
174 buf[13] = 0;
176 buf[14] = 0;
177 buf[15] = 0;
178
179 buf
180 }
181
182 pub fn encode_compact(&self) -> [u8; BEACON_COMPACT_SIZE] {
192 let mut buf = [0u8; BEACON_COMPACT_SIZE];
193
194 buf[0] = ((self.version & 0x0F) << 4) | ((self.capabilities >> 8) as u8 & 0x0F);
195 buf[1] = (self.capabilities & 0xFF) as u8;
196
197 let node_id = self.node_id.as_u32();
198 buf[2] = (node_id >> 24) as u8;
199 buf[3] = (node_id >> 16) as u8;
200 buf[4] = (node_id >> 8) as u8;
201 buf[5] = node_id as u8;
202
203 buf[6] = self.hierarchy_level.into();
204 buf[7] = self.battery_percent;
205
206 buf[8] = (self.seq_num >> 8) as u8;
207 buf[9] = self.seq_num as u8;
208
209 buf
210 }
211
212 pub fn decode(data: &[u8]) -> Option<Self> {
214 if data.len() < BEACON_SIZE {
215 return None;
216 }
217
218 let version = (data[0] >> 4) & 0x0F;
219 let capabilities = ((data[0] as u16 & 0x0F) << 8) | (data[1] as u16);
220
221 let node_id = NodeId::new(
222 ((data[2] as u32) << 24)
223 | ((data[3] as u32) << 16)
224 | ((data[4] as u32) << 8)
225 | (data[5] as u32),
226 );
227
228 let hierarchy_level = HierarchyLevel::from(data[6]);
229
230 let geohash = ((data[7] as u32) << 16) | ((data[8] as u32) << 8) | (data[9] as u32);
231
232 let battery_percent = data[10];
233
234 let seq_num = ((data[11] as u16) << 8) | (data[12] as u16);
235
236 Some(Self {
237 version,
238 capabilities,
239 node_id,
240 hierarchy_level,
241 geohash,
242 battery_percent,
243 seq_num,
244 })
245 }
246
247 pub fn decode_compact(data: &[u8]) -> Option<Self> {
249 if data.len() < BEACON_COMPACT_SIZE {
250 return None;
251 }
252
253 let version = (data[0] >> 4) & 0x0F;
254 let capabilities = ((data[0] as u16 & 0x0F) << 8) | (data[1] as u16);
255
256 let node_id = NodeId::new(
257 ((data[2] as u32) << 24)
258 | ((data[3] as u32) << 16)
259 | ((data[4] as u32) << 8)
260 | (data[5] as u32),
261 );
262
263 let hierarchy_level = HierarchyLevel::from(data[6]);
264 let battery_percent = data[7];
265 let seq_num = ((data[8] as u16) << 8) | (data[9] as u16);
266
267 Some(Self {
268 version,
269 capabilities,
270 node_id,
271 hierarchy_level,
272 geohash: 0, battery_percent,
274 seq_num,
275 })
276 }
277
278 pub fn is_lite_node(&self) -> bool {
280 self.capabilities & capabilities::LITE_NODE != 0
281 }
282
283 pub fn can_relay(&self) -> bool {
285 self.capabilities & capabilities::CAN_RELAY != 0
286 }
287
288 pub fn supports_coded_phy(&self) -> bool {
290 self.capabilities & capabilities::CODED_PHY != 0
291 }
292}
293
294impl Default for HiveBeacon {
295 fn default() -> Self {
296 Self::new(NodeId::default())
297 }
298}
299
300#[derive(Debug, Clone)]
302pub struct ParsedAdvertisement {
303 pub address: String,
305 pub rssi: i8,
307 pub beacon: Option<HiveBeacon>,
309 pub encrypted_service_data: Option<Vec<u8>>,
315 pub local_name: Option<String>,
317 pub tx_power: Option<i8>,
319 pub connectable: bool,
321}
322
323impl ParsedAdvertisement {
324 pub fn is_hive_device(&self) -> bool {
329 self.beacon.is_some() || self.encrypted_service_data.is_some()
330 }
331
332 pub fn node_id(&self) -> Option<&NodeId> {
334 self.beacon.as_ref().map(|b| &b.node_id)
335 }
336
337 #[cfg(feature = "std")]
345 pub fn estimated_distance_meters(&self) -> Option<f32> {
346 let tx_power = self.tx_power.unwrap_or(0) as f32;
347 let rssi = self.rssi as f32;
348 let n = 2.5; if rssi >= tx_power {
351 return Some(1.0); }
353
354 let distance = 10.0_f32.powf((tx_power - rssi) / (10.0 * n));
355 Some(distance)
356 }
357
358 #[cfg(not(feature = "std"))]
360 pub fn estimated_distance_meters(&self) -> Option<f32> {
361 None
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_beacon_encode_decode() {
371 let beacon = HiveBeacon::new(NodeId::new(0x12345678))
372 .with_capabilities(capabilities::LITE_NODE | capabilities::SENSOR_ACCEL)
373 .with_hierarchy_level(HierarchyLevel::Squad)
374 .with_geohash(0x98FF88)
375 .with_battery(75);
376
377 let encoded = beacon.encode();
378 let decoded = HiveBeacon::decode(&encoded).unwrap();
379
380 assert_eq!(decoded.version, beacon.version);
381 assert_eq!(decoded.capabilities, beacon.capabilities);
382 assert_eq!(decoded.node_id, beacon.node_id);
383 assert_eq!(decoded.hierarchy_level, beacon.hierarchy_level);
384 assert_eq!(decoded.geohash, beacon.geohash & 0x00FFFFFF);
385 assert_eq!(decoded.battery_percent, beacon.battery_percent);
386 }
387
388 #[test]
389 fn test_beacon_compact_encode_decode() {
390 let beacon = HiveBeacon::new(NodeId::new(0xDEADBEEF))
391 .with_capabilities(capabilities::CAN_RELAY)
392 .with_battery(50);
393
394 let encoded = beacon.encode_compact();
395 assert_eq!(encoded.len(), BEACON_COMPACT_SIZE);
396
397 let decoded = HiveBeacon::decode_compact(&encoded).unwrap();
398
399 assert_eq!(decoded.node_id, beacon.node_id);
400 assert_eq!(decoded.capabilities, beacon.capabilities);
401 assert_eq!(decoded.battery_percent, beacon.battery_percent);
402 assert_eq!(decoded.geohash, 0); }
404
405 #[test]
406 fn test_beacon_size() {
407 let beacon = HiveBeacon::new(NodeId::new(0x12345678));
408 let encoded = beacon.encode();
409 assert_eq!(encoded.len(), BEACON_SIZE);
410 assert_eq!(encoded.len(), 16);
411 }
412
413 #[test]
414 fn test_beacon_version() {
415 let beacon = HiveBeacon::new(NodeId::new(0x12345678));
416 let encoded = beacon.encode();
417 let version = (encoded[0] >> 4) & 0x0F;
418 assert_eq!(version, BEACON_VERSION);
419 }
420
421 #[test]
422 fn test_beacon_capabilities() {
423 let caps = capabilities::LITE_NODE | capabilities::CODED_PHY | capabilities::HAS_GPS;
424 let beacon = HiveBeacon::new(NodeId::new(0x12345678)).with_capabilities(caps);
425
426 assert!(beacon.is_lite_node());
427 assert!(beacon.supports_coded_phy());
428 assert!(!beacon.can_relay());
429
430 let encoded = beacon.encode();
431 let decoded = HiveBeacon::decode(&encoded).unwrap();
432 assert_eq!(decoded.capabilities, caps);
433 }
434
435 #[test]
436 fn test_sequence_number_wrap() {
437 let mut beacon = HiveBeacon::new(NodeId::new(0x12345678));
438 beacon.seq_num = 0xFFFF;
439 beacon.increment_seq();
440 assert_eq!(beacon.seq_num, 0);
441 }
442
443 #[test]
444 fn test_decode_invalid_length() {
445 let short_data = [0u8; 5];
446 assert!(HiveBeacon::decode(&short_data).is_none());
447 assert!(HiveBeacon::decode_compact(&short_data).is_none());
448 }
449
450 #[test]
451 fn test_estimated_distance() {
452 let adv = ParsedAdvertisement {
453 address: "00:11:22:33:44:55".to_string(),
454 rssi: -60,
455 beacon: None,
456 encrypted_service_data: None,
457 local_name: None,
458 tx_power: Some(-20), connectable: true,
460 };
461
462 let distance = adv.estimated_distance_meters().unwrap();
463 assert!(distance > 1.0 && distance < 100.0);
466 }
467
468 #[test]
469 fn test_hive_lite_beacon() {
470 let beacon = HiveBeacon::hive_lite(NodeId::new(0xCAFEBABE));
471 assert!(beacon.is_lite_node());
472 assert!(!beacon.can_relay());
473 }
474}