const DOP_STEP_CMS: f32 = 17.28;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Target {
pub x: f32,
pub y: f32,
pub dop_index: i32,
pub cluster_id: i32,
}
impl Target {
pub fn speed_ms(&self) -> f32 {
self.dop_index as f32 * DOP_STEP_CMS / 100.0
}
pub fn dist_m(&self) -> f32 {
libm::sqrtf(self.x * self.x + self.y * self.y)
}
pub fn angle_deg(&self) -> f32 {
libm::atan2f(self.x, self.y) * (180.0 / core::f32::consts::PI)
}
pub fn is_valid(&self) -> bool {
self.x.is_finite() && self.y.is_finite() && (self.x != 0.0 || self.y != 0.0)
}
pub fn from_bytes(data: &[u8; 16]) -> Self {
Self {
x: f32::from_le_bytes([data[0], data[1], data[2], data[3]]),
y: f32::from_le_bytes([data[4], data[5], data[6], data[7]]),
dop_index: i32::from_le_bytes([data[8], data[9], data[10], data[11]]),
cluster_id: i32::from_le_bytes([data[12], data[13], data[14], data[15]]),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct PointCloudFrame {
pub targets: [Target; 3],
pub count: u8,
}
impl PointCloudFrame {
pub fn active_targets(&self) -> impl Iterator<Item = &Target> {
self.targets[..self.count as usize]
.iter()
.filter(|t| t.is_valid())
}
pub fn from_bytes(data: &[u8]) -> Self {
if data.len() < 4 {
return Self::default();
}
let n = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let count = n.min(3) as u8;
let mut targets = [Target::default(); 3];
for (i, target) in targets.iter_mut().enumerate().take(count as usize) {
let start = 4 + i * 16;
if start + 16 > data.len() {
break;
}
let slice: &[u8; 16] = data[start..start + 16].try_into().unwrap();
*target = Target::from_bytes(slice);
}
PointCloudFrame { targets, count }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn target_bytes(x: f32, y: f32, dop: i32, cid: i32) -> [u8; 16] {
let mut b = [0u8; 16];
b[0..4].copy_from_slice(&x.to_le_bytes());
b[4..8].copy_from_slice(&y.to_le_bytes());
b[8..12].copy_from_slice(&dop.to_le_bytes());
b[12..16].copy_from_slice(&cid.to_le_bytes());
b
}
#[test]
fn target_roundtrip() {
let bytes = target_bytes(0.45, 1.10, -3, 1);
let t = Target::from_bytes(&bytes);
assert!((t.x - 0.45).abs() < 1e-6);
assert!((t.y - 1.10).abs() < 1e-6);
assert_eq!(t.dop_index, -3);
assert_eq!(t.cluster_id, 1);
}
#[test]
fn target_speed_ms() {
let bytes = target_bytes(0.0, 1.0, 10, 0);
let t = Target::from_bytes(&bytes);
assert!((t.speed_ms() - 1.728).abs() < 1e-4);
}
#[test]
fn target_dist_angle() {
let bytes = target_bytes(1.0, 1.0, 0, 0);
let t = Target::from_bytes(&bytes);
assert!((t.dist_m() - core::f32::consts::SQRT_2).abs() < 1e-5);
assert!((t.angle_deg() - 45.0).abs() < 1e-4);
}
#[test]
fn target_is_valid() {
let zero = Target::default();
assert!(!zero.is_valid());
let bytes = target_bytes(0.5, 1.2, 0, 0);
assert!(Target::from_bytes(&bytes).is_valid());
}
#[test]
fn point_cloud_single_target() {
let mut data = vec![0u8; 4 + 16];
data[0..4].copy_from_slice(&1u32.to_le_bytes());
let tb = target_bytes(0.5, 1.2, 2, 1);
data[4..20].copy_from_slice(&tb);
let frame = PointCloudFrame::from_bytes(&data);
assert_eq!(frame.count, 1);
assert!(frame.targets[0].is_valid());
assert_eq!(frame.active_targets().count(), 1);
}
#[test]
fn point_cloud_zero_targets() {
let data = 0u32.to_le_bytes();
let frame = PointCloudFrame::from_bytes(&data);
assert_eq!(frame.count, 0);
assert_eq!(frame.active_targets().count(), 0);
}
#[test]
fn point_cloud_caps_at_three() {
let mut data = vec![0u8; 4 + 5 * 16];
data[0..4].copy_from_slice(&5u32.to_le_bytes());
let frame = PointCloudFrame::from_bytes(&data);
assert_eq!(frame.count, 3);
}
}