dshot_codec/
dshot_telemetry_frame.rs1use core::ops::Deref;
2
3use super::{DshotError, Telemetry};
4
5#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
54pub struct DshotTelemetryFrame(u16);
55
56impl Default for DshotTelemetryFrame {
57 fn default() -> Self {
58 Self::from_raw_12(0)
59 }
60}
61
62impl TryFrom<u16> for DshotTelemetryFrame {
63 type Error = DshotError;
64
65 #[inline]
66 fn try_from(raw_16: u16) -> Result<Self, DshotError> {
67 Self::try_from_raw_16(raw_16)
68 }
69}
70
71impl From<DshotTelemetryFrame> for u16 {
72 #[inline]
73 fn from(frame: DshotTelemetryFrame) -> Self {
74 frame.raw_16()
75 }
76}
77
78impl Deref for DshotTelemetryFrame {
79 type Target = u16;
80
81 #[inline]
82 fn deref(&self) -> &Self::Target {
83 &self.0
84 }
85}
86
87impl DshotTelemetryFrame {
88 const CHECKSUM_BITS: u16 = 0x000F;
90 const MANTISSA_BITS: u16 = 0x1FF0; const EXPONENT_BITS: u16 = 0xE000; const ONE_MINUTE_IN_MICROSECONDS: u32 = 60_000_000;
93 const ONE_MINUTE_IN_MICROSECONDS_F32: f32 = 60_000_000.0;
94
95 #[inline]
96 #[must_use]
97 pub const fn from_raw_12(raw_12: u16) -> Self {
98 Self((raw_12 << 4) | Self::calculate_checksum(raw_12))
99 }
100
101 pub fn try_from_raw_16(raw_16: u16) -> Result<Self, DshotError> {
103 let ret = Self(raw_16);
104 if ret.checksum_is_ok() {
105 Ok(ret)
106 } else {
107 Err(DshotError::InvalidChecksum)
108 }
109 }
110
111 #[inline]
112 #[must_use]
113 pub const fn raw_16(self) -> u16 {
114 self.0
115 }
116
117 #[must_use]
118 pub const fn calculate_checksum(raw_12: u16) -> u16 {
119 (!(raw_12 ^ (raw_12 >> 4) ^ (raw_12 >> 8))) & 0x0F
120 }
121
122 #[inline]
123 #[must_use]
124 pub const fn checksum(self) -> u16 {
125 self.0 & Self::CHECKSUM_BITS
126 }
127
128 #[inline]
130 #[must_use]
131 pub const fn checksum_is_ok(self) -> bool {
132 let checksum = (self.0 ^ (self.0 >> 4) ^ (self.0 >> 8) ^ (self.0 >> 12)) & 0x0F;
133 checksum == 0x0F
134 }
135
136 #[inline]
137 #[must_use]
138 pub const fn from_exponent_mantissa(exponent: u16, mantissa: u16) -> Self {
139 let raw_12 = ((exponent & 0x07) << 9) | (mantissa & 0x01FF);
141 Self::from_raw_12(raw_12)
142 }
143
144 #[inline]
145 #[must_use]
146 pub fn from_type_value(data_type: u8, value: u8) -> Self {
147 let raw_12 = (u16::from(data_type & 0x07) << 9) | u16::from(value);
148 Self::from_raw_12(raw_12)
149 }
150
151 #[inline]
152 #[must_use]
153 pub const fn mantissa(self) -> u16 {
154 (self.0 & Self::MANTISSA_BITS) >> 4
155 }
156
157 #[inline]
158 #[must_use]
159 pub const fn exponent(self) -> u16 {
160 (self.0 & Self::EXPONENT_BITS) >> 13
161 }
162
163 #[inline]
164 #[must_use]
165 fn period_us(self) -> u32 {
166 u32::from(self.mantissa()) << self.exponent()
167 }
168
169 #[inline]
170 #[must_use]
171 pub fn erpm(self) -> u32 {
172 let raw_12 = (self.0 >> 4) & 0x0FFF;
173 if raw_12 == 0 || raw_12 == 0x0FFF {
175 return 0;
176 }
177
178 let period = self.period_us();
179 if period == 0 {
180 return 0;
181 }
182 Self::ONE_MINUTE_IN_MICROSECONDS / period
183 }
184
185 #[inline]
186 #[must_use]
187 pub fn erpm_f32(self) -> f32 {
188 let raw_12 = (self.0 >> 4) & 0x0FFF;
189 if raw_12 == 0 || raw_12 == 0x0FFF {
191 return 0.0;
192 }
193
194 let period = self.period_us();
195 if period == 0 {
196 return 0.0;
197 }
198 #[allow(clippy::cast_precision_loss)]
199 {
200 Self::ONE_MINUTE_IN_MICROSECONDS_F32 / (period as f32)
201 }
202 }
203
204 #[inline]
206 #[must_use]
207 pub const fn is_erpm_frame(self) -> bool {
208 let raw_12 = (self.0 >> 4) & 0x0FFF;
209 let prefix = (raw_12 >> 8) & 0x0F;
210 (prefix == 0) || ((prefix & 0x01) != 0)
211 }
212
213 #[inline]
215 pub fn try_decode_erpm(self) -> Result<u32, DshotError> {
216 if self.is_erpm_frame() {
217 Ok(self.erpm())
218 } else {
219 Err(DshotError::InvalidErpm)
220 }
221 }
222
223 #[inline]
225 pub fn try_decode_telemetry(self) -> Result<Telemetry, DshotError> {
226 if self.is_erpm_frame() {
227 return Ok(Telemetry::Erpm(self.erpm()));
228 }
229
230 let raw_12 = (self.0 >> 4) & 0x0FFF;
232 let prefix = (raw_12 >> 8) & 0x0F;
233
234 let data_type = prefix >> 1;
235 let data = (raw_12 & 0xFF) as u8;
236
237 match data_type {
238 1 => Ok(Telemetry::Temperature(data)),
239 2 => Ok(Telemetry::Voltage(u32::from(data) * 250)),
240 3 => Ok(Telemetry::Current(u32::from(data) * 1000)),
241 4 => Ok(Telemetry::Debug1(data)),
242 5 => Ok(Telemetry::Debug2(data)),
243 6 => Ok(Telemetry::Debug3(data)),
244 7 => Ok(Telemetry::StateEvent(data)),
245 _ => Err(DshotError::InvalidTelemetry),
246 }
247 }
248}
249
250#[cfg(test)]
251mod test_traits {
252 use super::*;
253
254 fn is_full<T: Sized + Send + Sync + Unpin + Copy + Clone + Default + PartialEq>() {}
255
256 #[test]
257 fn normal_types() {
258 is_full::<DshotTelemetryFrame>();
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 #![allow(clippy::unwrap_used)]
265 use super::*;
266
267 #[test]
268 fn test_default_constructor() {
269 let frame = DshotTelemetryFrame::default();
270 assert_eq!(frame.raw_16(), 0x000F); assert!(frame.checksum_is_ok());
272 assert_eq!(frame.erpm(), 0);
273 }
274
275 #[test]
276 fn test_from_exponent_mantissa_packing() {
277 let frame = DshotTelemetryFrame::from_exponent_mantissa(2, 0x5A);
283
284 assert_eq!(frame.raw_16(), 0x45A4);
285 assert!(frame.checksum_is_ok());
286 assert_eq!(frame.exponent(), 2);
287 assert_eq!(frame.mantissa(), 0x5A);
288 }
289
290 #[test]
291 fn test_erpm_calculation_active_motor() {
292 let frame = DshotTelemetryFrame::from_exponent_mantissa(1, 300);
298
299 assert_eq!(frame.raw_16(), 0x32C2);
300 assert!(frame.checksum_is_ok());
301 assert!(frame.is_erpm_frame(), "Expected 0x32C to resolve as a valid eRPM prefix");
302
303 assert_eq!(frame.erpm(), 100_000);
306
307 let decode_res = frame.try_decode_erpm();
308 assert_eq!(decode_res.unwrap(), 100_000);
309 }
310
311 #[test]
312 fn test_erpm_edge_cases_zero_and_max() {
313 let zero_frame = DshotTelemetryFrame::from_raw_12(0);
315 assert_eq!(zero_frame.erpm(), 0);
316 assert_eq!(zero_frame.try_decode_erpm().unwrap(), 0);
317
318 let max_frame = DshotTelemetryFrame::from_raw_12(0x0FFF);
320 assert_eq!(max_frame.erpm(), 0);
321 assert_eq!(max_frame.try_decode_erpm().unwrap(), 0);
322 }
323
324 #[test]
325 fn test_edt_temperature_decoding() {
326 let frame = DshotTelemetryFrame::from_type_value(1, 85);
330
331 assert!(!frame.is_erpm_frame());
332 assert!(frame.try_decode_erpm().is_err());
333
334 let telemetry = frame.try_decode_telemetry().unwrap();
335 assert_eq!(telemetry, Telemetry::Temperature(85));
336 }
337
338 #[test]
339 fn temperature() {
340 let frame = DshotTelemetryFrame::from_type_value(1, 25);
341 assert_eq!(frame.try_decode_telemetry(), Ok(Telemetry::Temperature(25)));
342 let frame = DshotTelemetryFrame::from_type_value(1, 100);
343 assert_eq!(frame.try_decode_telemetry(), Ok(Telemetry::Temperature(100)));
344 let frame = DshotTelemetryFrame::from_type_value(1, 255);
345 assert_eq!(frame.try_decode_telemetry(), Ok(Telemetry::Temperature(255)));
346 }
347 #[test]
348 fn test_edt_voltage_decoding() {
349 let frame = DshotTelemetryFrame::from_type_value(2, 64);
353
354 let telemetry = frame.try_decode_telemetry().unwrap();
355 assert_eq!(telemetry, Telemetry::Voltage(16_000));
356 }
357
358 #[test]
359 fn test_edt_current_decoding() {
360 let frame = DshotTelemetryFrame::from_type_value(3, 25);
364
365 let telemetry = frame.try_decode_telemetry().unwrap();
366 assert_eq!(telemetry, Telemetry::Current(25_000));
367 }
368
369 #[test]
370 fn test_edt_debug_and_state_events() {
371 let d1_frame = DshotTelemetryFrame::from_type_value(4, 42);
373 assert_eq!(d1_frame.try_decode_telemetry().unwrap(), Telemetry::Debug1(42));
374
375 let se_frame = DshotTelemetryFrame::from_type_value(7, 3);
377 assert_eq!(se_frame.try_decode_telemetry().unwrap(), Telemetry::StateEvent(3));
378 }
379
380 #[test]
381 fn test_invalid_checksum_rejection() {
382 let corrupted_raw = 0x45A0;
384 let frame_res = DshotTelemetryFrame::try_from_raw_16(corrupted_raw);
385
386 assert!(frame_res.is_err(), "Expected constructor to reject invalid checksum structures");
387 }
388}