dshot_codec/
dshot_command_frame.rs1use core::ops::Deref;
2
3use super::DshotCommand;
4
5#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, PartialOrd, Ord)]
21pub struct DshotCommandFrame(u16);
22
23impl TryFrom<u16> for DshotCommandFrame {
24 type Error = u16;
25
26 #[inline]
27 fn try_from(value: u16) -> Result<Self, u16> {
28 if value <= Self::MAX_RAW_VALUE {
29 Ok(DshotCommandFrame::encode_raw(value, DshotCommandFrame::NO_TELEMETRY))
30 } else {
31 Err(value)
32 }
33 }
34}
35
36impl From<DshotCommandFrame> for u16 {
37 #[inline]
38 fn from(frame: DshotCommandFrame) -> Self {
39 frame.raw()
40 }
41}
42
43impl Deref for DshotCommandFrame {
44 type Target = u16;
45
46 #[inline]
47 fn deref(&self) -> &Self::Target {
48 &self.0
49 }
50}
51impl DshotCommandFrame {
52 pub const NO_TELEMETRY: bool = false;
53 pub const WITH_TELEMETRY: bool = true;
54
55 pub const UNI_DIRECTIONAL: bool = false;
56 pub const BI_DIRECTIONAL: bool = true;
57
58 pub const MAX_RAW_VALUE: u16 = 2047;
60 pub const THROTTLE_OFFSET: u16 = 48;
61 pub const THROTTLE_MIN: u16 = 48;
62 pub const THROTTLE_MAX: u16 = 2047;
63
64 const TELEMETRY_BIT: u16 = 0x10;
65 const CHECKSUM_BITS: u16 = 0x0F;
66
67 pub(crate) const NIBBLE_TO_QUINTET: [u8; 16] =
69 [0x19, 0x1B, 0x12, 0x13, 0x1D, 0x15, 0x16, 0x17, 0x1A, 0x09, 0x0A, 0x0B, 0x1E, 0x0D, 0x0E, 0x0F];
70
71 #[inline]
72 #[must_use]
73 pub const fn new(value: u16) -> Self {
74 Self::encode_raw(value, Self::NO_TELEMETRY)
75 }
76
77 #[inline]
78 #[must_use]
79 pub const fn from_raw(value: u16) -> Self {
80 Self(value)
81 }
82
83 #[inline]
84 #[must_use]
85 pub const fn from_command(command: DshotCommand) -> Self {
86 Self::encode_raw(command as u16, Self::WITH_TELEMETRY)
88 }
89
90 #[inline]
91 #[must_use]
92 pub const fn raw(self) -> u16 {
93 self.0
94 }
95
96 #[inline]
98 #[must_use]
99 pub const fn value(self) -> u16 {
100 self.0 >> 5
101 }
102
103 #[inline]
104 #[must_use]
105 pub const fn is_telemetry_enabled(self) -> bool {
106 (self.0 & Self::TELEMETRY_BIT) != 0
107 }
108
109 #[inline]
110 #[must_use]
111 pub const fn checksum(self) -> u16 {
112 self.0 & Self::CHECKSUM_BITS
113 }
114
115 #[inline]
117 #[must_use]
118 pub const fn calculate_checksum(frame_raw: u16) -> u16 {
119 (frame_raw ^ (frame_raw >> 4) ^ (frame_raw >> 8)) & 0x0F
120 }
121
122 #[must_use]
124 pub const fn encode_raw(value: u16, with_telemetry: bool) -> Self {
125 let value = if value > Self::MAX_RAW_VALUE { Self::MAX_RAW_VALUE } else { value };
127
128 let frame_raw = if with_telemetry { (value << 1) | 0x01 } else { value << 1 };
130
131 let mut checksum = Self::calculate_checksum(frame_raw);
133
134 if with_telemetry {
136 checksum = (!checksum) & 0x0F;
137 }
138 Self((frame_raw << 4) | checksum)
139 }
140
141 #[must_use]
144 pub fn from_throttle_unidirectional(throttle: f32) -> Self {
145 #[allow(unused)]
146 use num_traits::float::FloatCore;
147
148 let throttle = throttle.clamp(0.0, 1.0);
150
151 let range = f32::from(Self::THROTTLE_MAX - Self::THROTTLE_MIN);
153 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
154 let dshot_value = (throttle * range).round() as u16 + Self::THROTTLE_MIN;
155
156 Self::encode_raw(dshot_value, Self::NO_TELEMETRY)
158 }
159
160 #[must_use]
163 pub fn from_throttle_bidirectional(throttle: f32) -> Self {
164 #[allow(unused)]
165 use num_traits::float::FloatCore;
166
167 let throttle = throttle.clamp(0.0, 1.0);
169
170 let range = f32::from(Self::THROTTLE_MAX - Self::THROTTLE_MIN);
172 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
173 let dshot_value = (throttle * range).round() as u16 + Self::THROTTLE_MIN;
174
175 Self::encode_raw(dshot_value, Self::WITH_TELEMETRY)
177 }
178
179 #[inline]
180 #[must_use]
181 pub fn from_throttle(throttle: f32, bidirectional: bool) -> Self {
182 if bidirectional {
183 Self::from_throttle_bidirectional(throttle)
184 } else {
185 Self::from_throttle_unidirectional(throttle)
186 }
187 }
188}
189
190impl DshotCommandFrame {
191 #[inline]
194 #[must_use]
195 pub fn to_gcr20(self) -> u32 {
196 let value = self.0;
197 let mut ret = u32::from(Self::NIBBLE_TO_QUINTET[(value & 0x0F) as usize]);
198 ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 4) & 0x0F) as usize]) << 5;
199 ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 8) & 0x0F) as usize]) << 10;
200 ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 12) & 0x0F) as usize]) << 15;
201 ret
202 }
203
204 #[must_use]
208 pub fn gcr20_to_nrzi21(input: u32) -> u32 {
209 let mut ret = 0;
210 let mut prev_gcr_bit = 0;
211 let mut mask = 1 << 19;
212
213 while mask != 0 {
214 ret <<= 1;
215 let input_bit = u32::from((input & mask) != 0);
216 let gcr_bit = input_bit ^ prev_gcr_bit;
217 prev_gcr_bit = gcr_bit;
218 ret |= gcr_bit;
219 mask >>= 1;
220 }
221 ret
222 }
223
224 #[must_use]
225 pub fn gcr_encode(self) -> u32 {
226 let gcr20 = self.to_gcr20();
227 Self::gcr20_to_nrzi21(gcr20)
228 }
229}
230
231#[cfg(test)]
232mod test_traits {
233 use super::*;
234
235 fn is_full<T: Sized + Send + Sync + Unpin + Copy + Clone + Default + PartialEq>() {}
236
237 #[test]
238 fn normal_types() {
239 is_full::<DshotCommandFrame>();
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn checksum() {
249 assert_eq!(DshotCommandFrame::calculate_checksum(0b_1000_0010_1100), 0b_0000_0000_0110,);
250 }
251 #[test]
252 fn throttle_unidirectional() {
253 let frame = DshotCommandFrame::from_throttle_unidirectional(0.0);
254 assert_eq!(48, frame.value());
255 assert!(!frame.is_telemetry_enabled());
256
257 let frame = DshotCommandFrame::from_throttle_unidirectional(0.25);
258 assert_eq!(548, frame.value());
259 assert!(!frame.is_telemetry_enabled());
260
261 let frame = DshotCommandFrame::from_throttle_unidirectional(0.50);
262 assert_eq!(1048, frame.value());
263 assert!(!frame.is_telemetry_enabled());
264
265 let frame = DshotCommandFrame::from_throttle_unidirectional(0.75);
266 assert_eq!(1547, frame.value());
267 assert!(!frame.is_telemetry_enabled());
268
269 let frame = DshotCommandFrame::from_throttle_unidirectional(1.00);
270 assert_eq!(2047, frame.value());
271 assert!(!frame.is_telemetry_enabled());
272 }
273 #[test]
274 fn throttle_bidirectional() {
275 let frame = DshotCommandFrame::from_throttle_bidirectional(0.0);
276 assert_eq!(48, frame.value());
277 assert!(frame.is_telemetry_enabled());
278
279 let frame = DshotCommandFrame::from_throttle_bidirectional(0.25);
280 assert_eq!(548, frame.value());
281 assert!(frame.is_telemetry_enabled());
282
283 let frame = DshotCommandFrame::from_throttle_bidirectional(0.50);
284 assert_eq!(1048, frame.value());
285 assert!(frame.is_telemetry_enabled());
286
287 let frame = DshotCommandFrame::from_throttle_bidirectional(0.75);
288 assert_eq!(1547, frame.value());
289 assert!(frame.is_telemetry_enabled());
290
291 let frame = DshotCommandFrame::from_throttle_bidirectional(1.00);
292 assert_eq!(2047, frame.value());
293 assert!(frame.is_telemetry_enabled());
294 }
295 #[test]
296 fn throttle() {
297 let frame = DshotCommandFrame::from_throttle(0.25, DshotCommandFrame::UNI_DIRECTIONAL);
298 assert_eq!(548, frame.value());
299 assert!(!frame.is_telemetry_enabled());
300
301 let frame = DshotCommandFrame::from_throttle(0.75, DshotCommandFrame::UNI_DIRECTIONAL);
302 assert_eq!(1547, frame.value());
303 assert!(!frame.is_telemetry_enabled());
304
305 let frame = DshotCommandFrame::from_throttle(0.25, DshotCommandFrame::BI_DIRECTIONAL);
306 assert_eq!(548, frame.value());
307 assert!(frame.is_telemetry_enabled());
308
309 let frame = DshotCommandFrame::from_throttle(0.75, DshotCommandFrame::BI_DIRECTIONAL);
310 assert_eq!(1547, frame.value());
311 assert!(frame.is_telemetry_enabled());
312 }
313 #[rustfmt::skip]
314 #[test]
315 fn commands() {
316 let frame = DshotCommandFrame::from_command(DshotCommand::Beep1);
317 assert_eq!(0b_0000_0000_0011_1100, frame.raw());
318
319 let frame = DshotCommandFrame::from_command(DshotCommand::SignalLineErpmTelemetry);
320 assert_eq!(0b_0000_0101_1101_0111, frame.raw());
321
322 let frame = DshotCommandFrame::from_command(DshotCommand::SignalLineErpmPeriodTelemetry);
323 assert_eq!(0b_0000_0101_1111_0101, frame.raw());
324 }
325}
326#[cfg(test)]
327mod command_frame_tests {
328 #![allow(clippy::unwrap_used)]
329 use super::*;
330
331 #[test]
332 fn test_encode_raw_no_telemetry() {
333 let frame = DshotCommandFrame::encode_raw(1000, DshotCommandFrame::NO_TELEMETRY);
340
341 assert_eq!(frame.raw(), 0x7D0A);
342 assert_eq!(frame.value(), 1000);
343 assert!(!frame.is_telemetry_enabled());
344 assert_eq!(frame.checksum(), 0x0A);
345 }
346
347 #[test]
348 fn test_encode_raw_with_telemetry() {
349 let frame = DshotCommandFrame::encode_raw(1000, DshotCommandFrame::WITH_TELEMETRY);
358
359 assert_eq!(frame.raw(), 0x7D14);
360 assert_eq!(frame.value(), 1000);
361 assert!(frame.is_telemetry_enabled());
362 assert_eq!(frame.checksum(), 0x04);
363 }
364
365 #[test]
366 fn test_try_from_valid_and_invalid() {
367 let valid_res = DshotCommandFrame::try_from(0);
368 assert!(valid_res.is_ok());
369 assert_eq!(valid_res.unwrap().value(), 0);
370
371 let valid_res = DshotCommandFrame::try_from(1);
372 assert!(valid_res.is_ok());
373 assert_eq!(valid_res.unwrap().value(), 1);
374
375 let valid_res = DshotCommandFrame::try_from(2047);
377 assert!(valid_res.is_ok());
378 assert_eq!(valid_res.unwrap().value(), 2047);
379
380 let invalid_res = DshotCommandFrame::try_from(2048);
382 assert!(invalid_res.is_err());
383 assert_eq!(invalid_res.unwrap_err(), 2048);
384 }
385
386 #[test]
387 fn from_command() {
388 let frame = DshotCommandFrame::from_command(DshotCommand::MotorStop);
390 assert_eq!(frame.value(), 0);
391 assert!(frame.is_telemetry_enabled());
392
393 let frame_telemetry = DshotCommandFrame::from_command(DshotCommand::Beep1);
395 assert_eq!(frame_telemetry.value(), 1);
396 assert!(frame_telemetry.is_telemetry_enabled());
397 }
398
399 #[test]
400 fn from_throttle_scaling() {
401 let frame_min = DshotCommandFrame::from_throttle_unidirectional(0.0);
403 assert_eq!(frame_min.value(), 48);
404 let frame_min = DshotCommandFrame::from_throttle_bidirectional(0.0);
405 assert_eq!(frame_min.value(), 48);
406
407 let frame_max = DshotCommandFrame::from_throttle_unidirectional(1.0);
409 assert_eq!(frame_max.value(), 2047);
410 let frame_max = DshotCommandFrame::from_throttle_bidirectional(1.0);
411 assert_eq!(frame_max.value(), 2047);
412
413 let frame_midpoint = DshotCommandFrame::from_throttle_unidirectional(0.5);
415 assert_eq!(frame_midpoint.value(), 1048);
416 let frame_midpoint = DshotCommandFrame::from_throttle_bidirectional(0.5);
417 assert_eq!(frame_midpoint.value(), 1048);
418 }
419
420 #[test]
421 fn from_throttle_clamping() {
422 let frame_neg = DshotCommandFrame::from_throttle_unidirectional(-0.25);
424 assert_eq!(frame_neg.value(), 48);
425
426 let frame_over = DshotCommandFrame::from_throttle_unidirectional(1.5);
428 assert_eq!(frame_over.value(), 2047);
429 }
430
431 #[test]
432 fn input_value_clamping_protection() {
433 let frame = DshotCommandFrame::encode_raw(9999, DshotCommandFrame::NO_TELEMETRY);
436 assert_eq!(frame.value(), 2047);
437 }
438}