use core::ops::Deref;
use super::DshotCommand;
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, PartialOrd, Ord)]
pub struct DshotCommandFrame(u16);
impl TryFrom<u16> for DshotCommandFrame {
type Error = u16;
#[inline]
fn try_from(value: u16) -> Result<Self, u16> {
if value <= Self::MAX_RAW_VALUE {
Ok(DshotCommandFrame::encode_raw(value, DshotCommandFrame::NO_TELEMETRY))
} else {
Err(value)
}
}
}
impl From<DshotCommandFrame> for u16 {
#[inline]
fn from(frame: DshotCommandFrame) -> Self {
frame.raw()
}
}
impl Deref for DshotCommandFrame {
type Target = u16;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DshotCommandFrame {
pub const NO_TELEMETRY: bool = false;
pub const WITH_TELEMETRY: bool = true;
pub const UNI_DIRECTIONAL: bool = false;
pub const BI_DIRECTIONAL: bool = true;
pub const MAX_RAW_VALUE: u16 = 2047;
pub const THROTTLE_OFFSET: u16 = 48;
pub const THROTTLE_MIN: u16 = 48;
pub const THROTTLE_MAX: u16 = 2047;
const TELEMETRY_BIT: u16 = 0x10;
const CHECKSUM_BITS: u16 = 0x0F;
pub(crate) const NIBBLE_TO_QUINTET: [u8; 16] =
[0x19, 0x1B, 0x12, 0x13, 0x1D, 0x15, 0x16, 0x17, 0x1A, 0x09, 0x0A, 0x0B, 0x1E, 0x0D, 0x0E, 0x0F];
#[inline]
#[must_use]
pub const fn new(value: u16) -> Self {
Self::encode_raw(value, Self::NO_TELEMETRY)
}
#[inline]
#[must_use]
pub const fn from_raw(value: u16) -> Self {
Self(value)
}
#[inline]
#[must_use]
pub const fn from_command(command: DshotCommand) -> Self {
Self::encode_raw(command as u16, Self::WITH_TELEMETRY)
}
#[inline]
#[must_use]
pub const fn raw(self) -> u16 {
self.0
}
#[inline]
#[must_use]
pub const fn value(self) -> u16 {
self.0 >> 5
}
#[inline]
#[must_use]
pub const fn is_telemetry_enabled(self) -> bool {
(self.0 & Self::TELEMETRY_BIT) != 0
}
#[inline]
#[must_use]
pub const fn checksum(self) -> u16 {
self.0 & Self::CHECKSUM_BITS
}
#[inline]
#[must_use]
pub const fn calculate_checksum(frame_raw: u16) -> u16 {
(frame_raw ^ (frame_raw >> 4) ^ (frame_raw >> 8)) & 0x0F
}
#[must_use]
pub const fn encode_raw(value: u16, with_telemetry: bool) -> Self {
let value = if value > Self::MAX_RAW_VALUE { Self::MAX_RAW_VALUE } else { value };
let frame_raw = if with_telemetry { (value << 1) | 0x01 } else { value << 1 };
let mut checksum = Self::calculate_checksum(frame_raw);
if with_telemetry {
checksum = (!checksum) & 0x0F;
}
Self((frame_raw << 4) | checksum)
}
#[must_use]
pub fn from_throttle_unidirectional(throttle: f32) -> Self {
#[allow(unused)]
use num_traits::float::FloatCore;
let throttle = throttle.clamp(0.0, 1.0);
let range = f32::from(Self::THROTTLE_MAX - Self::THROTTLE_MIN);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let dshot_value = (throttle * range).round() as u16 + Self::THROTTLE_MIN;
Self::encode_raw(dshot_value, Self::NO_TELEMETRY)
}
#[must_use]
pub fn from_throttle_bidirectional(throttle: f32) -> Self {
#[allow(unused)]
use num_traits::float::FloatCore;
let throttle = throttle.clamp(0.0, 1.0);
let range = f32::from(Self::THROTTLE_MAX - Self::THROTTLE_MIN);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let dshot_value = (throttle * range).round() as u16 + Self::THROTTLE_MIN;
Self::encode_raw(dshot_value, Self::WITH_TELEMETRY)
}
#[inline]
#[must_use]
pub fn from_throttle(throttle: f32, bidirectional: bool) -> Self {
if bidirectional {
Self::from_throttle_bidirectional(throttle)
} else {
Self::from_throttle_unidirectional(throttle)
}
}
}
impl DshotCommandFrame {
#[inline]
#[must_use]
pub fn to_gcr20(self) -> u32 {
let value = self.0;
let mut ret = u32::from(Self::NIBBLE_TO_QUINTET[(value & 0x0F) as usize]);
ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 4) & 0x0F) as usize]) << 5;
ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 8) & 0x0F) as usize]) << 10;
ret |= u32::from(Self::NIBBLE_TO_QUINTET[((value >> 12) & 0x0F) as usize]) << 15;
ret
}
#[must_use]
pub fn gcr20_to_nrzi21(input: u32) -> u32 {
let mut ret = 0;
let mut prev_gcr_bit = 0;
let mut mask = 1 << 19;
while mask != 0 {
ret <<= 1;
let input_bit = u32::from((input & mask) != 0);
let gcr_bit = input_bit ^ prev_gcr_bit;
prev_gcr_bit = gcr_bit;
ret |= gcr_bit;
mask >>= 1;
}
ret
}
#[must_use]
pub fn gcr_encode(self) -> u32 {
let gcr20 = self.to_gcr20();
Self::gcr20_to_nrzi21(gcr20)
}
}
#[cfg(test)]
mod test_traits {
use super::*;
fn is_full<T: Sized + Send + Sync + Unpin + Copy + Clone + Default + PartialEq>() {}
#[test]
fn normal_types() {
is_full::<DshotCommandFrame>();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checksum() {
assert_eq!(DshotCommandFrame::calculate_checksum(0b_1000_0010_1100), 0b_0000_0000_0110,);
}
#[test]
fn throttle_unidirectional() {
let frame = DshotCommandFrame::from_throttle_unidirectional(0.0);
assert_eq!(48, frame.value());
assert!(!frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle_unidirectional(0.25);
assert_eq!(548, frame.value());
assert!(!frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle_unidirectional(0.50);
assert_eq!(1048, frame.value());
assert!(!frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle_unidirectional(0.75);
assert_eq!(1547, frame.value());
assert!(!frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle_unidirectional(1.00);
assert_eq!(2047, frame.value());
assert!(!frame.is_telemetry_enabled());
}
#[test]
fn throttle_bidirectional() {
let frame = DshotCommandFrame::from_throttle_bidirectional(0.0);
assert_eq!(48, frame.value());
assert!(frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle_bidirectional(0.25);
assert_eq!(548, frame.value());
assert!(frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle_bidirectional(0.50);
assert_eq!(1048, frame.value());
assert!(frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle_bidirectional(0.75);
assert_eq!(1547, frame.value());
assert!(frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle_bidirectional(1.00);
assert_eq!(2047, frame.value());
assert!(frame.is_telemetry_enabled());
}
#[test]
fn throttle() {
let frame = DshotCommandFrame::from_throttle(0.25, DshotCommandFrame::UNI_DIRECTIONAL);
assert_eq!(548, frame.value());
assert!(!frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle(0.75, DshotCommandFrame::UNI_DIRECTIONAL);
assert_eq!(1547, frame.value());
assert!(!frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle(0.25, DshotCommandFrame::BI_DIRECTIONAL);
assert_eq!(548, frame.value());
assert!(frame.is_telemetry_enabled());
let frame = DshotCommandFrame::from_throttle(0.75, DshotCommandFrame::BI_DIRECTIONAL);
assert_eq!(1547, frame.value());
assert!(frame.is_telemetry_enabled());
}
#[rustfmt::skip]
#[test]
fn commands() {
let frame = DshotCommandFrame::from_command(DshotCommand::Beep1);
assert_eq!(0b_0000_0000_0011_1100, frame.raw());
let frame = DshotCommandFrame::from_command(DshotCommand::SignalLineErpmTelemetry);
assert_eq!(0b_0000_0101_1101_0111, frame.raw());
let frame = DshotCommandFrame::from_command(DshotCommand::SignalLineErpmPeriodTelemetry);
assert_eq!(0b_0000_0101_1111_0101, frame.raw());
}
}
#[cfg(test)]
mod command_frame_tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_encode_raw_no_telemetry() {
let frame = DshotCommandFrame::encode_raw(1000, DshotCommandFrame::NO_TELEMETRY);
assert_eq!(frame.raw(), 0x7D0A);
assert_eq!(frame.value(), 1000);
assert!(!frame.is_telemetry_enabled());
assert_eq!(frame.checksum(), 0x0A);
}
#[test]
fn test_encode_raw_with_telemetry() {
let frame = DshotCommandFrame::encode_raw(1000, DshotCommandFrame::WITH_TELEMETRY);
assert_eq!(frame.raw(), 0x7D14);
assert_eq!(frame.value(), 1000);
assert!(frame.is_telemetry_enabled());
assert_eq!(frame.checksum(), 0x04);
}
#[test]
fn test_try_from_valid_and_invalid() {
let valid_res = DshotCommandFrame::try_from(0);
assert!(valid_res.is_ok());
assert_eq!(valid_res.unwrap().value(), 0);
let valid_res = DshotCommandFrame::try_from(1);
assert!(valid_res.is_ok());
assert_eq!(valid_res.unwrap().value(), 1);
let valid_res = DshotCommandFrame::try_from(2047);
assert!(valid_res.is_ok());
assert_eq!(valid_res.unwrap().value(), 2047);
let invalid_res = DshotCommandFrame::try_from(2048);
assert!(invalid_res.is_err());
assert_eq!(invalid_res.unwrap_err(), 2048);
}
#[test]
fn from_command() {
let frame = DshotCommandFrame::from_command(DshotCommand::MotorStop);
assert_eq!(frame.value(), 0);
assert!(frame.is_telemetry_enabled());
let frame_telemetry = DshotCommandFrame::from_command(DshotCommand::Beep1);
assert_eq!(frame_telemetry.value(), 1);
assert!(frame_telemetry.is_telemetry_enabled());
}
#[test]
fn from_throttle_scaling() {
let frame_min = DshotCommandFrame::from_throttle_unidirectional(0.0);
assert_eq!(frame_min.value(), 48);
let frame_min = DshotCommandFrame::from_throttle_bidirectional(0.0);
assert_eq!(frame_min.value(), 48);
let frame_max = DshotCommandFrame::from_throttle_unidirectional(1.0);
assert_eq!(frame_max.value(), 2047);
let frame_max = DshotCommandFrame::from_throttle_bidirectional(1.0);
assert_eq!(frame_max.value(), 2047);
let frame_midpoint = DshotCommandFrame::from_throttle_unidirectional(0.5);
assert_eq!(frame_midpoint.value(), 1048);
let frame_midpoint = DshotCommandFrame::from_throttle_bidirectional(0.5);
assert_eq!(frame_midpoint.value(), 1048);
}
#[test]
fn from_throttle_clamping() {
let frame_neg = DshotCommandFrame::from_throttle_unidirectional(-0.25);
assert_eq!(frame_neg.value(), 48);
let frame_over = DshotCommandFrame::from_throttle_unidirectional(1.5);
assert_eq!(frame_over.value(), 2047);
}
#[test]
fn input_value_clamping_protection() {
let frame = DshotCommandFrame::encode_raw(9999, DshotCommandFrame::NO_TELEMETRY);
assert_eq!(frame.value(), 2047);
}
}