use crate::error::ConfigError;
use crate::register;
use crate::types::{
AngleEnable, ConversionAverage, I2cReadMode, InterruptMode, InterruptState, Lsb,
MagneticChannel, MagneticGainChannel, MagneticTempCoefficient, MagneticThresholdDirection,
MicrosIsr, MilliTesla, OperatingMode, PowerNoiseMode, Range, SleepTime, TempThresholdConfig,
ThresholdCrossingCount, TriggerMode,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[must_use]
#[non_exhaustive]
pub struct Config {
pub operating_mode: OperatingMode,
pub magnetic_channels_enabled: MagneticChannel,
pub temp_channel_enabled: bool,
pub angle_enabled: AngleEnable,
pub conversion_average: ConversionAverage,
pub xy_range: Range,
pub z_range: Range,
pub power_noise_mode: PowerNoiseMode,
pub sleep_time: SleepTime,
pub trigger_mode: TriggerMode,
pub i2c_glitch_filter: bool,
pub magnetic_temp_coefficient: MagneticTempCoefficient,
}
#[derive(Debug, Clone, Copy)]
pub struct ConfigBuilder {
operating_mode: OperatingMode,
magnetic_channels_enabled: MagneticChannel,
temp_channel_enabled: bool,
angle_enabled: AngleEnable,
conversion_average: ConversionAverage,
xy_range: Range,
z_range: Range,
power_noise_mode: PowerNoiseMode,
sleep_time: SleepTime,
trigger_mode: TriggerMode,
i2c_glitch_filter: bool,
magnetic_temp_coefficient: MagneticTempCoefficient,
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl ConfigBuilder {
pub const fn new() -> Self {
Self {
operating_mode: OperatingMode::ContinuousMeasure,
magnetic_channels_enabled: MagneticChannel::XYZ,
temp_channel_enabled: true,
angle_enabled: AngleEnable::None,
conversion_average: ConversionAverage::X1,
xy_range: Range::High,
z_range: Range::High,
power_noise_mode: PowerNoiseMode::LowActiveCurrent,
sleep_time: SleepTime::Ms1,
trigger_mode: TriggerMode::I2cCommand,
i2c_glitch_filter: true,
magnetic_temp_coefficient: MagneticTempCoefficient::None,
}
}
pub const fn operating_mode(mut self, mode: OperatingMode) -> Self {
self.operating_mode = mode;
self
}
pub const fn magnetic_channels_enabled(mut self, magnetic_channels: MagneticChannel) -> Self {
self.magnetic_channels_enabled = magnetic_channels;
self
}
pub const fn temp_channel_enabled(mut self, enabled: bool) -> Self {
self.temp_channel_enabled = enabled;
self
}
pub const fn angle_enabled(mut self, enabled: AngleEnable) -> Self {
self.angle_enabled = enabled;
self
}
pub const fn conversion_average(mut self, conversion_average: ConversionAverage) -> Self {
self.conversion_average = conversion_average;
self
}
pub const fn xy_range(mut self, range: Range) -> Self {
self.xy_range = range;
self
}
pub const fn z_range(mut self, range: Range) -> Self {
self.z_range = range;
self
}
pub const fn power_noise_mode(mut self, mode: PowerNoiseMode) -> Self {
self.power_noise_mode = mode;
self
}
pub const fn sleep_time(mut self, sleep_time: SleepTime) -> Self {
self.sleep_time = sleep_time;
self
}
pub const fn trigger_mode(mut self, mode: TriggerMode) -> Self {
self.trigger_mode = mode;
self
}
pub const fn i2c_glitch_filter(mut self, enabled: bool) -> Self {
self.i2c_glitch_filter = enabled;
self
}
pub const fn magnetic_temp_coefficient(mut self, coefficient: MagneticTempCoefficient) -> Self {
self.magnetic_temp_coefficient = coefficient;
self
}
pub fn build(self) -> Result<Config, ConfigError> {
if !matches!(self.angle_enabled, AngleEnable::None)
&& self.magnetic_channels_enabled.axis_count() < 2
{
return Err(ConfigError::AngleRequiresTwoChannels);
}
if matches!(self.trigger_mode, TriggerMode::IntPin)
&& !matches!(self.operating_mode, OperatingMode::Standby)
{
return Err(ConfigError::IntPinTriggerRequiresStandby);
}
if matches!(self.angle_enabled, AngleEnable::YZ | AngleEnable::XZ)
&& self.xy_range != self.z_range
{
return Err(ConfigError::AngleMixedRanges);
}
if matches!(self.operating_mode, OperatingMode::WakeUpAndSleep) {
let sleep = self.sleep_time.as_micros_isr();
let conversion = self
.conversion_average
.conversion_time(self.magnetic_channels_enabled, self.temp_channel_enabled);
if sleep.0 < conversion.0 {
return Err(ConfigError::SleepShorterThanConversion { sleep, conversion });
}
}
Ok(Config {
operating_mode: self.operating_mode,
magnetic_channels_enabled: self.magnetic_channels_enabled,
temp_channel_enabled: self.temp_channel_enabled,
angle_enabled: self.angle_enabled,
conversion_average: self.conversion_average,
xy_range: self.xy_range,
z_range: self.z_range,
power_noise_mode: self.power_noise_mode,
sleep_time: self.sleep_time,
trigger_mode: self.trigger_mode,
i2c_glitch_filter: self.i2c_glitch_filter,
magnetic_temp_coefficient: self.magnetic_temp_coefficient,
})
}
}
impl Config {
pub const fn conversion_time(&self) -> MicrosIsr {
self.conversion_average
.conversion_time(self.magnetic_channels_enabled, self.temp_channel_enabled)
}
pub(crate) fn to_register_writes(self) -> [(u8, u8); 5] {
#[cfg(feature = "crc")]
let crc_en: u8 = 1;
#[cfg(not(feature = "crc"))]
let crc_en: u8 = 0;
let dc1 = register::insert(0, register::CRC_EN_MASK, register::CRC_EN_SHIFT, crc_en)
| register::insert(
0,
register::MAG_TEMPCO_MASK,
register::MAG_TEMPCO_SHIFT,
self.magnetic_temp_coefficient as u8,
)
| register::insert(
0,
register::CONV_AVG_MASK,
register::CONV_AVG_SHIFT,
self.conversion_average as u8,
)
| register::insert(
0,
register::I2C_RD_MASK,
register::I2C_RD_SHIFT,
I2cReadMode::Standard as u8,
);
let dc2 = register::insert(
0,
register::LP_LN_MASK,
register::LP_LN_SHIFT,
self.power_noise_mode as u8,
) | register::insert(
0,
register::I2C_GLITCH_FILTER_MASK,
register::I2C_GLITCH_FILTER_SHIFT,
u8::from(!self.i2c_glitch_filter), ) | register::insert(
0,
register::TRIGGER_MODE_MASK,
register::TRIGGER_MODE_SHIFT,
self.trigger_mode as u8,
) | register::insert(
0,
register::OPERATING_MODE_MASK,
register::OPERATING_MODE_SHIFT,
self.operating_mode as u8,
);
let sc1 = register::insert(
0,
register::MAG_CH_EN_MASK,
register::MAG_CH_EN_SHIFT,
self.magnetic_channels_enabled as u8,
) | register::insert(
0,
register::SLEEPTIME_MASK,
register::SLEEPTIME_SHIFT,
self.sleep_time as u8,
);
let sc2 = register::insert(
0,
register::ANGLE_EN_MASK,
register::ANGLE_EN_SHIFT,
self.angle_enabled as u8,
) | register::insert(
0,
register::X_Y_RANGE_MASK,
register::X_Y_RANGE_SHIFT,
self.xy_range as u8,
) | register::insert(
0,
register::Z_RANGE_MASK,
register::Z_RANGE_SHIFT,
self.z_range as u8,
);
let tc = register::insert(
0,
register::T_CH_EN_MASK,
register::T_CH_EN_SHIFT,
u8::from(self.temp_channel_enabled),
);
[
(register::DEVICE_CONFIG_1, dc1),
(register::DEVICE_CONFIG_2, dc2),
(register::SENSOR_CONFIG_1, sc1),
(register::SENSOR_CONFIG_2, sc2),
(register::T_CONFIG, tc),
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[repr(u8)]
pub enum ThresholdHysteresis {
LimitCross = 0,
SymmetricBand = 1,
}
impl_try_from_u8!(ThresholdHysteresis {
0 => LimitCross,
1 => SymmetricBand,
});
impl ThresholdHysteresis {
#[inline]
pub fn decode_threshold(&self, lsb: Lsb, range: MilliTesla) -> MilliTesla {
let raw = match self {
Self::LimitCross => lsb.0 as f32,
Self::SymmetricBand => (lsb.0 as u8 & register::LSB_7BIT_MASK) as f32,
};
MilliTesla(raw * range.mt_per_lsb())
}
}
#[cfg(feature = "libm")]
impl ThresholdHysteresis {
#[inline]
pub fn encode_threshold(&self, mt: MilliTesla, range: MilliTesla, margin: i8) -> Option<Lsb> {
if !mt.0.is_finite() || !range.0.is_finite() || range.0 == 0.0 {
return None;
}
match self {
Self::LimitCross => {
let scaled = libm::roundf(mt.0 * range.lsb_per_mt()) as i32;
let with_margin = scaled + i32::from(margin);
Some(Lsb(with_margin.clamp(-128, 127) as i8))
}
Self::SymmetricBand => {
if margin < 0 {
return None;
}
let scaled = libm::roundf(mt.abs().0 * range.lsb_per_mt()) as i32;
let with_margin = scaled + i32::from(margin);
Some(Lsb(with_margin.clamp(0, 127) as i8))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct ThresholdConfig {
pub x: Lsb,
pub y: Lsb,
pub z: Lsb,
pub temperature: TempThresholdConfig,
pub hysteresis: ThresholdHysteresis,
pub crossing_count: ThresholdCrossingCount,
pub direction: MagneticThresholdDirection,
}
impl Default for ThresholdConfig {
fn default() -> Self {
Self {
x: Lsb(0),
y: Lsb(0),
z: Lsb(0),
temperature: TempThresholdConfig::DISABLED,
hysteresis: ThresholdHysteresis::LimitCross,
crossing_count: ThresholdCrossingCount::One,
direction: MagneticThresholdDirection::Above,
}
}
}
#[cfg(feature = "libm")]
impl ThresholdConfig {
#[expect(
clippy::too_many_arguments,
reason = "mirrors ThresholdConfig fields 1:1 — a builder would add complexity without value"
)]
pub fn from_mt(
baselines: [MilliTesla; 3],
range_xy: MilliTesla,
range_z: MilliTesla,
margin: i8,
hysteresis: ThresholdHysteresis,
direction: MagneticThresholdDirection,
crossing_count: ThresholdCrossingCount,
temperature: TempThresholdConfig,
) -> Option<Self> {
let x = hysteresis.encode_threshold(baselines[0], range_xy, margin)?;
let y = hysteresis.encode_threshold(baselines[1], range_xy, margin)?;
let z = hysteresis.encode_threshold(baselines[2], range_z, margin)?;
Some(Self {
x,
y,
z,
temperature,
hysteresis,
crossing_count,
direction,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct InterruptConfig {
pub mode: InterruptMode,
pub on_conversion_complete: bool,
pub on_threshold_crossing: bool,
pub pin_behavior: InterruptState,
pub mask: bool,
}
impl Default for InterruptConfig {
fn default() -> Self {
Self {
mode: InterruptMode::None,
on_conversion_complete: false,
on_threshold_crossing: false,
pin_behavior: InterruptState::Latched,
mask: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct CalibrationConfig {
pub gain: u8,
pub offset_1: i8,
pub offset_2: i8,
pub gain_channel: MagneticGainChannel,
}
impl Default for CalibrationConfig {
fn default() -> Self {
Self {
gain: 0,
offset_1: 0,
offset_2: 0,
gain_channel: MagneticGainChannel::First,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::register;
#[test]
fn builder_defaults_match_sparkfun_begin() {
let config = ConfigBuilder::new().build().unwrap();
assert_eq!(config.operating_mode, OperatingMode::ContinuousMeasure);
assert_eq!(config.magnetic_channels_enabled, MagneticChannel::XYZ);
assert!(config.temp_channel_enabled);
assert_eq!(config.angle_enabled, AngleEnable::None);
assert_eq!(config.xy_range, Range::High);
assert_eq!(config.z_range, Range::High);
assert_eq!(config.power_noise_mode, PowerNoiseMode::LowActiveCurrent);
}
#[test]
fn angle_requires_two_channels_x_only() {
let result = ConfigBuilder::new()
.magnetic_channels_enabled(MagneticChannel::X)
.angle_enabled(AngleEnable::XY)
.build();
assert_eq!(result.unwrap_err(), ConfigError::AngleRequiresTwoChannels);
}
#[test]
fn angle_with_two_channels_ok() {
let result = ConfigBuilder::new()
.magnetic_channels_enabled(MagneticChannel::XY)
.angle_enabled(AngleEnable::XY)
.build();
assert!(result.is_ok());
}
#[test]
fn build_wakeup_sleep_accepts_ms1_with_default_x1_averaging() {
let result = ConfigBuilder::new()
.operating_mode(OperatingMode::WakeUpAndSleep)
.sleep_time(SleepTime::Ms1)
.build();
assert!(result.is_ok());
}
#[test]
fn register_writes_default_config() {
let config = ConfigBuilder::new().build().unwrap();
let writes = config.to_register_writes();
assert_eq!(writes.len(), 5);
let sc1 = writes.iter().find(|(addr, _)| *addr == 0x02).unwrap().1;
assert_eq!(sc1 & 0xF0, 0x70);
let sc2 = writes.iter().find(|(addr, _)| *addr == 0x03).unwrap().1;
assert_eq!(sc2 & 0x03, 0x03); assert_eq!(sc2 & 0x0C, 0x00);
let tc = writes
.iter()
.find(|(addr, _)| *addr == register::T_CONFIG)
.unwrap()
.1;
assert_eq!(tc & 0x01, 0x01);
}
#[test]
fn conversion_time_default_config_returns_100us() {
let config = ConfigBuilder::new().build().unwrap();
assert_eq!(config.conversion_time().0, 100);
}
#[test]
fn build_wakeup_sleep_rejects_ms1_when_x32_xyz_temp_exceeds_1000us() {
let result = ConfigBuilder::new()
.operating_mode(OperatingMode::WakeUpAndSleep)
.sleep_time(SleepTime::Ms1)
.conversion_average(ConversionAverage::X32)
.magnetic_channels_enabled(MagneticChannel::XYZ)
.temp_channel_enabled(true)
.build();
assert_eq!(
result.unwrap_err(),
ConfigError::SleepShorterThanConversion {
sleep: MicrosIsr(1_000),
conversion: MicrosIsr(3_225),
},
);
}
#[test]
fn build_wakeup_sleep_accepts_ms5_with_x32_xyz_temp() {
let result = ConfigBuilder::new()
.operating_mode(OperatingMode::WakeUpAndSleep)
.sleep_time(SleepTime::Ms5)
.conversion_average(ConversionAverage::X32)
.magnetic_channels_enabled(MagneticChannel::XYZ)
.temp_channel_enabled(true)
.build();
assert!(result.is_ok());
}
#[test]
fn int_pin_trigger_with_standby_ok() {
let result = ConfigBuilder::new()
.operating_mode(OperatingMode::Standby)
.trigger_mode(TriggerMode::IntPin)
.build();
assert!(result.is_ok());
}
#[test]
fn int_pin_trigger_with_continuous_rejected() {
let result = ConfigBuilder::new()
.operating_mode(OperatingMode::ContinuousMeasure)
.trigger_mode(TriggerMode::IntPin)
.build();
assert_eq!(
result.unwrap_err(),
ConfigError::IntPinTriggerRequiresStandby,
);
}
#[test]
fn int_pin_trigger_with_wakeup_sleep_rejected() {
let result = ConfigBuilder::new()
.operating_mode(OperatingMode::WakeUpAndSleep)
.trigger_mode(TriggerMode::IntPin)
.sleep_time(SleepTime::Ms5)
.build();
assert_eq!(
result.unwrap_err(),
ConfigError::IntPinTriggerRequiresStandby,
);
}
#[test]
fn i2c_command_trigger_with_continuous_ok() {
let result = ConfigBuilder::new()
.operating_mode(OperatingMode::ContinuousMeasure)
.trigger_mode(TriggerMode::I2cCommand)
.build();
assert!(result.is_ok());
}
#[test]
fn angle_yz_rejects_mixed_ranges() {
let result = ConfigBuilder::new()
.magnetic_channels_enabled(MagneticChannel::YZ)
.angle_enabled(AngleEnable::YZ)
.xy_range(Range::Low)
.z_range(Range::High)
.build();
assert_eq!(result.unwrap_err(), ConfigError::AngleMixedRanges);
}
#[test]
fn angle_xz_rejects_mixed_ranges() {
let result = ConfigBuilder::new()
.magnetic_channels_enabled(MagneticChannel::ZX)
.angle_enabled(AngleEnable::XZ)
.xy_range(Range::High)
.z_range(Range::Low)
.build();
assert_eq!(result.unwrap_err(), ConfigError::AngleMixedRanges);
}
#[test]
fn angle_yz_accepts_matching_ranges() {
let result = ConfigBuilder::new()
.magnetic_channels_enabled(MagneticChannel::YZ)
.angle_enabled(AngleEnable::YZ)
.xy_range(Range::High)
.z_range(Range::High)
.build();
assert!(result.is_ok());
}
#[test]
fn angle_xy_ignores_z_range_mismatch() {
let result = ConfigBuilder::new()
.magnetic_channels_enabled(MagneticChannel::XY)
.angle_enabled(AngleEnable::XY)
.xy_range(Range::High)
.z_range(Range::Low)
.build();
assert!(result.is_ok());
}
#[test]
fn decode_threshold_limit_cross_zero_lsb() {
let mt = ThresholdHysteresis::LimitCross.decode_threshold(Lsb(0), MilliTesla(80.0));
assert_eq!(mt, MilliTesla(0.0));
}
#[test]
fn decode_threshold_limit_cross_positive_max() {
let mt = ThresholdHysteresis::LimitCross.decode_threshold(Lsb(127), MilliTesla(80.0));
assert_eq!(mt, MilliTesla(79.375));
}
#[test]
fn decode_threshold_limit_cross_negative_max() {
let mt = ThresholdHysteresis::LimitCross.decode_threshold(Lsb(-128), MilliTesla(80.0));
assert_eq!(mt, MilliTesla(-80.0));
}
#[test]
fn decode_threshold_symmetric_band_positive_max() {
let mt = ThresholdHysteresis::SymmetricBand.decode_threshold(Lsb(127), MilliTesla(80.0));
assert_eq!(mt, MilliTesla(79.375));
}
#[test]
fn decode_threshold_symmetric_band_masks_sign_bit() {
let mt = ThresholdHysteresis::SymmetricBand.decode_threshold(Lsb(-128), MilliTesla(80.0));
assert_eq!(mt, MilliTesla(0.0));
}
#[test]
fn decode_threshold_zero_range() {
let mt = ThresholdHysteresis::LimitCross.decode_threshold(Lsb(1), MilliTesla(0.0));
assert_eq!(mt, MilliTesla(0.0));
}
#[cfg(feature = "libm")]
mod encode_threshold_tests {
use super::*;
#[test]
fn limit_cross_zero_mt() {
let r = ThresholdHysteresis::LimitCross.encode_threshold(
MilliTesla(0.0),
MilliTesla(80.0),
0,
);
assert_eq!(r, Some(Lsb(0)));
}
#[test]
fn limit_cross_half_range() {
let r = ThresholdHysteresis::LimitCross.encode_threshold(
MilliTesla(40.0),
MilliTesla(80.0),
0,
);
assert_eq!(r, Some(Lsb(64)));
}
#[test]
fn symmetric_band_abs_with_margin() {
let r = ThresholdHysteresis::SymmetricBand.encode_threshold(
MilliTesla(-5.0),
MilliTesla(80.0),
2,
);
assert_eq!(r, Some(Lsb(10)));
}
#[test]
fn limit_cross_clamps_above_127() {
let r = ThresholdHysteresis::LimitCross.encode_threshold(
MilliTesla(80.0),
MilliTesla(80.0),
5,
);
assert_eq!(r, Some(Lsb(127)));
}
#[test]
fn symmetric_band_negative_margin_returns_none() {
let r = ThresholdHysteresis::SymmetricBand.encode_threshold(
MilliTesla(1.0),
MilliTesla(80.0),
-1,
);
assert_eq!(r, None);
}
#[test]
fn zero_range_returns_none() {
let r = ThresholdHysteresis::LimitCross.encode_threshold(
MilliTesla(1.0),
MilliTesla(0.0),
0,
);
assert_eq!(r, None);
}
#[test]
fn nan_returns_none() {
let r = ThresholdHysteresis::LimitCross.encode_threshold(
MilliTesla(f32::NAN),
MilliTesla(80.0),
0,
);
assert_eq!(r, None);
}
#[test]
fn round_trip_limit_cross() {
let mt = MilliTesla(10.0);
let range = MilliTesla(80.0);
let lsb = ThresholdHysteresis::LimitCross
.encode_threshold(mt, range, 0)
.unwrap();
let decoded = ThresholdHysteresis::LimitCross.decode_threshold(lsb, range);
assert!((decoded.0 - mt.0).abs() <= 0.3125);
}
}
#[cfg(feature = "libm")]
mod from_mt_tests {
use super::*;
#[test]
fn all_zeros() {
let cfg = ThresholdConfig::from_mt(
[MilliTesla(0.0), MilliTesla(0.0), MilliTesla(0.0)],
MilliTesla(80.0),
MilliTesla(80.0),
0,
ThresholdHysteresis::LimitCross,
MagneticThresholdDirection::Above,
ThresholdCrossingCount::One,
TempThresholdConfig::DISABLED,
)
.unwrap();
assert_eq!(cfg.x, Lsb(0));
assert_eq!(cfg.y, Lsb(0));
assert_eq!(cfg.z, Lsb(0));
}
#[test]
fn mixed_axes_with_margin() {
let cfg = ThresholdConfig::from_mt(
[MilliTesla(40.0), MilliTesla(20.0), MilliTesla(10.0)],
MilliTesla(80.0),
MilliTesla(80.0),
5,
ThresholdHysteresis::LimitCross,
MagneticThresholdDirection::Above,
ThresholdCrossingCount::One,
TempThresholdConfig::DISABLED,
)
.unwrap();
assert_eq!(cfg.x, Lsb(69));
assert_eq!(cfg.y, Lsb(37));
assert_eq!(cfg.z, Lsb(21));
}
#[test]
fn symmetric_band_negative_margin_returns_none() {
let r = ThresholdConfig::from_mt(
[MilliTesla(1.0), MilliTesla(1.0), MilliTesla(1.0)],
MilliTesla(80.0),
MilliTesla(80.0),
-1,
ThresholdHysteresis::SymmetricBand,
MagneticThresholdDirection::Above,
ThresholdCrossingCount::One,
TempThresholdConfig::DISABLED,
);
assert_eq!(r, None);
}
#[test]
fn different_xy_z_ranges() {
let cfg = ThresholdConfig::from_mt(
[MilliTesla(10.0), MilliTesla(10.0), MilliTesla(10.0)],
MilliTesla(80.0),
MilliTesla(40.0),
0,
ThresholdHysteresis::LimitCross,
MagneticThresholdDirection::Above,
ThresholdCrossingCount::One,
TempThresholdConfig::DISABLED,
)
.unwrap();
assert_eq!(cfg.x, Lsb(16));
assert_eq!(cfg.y, Lsb(16));
assert_eq!(cfg.z, Lsb(32));
}
}
}