hive_btle/discovery/
beacon.rs1#[cfg(not(feature = "std"))]
52use alloc::string::String;
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 local_name: Option<String>,
311 pub tx_power: Option<i8>,
313 pub connectable: bool,
315}
316
317impl ParsedAdvertisement {
318 pub fn is_hive_device(&self) -> bool {
320 self.beacon.is_some()
321 }
322
323 pub fn node_id(&self) -> Option<&NodeId> {
325 self.beacon.as_ref().map(|b| &b.node_id)
326 }
327
328 #[cfg(feature = "std")]
336 pub fn estimated_distance_meters(&self) -> Option<f32> {
337 let tx_power = self.tx_power.unwrap_or(0) as f32;
338 let rssi = self.rssi as f32;
339 let n = 2.5; if rssi >= tx_power {
342 return Some(1.0); }
344
345 let distance = 10.0_f32.powf((tx_power - rssi) / (10.0 * n));
346 Some(distance)
347 }
348
349 #[cfg(not(feature = "std"))]
351 pub fn estimated_distance_meters(&self) -> Option<f32> {
352 None
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_beacon_encode_decode() {
362 let beacon = HiveBeacon::new(NodeId::new(0x12345678))
363 .with_capabilities(capabilities::LITE_NODE | capabilities::SENSOR_ACCEL)
364 .with_hierarchy_level(HierarchyLevel::Squad)
365 .with_geohash(0x98FF88)
366 .with_battery(75);
367
368 let encoded = beacon.encode();
369 let decoded = HiveBeacon::decode(&encoded).unwrap();
370
371 assert_eq!(decoded.version, beacon.version);
372 assert_eq!(decoded.capabilities, beacon.capabilities);
373 assert_eq!(decoded.node_id, beacon.node_id);
374 assert_eq!(decoded.hierarchy_level, beacon.hierarchy_level);
375 assert_eq!(decoded.geohash, beacon.geohash & 0x00FFFFFF);
376 assert_eq!(decoded.battery_percent, beacon.battery_percent);
377 }
378
379 #[test]
380 fn test_beacon_compact_encode_decode() {
381 let beacon = HiveBeacon::new(NodeId::new(0xDEADBEEF))
382 .with_capabilities(capabilities::CAN_RELAY)
383 .with_battery(50);
384
385 let encoded = beacon.encode_compact();
386 assert_eq!(encoded.len(), BEACON_COMPACT_SIZE);
387
388 let decoded = HiveBeacon::decode_compact(&encoded).unwrap();
389
390 assert_eq!(decoded.node_id, beacon.node_id);
391 assert_eq!(decoded.capabilities, beacon.capabilities);
392 assert_eq!(decoded.battery_percent, beacon.battery_percent);
393 assert_eq!(decoded.geohash, 0); }
395
396 #[test]
397 fn test_beacon_size() {
398 let beacon = HiveBeacon::new(NodeId::new(0x12345678));
399 let encoded = beacon.encode();
400 assert_eq!(encoded.len(), BEACON_SIZE);
401 assert_eq!(encoded.len(), 16);
402 }
403
404 #[test]
405 fn test_beacon_version() {
406 let beacon = HiveBeacon::new(NodeId::new(0x12345678));
407 let encoded = beacon.encode();
408 let version = (encoded[0] >> 4) & 0x0F;
409 assert_eq!(version, BEACON_VERSION);
410 }
411
412 #[test]
413 fn test_beacon_capabilities() {
414 let caps = capabilities::LITE_NODE | capabilities::CODED_PHY | capabilities::HAS_GPS;
415 let beacon = HiveBeacon::new(NodeId::new(0x12345678)).with_capabilities(caps);
416
417 assert!(beacon.is_lite_node());
418 assert!(beacon.supports_coded_phy());
419 assert!(!beacon.can_relay());
420
421 let encoded = beacon.encode();
422 let decoded = HiveBeacon::decode(&encoded).unwrap();
423 assert_eq!(decoded.capabilities, caps);
424 }
425
426 #[test]
427 fn test_sequence_number_wrap() {
428 let mut beacon = HiveBeacon::new(NodeId::new(0x12345678));
429 beacon.seq_num = 0xFFFF;
430 beacon.increment_seq();
431 assert_eq!(beacon.seq_num, 0);
432 }
433
434 #[test]
435 fn test_decode_invalid_length() {
436 let short_data = [0u8; 5];
437 assert!(HiveBeacon::decode(&short_data).is_none());
438 assert!(HiveBeacon::decode_compact(&short_data).is_none());
439 }
440
441 #[test]
442 fn test_estimated_distance() {
443 let adv = ParsedAdvertisement {
444 address: "00:11:22:33:44:55".to_string(),
445 rssi: -60,
446 beacon: None,
447 local_name: None,
448 tx_power: Some(-20), connectable: true,
450 };
451
452 let distance = adv.estimated_distance_meters().unwrap();
453 assert!(distance > 1.0 && distance < 100.0);
456 }
457
458 #[test]
459 fn test_hive_lite_beacon() {
460 let beacon = HiveBeacon::hive_lite(NodeId::new(0xCAFEBABE));
461 assert!(beacon.is_lite_node());
462 assert!(!beacon.can_relay());
463 }
464}