hive_btle/discovery/
beacon.rs1#[cfg(not(feature = "std"))]
37use alloc::string::String;
38
39use crate::{capabilities, HierarchyLevel, NodeId};
40
41pub const BEACON_VERSION: u8 = 1;
43
44pub const BEACON_SIZE: usize = 16;
46
47pub const BEACON_COMPACT_SIZE: usize = 10;
49
50#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct HiveBeacon {
55 pub version: u8,
57 pub capabilities: u16,
59 pub node_id: NodeId,
61 pub hierarchy_level: HierarchyLevel,
63 pub geohash: u32,
65 pub battery_percent: u8,
67 pub seq_num: u16,
69}
70
71impl HiveBeacon {
72 pub fn new(node_id: NodeId) -> Self {
74 Self {
75 version: BEACON_VERSION,
76 capabilities: 0,
77 node_id,
78 hierarchy_level: HierarchyLevel::Platform,
79 geohash: 0,
80 battery_percent: 255, seq_num: 0,
82 }
83 }
84
85 pub fn hive_lite(node_id: NodeId) -> Self {
87 Self {
88 version: BEACON_VERSION,
89 capabilities: capabilities::LITE_NODE,
90 node_id,
91 hierarchy_level: HierarchyLevel::Platform,
92 geohash: 0,
93 battery_percent: 255,
94 seq_num: 0,
95 }
96 }
97
98 pub fn with_capabilities(mut self, capabilities: u16) -> Self {
100 self.capabilities = capabilities;
101 self
102 }
103
104 pub fn with_hierarchy_level(mut self, level: HierarchyLevel) -> Self {
106 self.hierarchy_level = level;
107 self
108 }
109
110 pub fn with_geohash(mut self, geohash: u32) -> Self {
112 self.geohash = geohash & 0x00FFFFFF; self
114 }
115
116 pub fn with_battery(mut self, percent: u8) -> Self {
118 self.battery_percent = percent.min(100);
119 self
120 }
121
122 pub fn increment_seq(&mut self) {
124 self.seq_num = self.seq_num.wrapping_add(1);
125 }
126
127 pub fn encode(&self) -> [u8; BEACON_SIZE] {
129 let mut buf = [0u8; BEACON_SIZE];
130
131 buf[0] = ((self.version & 0x0F) << 4) | ((self.capabilities >> 8) as u8 & 0x0F);
133
134 buf[1] = (self.capabilities & 0xFF) as u8;
136
137 let node_id = self.node_id.as_u32();
139 buf[2] = (node_id >> 24) as u8;
140 buf[3] = (node_id >> 16) as u8;
141 buf[4] = (node_id >> 8) as u8;
142 buf[5] = node_id as u8;
143
144 buf[6] = self.hierarchy_level.into();
146
147 buf[7] = (self.geohash >> 16) as u8;
149 buf[8] = (self.geohash >> 8) as u8;
150 buf[9] = self.geohash as u8;
151
152 buf[10] = self.battery_percent;
154
155 buf[11] = (self.seq_num >> 8) as u8;
157 buf[12] = self.seq_num as u8;
158
159 buf[13] = 0;
161 buf[14] = 0;
162 buf[15] = 0;
163
164 buf
165 }
166
167 pub fn encode_compact(&self) -> [u8; BEACON_COMPACT_SIZE] {
177 let mut buf = [0u8; BEACON_COMPACT_SIZE];
178
179 buf[0] = ((self.version & 0x0F) << 4) | ((self.capabilities >> 8) as u8 & 0x0F);
180 buf[1] = (self.capabilities & 0xFF) as u8;
181
182 let node_id = self.node_id.as_u32();
183 buf[2] = (node_id >> 24) as u8;
184 buf[3] = (node_id >> 16) as u8;
185 buf[4] = (node_id >> 8) as u8;
186 buf[5] = node_id as u8;
187
188 buf[6] = self.hierarchy_level.into();
189 buf[7] = self.battery_percent;
190
191 buf[8] = (self.seq_num >> 8) as u8;
192 buf[9] = self.seq_num as u8;
193
194 buf
195 }
196
197 pub fn decode(data: &[u8]) -> Option<Self> {
199 if data.len() < BEACON_SIZE {
200 return None;
201 }
202
203 let version = (data[0] >> 4) & 0x0F;
204 let capabilities = ((data[0] as u16 & 0x0F) << 8) | (data[1] as u16);
205
206 let node_id = NodeId::new(
207 ((data[2] as u32) << 24)
208 | ((data[3] as u32) << 16)
209 | ((data[4] as u32) << 8)
210 | (data[5] as u32),
211 );
212
213 let hierarchy_level = HierarchyLevel::from(data[6]);
214
215 let geohash = ((data[7] as u32) << 16) | ((data[8] as u32) << 8) | (data[9] as u32);
216
217 let battery_percent = data[10];
218
219 let seq_num = ((data[11] as u16) << 8) | (data[12] as u16);
220
221 Some(Self {
222 version,
223 capabilities,
224 node_id,
225 hierarchy_level,
226 geohash,
227 battery_percent,
228 seq_num,
229 })
230 }
231
232 pub fn decode_compact(data: &[u8]) -> Option<Self> {
234 if data.len() < BEACON_COMPACT_SIZE {
235 return None;
236 }
237
238 let version = (data[0] >> 4) & 0x0F;
239 let capabilities = ((data[0] as u16 & 0x0F) << 8) | (data[1] as u16);
240
241 let node_id = NodeId::new(
242 ((data[2] as u32) << 24)
243 | ((data[3] as u32) << 16)
244 | ((data[4] as u32) << 8)
245 | (data[5] as u32),
246 );
247
248 let hierarchy_level = HierarchyLevel::from(data[6]);
249 let battery_percent = data[7];
250 let seq_num = ((data[8] as u16) << 8) | (data[9] as u16);
251
252 Some(Self {
253 version,
254 capabilities,
255 node_id,
256 hierarchy_level,
257 geohash: 0, battery_percent,
259 seq_num,
260 })
261 }
262
263 pub fn is_lite_node(&self) -> bool {
265 self.capabilities & capabilities::LITE_NODE != 0
266 }
267
268 pub fn can_relay(&self) -> bool {
270 self.capabilities & capabilities::CAN_RELAY != 0
271 }
272
273 pub fn supports_coded_phy(&self) -> bool {
275 self.capabilities & capabilities::CODED_PHY != 0
276 }
277}
278
279impl Default for HiveBeacon {
280 fn default() -> Self {
281 Self::new(NodeId::default())
282 }
283}
284
285#[derive(Debug, Clone)]
287pub struct ParsedAdvertisement {
288 pub address: String,
290 pub rssi: i8,
292 pub beacon: Option<HiveBeacon>,
294 pub local_name: Option<String>,
296 pub tx_power: Option<i8>,
298 pub connectable: bool,
300}
301
302impl ParsedAdvertisement {
303 pub fn is_hive_device(&self) -> bool {
305 self.beacon.is_some()
306 }
307
308 pub fn node_id(&self) -> Option<&NodeId> {
310 self.beacon.as_ref().map(|b| &b.node_id)
311 }
312
313 #[cfg(feature = "std")]
321 pub fn estimated_distance_meters(&self) -> Option<f32> {
322 let tx_power = self.tx_power.unwrap_or(0) as f32;
323 let rssi = self.rssi as f32;
324 let n = 2.5; if rssi >= tx_power {
327 return Some(1.0); }
329
330 let distance = 10.0_f32.powf((tx_power - rssi) / (10.0 * n));
331 Some(distance)
332 }
333
334 #[cfg(not(feature = "std"))]
336 pub fn estimated_distance_meters(&self) -> Option<f32> {
337 None
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn test_beacon_encode_decode() {
347 let beacon = HiveBeacon::new(NodeId::new(0x12345678))
348 .with_capabilities(capabilities::LITE_NODE | capabilities::SENSOR_ACCEL)
349 .with_hierarchy_level(HierarchyLevel::Squad)
350 .with_geohash(0x98FF88)
351 .with_battery(75);
352
353 let encoded = beacon.encode();
354 let decoded = HiveBeacon::decode(&encoded).unwrap();
355
356 assert_eq!(decoded.version, beacon.version);
357 assert_eq!(decoded.capabilities, beacon.capabilities);
358 assert_eq!(decoded.node_id, beacon.node_id);
359 assert_eq!(decoded.hierarchy_level, beacon.hierarchy_level);
360 assert_eq!(decoded.geohash, beacon.geohash & 0x00FFFFFF);
361 assert_eq!(decoded.battery_percent, beacon.battery_percent);
362 }
363
364 #[test]
365 fn test_beacon_compact_encode_decode() {
366 let beacon = HiveBeacon::new(NodeId::new(0xDEADBEEF))
367 .with_capabilities(capabilities::CAN_RELAY)
368 .with_battery(50);
369
370 let encoded = beacon.encode_compact();
371 assert_eq!(encoded.len(), BEACON_COMPACT_SIZE);
372
373 let decoded = HiveBeacon::decode_compact(&encoded).unwrap();
374
375 assert_eq!(decoded.node_id, beacon.node_id);
376 assert_eq!(decoded.capabilities, beacon.capabilities);
377 assert_eq!(decoded.battery_percent, beacon.battery_percent);
378 assert_eq!(decoded.geohash, 0); }
380
381 #[test]
382 fn test_beacon_size() {
383 let beacon = HiveBeacon::new(NodeId::new(0x12345678));
384 let encoded = beacon.encode();
385 assert_eq!(encoded.len(), BEACON_SIZE);
386 assert_eq!(encoded.len(), 16);
387 }
388
389 #[test]
390 fn test_beacon_version() {
391 let beacon = HiveBeacon::new(NodeId::new(0x12345678));
392 let encoded = beacon.encode();
393 let version = (encoded[0] >> 4) & 0x0F;
394 assert_eq!(version, BEACON_VERSION);
395 }
396
397 #[test]
398 fn test_beacon_capabilities() {
399 let caps = capabilities::LITE_NODE | capabilities::CODED_PHY | capabilities::HAS_GPS;
400 let beacon = HiveBeacon::new(NodeId::new(0x12345678)).with_capabilities(caps);
401
402 assert!(beacon.is_lite_node());
403 assert!(beacon.supports_coded_phy());
404 assert!(!beacon.can_relay());
405
406 let encoded = beacon.encode();
407 let decoded = HiveBeacon::decode(&encoded).unwrap();
408 assert_eq!(decoded.capabilities, caps);
409 }
410
411 #[test]
412 fn test_sequence_number_wrap() {
413 let mut beacon = HiveBeacon::new(NodeId::new(0x12345678));
414 beacon.seq_num = 0xFFFF;
415 beacon.increment_seq();
416 assert_eq!(beacon.seq_num, 0);
417 }
418
419 #[test]
420 fn test_decode_invalid_length() {
421 let short_data = [0u8; 5];
422 assert!(HiveBeacon::decode(&short_data).is_none());
423 assert!(HiveBeacon::decode_compact(&short_data).is_none());
424 }
425
426 #[test]
427 fn test_estimated_distance() {
428 let adv = ParsedAdvertisement {
429 address: "00:11:22:33:44:55".to_string(),
430 rssi: -60,
431 beacon: None,
432 local_name: None,
433 tx_power: Some(-20), connectable: true,
435 };
436
437 let distance = adv.estimated_distance_meters().unwrap();
438 assert!(distance > 1.0 && distance < 100.0);
441 }
442
443 #[test]
444 fn test_hive_lite_beacon() {
445 let beacon = HiveBeacon::hive_lite(NodeId::new(0xCAFEBABE));
446 assert!(beacon.is_lite_node());
447 assert!(!beacon.can_relay());
448 }
449}