#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
use crate::{capabilities, HierarchyLevel, NodeId};
pub const BEACON_VERSION: u8 = 1;
pub const BEACON_SIZE: usize = 16;
pub const BEACON_COMPACT_SIZE: usize = 10;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeatBeacon {
pub version: u8,
pub capabilities: u16,
pub node_id: NodeId,
pub hierarchy_level: HierarchyLevel,
pub geohash: u32,
pub battery_percent: u8,
pub seq_num: u16,
}
impl PeatBeacon {
pub fn new(node_id: NodeId) -> Self {
Self {
version: BEACON_VERSION,
capabilities: 0,
node_id,
hierarchy_level: HierarchyLevel::Platform,
geohash: 0,
battery_percent: 255, seq_num: 0,
}
}
pub fn peat_lite(node_id: NodeId) -> Self {
Self {
version: BEACON_VERSION,
capabilities: capabilities::LITE_NODE,
node_id,
hierarchy_level: HierarchyLevel::Platform,
geohash: 0,
battery_percent: 255,
seq_num: 0,
}
}
pub fn with_capabilities(mut self, capabilities: u16) -> Self {
self.capabilities = capabilities;
self
}
pub fn with_hierarchy_level(mut self, level: HierarchyLevel) -> Self {
self.hierarchy_level = level;
self
}
pub fn with_geohash(mut self, geohash: u32) -> Self {
self.geohash = geohash & 0x00FFFFFF; self
}
pub fn with_battery(mut self, percent: u8) -> Self {
self.battery_percent = percent.min(100);
self
}
pub fn increment_seq(&mut self) {
self.seq_num = self.seq_num.wrapping_add(1);
}
pub fn encode(&self) -> [u8; BEACON_SIZE] {
let mut buf = [0u8; BEACON_SIZE];
buf[0] = ((self.version & 0x0F) << 4) | ((self.capabilities >> 8) as u8 & 0x0F);
buf[1] = (self.capabilities & 0xFF) as u8;
let node_id = self.node_id.as_u32();
buf[2] = (node_id >> 24) as u8;
buf[3] = (node_id >> 16) as u8;
buf[4] = (node_id >> 8) as u8;
buf[5] = node_id as u8;
buf[6] = self.hierarchy_level.into();
buf[7] = (self.geohash >> 16) as u8;
buf[8] = (self.geohash >> 8) as u8;
buf[9] = self.geohash as u8;
buf[10] = self.battery_percent;
buf[11] = (self.seq_num >> 8) as u8;
buf[12] = self.seq_num as u8;
buf[13] = 0;
buf[14] = 0;
buf[15] = 0;
buf
}
pub fn encode_compact(&self) -> [u8; BEACON_COMPACT_SIZE] {
let mut buf = [0u8; BEACON_COMPACT_SIZE];
buf[0] = ((self.version & 0x0F) << 4) | ((self.capabilities >> 8) as u8 & 0x0F);
buf[1] = (self.capabilities & 0xFF) as u8;
let node_id = self.node_id.as_u32();
buf[2] = (node_id >> 24) as u8;
buf[3] = (node_id >> 16) as u8;
buf[4] = (node_id >> 8) as u8;
buf[5] = node_id as u8;
buf[6] = self.hierarchy_level.into();
buf[7] = self.battery_percent;
buf[8] = (self.seq_num >> 8) as u8;
buf[9] = self.seq_num as u8;
buf
}
pub fn decode(data: &[u8]) -> Option<Self> {
if data.len() < BEACON_SIZE {
return None;
}
let version = (data[0] >> 4) & 0x0F;
let capabilities = ((data[0] as u16 & 0x0F) << 8) | (data[1] as u16);
let node_id = NodeId::new(
((data[2] as u32) << 24)
| ((data[3] as u32) << 16)
| ((data[4] as u32) << 8)
| (data[5] as u32),
);
let hierarchy_level = HierarchyLevel::from(data[6]);
let geohash = ((data[7] as u32) << 16) | ((data[8] as u32) << 8) | (data[9] as u32);
let battery_percent = data[10];
let seq_num = ((data[11] as u16) << 8) | (data[12] as u16);
Some(Self {
version,
capabilities,
node_id,
hierarchy_level,
geohash,
battery_percent,
seq_num,
})
}
pub fn decode_compact(data: &[u8]) -> Option<Self> {
if data.len() < BEACON_COMPACT_SIZE {
return None;
}
let version = (data[0] >> 4) & 0x0F;
let capabilities = ((data[0] as u16 & 0x0F) << 8) | (data[1] as u16);
let node_id = NodeId::new(
((data[2] as u32) << 24)
| ((data[3] as u32) << 16)
| ((data[4] as u32) << 8)
| (data[5] as u32),
);
let hierarchy_level = HierarchyLevel::from(data[6]);
let battery_percent = data[7];
let seq_num = ((data[8] as u16) << 8) | (data[9] as u16);
Some(Self {
version,
capabilities,
node_id,
hierarchy_level,
geohash: 0, battery_percent,
seq_num,
})
}
pub fn is_lite_node(&self) -> bool {
self.capabilities & capabilities::LITE_NODE != 0
}
pub fn can_relay(&self) -> bool {
self.capabilities & capabilities::CAN_RELAY != 0
}
pub fn supports_coded_phy(&self) -> bool {
self.capabilities & capabilities::CODED_PHY != 0
}
}
impl Default for PeatBeacon {
fn default() -> Self {
Self::new(NodeId::default())
}
}
#[derive(Debug, Clone)]
pub struct ParsedAdvertisement {
pub address: String,
pub rssi: i8,
pub beacon: Option<PeatBeacon>,
pub encrypted_service_data: Option<Vec<u8>>,
pub local_name: Option<String>,
pub tx_power: Option<i8>,
pub connectable: bool,
}
impl ParsedAdvertisement {
pub fn is_peat_device(&self) -> bool {
self.beacon.is_some() || self.encrypted_service_data.is_some()
}
pub fn node_id(&self) -> Option<&NodeId> {
self.beacon.as_ref().map(|b| &b.node_id)
}
#[cfg(feature = "std")]
pub fn estimated_distance_meters(&self) -> Option<f32> {
let tx_power = self.tx_power.unwrap_or(0) as f32;
let rssi = self.rssi as f32;
let n = 2.5;
if rssi >= tx_power {
return Some(1.0); }
let distance = 10.0_f32.powf((tx_power - rssi) / (10.0 * n));
Some(distance)
}
#[cfg(not(feature = "std"))]
pub fn estimated_distance_meters(&self) -> Option<f32> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_beacon_encode_decode() {
let beacon = PeatBeacon::new(NodeId::new(0x12345678))
.with_capabilities(capabilities::LITE_NODE | capabilities::SENSOR_ACCEL)
.with_hierarchy_level(HierarchyLevel::Squad)
.with_geohash(0x98FF88)
.with_battery(75);
let encoded = beacon.encode();
let decoded = PeatBeacon::decode(&encoded).unwrap();
assert_eq!(decoded.version, beacon.version);
assert_eq!(decoded.capabilities, beacon.capabilities);
assert_eq!(decoded.node_id, beacon.node_id);
assert_eq!(decoded.hierarchy_level, beacon.hierarchy_level);
assert_eq!(decoded.geohash, beacon.geohash & 0x00FFFFFF);
assert_eq!(decoded.battery_percent, beacon.battery_percent);
}
#[test]
fn test_beacon_compact_encode_decode() {
let beacon = PeatBeacon::new(NodeId::new(0xDEADBEEF))
.with_capabilities(capabilities::CAN_RELAY)
.with_battery(50);
let encoded = beacon.encode_compact();
assert_eq!(encoded.len(), BEACON_COMPACT_SIZE);
let decoded = PeatBeacon::decode_compact(&encoded).unwrap();
assert_eq!(decoded.node_id, beacon.node_id);
assert_eq!(decoded.capabilities, beacon.capabilities);
assert_eq!(decoded.battery_percent, beacon.battery_percent);
assert_eq!(decoded.geohash, 0); }
#[test]
fn test_beacon_size() {
let beacon = PeatBeacon::new(NodeId::new(0x12345678));
let encoded = beacon.encode();
assert_eq!(encoded.len(), BEACON_SIZE);
assert_eq!(encoded.len(), 16);
}
#[test]
fn test_beacon_version() {
let beacon = PeatBeacon::new(NodeId::new(0x12345678));
let encoded = beacon.encode();
let version = (encoded[0] >> 4) & 0x0F;
assert_eq!(version, BEACON_VERSION);
}
#[test]
fn test_beacon_capabilities() {
let caps = capabilities::LITE_NODE | capabilities::CODED_PHY | capabilities::HAS_GPS;
let beacon = PeatBeacon::new(NodeId::new(0x12345678)).with_capabilities(caps);
assert!(beacon.is_lite_node());
assert!(beacon.supports_coded_phy());
assert!(!beacon.can_relay());
let encoded = beacon.encode();
let decoded = PeatBeacon::decode(&encoded).unwrap();
assert_eq!(decoded.capabilities, caps);
}
#[test]
fn test_sequence_number_wrap() {
let mut beacon = PeatBeacon::new(NodeId::new(0x12345678));
beacon.seq_num = 0xFFFF;
beacon.increment_seq();
assert_eq!(beacon.seq_num, 0);
}
#[test]
fn test_decode_invalid_length() {
let short_data = [0u8; 5];
assert!(PeatBeacon::decode(&short_data).is_none());
assert!(PeatBeacon::decode_compact(&short_data).is_none());
}
#[test]
fn test_estimated_distance() {
let adv = ParsedAdvertisement {
address: "00:11:22:33:44:55".to_string(),
rssi: -60,
beacon: None,
encrypted_service_data: None,
local_name: None,
tx_power: Some(-20), connectable: true,
};
let distance = adv.estimated_distance_meters().unwrap();
assert!(distance > 1.0 && distance < 100.0);
}
#[test]
fn test_peat_lite_beacon() {
let beacon = PeatBeacon::peat_lite(NodeId::new(0xCAFEBABE));
assert!(beacon.is_lite_node());
assert!(!beacon.can_relay());
}
}