use core::ops::Deref;
use super::{DshotError, Telemetry};
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
pub struct DshotTelemetryFrame(u16);
impl Default for DshotTelemetryFrame {
fn default() -> Self {
Self::from_raw_12(0)
}
}
impl TryFrom<u16> for DshotTelemetryFrame {
type Error = DshotError;
#[inline]
fn try_from(raw_16: u16) -> Result<Self, DshotError> {
Self::try_from_raw_16(raw_16)
}
}
impl From<DshotTelemetryFrame> for u16 {
#[inline]
fn from(frame: DshotTelemetryFrame) -> Self {
frame.raw_16()
}
}
impl Deref for DshotTelemetryFrame {
type Target = u16;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DshotTelemetryFrame {
const CHECKSUM_BITS: u16 = 0x000F;
const MANTISSA_BITS: u16 = 0x1FF0; const EXPONENT_BITS: u16 = 0xE000; const ONE_MINUTE_IN_MICROSECONDS: u32 = 60_000_000;
const ONE_MINUTE_IN_MICROSECONDS_F32: f32 = 60_000_000.0;
#[inline]
#[must_use]
pub const fn from_raw_12(raw_12: u16) -> Self {
Self((raw_12 << 4) | Self::calculate_checksum(raw_12))
}
pub fn try_from_raw_16(raw_16: u16) -> Result<Self, DshotError> {
let ret = Self(raw_16);
if ret.checksum_is_ok() {
Ok(ret)
} else {
Err(DshotError::InvalidChecksum)
}
}
#[inline]
#[must_use]
pub const fn raw_16(self) -> u16 {
self.0
}
#[must_use]
pub const fn calculate_checksum(raw_12: u16) -> u16 {
(!(raw_12 ^ (raw_12 >> 4) ^ (raw_12 >> 8))) & 0x0F
}
#[inline]
#[must_use]
pub const fn checksum(self) -> u16 {
self.0 & Self::CHECKSUM_BITS
}
#[inline]
#[must_use]
pub const fn checksum_is_ok(self) -> bool {
let checksum = (self.0 ^ (self.0 >> 4) ^ (self.0 >> 8) ^ (self.0 >> 12)) & 0x0F;
checksum == 0x0F
}
#[inline]
#[must_use]
pub const fn from_exponent_mantissa(exponent: u16, mantissa: u16) -> Self {
let raw_12 = ((exponent & 0x07) << 9) | (mantissa & 0x01FF);
Self::from_raw_12(raw_12)
}
#[inline]
#[must_use]
pub fn from_type_value(data_type: u8, value: u8) -> Self {
let raw_12 = (u16::from(data_type & 0x07) << 9) | u16::from(value);
Self::from_raw_12(raw_12)
}
#[inline]
#[must_use]
pub const fn mantissa(self) -> u16 {
(self.0 & Self::MANTISSA_BITS) >> 4
}
#[inline]
#[must_use]
pub const fn exponent(self) -> u16 {
(self.0 & Self::EXPONENT_BITS) >> 13
}
#[inline]
#[must_use]
fn period_us(self) -> u32 {
u32::from(self.mantissa()) << self.exponent()
}
#[inline]
#[must_use]
pub fn erpm(self) -> u32 {
let raw_12 = (self.0 >> 4) & 0x0FFF;
if raw_12 == 0 || raw_12 == 0x0FFF {
return 0;
}
let period = self.period_us();
if period == 0 {
return 0;
}
Self::ONE_MINUTE_IN_MICROSECONDS / period
}
#[inline]
#[must_use]
pub fn erpm_f32(self) -> f32 {
let raw_12 = (self.0 >> 4) & 0x0FFF;
if raw_12 == 0 || raw_12 == 0x0FFF {
return 0.0;
}
let period = self.period_us();
if period == 0 {
return 0.0;
}
#[allow(clippy::cast_precision_loss)]
{
Self::ONE_MINUTE_IN_MICROSECONDS_F32 / (period as f32)
}
}
#[inline]
#[must_use]
pub const fn is_erpm_frame(self) -> bool {
let raw_12 = (self.0 >> 4) & 0x0FFF;
let prefix = (raw_12 >> 8) & 0x0F;
(prefix == 0) || ((prefix & 0x01) != 0)
}
#[inline]
pub fn try_decode_erpm(self) -> Result<u32, DshotError> {
if self.is_erpm_frame() {
Ok(self.erpm())
} else {
Err(DshotError::InvalidErpm)
}
}
#[inline]
pub fn try_decode_telemetry(self) -> Result<Telemetry, DshotError> {
if self.is_erpm_frame() {
return Ok(Telemetry::Erpm(self.erpm()));
}
let raw_12 = (self.0 >> 4) & 0x0FFF;
let prefix = (raw_12 >> 8) & 0x0F;
let data_type = prefix >> 1;
let data = (raw_12 & 0xFF) as u8;
match data_type {
1 => Ok(Telemetry::Temperature(data)),
2 => Ok(Telemetry::Voltage(u32::from(data) * 250)),
3 => Ok(Telemetry::Current(u32::from(data) * 1000)),
4 => Ok(Telemetry::Debug1(data)),
5 => Ok(Telemetry::Debug2(data)),
6 => Ok(Telemetry::Debug3(data)),
7 => Ok(Telemetry::StateEvent(data)),
_ => Err(DshotError::InvalidTelemetry),
}
}
}
#[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::<DshotTelemetryFrame>();
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_default_constructor() {
let frame = DshotTelemetryFrame::default();
assert_eq!(frame.raw_16(), 0x000F); assert!(frame.checksum_is_ok());
assert_eq!(frame.erpm(), 0);
}
#[test]
fn test_from_exponent_mantissa_packing() {
let frame = DshotTelemetryFrame::from_exponent_mantissa(2, 0x5A);
assert_eq!(frame.raw_16(), 0x45A4);
assert!(frame.checksum_is_ok());
assert_eq!(frame.exponent(), 2);
assert_eq!(frame.mantissa(), 0x5A);
}
#[test]
fn test_erpm_calculation_active_motor() {
let frame = DshotTelemetryFrame::from_exponent_mantissa(1, 300);
assert_eq!(frame.raw_16(), 0x32C2);
assert!(frame.checksum_is_ok());
assert!(frame.is_erpm_frame(), "Expected 0x32C to resolve as a valid eRPM prefix");
assert_eq!(frame.erpm(), 100_000);
let decode_res = frame.try_decode_erpm();
assert_eq!(decode_res.unwrap(), 100_000);
}
#[test]
fn test_erpm_edge_cases_zero_and_max() {
let zero_frame = DshotTelemetryFrame::from_raw_12(0);
assert_eq!(zero_frame.erpm(), 0);
assert_eq!(zero_frame.try_decode_erpm().unwrap(), 0);
let max_frame = DshotTelemetryFrame::from_raw_12(0x0FFF);
assert_eq!(max_frame.erpm(), 0);
assert_eq!(max_frame.try_decode_erpm().unwrap(), 0);
}
#[test]
fn test_edt_temperature_decoding() {
let frame = DshotTelemetryFrame::from_type_value(1, 85);
assert!(!frame.is_erpm_frame());
assert!(frame.try_decode_erpm().is_err());
let telemetry = frame.try_decode_telemetry().unwrap();
assert_eq!(telemetry, Telemetry::Temperature(85));
}
#[test]
fn temperature() {
let frame = DshotTelemetryFrame::from_type_value(1, 25);
assert_eq!(frame.try_decode_telemetry(), Ok(Telemetry::Temperature(25)));
let frame = DshotTelemetryFrame::from_type_value(1, 100);
assert_eq!(frame.try_decode_telemetry(), Ok(Telemetry::Temperature(100)));
let frame = DshotTelemetryFrame::from_type_value(1, 255);
assert_eq!(frame.try_decode_telemetry(), Ok(Telemetry::Temperature(255)));
}
#[test]
fn test_edt_voltage_decoding() {
let frame = DshotTelemetryFrame::from_type_value(2, 64);
let telemetry = frame.try_decode_telemetry().unwrap();
assert_eq!(telemetry, Telemetry::Voltage(16_000));
}
#[test]
fn test_edt_current_decoding() {
let frame = DshotTelemetryFrame::from_type_value(3, 25);
let telemetry = frame.try_decode_telemetry().unwrap();
assert_eq!(telemetry, Telemetry::Current(25_000));
}
#[test]
fn test_edt_debug_and_state_events() {
let d1_frame = DshotTelemetryFrame::from_type_value(4, 42);
assert_eq!(d1_frame.try_decode_telemetry().unwrap(), Telemetry::Debug1(42));
let se_frame = DshotTelemetryFrame::from_type_value(7, 3);
assert_eq!(se_frame.try_decode_telemetry().unwrap(), Telemetry::StateEvent(3));
}
#[test]
fn test_invalid_checksum_rejection() {
let corrupted_raw = 0x45A0;
let frame_res = DshotTelemetryFrame::try_from_raw_16(corrupted_raw);
assert!(frame_res.is_err(), "Expected constructor to reject invalid checksum structures");
}
}