use crate::register::{
DIAG_STATUS_MASK, DIAG_STATUS_SHIFT, INT_ER_MASK, INT_ER_SHIFT, INTB_RB_MASK, INTB_RB_SHIFT,
OSC_ER_MASK, OSC_ER_SHIFT, OTP_CRC_ER_MASK, OTP_CRC_ER_SHIFT, POR_MASK, POR_SHIFT,
RESULT_STATUS_MASK, RESULT_STATUS_SHIFT, SET_COUNT_MASK, SET_COUNT_SHIFT, T_THR_CONFIG_MAX,
T_THR_CONFIG_MIN, VCC_UV_ER_MASK, VCC_UV_ER_SHIFT, VER_V1, VER_V2, extract,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum I2cReadMode {
Standard = 0x00,
OneByte16Bit = 0x01,
OneByte8Bit = 0x02,
}
impl_try_from_u8!(I2cReadMode {
0x00 => Standard,
0x01 => OneByte16Bit,
0x02 => OneByte8Bit,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum OperatingMode {
Standby = 0x00,
Sleep = 0x01,
ContinuousMeasure = 0x02,
WakeUpAndSleep = 0x03,
}
impl_try_from_u8!(OperatingMode {
0x00 => Standby,
0x01 => Sleep,
0x02 => ContinuousMeasure,
0x03 => WakeUpAndSleep,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum Axis {
X = 0,
Y = 1,
Z = 2,
}
impl_try_from_u8!(Axis {
0 => X,
1 => Y,
2 => Z,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum PoleCount {
Two = 2,
Three = 3,
Four = 4,
}
impl_try_from_u8!(PoleCount {
2 => Two,
3 => Three,
4 => Four,
});
impl PoleCount {
#[inline]
pub const fn count(self) -> u8 {
self as u8
}
}
impl From<PoleCount> for u8 {
#[inline]
fn from(v: PoleCount) -> Self {
v.count()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum MagneticChannel {
Off = 0x0,
X = 0x1,
Y = 0x2,
XY = 0x3,
Z = 0x4,
ZX = 0x5,
YZ = 0x6,
XYZ = 0x7,
XYX = 0x8,
YXY = 0x9,
YZY = 0xA,
XZX = 0xB,
}
impl_try_from_u8!(MagneticChannel {
0x0 => Off,
0x1 => X,
0x2 => Y,
0x3 => XY,
0x4 => Z,
0x5 => ZX,
0x6 => YZ,
0x7 => XYZ,
0x8 => XYX,
0x9 => YXY,
0xA => YZY,
0xB => XZX,
});
impl From<Axis> for MagneticChannel {
#[inline]
fn from(axis: Axis) -> Self {
match axis {
Axis::X => Self::X,
Axis::Y => Self::Y,
Axis::Z => Self::Z,
}
}
}
impl MagneticChannel {
pub(crate) const fn axis_count(self) -> u8 {
match self {
Self::Off => 0,
Self::X | Self::Y | Self::Z => 1,
Self::XY | Self::ZX | Self::YZ | Self::XYX | Self::YXY | Self::YZY | Self::XZX => 2,
Self::XYZ => 3,
}
}
pub(crate) const fn measurement_slots(self) -> u8 {
match self {
Self::Off => 0,
Self::X | Self::Y | Self::Z => 1,
Self::XY | Self::ZX | Self::YZ => 2,
Self::XYZ | Self::XYX | Self::YXY | Self::YZY | Self::XZX => 3,
}
}
pub(crate) const fn has_x(self) -> bool {
matches!(
self,
Self::X | Self::XY | Self::ZX | Self::XYZ | Self::XYX | Self::YXY | Self::XZX
)
}
pub(crate) const fn has_y(self) -> bool {
matches!(
self,
Self::Y | Self::XY | Self::YZ | Self::XYZ | Self::XYX | Self::YXY | Self::YZY
)
}
pub(crate) const fn has_z(self) -> bool {
matches!(
self,
Self::Z | Self::ZX | Self::YZ | Self::XYZ | Self::YZY | Self::XZX
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum ConversionAverage {
X1 = 0b000,
X2 = 0b001,
X4 = 0b010,
X8 = 0b011,
X16 = 0b100,
X32 = 0b101,
}
impl_try_from_u8!(ConversionAverage {
0b000 => X1,
0b001 => X2,
0b010 => X4,
0b011 => X8,
0b100 => X16,
0b101 => X32,
});
impl ConversionAverage {
const T_SAMPLE_US: u32 = 25;
#[inline]
pub const fn samples_per_channel(self) -> u32 {
1 << (self as u32)
}
pub const fn conversion_time(
self,
channels: MagneticChannel,
temp_channel_enabled: bool,
) -> MicrosIsr {
let temp_adds_slot = !matches!(self, Self::X1);
let temp_channel: u32 = if temp_channel_enabled && temp_adds_slot {
1
} else {
0
};
let num_channels = channels.measurement_slots() as u32 + temp_channel;
let us = Self::T_SAMPLE_US * (self.samples_per_channel() * num_channels + 1);
MicrosIsr(us)
}
#[must_use]
#[inline]
pub const fn lower(self) -> Option<Self> {
match self {
Self::X32 => Some(Self::X16),
Self::X16 => Some(Self::X8),
Self::X8 => Some(Self::X4),
Self::X4 => Some(Self::X2),
Self::X2 => Some(Self::X1),
Self::X1 => None,
}
}
pub(crate) const fn max_polls(self) -> u16 {
match self {
Self::X1 => 10,
Self::X2 => 15,
Self::X4 => 25,
Self::X8 => 40,
Self::X16 => 70,
Self::X32 => 100,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum SleepTime {
Ms1 = 0x0,
Ms5 = 0x1,
Ms10 = 0x2,
Ms15 = 0x3,
Ms20 = 0x4,
Ms30 = 0x5,
Ms50 = 0x6,
Ms100 = 0x7,
Ms500 = 0x8,
Ms1000 = 0x9,
Ms2000 = 0xA,
Ms5000 = 0xB,
Ms20000 = 0xC,
}
impl_try_from_u8!(SleepTime {
0x0 => Ms1,
0x1 => Ms5,
0x2 => Ms10,
0x3 => Ms15,
0x4 => Ms20,
0x5 => Ms30,
0x6 => Ms50,
0x7 => Ms100,
0x8 => Ms500,
0x9 => Ms1000,
0xA => Ms2000,
0xB => Ms5000,
0xC => Ms20000,
});
impl SleepTime {
pub const fn as_micros_isr(self) -> MicrosIsr {
MicrosIsr(match self {
Self::Ms1 => 1_000,
Self::Ms5 => 5_000,
Self::Ms10 => 10_000,
Self::Ms15 => 15_000,
Self::Ms20 => 20_000,
Self::Ms30 => 30_000,
Self::Ms50 => 50_000,
Self::Ms100 => 100_000,
Self::Ms500 => 500_000,
Self::Ms1000 => 1_000_000,
Self::Ms2000 => 2_000_000,
Self::Ms5000 => 5_000_000,
Self::Ms20000 => 20_000_000,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum PowerNoiseMode {
LowActiveCurrent = 0,
LowNoise = 1,
}
impl_try_from_u8!(PowerNoiseMode {
0 => LowActiveCurrent,
1 => LowNoise,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum TriggerMode {
I2cCommand = 0,
IntPin = 1,
}
impl_try_from_u8!(TriggerMode {
0 => I2cCommand,
1 => IntPin,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum MagneticTempCoefficient {
None = 0b00,
NdBFe = 0b01,
Ceramic = 0b11,
}
impl_try_from_u8!(MagneticTempCoefficient {
0b00 => None,
0b01 => NdBFe,
0b11 => Ceramic,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum AngleEnable {
None = 0b00,
XY = 0b01,
YZ = 0b10,
XZ = 0b11,
}
impl_try_from_u8!(AngleEnable {
0b00 => None,
0b01 => XY,
0b10 => YZ,
0b11 => XZ,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum MagneticThresholdDirection {
Above = 0,
Below = 1,
}
impl_try_from_u8!(MagneticThresholdDirection {
0 => Above,
1 => Below,
});
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum ThresholdCrossingCount {
#[default]
One = 0,
Four = 1,
}
impl_try_from_u8!(ThresholdCrossingCount {
0 => One,
1 => Four,
});
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TempThresholdConfig(u8);
impl TempThresholdConfig {
pub const DISABLED: Self = Self(0);
#[inline]
pub const fn from_code(code: u8) -> Option<Self> {
if code == 0 || (code >= T_THR_CONFIG_MIN && code <= T_THR_CONFIG_MAX) {
Some(Self(code))
} else {
None
}
}
#[inline]
pub const fn code(self) -> u8 {
self.0
}
#[inline]
pub const fn is_enabled(self) -> bool {
self.0 != 0
}
}
impl_try_from_u8!(TempThresholdConfig);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum InterruptMode {
None = 0b000,
ThroughInt = 0b001,
ThroughIntExceptI2cBusy = 0b010,
ThroughScl = 0b011,
ThroughSclExceptI2cBusy = 0b100,
}
impl_try_from_u8!(InterruptMode {
0b000 => None,
0b001 => ThroughInt,
0b010 => ThroughIntExceptI2cBusy,
0b011 => ThroughScl,
0b100 => ThroughSclExceptI2cBusy,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum InterruptState {
Latched = 0,
Pulse10us = 1,
}
impl_try_from_u8!(InterruptState {
0 => Latched,
1 => Pulse10us,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum MagneticGainChannel {
First = 0,
Second = 1,
}
impl_try_from_u8!(MagneticGainChannel {
0 => First,
1 => Second,
});
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum Diagnostics {
#[default]
Off = 0,
Halt = 1,
Warn = 2,
Ignore = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum Range {
Low = 0,
High = 1,
}
impl_try_from_u8!(Range {
0 => Low,
1 => High,
});
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum DeviceVariant {
A1,
A2,
B1,
B2,
C1,
C2,
D1,
D2,
}
impl DeviceVariant {
const I2C_ADDR_A: u8 = 0x35;
const I2C_ADDR_B: u8 = 0x22;
const I2C_ADDR_C: u8 = 0x78;
const I2C_ADDR_D: u8 = 0x44;
const XY_RANGE_V1_LOW: f32 = 40.0;
const XY_RANGE_V1_HIGH: f32 = 80.0;
const XY_RANGE_V2_LOW: f32 = 133.0;
const XY_RANGE_V2_HIGH: f32 = 266.0;
const Z_RANGE_V1_LOW: f32 = 40.0;
const Z_RANGE_V1_HIGH: f32 = 80.0;
const Z_RANGE_V2_LOW: f32 = 133.0;
const Z_RANGE_V2_HIGH: f32 = 266.0;
pub const KNOWN_ADDRESSES: [u8; 4] = [
Self::I2C_ADDR_B,
Self::I2C_ADDR_A,
Self::I2C_ADDR_D,
Self::I2C_ADDR_C,
];
#[inline]
pub const fn from_address_version(address: u8, version: u8) -> Option<Self> {
match (address, version) {
(Self::I2C_ADDR_A, VER_V1) => Some(Self::A1),
(Self::I2C_ADDR_A, VER_V2) => Some(Self::A2),
(Self::I2C_ADDR_B, VER_V1) => Some(Self::B1),
(Self::I2C_ADDR_B, VER_V2) => Some(Self::B2),
(Self::I2C_ADDR_C, VER_V1) => Some(Self::C1),
(Self::I2C_ADDR_C, VER_V2) => Some(Self::C2),
(Self::I2C_ADDR_D, VER_V1) => Some(Self::D1),
(Self::I2C_ADDR_D, VER_V2) => Some(Self::D2),
_ => None,
}
}
#[inline]
pub const fn default_address(&self) -> u8 {
match self {
Self::A1 | Self::A2 => Self::I2C_ADDR_A,
Self::B1 | Self::B2 => Self::I2C_ADDR_B,
Self::C1 | Self::C2 => Self::I2C_ADDR_C,
Self::D1 | Self::D2 => Self::I2C_ADDR_D,
}
}
#[inline]
pub const fn version(&self) -> u8 {
match self {
Self::A1 | Self::B1 | Self::C1 | Self::D1 => VER_V1,
Self::A2 | Self::B2 | Self::C2 | Self::D2 => VER_V2,
}
}
#[inline]
pub const fn range_xy(&self, range: Range) -> MilliTesla {
MilliTesla(match (self, range) {
(Self::A1 | Self::B1 | Self::C1 | Self::D1, Range::Low) => Self::XY_RANGE_V1_LOW,
(Self::A1 | Self::B1 | Self::C1 | Self::D1, Range::High) => Self::XY_RANGE_V1_HIGH,
(Self::A2 | Self::B2 | Self::C2 | Self::D2, Range::Low) => Self::XY_RANGE_V2_LOW,
(Self::A2 | Self::B2 | Self::C2 | Self::D2, Range::High) => Self::XY_RANGE_V2_HIGH,
})
}
#[inline]
pub const fn range_z(&self, range: Range) -> MilliTesla {
MilliTesla(match (self, range) {
(Self::A1 | Self::B1 | Self::C1 | Self::D1, Range::Low) => Self::Z_RANGE_V1_LOW,
(Self::A1 | Self::B1 | Self::C1 | Self::D1, Range::High) => Self::Z_RANGE_V1_HIGH,
(Self::A2 | Self::B2 | Self::C2 | Self::D2, Range::Low) => Self::Z_RANGE_V2_LOW,
(Self::A2 | Self::B2 | Self::C2 | Self::D2, Range::High) => Self::Z_RANGE_V2_HIGH,
})
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Lsb(pub i8);
impl From<Lsb> for i8 {
#[inline]
fn from(v: Lsb) -> Self {
v.0
}
}
impl From<Lsb> for u8 {
#[inline]
fn from(v: Lsb) -> Self {
v.0 as u8
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct MilliTesla(pub f32);
impl From<MilliTesla> for f32 {
#[inline]
fn from(v: MilliTesla) -> Self {
v.0
}
}
impl core::ops::Add for MilliTesla {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self {
Self(self.0 + rhs.0)
}
}
impl core::ops::Sub for MilliTesla {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self {
Self(self.0 - rhs.0)
}
}
impl core::ops::AddAssign for MilliTesla {
#[inline]
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl core::ops::SubAssign for MilliTesla {
#[inline]
fn sub_assign(&mut self, rhs: Self) {
self.0 -= rhs.0;
}
}
impl core::ops::Neg for MilliTesla {
type Output = Self;
#[inline]
fn neg(self) -> Self {
Self(-self.0)
}
}
impl MilliTesla {
const ADC_FULL_SCALE: f32 = 32768.0;
const INV_ADC_FULL_SCALE: f32 = 1.0 / Self::ADC_FULL_SCALE;
const EIGHT_BIT_FULL_SCALE: f32 = 128.0;
#[inline]
pub(crate) fn mt_per_lsb(self) -> f32 {
self.0 / Self::EIGHT_BIT_FULL_SCALE
}
#[inline]
#[cfg(feature = "libm")]
pub(crate) fn lsb_per_mt(self) -> f32 {
Self::EIGHT_BIT_FULL_SCALE / self.0
}
#[inline]
pub fn abs(self) -> Self {
Self(self.0.abs())
}
#[inline]
pub fn from_raw(raw: i16, range: MilliTesla) -> Self {
Self((raw as f32) * range.0 * Self::INV_ADC_FULL_SCALE)
}
}
#[cfg(feature = "libm")]
impl MilliTesla {
#[inline]
pub fn to_raw(self, range: MilliTesla) -> Option<i16> {
if !self.0.is_finite() || !range.0.is_finite() || range == MilliTesla(0.0) {
return None;
}
let rounded = libm::roundf(self.0 * Self::ADC_FULL_SCALE / range.0);
if rounded < i16::MIN as f32 || rounded > i16::MAX as f32 {
return None;
}
Some(rounded as i16)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct CordicMagnitude(pub f32);
impl From<CordicMagnitude> for f32 {
#[inline]
fn from(v: CordicMagnitude) -> Self {
v.0
}
}
impl CordicMagnitude {
#[inline]
pub fn from_raw(raw: u8, range: MilliTesla) -> Self {
Self(raw as f32 * range.mt_per_lsb())
}
}
#[cfg(feature = "libm")]
impl CordicMagnitude {
#[inline]
pub fn to_raw(self, range: MilliTesla) -> Option<u8> {
if !self.0.is_finite() || self.0 < 0.0 || !range.0.is_finite() || range.0 == 0.0 {
return None;
}
let rounded = libm::roundf(self.0 * range.lsb_per_mt());
if rounded > u8::MAX as f32 {
return None;
}
Some(rounded as u8)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct Celsius(pub f32);
impl From<Celsius> for f32 {
#[inline]
fn from(v: Celsius) -> Self {
v.0
}
}
impl Celsius {
const T_SENS_T0: f32 = 25.0;
const T_ADC_T0: f32 = 17508.0;
const T_ADC_RES_INV: f32 = 1.0 / 60.1;
#[inline]
pub fn from_raw(raw: i16) -> Self {
Self(Self::T_SENS_T0 + ((raw as f32) - Self::T_ADC_T0) * Self::T_ADC_RES_INV)
}
}
#[cfg(feature = "libm")]
impl Celsius {
#[inline]
pub fn to_raw(self) -> Option<i16> {
if !self.0.is_finite() {
return None;
}
let rounded =
libm::roundf((self.0 - Self::T_SENS_T0) / Self::T_ADC_RES_INV + Self::T_ADC_T0);
if rounded < i16::MIN as f32 || rounded > i16::MAX as f32 {
return None;
}
Some(rounded as i16)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct Rpm(pub f32);
impl From<Rpm> for f32 {
#[inline]
fn from(v: Rpm) -> Self {
v.0
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct Degrees(pub f32);
impl From<Degrees> for f32 {
#[inline]
fn from(v: Degrees) -> Self {
v.0
}
}
impl core::ops::Sub for Degrees {
type Output = SignedDegrees;
#[inline]
fn sub(self, rhs: Self) -> SignedDegrees {
SignedDegrees(self.0 - rhs.0)
}
}
impl Degrees {
pub const MIN: Self = Self(0.0);
pub const MAX: Self = Self(360.0);
#[inline]
pub fn is_valid(self) -> bool {
self.0 >= Self::MIN.0 && self.0 <= Self::MAX.0
}
const INT_MASK: u16 = 0x1FF;
const INT_SHIFT: u16 = 4;
const FRAC_MASK: u16 = 0x0F;
const FRAC_SCALE: f32 = 1.0 / 16.0;
#[inline]
pub fn from_raw(raw: u16) -> Option<Self> {
let integer = ((raw >> Self::INT_SHIFT) & Self::INT_MASK) as f32;
let fraction = (raw & Self::FRAC_MASK) as f32 * Self::FRAC_SCALE;
let candidate = Self(integer + fraction);
if candidate.is_valid() {
Some(candidate)
} else {
defmt_warn!(
"from_raw: angle {}° out of valid range, corrupted I2C read",
candidate.0
);
None
}
}
}
#[cfg(feature = "libm")]
impl Degrees {
#[inline]
pub fn to_raw(self) -> Option<u16> {
if !self.0.is_finite() || self.0 < 0.0 || self.0 > 360.0 {
return None;
}
let rounded = libm::roundf(self.0 * 16.0);
if rounded > 5760.0 {
return None;
}
Some(rounded as u16)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct SignedDegrees(pub f32);
impl From<SignedDegrees> for f32 {
#[inline]
fn from(v: SignedDegrees) -> Self {
v.0
}
}
impl core::ops::Neg for SignedDegrees {
type Output = Self;
#[inline]
fn neg(self) -> Self {
Self(-self.0)
}
}
impl core::ops::Add for SignedDegrees {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self {
Self(self.0 + rhs.0)
}
}
impl core::ops::Sub for SignedDegrees {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self {
Self(self.0 - rhs.0)
}
}
impl core::ops::AddAssign for SignedDegrees {
#[inline]
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl core::ops::SubAssign for SignedDegrees {
#[inline]
fn sub_assign(&mut self, rhs: Self) {
self.0 -= rhs.0;
}
}
impl SignedDegrees {
pub const MAX: Self = Self(360.0);
pub const MIN: Self = Self(-360.0);
#[inline]
pub fn abs(self) -> Self {
Self(self.0.abs())
}
}
impl core::ops::AddAssign<SignedDegrees> for Degrees {
#[inline]
fn add_assign(&mut self, rhs: SignedDegrees) {
self.0 += rhs.0;
}
}
impl core::ops::Add<SignedDegrees> for Degrees {
type Output = Degrees;
#[inline]
fn add(self, rhs: SignedDegrees) -> Degrees {
Degrees(self.0 + rhs.0)
}
}
impl core::ops::SubAssign<SignedDegrees> for Degrees {
#[inline]
fn sub_assign(&mut self, rhs: SignedDegrees) {
self.0 -= rhs.0;
}
}
impl core::ops::Sub<SignedDegrees> for Degrees {
type Output = Degrees;
#[inline]
fn sub(self, rhs: SignedDegrees) -> Degrees {
Degrees(self.0 - rhs.0)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct MicrosIsr(pub u32);
impl MicrosIsr {
#[inline]
pub const fn to_millis(self) -> f32 {
self.0 as f32 / 1_000.0
}
#[inline]
pub const fn to_seconds(self) -> f32 {
self.0 as f32 / 1_000_000.0
}
}
impl From<MicrosIsr> for u32 {
#[inline]
fn from(v: MicrosIsr) -> Self {
v.0
}
}
impl core::fmt::Display for MicrosIsr {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct MicrosRange {
pub min: MicrosIsr,
pub max: MicrosIsr,
}
pub const WAKE_DELAY: MicrosRange = MicrosRange {
min: MicrosIsr(80),
max: MicrosIsr(200),
};
pub const WAKE_RETRY_DELAY: MicrosIsr = MicrosIsr(5_000);
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct Crossings(pub u32);
impl Crossings {
#[inline]
pub const fn saturating_add(self, n: u32) -> Self {
Self(self.0.saturating_add(n))
}
pub const fn saturating_sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
}
impl From<Crossings> for u32 {
#[inline]
fn from(v: Crossings) -> Self {
v.0
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct NoDelay;
impl embedded_hal::delay::DelayNs for NoDelay {
#[inline]
fn delay_ns(&mut self, _ns: u32) {}
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct MagneticReading {
pub x: Option<MilliTesla>,
pub y: Option<MilliTesla>,
pub z: Option<MilliTesla>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct AngleReading {
pub angle: Degrees,
pub magnitude: CordicMagnitude,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct SensorReading {
pub temperature: Option<Celsius>,
pub magnetic: MagneticReading,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct DeviceStatus {
pub vcc_undervoltage_error: bool,
pub otp_crc_error: bool,
pub int_error: bool,
pub oscillator_error: bool,
pub int_bar_readback_high: bool,
}
impl DeviceStatus {
#[inline]
pub const fn is_ok(&self) -> bool {
!self.vcc_undervoltage_error
&& !self.otp_crc_error
&& !self.int_error
&& !self.oscillator_error
}
#[inline]
pub const fn from_register(register: u8) -> Self {
Self {
vcc_undervoltage_error: extract(register, VCC_UV_ER_MASK, VCC_UV_ER_SHIFT) != 0,
otp_crc_error: extract(register, OTP_CRC_ER_MASK, OTP_CRC_ER_SHIFT) != 0,
int_error: extract(register, INT_ER_MASK, INT_ER_SHIFT) != 0,
oscillator_error: extract(register, OSC_ER_MASK, OSC_ER_SHIFT) != 0,
int_bar_readback_high: extract(register, INTB_RB_MASK, INTB_RB_SHIFT) != 0,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
pub struct ConversionStatus {
pub set_count: u8,
pub power_on_reset: bool,
pub diag_status_fail: bool,
pub result_status_ready: bool,
}
impl ConversionStatus {
#[inline]
pub const fn result_status_ready(&self) -> bool {
self.result_status_ready
}
#[inline]
pub const fn from_register(register: u8) -> Self {
Self {
set_count: extract(register, SET_COUNT_MASK, SET_COUNT_SHIFT),
power_on_reset: extract(register, POR_MASK, POR_SHIFT) != 0,
diag_status_fail: extract(register, DIAG_STATUS_MASK, DIAG_STATUS_SHIFT) != 0,
result_status_ready: extract(register, RESULT_STATUS_MASK, RESULT_STATUS_SHIFT) != 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::register::CONV_AVG_SHIFT;
const EPSILON: f32 = 0.15;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn variant_a1_address() {
assert_eq!(DeviceVariant::A1.default_address(), 0x35);
}
#[test]
fn variant_a2_address() {
assert_eq!(DeviceVariant::A2.default_address(), 0x35);
}
#[test]
fn variant_b1_address() {
assert_eq!(DeviceVariant::B1.default_address(), 0x22);
}
#[test]
fn variant_b2_address() {
assert_eq!(DeviceVariant::B2.default_address(), 0x22);
}
#[test]
fn variant_c1_address() {
assert_eq!(DeviceVariant::C1.default_address(), 0x78);
}
#[test]
fn variant_c2_address() {
assert_eq!(DeviceVariant::C2.default_address(), 0x78);
}
#[test]
fn variant_d1_address() {
assert_eq!(DeviceVariant::D1.default_address(), 0x44);
}
#[test]
fn variant_d2_address() {
assert_eq!(DeviceVariant::D2.default_address(), 0x44);
}
#[test]
fn version_1_variants() {
assert_eq!(DeviceVariant::A1.version(), 1);
assert_eq!(DeviceVariant::B1.version(), 1);
assert_eq!(DeviceVariant::C1.version(), 1);
assert_eq!(DeviceVariant::D1.version(), 1);
}
#[test]
fn version_2_variants() {
assert_eq!(DeviceVariant::A2.version(), 2);
assert_eq!(DeviceVariant::B2.version(), 2);
assert_eq!(DeviceVariant::C2.version(), 2);
assert_eq!(DeviceVariant::D2.version(), 2);
}
#[test]
fn from_address_version_all_variants() {
assert_eq!(
DeviceVariant::from_address_version(0x35, 1),
Some(DeviceVariant::A1)
);
assert_eq!(
DeviceVariant::from_address_version(0x35, 2),
Some(DeviceVariant::A2)
);
assert_eq!(
DeviceVariant::from_address_version(0x22, 1),
Some(DeviceVariant::B1)
);
assert_eq!(
DeviceVariant::from_address_version(0x22, 2),
Some(DeviceVariant::B2)
);
assert_eq!(
DeviceVariant::from_address_version(0x78, 1),
Some(DeviceVariant::C1)
);
assert_eq!(
DeviceVariant::from_address_version(0x78, 2),
Some(DeviceVariant::C2)
);
assert_eq!(
DeviceVariant::from_address_version(0x44, 1),
Some(DeviceVariant::D1)
);
assert_eq!(
DeviceVariant::from_address_version(0x44, 2),
Some(DeviceVariant::D2)
);
}
#[test]
fn from_address_version_unknown_address() {
assert_eq!(DeviceVariant::from_address_version(0x30, 1), None);
}
#[test]
fn from_address_version_unknown_version() {
assert_eq!(DeviceVariant::from_address_version(0x22, 3), None);
assert_eq!(DeviceVariant::from_address_version(0x22, 0), None);
}
#[test]
fn range_v1_low_xy() {
assert_eq!(DeviceVariant::A1.range_xy(Range::Low), MilliTesla(40.0));
}
#[test]
fn range_v1_high_xy() {
assert_eq!(DeviceVariant::B1.range_xy(Range::High), MilliTesla(80.0));
}
#[test]
fn range_v2_low_xy() {
assert_eq!(DeviceVariant::C2.range_xy(Range::Low), MilliTesla(133.0));
}
#[test]
fn range_v2_high_xy() {
assert_eq!(DeviceVariant::D2.range_xy(Range::High), MilliTesla(266.0));
}
#[test]
fn range_v1_low_z() {
assert_eq!(DeviceVariant::A1.range_z(Range::Low), MilliTesla(40.0));
}
#[test]
fn range_v1_high_z() {
assert_eq!(DeviceVariant::B1.range_z(Range::High), MilliTesla(80.0));
}
#[test]
fn range_v2_low_z() {
assert_eq!(DeviceVariant::C2.range_z(Range::Low), MilliTesla(133.0));
}
#[test]
fn range_v2_high_z() {
assert_eq!(DeviceVariant::D2.range_z(Range::High), MilliTesla(266.0));
}
#[test]
fn temp_reference_point() {
let c = Celsius::from_raw(17508);
assert!(approx_eq(c.0, 25.0), "expected ~25.0, got {}", c.0);
}
#[test]
fn temp_raw_zero() {
let c = Celsius::from_raw(0);
assert!(approx_eq(c.0, -266.36), "expected ~-266.36, got {}", c.0);
}
#[test]
fn celsius_from_raw_i16_min() {
let c = Celsius::from_raw(i16::MIN);
assert!(approx_eq(c.0, -811.6), "expected ~-811.6, got {}", c.0);
}
#[test]
fn temp_raw_max() {
let c = Celsius::from_raw(i16::MAX);
assert!(approx_eq(c.0, 279.0), "expected ~279.0, got {}", c.0);
}
#[test]
fn from_raw_zero_returns_zero_mt() {
let m = MilliTesla::from_raw(0, MilliTesla(40.0));
assert_eq!(m.0, 0.0);
}
#[test]
fn from_raw_i16_min_returns_negative_range() {
let m = MilliTesla::from_raw(i16::MIN, MilliTesla(40.0));
assert!(approx_eq(m.0, -40.0), "expected ~-40.0, got {}", m.0);
}
#[test]
fn from_raw_i16_max_returns_positive_range() {
let m = MilliTesla::from_raw(i16::MAX, MilliTesla(40.0));
assert!(approx_eq(m.0, 40.0), "expected ~40.0, got {}", m.0);
}
#[test]
fn from_raw_i16_min_v2_high_returns_negative_266mt() {
let m = MilliTesla::from_raw(i16::MIN, MilliTesla(266.0));
assert!(approx_eq(m.0, -266.0), "expected ~-266.0, got {}", m.0);
}
#[test]
fn angle_zero() {
let d = Degrees::from_raw(0x0000).unwrap();
assert_eq!(d.0, 0.0);
}
#[test]
fn angle_180() {
let d = Degrees::from_raw(0x0B40).unwrap();
assert!(approx_eq(d.0, 180.0), "expected 180.0, got {}", d.0);
}
#[test]
fn angle_360() {
let d = Degrees::from_raw(0x1680).unwrap();
assert!(approx_eq(d.0, 360.0), "expected 360.0, got {}", d.0);
}
#[test]
fn angle_fractional() {
let d = Degrees::from_raw(0x05A8).unwrap();
assert!(approx_eq(d.0, 90.5), "expected 90.5, got {}", d.0);
}
#[test]
fn angle_out_of_range_returns_none() {
assert!(Degrees::from_raw(0xFFFF).is_none(), "0xFFFF must be None");
assert!(Degrees::from_raw(0x1690).is_none(), "361° must be None");
assert!(Degrees::from_raw(0x1680).is_some(), "360° must be Some");
}
#[test]
fn device_status_all_clear() {
let s = DeviceStatus::from_register(0x00);
assert!(!s.vcc_undervoltage_error);
assert!(!s.otp_crc_error);
assert!(!s.int_error);
assert!(!s.oscillator_error);
assert!(!s.int_bar_readback_high);
}
#[test]
fn device_status_all_set() {
let s = DeviceStatus::from_register(0x1F);
assert!(s.vcc_undervoltage_error);
assert!(s.otp_crc_error);
assert!(s.int_error);
assert!(s.oscillator_error);
assert!(s.int_bar_readback_high);
}
#[test]
fn device_status_individual_bits() {
let s = DeviceStatus::from_register(0b0000_0001);
assert!(s.vcc_undervoltage_error);
assert!(!s.otp_crc_error);
let s = DeviceStatus::from_register(0b0000_0010);
assert!(!s.vcc_undervoltage_error);
assert!(s.otp_crc_error);
let s = DeviceStatus::from_register(0b0000_0100);
assert!(s.int_error);
assert!(!s.oscillator_error);
let s = DeviceStatus::from_register(0b0000_1000);
assert!(!s.int_error);
assert!(s.oscillator_error);
}
#[test]
fn device_status_int_bar_readback_high() {
let s = DeviceStatus::from_register(0b0001_0000);
assert!(s.int_bar_readback_high);
assert!(s.is_ok()); }
#[test]
fn conv_status_all_clear() {
let s = ConversionStatus::from_register(0x00);
assert_eq!(s.set_count, 0);
assert!(!s.power_on_reset);
assert!(!s.diag_status_fail);
assert!(!s.result_status_ready);
}
#[test]
fn conv_status_set_count() {
let s = ConversionStatus::from_register(0b1010_0000);
assert_eq!(s.set_count, 5);
assert!(!s.power_on_reset);
}
#[test]
fn conv_status_set_count_max() {
let s = ConversionStatus::from_register(0b1110_0000);
assert_eq!(s.set_count, 7);
}
#[test]
fn conv_status_power_on_reset() {
let s = ConversionStatus::from_register(0b0001_0000);
assert_eq!(s.set_count, 0);
assert!(s.power_on_reset);
assert!(!s.diag_status_fail);
assert!(!s.result_status_ready);
}
#[test]
fn conv_status_diag_fail() {
let s = ConversionStatus::from_register(0b0000_0010);
assert!(s.diag_status_fail);
assert!(!s.result_status_ready);
}
#[test]
fn conv_status_result_ready() {
let s = ConversionStatus::from_register(0b0000_0001);
assert!(s.result_status_ready);
assert!(!s.diag_status_fail);
}
#[test]
fn conv_status_all_flags() {
let s = ConversionStatus::from_register(0b1111_0011);
assert_eq!(s.set_count, 7);
assert!(s.power_on_reset);
assert!(s.diag_status_fail);
assert!(s.result_status_ready);
}
#[test]
fn cordic_magnitude_from_raw_zero_returns_zero() {
let m = CordicMagnitude::from_raw(0, MilliTesla(80.0));
assert_eq!(m.0, 0.0);
}
#[test]
fn cordic_magnitude_from_raw_monotonic() {
let range = MilliTesla(80.0);
let mut prev = CordicMagnitude::from_raw(0, range).0;
for raw in 1u8..=255 {
let cur = CordicMagnitude::from_raw(raw, range).0;
assert!(cur > prev, "non-monotonic at raw={raw}: {prev} >= {cur}");
prev = cur;
}
}
#[test]
fn wake_delay_constants() {
assert_eq!(WAKE_DELAY.min.0, 80);
assert_eq!(WAKE_DELAY.max.0, 200);
}
#[test]
fn magnitude_raw_128_is_single_axis_full_scale() {
let m = CordicMagnitude::from_raw(128, MilliTesla(80.0));
assert!(approx_eq(m.0, 80.0), "expected ~80.0, got {}", m.0);
}
#[test]
fn magnitude_raw_181_is_two_axis_full_scale() {
let m = CordicMagnitude::from_raw(181, MilliTesla(80.0));
let expected = 80.0_f32 * core::f32::consts::SQRT_2;
assert!(
(m.0 - expected).abs() < 1.0,
"expected ~{expected:.3}, got {}",
m.0
);
}
#[test]
fn magnitude_raw_255_is_finite_and_positive() {
let m = CordicMagnitude::from_raw(255, MilliTesla(80.0));
assert!(
m.0.is_finite() && m.0 > 0.0,
"expected finite >0, got {}",
m.0
);
assert!(approx_eq(m.0, 159.375), "expected ~159.375, got {}", m.0);
}
#[test]
fn millitesla_max_positive_raw() {
let m = MilliTesla::from_raw(i16::MAX, MilliTesla(80.0));
assert!(m.0 > 0.0, "max positive raw should give positive mT");
assert!(approx_eq(m.0, 80.0), "expected ~80.0, got {}", m.0);
}
#[test]
fn millitesla_min_negative_raw() {
let m = MilliTesla::from_raw(i16::MIN, MilliTesla(80.0));
assert!(m.0 < 0.0, "min negative raw should give negative mT");
assert!(approx_eq(m.0, -80.0), "expected ~-80.0, got {}", m.0);
}
#[test]
fn millitesla_zero_raw() {
let m = MilliTesla::from_raw(0, MilliTesla(80.0));
assert_eq!(m.0, 0.0);
}
#[test]
fn millitesla_zero_range() {
let m = MilliTesla::from_raw(16384, MilliTesla(0.0));
assert_eq!(m.0, 0.0);
}
#[test]
fn millitesla_into_f32() {
assert_eq!(f32::from(MilliTesla(3.14)), 3.14);
}
#[test]
fn celsius_into_f32() {
assert_eq!(f32::from(Celsius(25.0)), 25.0);
}
#[test]
fn degrees_into_f32() {
assert_eq!(f32::from(Degrees(180.0)), 180.0);
}
#[test]
fn degrees_constants() {
assert_eq!(Degrees::MIN.0, 0.0);
assert_eq!(Degrees::MAX.0, 360.0);
}
#[test]
fn degrees_is_valid_in_range() {
assert!(Degrees(0.0).is_valid());
assert!(Degrees(180.0).is_valid());
assert!(Degrees(360.0).is_valid());
}
#[test]
fn degrees_is_valid_out_of_range() {
assert!(!Degrees(-0.1).is_valid());
assert!(!Degrees(360.1).is_valid());
assert!(!Degrees(f32::NAN).is_valid());
assert!(!Degrees(f32::INFINITY).is_valid());
assert!(!Degrees(f32::NEG_INFINITY).is_valid());
}
#[test]
fn signed_degrees_into_f32() {
assert_eq!(f32::from(SignedDegrees(90.5)), 90.5);
}
#[test]
fn signed_degrees_add() {
assert_eq!(
SignedDegrees(90.0) + SignedDegrees(30.0),
SignedDegrees(120.0)
);
}
#[test]
fn signed_degrees_sub_signed() {
assert_eq!(
SignedDegrees(10.0) - SignedDegrees(30.0),
SignedDegrees(-20.0)
);
}
#[test]
fn signed_degrees_neg() {
assert_eq!(-SignedDegrees(45.0), SignedDegrees(-45.0));
}
#[test]
fn signed_degrees_abs_negative() {
assert_eq!(SignedDegrees(-45.0).abs(), SignedDegrees(45.0));
}
#[test]
fn signed_degrees_abs_positive() {
assert_eq!(SignedDegrees(45.0).abs(), SignedDegrees(45.0));
}
#[test]
fn signed_degrees_abs_zero() {
assert_eq!(SignedDegrees(0.0).abs(), SignedDegrees(0.0));
}
#[test]
fn signed_degrees_abs_nan_no_panic() {
assert!(SignedDegrees(f32::NAN).abs().0.is_nan());
}
#[test]
fn degrees_add_signed_degrees_cross_type() {
assert_eq!(Degrees(270.0) + SignedDegrees(45.0), Degrees(315.0));
}
#[test]
fn degrees_add_assign_signed_degrees_positive() {
let mut d = Degrees(100.0);
d += SignedDegrees(30.0);
assert_eq!(d, Degrees(130.0));
}
#[test]
fn degrees_add_assign_signed_degrees_negative() {
let mut d = Degrees(100.0);
d += SignedDegrees(-30.0);
assert_eq!(d, Degrees(70.0));
}
#[test]
fn degrees_is_valid_still_works_after_refactor() {
assert!(Degrees(0.0).is_valid());
assert!(Degrees(180.0).is_valid());
assert!(Degrees(360.0).is_valid());
assert!(!Degrees(-0.1).is_valid());
assert!(!Degrees(f32::NAN).is_valid());
}
#[test]
fn cordic_magnitude_into_f32() {
assert_eq!(f32::from(CordicMagnitude(3.14)), 3.14);
}
#[test]
fn micros_isr_into_u32() {
assert_eq!(u32::from(MicrosIsr(1000)), 1000);
}
#[test]
fn angle_reading_magnitude_field() {
let reading = AngleReading {
angle: Degrees(90.0),
magnitude: CordicMagnitude(80.0),
};
let m = reading.magnitude;
assert!(approx_eq(m.0, 80.0), "expected ~80.0, got {}", m.0);
}
#[test]
fn angle_reading_magnitude_field_zero() {
let reading = AngleReading {
angle: Degrees(0.0),
magnitude: CordicMagnitude(0.0),
};
assert_eq!(reading.magnitude.0, 0.0);
}
#[test]
fn axis_try_from() {
for v in [Axis::X, Axis::Y, Axis::Z] {
assert_eq!(Axis::try_from(v as u8), Ok(v));
}
assert_eq!(Axis::try_from(0x03), Err(0x03));
}
#[test]
fn poles_try_from() {
for v in [PoleCount::Two, PoleCount::Three, PoleCount::Four] {
assert_eq!(PoleCount::try_from(v as u8), Ok(v));
}
assert_eq!(PoleCount::try_from(0x00), Err(0x00));
assert_eq!(PoleCount::try_from(0x05), Err(0x05));
}
#[test]
fn magnetic_channel_axis_count() {
assert_eq!(MagneticChannel::Off.axis_count(), 0);
assert_eq!(MagneticChannel::X.axis_count(), 1);
assert_eq!(MagneticChannel::XY.axis_count(), 2);
assert_eq!(MagneticChannel::XYZ.axis_count(), 3);
assert_eq!(MagneticChannel::XYX.axis_count(), 2);
assert_eq!(MagneticChannel::YXY.axis_count(), 2);
assert_eq!(MagneticChannel::YZY.axis_count(), 2);
assert_eq!(MagneticChannel::XZX.axis_count(), 2);
}
#[test]
fn magnetic_channel_measurement_slots() {
assert_eq!(MagneticChannel::Off.measurement_slots(), 0);
assert_eq!(MagneticChannel::X.measurement_slots(), 1);
assert_eq!(MagneticChannel::XY.measurement_slots(), 2);
assert_eq!(MagneticChannel::XYZ.measurement_slots(), 3);
assert_eq!(MagneticChannel::XYX.measurement_slots(), 3);
assert_eq!(MagneticChannel::YXY.measurement_slots(), 3);
assert_eq!(MagneticChannel::YZY.measurement_slots(), 3);
assert_eq!(MagneticChannel::XZX.measurement_slots(), 3);
}
#[test]
fn magnetic_channel_has_axes_all_variants() {
let cases: &[(MagneticChannel, bool, bool, bool)] = &[
(MagneticChannel::Off, false, false, false),
(MagneticChannel::X, true, false, false),
(MagneticChannel::Y, false, true, false),
(MagneticChannel::XY, true, true, false),
(MagneticChannel::Z, false, false, true),
(MagneticChannel::ZX, true, false, true),
(MagneticChannel::YZ, false, true, true),
(MagneticChannel::XYZ, true, true, true),
(MagneticChannel::XYX, true, true, false), (MagneticChannel::YXY, true, true, false), (MagneticChannel::YZY, false, true, true), (MagneticChannel::XZX, true, false, true), ];
for &(ch, x, y, z) in cases {
assert_eq!(ch.has_x(), x, "{ch:?}.has_x()");
assert_eq!(ch.has_y(), y, "{ch:?}.has_y()");
assert_eq!(ch.has_z(), z, "{ch:?}.has_z()");
}
}
#[test]
fn i2c_read_mode_try_from() {
for v in [
I2cReadMode::Standard,
I2cReadMode::OneByte16Bit,
I2cReadMode::OneByte8Bit,
] {
assert_eq!(I2cReadMode::try_from(v as u8), Ok(v));
}
assert_eq!(I2cReadMode::try_from(0x03), Err(0x03));
}
#[test]
fn fuzz_device_status_from_register() {
for byte in 0u8..=255 {
let s = DeviceStatus::from_register(byte);
let _ = s.vcc_undervoltage_error;
let _ = s.otp_crc_error;
let _ = s.int_error;
let _ = s.oscillator_error;
let _ = s.int_bar_readback_high;
let _ = s.is_ok();
}
let s = DeviceStatus::from_register(0x00);
assert!(!s.vcc_undervoltage_error);
assert!(!s.otp_crc_error);
assert!(!s.int_error);
assert!(!s.oscillator_error);
assert!(!s.int_bar_readback_high);
assert!(s.is_ok());
let s = DeviceStatus::from_register(0x1F);
assert!(s.vcc_undervoltage_error);
assert!(s.otp_crc_error);
assert!(s.int_error);
assert!(s.oscillator_error);
assert!(s.int_bar_readback_high);
assert!(!s.is_ok()); }
#[test]
fn fuzz_conversion_status_from_register() {
for byte in 0u8..=255 {
let s = ConversionStatus::from_register(byte);
assert!(
s.set_count <= 7,
"set_count out of range for byte 0x{byte:02X}: {}",
s.set_count
);
let _ = s.power_on_reset;
let _ = s.diag_status_fail;
let _ = s.result_status_ready;
let _ = s.result_status_ready();
}
let s = ConversionStatus::from_register(0x00);
assert_eq!(s.set_count, 0);
assert!(!s.power_on_reset);
assert!(!s.diag_status_fail);
assert!(!s.result_status_ready);
let s = ConversionStatus::from_register(0xFF);
assert_eq!(s.set_count, 7);
assert!(s.power_on_reset);
assert!(s.diag_status_fail);
assert!(s.result_status_ready);
}
fn assert_exhaustive_try_from<T>(name: &str, expected_valid: usize, to_u8: impl Fn(T) -> u8)
where
T: TryFrom<u8, Error = u8> + Copy + core::fmt::Debug + PartialEq,
{
let mut valid = 0usize;
for byte in 0..=255u8 {
match T::try_from(byte) {
Ok(v) => {
let back = to_u8(v);
assert_eq!(
back, byte,
"{name}: round-trip failed for Ok({v:?}): {back} != {byte}"
);
valid += 1;
}
Err(e) => {
assert_eq!(e, byte, "{name}: Err payload mismatch: {e} != {byte}");
}
}
}
assert_eq!(
valid, expected_valid,
"{name}: expected {expected_valid} valid variants, got {valid}"
);
}
#[test]
fn exhaustive_i2c_read_mode() {
assert_exhaustive_try_from::<I2cReadMode>("I2cReadMode", 3, |v| v as u8);
}
#[test]
fn exhaustive_operating_mode() {
assert_exhaustive_try_from::<OperatingMode>("OperatingMode", 4, |v| v as u8);
}
#[test]
fn exhaustive_axis() {
assert_exhaustive_try_from::<Axis>("Axis", 3, |v| v as u8);
}
#[test]
fn exhaustive_poles() {
assert_exhaustive_try_from::<PoleCount>("PoleCount", 3, |v| v as u8);
}
#[test]
fn exhaustive_magnetic_channel() {
assert_exhaustive_try_from::<MagneticChannel>("MagneticChannel", 12, |v| v as u8);
}
#[test]
fn exhaustive_conversion_average() {
assert_exhaustive_try_from::<ConversionAverage>("ConversionAverage", 6, |v| v as u8);
}
#[test]
fn exhaustive_sleep_time() {
assert_exhaustive_try_from::<SleepTime>("SleepTime", 13, |v| v as u8);
}
#[test]
fn exhaustive_power_noise_mode() {
assert_exhaustive_try_from::<PowerNoiseMode>("PowerNoiseMode", 2, |v| v as u8);
}
#[test]
fn exhaustive_trigger_mode() {
assert_exhaustive_try_from::<TriggerMode>("TriggerMode", 2, |v| v as u8);
}
#[test]
fn exhaustive_magnetic_temp_coefficient() {
assert_exhaustive_try_from::<MagneticTempCoefficient>("MagneticTempCoefficient", 3, |v| {
v as u8
});
}
#[test]
fn exhaustive_angle_enabled() {
assert_exhaustive_try_from::<AngleEnable>("AngleEnable", 4, |v| v as u8);
}
#[test]
fn exhaustive_threshold_direction() {
assert_exhaustive_try_from::<MagneticThresholdDirection>("ThresholdDirection", 2, |v| {
v as u8
});
}
#[test]
fn exhaustive_threshold_crossing_count() {
assert_exhaustive_try_from::<ThresholdCrossingCount>("ThresholdCrossingCount", 2, |v| {
v as u8
});
}
#[test]
fn temp_threshold_disabled_is_zero() {
assert_eq!(TempThresholdConfig::DISABLED.code(), 0);
assert!(!TempThresholdConfig::DISABLED.is_enabled());
}
#[test]
fn temp_threshold_valid_range_boundaries() {
assert!(TempThresholdConfig::from_code(0x19).is_none());
assert!(TempThresholdConfig::from_code(0x1A).is_some());
assert!(TempThresholdConfig::from_code(0x34).is_some());
assert!(TempThresholdConfig::from_code(0x35).is_none());
}
#[test]
fn temp_threshold_all_valid_codes_accepted() {
assert!(TempThresholdConfig::try_from(0u8).is_ok());
for code in 0x1A..=0x34u8 {
assert!(
TempThresholdConfig::try_from(code).is_ok(),
"code 0x{code:02X} should be valid"
);
}
}
#[test]
fn temp_threshold_invalid_codes_rejected() {
for code in 1..0x1Au8 {
assert!(
TempThresholdConfig::try_from(code).is_err(),
"code 0x{code:02X} should be invalid"
);
}
for code in 0x35..=0x7Fu8 {
assert!(
TempThresholdConfig::try_from(code).is_err(),
"code 0x{code:02X} should be invalid"
);
}
}
#[test]
fn temp_threshold_default_is_disabled() {
let t = TempThresholdConfig::default();
assert_eq!(t, TempThresholdConfig::DISABLED);
}
#[test]
fn exhaustive_interrupt_mode() {
assert_exhaustive_try_from::<InterruptMode>("InterruptMode", 5, |v| v as u8);
}
#[test]
fn exhaustive_int_pin_behavior() {
assert_exhaustive_try_from::<InterruptState>("InterruptState", 2, |v| v as u8);
}
#[test]
fn exhaustive_gain_channel() {
assert_exhaustive_try_from::<MagneticGainChannel>("MagneticGainChannel", 2, |v| v as u8);
}
#[test]
fn exhaustive_range() {
assert_exhaustive_try_from::<Range>("Range", 2, |v| v as u8);
}
#[test]
fn linux_xref_averaging_modes() {
let table: &[(ConversionAverage, u8, &str)] = &[
(ConversionAverage::X1, 0x00, "TMAG5273_AVG_1"),
(ConversionAverage::X2, 0x04, "TMAG5273_AVG_2"),
(ConversionAverage::X4, 0x08, "TMAG5273_AVG_4"),
(ConversionAverage::X8, 0x0C, "TMAG5273_AVG_8"),
(ConversionAverage::X16, 0x10, "TMAG5273_AVG_16"),
(ConversionAverage::X32, 0x14, "TMAG5273_AVG_32"),
];
for &(variant, linux_shifted, name) in table {
let shifted = (variant as u8) << CONV_AVG_SHIFT;
assert_eq!(
shifted, linux_shifted,
"averaging mode mismatch for {name}: (rust {variant:?} as u8) << {CONV_AVG_SHIFT} = 0x{shifted:02X}, linux=0x{linux_shifted:02X}"
);
}
}
#[test]
fn linux_xref_operating_modes() {
let table: &[(OperatingMode, u8, &str)] = &[
(OperatingMode::Standby, 0, "TMAG5273_OP_MODE_STANDBY"),
(OperatingMode::Sleep, 1, "TMAG5273_OP_MODE_SLEEP"),
(
OperatingMode::ContinuousMeasure,
2,
"TMAG5273_OP_MODE_CONTINUOUS",
),
(OperatingMode::WakeUpAndSleep, 3, "TMAG5273_OP_MODE_WAKEUP"),
];
for &(variant, linux_value, name) in table {
assert_eq!(
variant as u8, linux_value,
"operating mode mismatch for {name}: rust={}, linux={linux_value}",
variant as u8
);
}
}
#[test]
fn linux_xref_angle_enable() {
let table: &[(AngleEnable, u8, &str)] = &[
(AngleEnable::None, 0, "TMAG5273_ANGLE_EN_OFF"),
(AngleEnable::XY, 1, "TMAG5273_ANGLE_EN_X_Y"),
(AngleEnable::YZ, 2, "TMAG5273_ANGLE_EN_Y_Z"),
(AngleEnable::XZ, 3, "TMAG5273_ANGLE_EN_X_Z"),
];
for &(variant, linux_value, name) in table {
assert_eq!(
variant as u8, linux_value,
"angle enable mismatch for {name}: rust={}, linux={linux_value}",
variant as u8
);
}
}
#[test]
fn linux_xref_mag_channel_xyz() {
assert_eq!(
MagneticChannel::XYZ as u8,
7,
"MagneticChannel::XYZ should be 7 (TMAG5273_MAG_CH_EN_X_Y_Z)"
);
}
#[test]
fn linux_xref_magnetic_scale() {
let table: &[(f64, f64, &str)] = &[
(40.0, 12200.0, "v1 low (40 mT)"),
(80.0, 24400.0, "v1 high (80 mT)"),
(133.0, 40600.0, "v2 low (133 mT)"),
(266.0, 81200.0, "v2 high (266 mT)"),
];
for &(range, linux_ugauss_per_lsb, name) in table {
let our_ugauss_per_lsb = range * 10_000_000.0 / 32768.0;
let rel_error =
((our_ugauss_per_lsb - linux_ugauss_per_lsb) / linux_ugauss_per_lsb).abs();
assert!(
rel_error < 0.005,
"magnetic scale mismatch for {name}: ours={our_ugauss_per_lsb:.2}, linux={linux_ugauss_per_lsb}, error={:.3}%",
rel_error * 100.0
);
}
}
#[test]
fn linux_xref_temp_formula() {
let table: &[(i16, f64, &str)] = &[
(0, -266.305, "raw=0"),
(17508, 25.008, "raw=17508 (reference)"),
(32767, 278.902, "raw=i16::MAX"),
];
for &(raw, linux_millideg_approx, name) in table {
let linux_celsius = ((raw as i64 - 16005) * 10000) as f64 / 601.0 / 1000.0;
assert!(
(linux_celsius - linux_millideg_approx).abs() < 0.01,
"Linux formula sanity check failed for {name}: computed={linux_celsius}, expected={linux_millideg_approx}"
);
let our_celsius = 25.0_f64 + (raw as f64 - 17508.0) / 60.1;
let diff = (our_celsius - linux_celsius).abs();
assert!(
diff < 0.5,
"temperature formula mismatch for {name}: ours={our_celsius:.3}°C, linux={linux_celsius:.3}°C, diff={diff:.3}°C"
);
}
}
#[test]
fn max_polls_all_variants() {
assert_eq!(ConversionAverage::X1.max_polls(), 10);
assert_eq!(ConversionAverage::X2.max_polls(), 15);
assert_eq!(ConversionAverage::X4.max_polls(), 25);
assert_eq!(ConversionAverage::X8.max_polls(), 40);
assert_eq!(ConversionAverage::X16.max_polls(), 70);
assert_eq!(ConversionAverage::X32.max_polls(), 100);
}
#[test]
fn lower_full_chain() {
assert_eq!(ConversionAverage::X32.lower(), Some(ConversionAverage::X16));
assert_eq!(ConversionAverage::X16.lower(), Some(ConversionAverage::X8));
assert_eq!(ConversionAverage::X8.lower(), Some(ConversionAverage::X4));
assert_eq!(ConversionAverage::X4.lower(), Some(ConversionAverage::X2));
assert_eq!(ConversionAverage::X2.lower(), Some(ConversionAverage::X1));
}
#[test]
fn lower_x1_is_none() {
assert_eq!(ConversionAverage::X1.lower(), None);
}
#[test]
fn lower_chain_has_exactly_5_steps() {
let mut current = ConversionAverage::X32;
let mut steps = 0u32;
while let Some(next) = current.lower() {
steps += 1;
current = next;
}
assert_eq!(steps, 5);
assert_eq!(current, ConversionAverage::X1);
}
#[test]
fn sleep_time_as_micros_isr_all_variants() {
assert_eq!(SleepTime::Ms1.as_micros_isr(), MicrosIsr(1_000));
assert_eq!(SleepTime::Ms5.as_micros_isr(), MicrosIsr(5_000));
assert_eq!(SleepTime::Ms10.as_micros_isr(), MicrosIsr(10_000));
assert_eq!(SleepTime::Ms15.as_micros_isr(), MicrosIsr(15_000));
assert_eq!(SleepTime::Ms20.as_micros_isr(), MicrosIsr(20_000));
assert_eq!(SleepTime::Ms30.as_micros_isr(), MicrosIsr(30_000));
assert_eq!(SleepTime::Ms50.as_micros_isr(), MicrosIsr(50_000));
assert_eq!(SleepTime::Ms100.as_micros_isr(), MicrosIsr(100_000));
assert_eq!(SleepTime::Ms500.as_micros_isr(), MicrosIsr(500_000));
assert_eq!(SleepTime::Ms1000.as_micros_isr(), MicrosIsr(1_000_000));
assert_eq!(SleepTime::Ms2000.as_micros_isr(), MicrosIsr(2_000_000));
assert_eq!(SleepTime::Ms5000.as_micros_isr(), MicrosIsr(5_000_000));
assert_eq!(SleepTime::Ms20000.as_micros_isr(), MicrosIsr(20_000_000));
}
#[test]
fn crossings_default_and_ordering() {
assert_eq!(Crossings::default(), Crossings(0));
assert!(Crossings(1) < Crossings(2));
assert_eq!(Crossings(42).0, 42);
}
#[test]
fn crossings_from_u32() {
assert_eq!(u32::from(Crossings(0)), 0_u32);
assert_eq!(u32::from(Crossings(42)), 42_u32);
assert_eq!(u32::from(Crossings(u32::MAX)), u32::MAX);
}
#[test]
fn crossings_saturating_add() {
assert_eq!(Crossings(0).saturating_add(1), Crossings(1));
assert_eq!(Crossings(10).saturating_add(5), Crossings(15));
assert_eq!(Crossings(u32::MAX).saturating_add(1), Crossings(u32::MAX));
assert_eq!(
Crossings(u32::MAX - 1).saturating_add(2),
Crossings(u32::MAX)
);
}
#[cfg(feature = "libm")]
mod to_raw {
use super::*;
#[test]
fn millitesla_round_trip_zero() {
let ranges = [40.0, 80.0, 133.0, 266.0];
for r in ranges {
let range = MilliTesla(r);
assert_eq!(MilliTesla::from_raw(0, range).to_raw(range), Some(0));
}
}
#[test]
fn millitesla_round_trip_one() {
let range = MilliTesla(40.0);
assert_eq!(MilliTesla::from_raw(1, range).to_raw(range), Some(1));
}
#[test]
fn millitesla_round_trip_neg_one() {
let range = MilliTesla(40.0);
assert_eq!(MilliTesla::from_raw(-1, range).to_raw(range), Some(-1));
}
#[test]
fn millitesla_round_trip_extremes() {
let range = MilliTesla(40.0);
assert_eq!(
MilliTesla::from_raw(i16::MAX, range).to_raw(range),
Some(i16::MAX)
);
assert_eq!(
MilliTesla::from_raw(i16::MIN, range).to_raw(range),
Some(i16::MIN)
);
}
#[test]
fn millitesla_round_trip_midpoint() {
let range = MilliTesla(80.0);
assert_eq!(
MilliTesla::from_raw(16384, range).to_raw(range),
Some(16384)
);
}
#[test]
fn millitesla_round_trip_all_ranges() {
let ranges = [40.0, 80.0, 133.0, 266.0];
let raws: [i16; 6] = [0, 1, -1, i16::MAX, i16::MIN, 16384];
for r in ranges {
let range = MilliTesla(r);
for raw in raws {
assert_eq!(
MilliTesla::from_raw(raw, range).to_raw(range),
Some(raw),
"MilliTesla round-trip failed: raw={raw}, range={r}"
);
}
}
}
#[test]
fn millitesla_overflow_returns_none() {
assert_eq!(MilliTesla(100.0).to_raw(MilliTesla(40.0)), None);
}
#[test]
fn millitesla_nan_returns_none() {
assert_eq!(MilliTesla(f32::NAN).to_raw(MilliTesla(40.0)), None);
}
#[test]
fn millitesla_infinity_returns_none() {
assert_eq!(MilliTesla(f32::INFINITY).to_raw(MilliTesla(40.0)), None);
assert_eq!(MilliTesla(f32::NEG_INFINITY).to_raw(MilliTesla(40.0)), None);
}
#[test]
fn millitesla_zero_range_returns_none() {
assert_eq!(MilliTesla(0.0).to_raw(MilliTesla(0.0)), None);
}
#[test]
fn millitesla_nan_range_returns_none() {
assert_eq!(MilliTesla(1.0).to_raw(MilliTesla(f32::NAN)), None);
}
#[test]
fn cordic_magnitude_round_trip_key_values() {
let range = MilliTesla(80.0);
for raw in [0_u8, 1, 128, 181, 255] {
assert_eq!(
CordicMagnitude::from_raw(raw, range).to_raw(range),
Some(raw),
"CordicMagnitude round-trip failed: raw={raw}"
);
}
}
#[test]
fn cordic_magnitude_negative_returns_none() {
assert_eq!(CordicMagnitude(-1.0).to_raw(MilliTesla(80.0)), None);
}
#[test]
fn cordic_magnitude_nan_returns_none() {
assert_eq!(CordicMagnitude(f32::NAN).to_raw(MilliTesla(80.0)), None);
}
#[test]
fn cordic_magnitude_zero_range_returns_none() {
assert_eq!(CordicMagnitude(1.0).to_raw(MilliTesla(0.0)), None);
}
#[test]
fn celsius_round_trip_reference_point() {
assert_eq!(Celsius::from_raw(17508).to_raw(), Some(17508));
}
#[test]
fn celsius_round_trip_zero() {
assert_eq!(Celsius::from_raw(0).to_raw(), Some(0));
}
#[test]
fn celsius_round_trip_extremes() {
assert_eq!(Celsius::from_raw(i16::MIN).to_raw(), Some(i16::MIN));
assert_eq!(Celsius::from_raw(i16::MAX).to_raw(), Some(i16::MAX));
}
#[test]
fn celsius_nan_returns_none() {
assert_eq!(Celsius(f32::NAN).to_raw(), None);
}
#[test]
fn celsius_infinity_returns_none() {
assert_eq!(Celsius(f32::INFINITY).to_raw(), None);
assert_eq!(Celsius(f32::NEG_INFINITY).to_raw(), None);
}
#[test]
fn degrees_round_trip_zero() {
assert_eq!(Degrees::from_raw(0x0000).unwrap().to_raw(), Some(0x0000));
}
#[test]
fn degrees_round_trip_180() {
assert_eq!(Degrees::from_raw(0x0B40).unwrap().to_raw(), Some(0x0B40));
}
#[test]
fn degrees_round_trip_360() {
assert_eq!(Degrees::from_raw(0x1680).unwrap().to_raw(), Some(0x1680));
}
#[test]
fn degrees_round_trip_fractional() {
assert_eq!(Degrees::from_raw(0x05A8).unwrap().to_raw(), Some(0x05A8));
}
#[test]
fn degrees_negative_returns_none() {
assert_eq!(Degrees(-1.0).to_raw(), None);
}
#[test]
fn degrees_above_360_returns_none() {
assert_eq!(Degrees(361.0).to_raw(), None);
}
#[test]
fn degrees_nan_returns_none() {
assert_eq!(Degrees(f32::NAN).to_raw(), None);
}
#[test]
fn degrees_infinity_returns_none() {
assert_eq!(Degrees(f32::INFINITY).to_raw(), None);
}
}
#[cfg(feature = "libm")]
mod exhaustive_round_trip {
use super::*;
#[test]
fn millitesla_exhaustive_round_trip() {
let ranges = [
MilliTesla(40.0),
MilliTesla(80.0),
MilliTesla(133.0),
MilliTesla(266.0),
];
for range in ranges {
for raw in i16::MIN..=i16::MAX {
let mt = MilliTesla::from_raw(raw, range);
assert_eq!(
mt.to_raw(range),
Some(raw),
"MilliTesla round-trip failed: raw={raw}, range={}",
range.0,
);
}
}
}
#[test]
fn cordic_magnitude_exhaustive_round_trip() {
let ranges = [
MilliTesla(40.0),
MilliTesla(80.0),
MilliTesla(133.0),
MilliTesla(266.0),
];
for range in ranges {
for raw in 0..=u8::MAX {
let cm = CordicMagnitude::from_raw(raw, range);
assert_eq!(
cm.to_raw(range),
Some(raw),
"CordicMagnitude round-trip failed: raw={raw}, range={}",
range.0,
);
}
}
}
#[test]
fn celsius_exhaustive_round_trip() {
for raw in i16::MIN..=i16::MAX {
let c = Celsius::from_raw(raw);
assert_eq!(
c.to_raw(),
Some(raw),
"Celsius round-trip failed: raw={raw}, celsius={}",
c.0,
);
}
}
#[test]
fn degrees_exhaustive_round_trip() {
for raw in 0u16..=0x1680 {
if let Some(d) = Degrees::from_raw(raw) {
assert_eq!(
d.to_raw(),
Some(raw),
"Degrees round-trip failed: raw=0x{raw:04X}, degrees={}",
d.0,
);
}
}
}
}
}