1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3pub struct Target {
4 pub x: i16,
6 pub y: i16,
8 pub speed: i16,
10 pub distance_resolution: u16,
12}
13
14impl Target {
15 pub fn is_empty(&self) -> bool {
17 self.x == 0 && self.y == 0 && self.speed == 0 && self.distance_resolution == 0
18 }
19
20 pub fn dist_m(&self) -> f32 {
22 let x = self.x as f32;
23 let y = self.y as f32;
24 libm::sqrtf(x * x + y * y) / 1000.0
25 }
26
27 pub fn x_m(&self) -> f32 {
29 self.x as f32 / 1000.0
30 }
31
32 pub fn y_m(&self) -> f32 {
34 self.y as f32 / 1000.0
35 }
36
37 pub fn speed_ms(&self) -> f32 {
39 self.speed as f32 / 100.0
40 }
41
42 pub fn angle_deg(&self) -> f32 {
44 libm::atan2f(self.x as f32, self.y as f32) * (180.0 / core::f32::consts::PI)
45 }
46
47 pub fn from_bytes(data: &[u8; 8]) -> Self {
55 Self {
56 x: decode_coord(u16::from_le_bytes([data[0], data[1]])),
57 y: decode_coord(u16::from_le_bytes([data[2], data[3]])),
58 speed: decode_coord(u16::from_le_bytes([data[4], data[5]])),
59 distance_resolution: u16::from_le_bytes([data[6], data[7]]),
60 }
61 }
62}
63
64fn decode_coord(raw: u16) -> i16 {
68 let magnitude = (raw & 0x7FFF) as i16;
69 if raw & 0x8000 != 0 {
70 magnitude
71 } else {
72 -magnitude
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub struct RadarFrame {
79 pub targets: [Target; 3],
80}
81
82impl RadarFrame {
83 pub fn active_count(&self) -> usize {
85 self.targets.iter().filter(|t| !t.is_empty()).count()
86 }
87
88 pub fn from_bytes(data: &[u8; 24]) -> Self {
90 Self {
91 targets: [
92 Target::from_bytes(data[0..8].try_into().unwrap()),
93 Target::from_bytes(data[8..16].try_into().unwrap()),
94 Target::from_bytes(data[16..24].try_into().unwrap()),
95 ],
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[repr(u16)]
103pub enum TrackingMode {
104 Single = 0x0001,
105 Multi = 0x0002,
106}
107
108impl TrackingMode {
109 pub fn from_u16(val: u16) -> Option<Self> {
110 match val {
111 0x0001 => Some(Self::Single),
112 0x0002 => Some(Self::Multi),
113 _ => None,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120#[repr(u16)]
121pub enum ZoneFilterType {
122 Disabled = 0x0000,
123 DetectOnly = 0x0001,
124 Exclude = 0x0002,
125}
126
127impl ZoneFilterType {
128 pub fn from_u16(val: u16) -> Option<Self> {
129 match val {
130 0x0000 => Some(Self::Disabled),
131 0x0001 => Some(Self::DetectOnly),
132 0x0002 => Some(Self::Exclude),
133 _ => None,
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn decode_coord_positive() {
144 assert_eq!(decode_coord(0x830E), 782);
146 }
147
148 #[test]
149 fn decode_coord_negative() {
150 assert_eq!(decode_coord(0x030E), -782);
152 }
153
154 #[test]
155 fn target_from_datasheet_example() {
156 let data = [0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00, 0x40, 0x01];
162 let t = Target::from_bytes(&data);
163 assert_eq!(t.x, -782);
164 assert_eq!(t.y, 1713);
165 assert_eq!(t.speed, -16);
166 assert_eq!(t.distance_resolution, 320);
167 }
168
169 #[test]
170 fn empty_target() {
171 let t = Target::from_bytes(&[0; 8]);
172 assert!(t.is_empty());
173 }
174
175 #[test]
176 fn si_unit_conversions() {
177 let data = [0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00, 0x40, 0x01];
179 let t = Target::from_bytes(&data);
180 assert!((t.x_m() - (-0.782)).abs() < 1e-5);
181 assert!((t.y_m() - 1.713).abs() < 1e-5);
182 assert!((t.speed_ms() - (-0.16)).abs() < 1e-5);
183 let expected = libm::sqrtf(0.782f32 * 0.782 + 1.713 * 1.713);
185 assert!((t.dist_m() - expected).abs() < 1e-4);
186 }
187
188 #[test]
189 fn radar_frame_active_count() {
190 let mut payload = [0u8; 24];
191 payload[..8].copy_from_slice(&[0x0E, 0x03, 0xB1, 0x86, 0x10, 0x00, 0x40, 0x01]);
193 let frame = RadarFrame::from_bytes(&payload);
194 assert_eq!(frame.active_count(), 1);
195 assert!(!frame.targets[0].is_empty());
196 assert!(frame.targets[1].is_empty());
197 assert!(frame.targets[2].is_empty());
198 }
199}