Skip to main content

ld2450_proto/
types.rs

1/// A single tracked target reported by the radar.
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3pub struct Target {
4    /// X coordinate in mm. Positive = right of sensor, negative = left.
5    pub x: i16,
6    /// Y coordinate in mm. Always positive (in front of sensor).
7    pub y: i16,
8    /// Speed in cm/s. Positive = approaching, negative = receding.
9    pub speed: i16,
10    /// Distance gate resolution in mm.
11    pub distance_resolution: u16,
12}
13
14impl Target {
15    /// Returns true if this target slot is empty (all zeros).
16    pub fn is_empty(&self) -> bool {
17        self.x == 0 && self.y == 0 && self.speed == 0 && self.distance_resolution == 0
18    }
19
20    /// Distance from sensor in metres.
21    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    /// X coordinate in metres.
28    pub fn x_m(&self) -> f32 {
29        self.x as f32 / 1000.0
30    }
31
32    /// Y coordinate in metres.
33    pub fn y_m(&self) -> f32 {
34        self.y as f32 / 1000.0
35    }
36
37    /// Radial speed in m/s. Positive = approaching, negative = receding.
38    pub fn speed_ms(&self) -> f32 {
39        self.speed as f32 / 100.0
40    }
41
42    /// Angle in degrees from sensor boresight (-90 to +90).
43    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    /// Parse a target from 8 bytes of in-frame data.
48    ///
49    /// Byte layout (little-endian):
50    /// [0..2] x coordinate (signed, bit15: 1=positive, 0=negative)
51    /// [2..4] y coordinate (signed, bit15: 1=positive, 0=negative)
52    /// [4..6] speed (signed, bit15: 1=positive/approaching, 0=negative/receding)
53    /// [6..8] distance resolution (unsigned)
54    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
64/// Decode the LD2450's signed format:
65/// Bit 15 = 1 → positive value (bits 0-14)
66/// Bit 15 = 0 → negative value (negate bits 0-14)
67fn 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/// A complete radar data frame containing up to 3 targets.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub struct RadarFrame {
79    pub targets: [Target; 3],
80}
81
82impl RadarFrame {
83    /// Number of active (non-empty) targets.
84    pub fn active_count(&self) -> usize {
85        self.targets.iter().filter(|t| !t.is_empty()).count()
86    }
87
88    /// Parse from 24 bytes of payload (3 × 8 bytes per target).
89    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/// Tracking mode of the sensor.
101#[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/// Zone filtering type.
119#[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        // 0x8000 | 782 = 0x830E → bit15 set → positive 782
145        assert_eq!(decode_coord(0x830E), 782);
146    }
147
148    #[test]
149    fn decode_coord_negative() {
150        // 782 without bit15 → negative
151        assert_eq!(decode_coord(0x030E), -782);
152    }
153
154    #[test]
155    fn target_from_datasheet_example() {
156        // From protocol doc: 0E 03 B1 86 10 00 40 01
157        // x: 0x030E = 782, bit15=0 → -782mm
158        // y: 0x86B1 = 34481, bit15=1 → 34481-32768 = 1713mm
159        // speed: 0x0010 = 16, bit15=0 → -16 cm/s
160        // resolution: 0x0140 = 320mm
161        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        // x=-782mm, y=1713mm, speed=-16cm/s (from datasheet example)
178        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        // dist_m = sqrt(0.782² + 1.713²) ≈ 1.883 m
184        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        // Only target 1 has data
192        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}