diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f70cf16..8695cc9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,7 +43,7 @@ jobs: run: cargo fmt --all --check - name: Run clippy - run: cargo clippy --all-targets --all-features + run: cargo clippy --all-targets --features=sync,async - name: Install cargo-binstall uses: cargo-bins/cargo-binstall@main diff --git a/Cargo.toml b/Cargo.toml index e4c5fd1..ed6b655 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,29 @@ [package] authors = ["w3irdrobot "] categories = ["embedded", "hardware-support", "no-std"] -description = "Platform-agnostic Rust driver for the Maxim DS4432 Dual-Channel, I^2C, 7-Bit Sink/Source Current Digital To Analog (DAC) converter." +description = "Platform-agnostic Rust driver for the Maxim DS4432 Dual-Channel, I2C, 7-Bit Sink/Source Current Digital To Analog (DAC) converter." documentation = "https://docs.rs/ds4432" edition = "2021" keywords = ["no-std", "embedded-hal-driver", "dac"] license = " AGPL-3.0-only" name = "ds4432" repository = "https://github.com/w3irdrobot/ds4432" +rust-version = "1.71.1" version = "0.1.0" [dependencies] -defmt = { version = "0.3.8", optional = true } -embedded-hal = { version = "1.0.0", optional = true } -embedded-hal-async = { version = "1.0.0", optional = true } -maybe-async-cfg = "0.2.4" +defmt = { version = "0.3", optional = true } +embedded-hal = { version = "1.0", optional = true } +embedded-hal-async = { version = "1.0", optional = true } +log = { version = "0.4", optional = true } +maybe-async-cfg = "0.2" [features] async = ["dep:embedded-hal-async"] -blocking = ["dep:embedded-hal"] -default = ["blocking"] -defmt = ["dep:defmt"] +default = ["sync"] +defmt-03 = ["dep:defmt"] +not-recommended-rfs = [] +sync = ["dep:embedded-hal"] [dev-dependencies] embedded-hal-mock = { version = "0.11.1", default-features = false, features = [ diff --git a/README.md b/README.md index ef1f7bc..fb692b7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Docs][docs-image]][docs-link] ![AGPVv3 licensed][license-image] -Platform-agnostic Rust driver for the Maxim DS4432 Dual-Channel, I^2C, 7-Bit Sink/Source Current Digital To Analog (DAC) converter. +Platform-agnostic Rust driver for the Maxim DS4432 Dual-Channel, I2C, 7-Bit Sink/Source Current Digital To Analog (DAC) converter. ## Resources @@ -15,6 +15,14 @@ Platform-agnostic Rust driver for the Maxim DS4432 Dual-Channel, I^2C, 7-Bit Sin Distributed under the AGPLv3 License. See [LICENSE.txt](./LICENSE.txt) for more information. +## Features + +- `defmt-03` add support for defmt Formatting of public enums and structs. +- `sync` (default) use `embedded_hal::i2c::I2c` trait to provide a sync driver. +- `async` use `embedded_hal_async::i2c::I2c` trait to provide an async driver. Both `sync` and `async` can be enable at the same time, but enabling none is pointless. +- `not-recommended-rfs` allow driver to use not recommended Rfs value for microamps convertions + + ## Support PRs are more than welcome! I don't know how much more needs to be added, but I'm open to ideas. diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ac3510a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,18 @@ +/// Driver Result type. +pub type Result = core::result::Result>; + +/// Driver errors. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +pub enum Error { + /// I2C bus error. + I2c(E), + /// The given code is too high + InvalidCode(u8), + /// The given Iout is out of range + InvalidIout, + /// The given RFS is out of range + InvalidRfs, + /// Try to set a Current value without giving the Rfs value + UnknownRfs, +} diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 0000000..1d91499 --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,226 @@ +#![macro_use] +#![allow(unused_macros)] + +#[cfg(all(feature = "defmt-03", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::todo!($($x)*); + } + }; +} + +macro_rules! unreachable { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::unreachable!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::unreachable!($($x)*); + } + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt-03"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt-03")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt-03")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt-03")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt-03")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt-03"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +#[allow(dead_code)] +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} diff --git a/src/lib.rs b/src/lib.rs index 2f6cf93..1a5dbf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -#![no_std] //! DS4432 driver. //! //! The DS4432 contains two I2C programmable current @@ -11,26 +10,44 @@ //! - [DS4432 product page](https://www.digikey.com/en/products/detail/analog-devices-inc-maxim-integrated/DS4432U-T-R/2062898) //! - [DS4432 datasheet](https://www.analog.com/media/en/technical-documentation/data-sheets/DS4432.pdf) -#[cfg(feature = "defmt")] -use defmt::{debug, error, trace, Format}; +#![no_std] +#![macro_use] +pub(crate) mod fmt; + +mod error; +pub use error::{Error, Result}; -#[cfg(all(feature = "blocking", feature = "async"))] -compile_error!("feature \"blocking\" and feature \"async\" cannot be enabled at the same time"); +#[cfg(not(any(feature = "sync", feature = "async")))] +compile_error!("You should probably choose at least one of `sync` and `async` features."); -#[cfg(feature = "blocking")] +#[cfg(feature = "sync")] +use embedded_hal::i2c::ErrorType; +#[cfg(feature = "sync")] use embedded_hal::i2c::I2c; #[cfg(feature = "async")] -use embedded_hal_async::i2c::I2c; +use embedded_hal_async::i2c::ErrorType as AsyncErrorType; +#[cfg(feature = "async")] +use embedded_hal_async::i2c::I2c as AsyncI2c; /// The DS4432's I2C addresses. -const SLAVE_ADDRESS: u8 = 0x90; +#[cfg(any(feature = "async", feature = "sync"))] +const SLAVE_ADDRESS: u8 = 0b1001000; // This is I2C address 0x48 + +#[cfg(not(feature = "not-recommended-rfs"))] +const RECOMMENDED_RFS_MIN: u32 = 40_000; +#[cfg(not(feature = "not-recommended-rfs"))] +const RECOMMENDED_RFS_MAX: u32 = 160_000; + +const IOUT_UA_MIN: f32 = 50.0; +const IOUT_UA_MAX: f32 = 200.0; /// An output controllable by the DS4432. This device has two. #[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] #[repr(u8)] pub enum Output { - One = 0xF8, - Two = 0xF9, + Zero = 0xF8, + One = 0xF9, } impl From for u8 { @@ -39,44 +56,63 @@ impl From for u8 { } } -/// Driver errors. -#[derive(Debug, PartialEq, Eq)] -pub enum Error { - /// I2C bus error. - I2c(E), - /// The given level is too high - InvalidLevel(u8), -} - /// The status of an output. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] pub enum Status { - /// The output should sink at the given level - Sink(u8), - /// The output should source at the given level - Source(u8), /// The output is completely disabled Disable, + /// The output sink at the given code + Sink(u8), + /// The output sink at the given current value + SinkMicroAmp(f32), + /// The output source at the given code + Source(u8), + /// The output source at the given current value + SourceMicroAmp(f32), } impl Status { - pub fn code(&self) -> u8 { + /// Return the raw DAC code for a given Status + /// MicroAmp variants return None because Rfs is unknown to make the conversion. + /// + /// # Example + /// ``` + /// use ds4432::Status; + /// + /// assert_eq!(Status::Sink(42).code(), Some(0x2A)); + /// assert_eq!(Status::Source(42).code(), Some(0x2A)); + /// assert_eq!(Status::Disable.code(), Some(0x00)); + /// assert_eq!(Status::Sink(0).code(), Some(0x00)); + /// assert_eq!(Status::Source(0).code(), Some(0x00)); + /// assert_eq!(Status::SinkMicroAmp(42.0).code(), None); + /// assert_eq!(Status::SourceMicroAmp(42.0).code(), None); + /// ``` + pub fn code(&self) -> Option { match self { - Self::Sink(c) | Self::Source(c) => *c, - Self::Disable => 0, + Self::Sink(c) | Self::Source(c) => Some(*c), + Self::Disable => Some(0), + _ => None, } } -} -impl From for u8 { - fn from(value: Status) -> Self { - match value { - Status::Disable | Status::Sink(0) | Status::Source(0) => 0, - // ensures MSB is 0 - Status::Sink(code) => code & 0x7F, - // ensures MSB is 1 - Status::Source(code) => code | 0x80, - } + /// Convert a raw DAC code into its Current value in microamps according to the Rfs value. + /// MicroAmp variants return None because conversion is pointless. + /// + /// # Example + /// ``` + /// use ds4432::Status; + /// + /// // example from datasheet + /// assert_eq!(Status::Source(42).current_ua(80_000), Some(32.71406)); + /// assert_eq!(Status::Sink(42).current_ua(80_000), Some(32.71406)); + /// assert_eq!(Status::Disable.current_ua(1000), Some(0.0)); + /// assert_eq!(Status::SourceMicroAmp(42.0).current_ua(80_000), None); + /// assert_eq!(Status::SinkMicroAmp(42.0).current_ua(80_000), None); + /// ``` + pub fn current_ua(&self, rfs_ohm: u32) -> Option { + self.code() + .map(|code| ((62_312.5 * code as f64) / (rfs_ohm as f64)) as f32) } } @@ -86,6 +122,7 @@ impl From for Status { let code = value & 0x7F; match (sourcing, code) { + (true, 0) => Self::Disable, (false, 0) => Self::Disable, (true, c) => Self::Source(c), (false, c) => Self::Sink(c), @@ -94,78 +131,161 @@ impl From for Status { } /// A DS4432 Digital To Analog (DAC) converter on the I2C bus `I`. -#[maybe_async_cfg::maybe(sync(feature = "blocking", keep_self), async(feature = "async"))] -pub struct DS4432 { +#[maybe_async_cfg::maybe( + sync(feature = "sync", self = "DS4432"), + async(feature = "async", keep_self) +)] +pub struct AsyncDS4432 { i2c: I, + rfs0_ohm: Option, + rfs1_ohm: Option, } -#[maybe_async_cfg::maybe(sync(feature = "blocking", keep_self), async(feature = "async"))] -impl DS4432 -where - I: I2c, -{ - /// Create a new DS4432 using the given I2C implementation - pub async fn new(i2c: I) -> Self { - #[cfg(feature = "defmt")] +#[maybe_async_cfg::maybe( + sync( + feature = "sync", + self = "DS4432", + idents(AsyncI2c(sync = "I2c"), AsyncErrorType(sync = "ErrorType")) + ), + async(feature = "async", keep_self) +)] +impl AsyncDS4432 { + /// Create a new DS4432 using the given I2C implementation. + /// + /// Using this constructor doesn't allow the driver to know the Rfs values so only raw DAC code + /// are supported in the Status. + pub fn new(i2c: I) -> Self { trace!("new"); + Self::with_rfs(i2c, None, None).unwrap() + } - Self { i2c } + /// Create a new DS4432 using the given I2C implementation and the optinal Rfs values. + /// + /// If a Rfs value is given for an Output: + /// - reading status will automatically convert to microamps value instead of giving the raw DAC code. + /// - writing status will allow automatic convertion from microamps value to raw DAC code. + /// + /// Note: if you want to only deal with raw DAC code, use `new` instead and use Status::current_ua() to + /// do manual convertion into microamps. + pub fn with_rfs( + i2c: I, + rfs0_ohm: Option, + rfs1_ohm: Option, + ) -> Result { + for rfs in [rfs0_ohm, rfs1_ohm].into_iter().flatten() { + #[cfg(feature = "not-recommended-rfs")] + if rfs == 0 { + return Err(Error::InvalidRfs); + } + #[cfg(not(feature = "not-recommended-rfs"))] + if !(RECOMMENDED_RFS_MIN..=RECOMMENDED_RFS_MAX).contains(&rfs) { + return Err(Error::InvalidRfs); + } + } + Ok(Self { + i2c, + rfs0_ohm, + rfs1_ohm, + }) } /// Set the current sink/source status and code of an output - pub async fn set_status(&mut self, output: Output, status: Status) -> Result<(), Error> { - #[cfg(feature = "defmt")] + pub async fn set_status(&mut self, output: Output, status: Status) -> Result<(), I::Error> { trace!("set_status"); - self.write_reg(output, status).await - } + let reg = output.into(); + let value = match status { + Status::Disable | Status::Sink(0) | Status::Source(0) => 0, + Status::Sink(code) => { + if code > 127 { + return Err(Error::InvalidCode(code)); + } else { + code + } + } + Status::Source(code) => { + if code > 127 { + return Err(Error::InvalidCode(code)); + } else { + // ensures MSB is 1 + code | 0x80 + } + } + Status::SinkMicroAmp(current) => { + if !(IOUT_UA_MIN..=IOUT_UA_MAX).contains(¤t) { + return Err(Error::InvalidIout); + } + let rfs = match output { + Output::Zero => self.rfs0_ohm.ok_or(Error::UnknownRfs)?, + Output::One => self.rfs1_ohm.ok_or(Error::UnknownRfs)?, + }; + ((current * (rfs as f32)) / 62_312.5) as u8 + } + Status::SourceMicroAmp(current) => { + if !(IOUT_UA_MIN..=IOUT_UA_MAX).contains(¤t) { + return Err(Error::InvalidIout); + } + let rfs = match output { + Output::Zero => self.rfs0_ohm.ok_or(Error::UnknownRfs)?, + Output::One => self.rfs1_ohm.ok_or(Error::UnknownRfs)?, + }; + // ensures MSB is 1 + ((current * (rfs as f32)) / 62_312.5) as u8 | 0x80 + } + }; - /// Get the current sink/source status and code of an output - pub async fn status(&mut self, output: Output) -> Result> { - #[cfg(feature = "defmt")] - trace!("status"); + debug!("W @0x{:x}={:x}", reg, value); - self.read_reg(output).await.map(|v| v.into()) + self.i2c + .write(SLAVE_ADDRESS, &[reg, value]) + .await + .map_err(Error::I2c) } - /// Read a register value. - async fn read_reg>(&mut self, reg: R) -> Result> { - #[cfg(feature = "defmt")] - trace!("read_reg"); + /// Get the current sink/source status and code of an output + pub async fn status(&mut self, output: Output) -> Result { + trace!("status"); let mut buf = [0x00]; - let reg = reg.into(); + let reg = output.into(); self.i2c .write_read(SLAVE_ADDRESS, &[reg], &mut buf) .await .map_err(Error::I2c)?; - #[cfg(feature = "defmt")] debug!("R @0x{:x}={:x}", reg, buf[0]); - Ok(buf[0]) - } - - /// Blindly write a single memory address with a fixed value. - async fn write_reg(&mut self, reg: R, value: V) -> Result<(), Error> - where - R: Into, - V: Into, - { - #[cfg(feature = "defmt")] - trace!("write_reg"); - - let reg = reg.into(); - let value = value.into(); - - #[cfg(feature = "defmt")] - debug!("W @0x{:x}={:x}", reg, value); - - self.i2c - .write(SLAVE_ADDRESS, &[reg, value]) - .await - .map_err(Error::I2c) + let mut status = buf[0].into(); + match output { + Output::Zero => { + if let Some(rfs) = self.rfs0_ohm { + status = match status { + Status::Sink(code) => { + Status::SinkMicroAmp(Status::Sink(code).current_ua(rfs).unwrap()) + } + Status::Source(code) => { + Status::SourceMicroAmp(Status::Source(code).current_ua(rfs).unwrap()) + } + _ => status, + } + } + } + Output::One => { + if let Some(rfs) = self.rfs1_ohm { + status = match status { + Status::Sink(code) => { + Status::SinkMicroAmp(Status::Sink(code).current_ua(rfs).unwrap()) + } + Status::Source(code) => { + Status::SourceMicroAmp(Status::Source(code).current_ua(rfs).unwrap()) + } + _ => status, + } + } + } + } + Ok(status) } /// Return the underlying I2C device @@ -189,12 +309,24 @@ mod test { use std::vec; #[test] - fn can_get_output_1_status() { - let expectations = [i2c::Transaction::write_read(0x90, vec![0xF8], vec![0xAA])]; + fn u8_to_status_conversion() { + assert_eq!(Status::from(0x2A), Status::Sink(42)); + assert_eq!(Status::from(0xAA), Status::Source(42)); + assert_eq!(Status::from(0x00), Status::Disable); + assert_eq!(Status::from(0x80), Status::Disable); + } + + #[test] + fn can_get_output_0_status() { + let expectations = [i2c::Transaction::write_read( + SLAVE_ADDRESS, + vec![Output::Zero as u8], + vec![0xAA], + )]; let mock = i2c::Mock::new(&expectations); let mut ds4432 = DS4432::new(mock); - let status = ds4432.status(Output::One).unwrap(); + let status = ds4432.status(Output::Zero).unwrap(); assert!(matches!(status, Status::Source(42))); let mut mock = ds4432.release(); @@ -202,13 +334,51 @@ mod test { } #[test] - fn can_set_output_2_status() { - let expectations = [i2c::Transaction::write(0x90, vec![0xF9, 0x2A])]; + fn can_set_output_1_status() { + let expectations = [i2c::Transaction::write( + SLAVE_ADDRESS, + vec![Output::One as u8, 0x2A], + )]; let mock = i2c::Mock::new(&expectations); let mut ds4432 = DS4432::new(mock); // just making sure it doesn't error - ds4432.set_status(Output::Two, Status::Sink(42)).unwrap(); + ds4432.set_status(Output::One, Status::Sink(42)).unwrap(); + + let mut mock = ds4432.release(); + mock.done(); + } + + #[test] + fn can_get_output_0_status_current() { + let expectations = [i2c::Transaction::write_read( + SLAVE_ADDRESS, + vec![Output::Zero as u8], + vec![0xAA], + )]; + let mock = i2c::Mock::new(&expectations); + let mut ds4432 = DS4432::with_rfs(mock, Some(80_000), None).unwrap(); + + let status = ds4432.status(Output::Zero).unwrap(); + assert!(matches!(status, Status::SourceMicroAmp(32.71406))); + + let mut mock = ds4432.release(); + mock.done(); + } + + #[test] + fn can_set_output_1_status_current() { + let expectations = [i2c::Transaction::write( + SLAVE_ADDRESS, + vec![Output::One as u8, 0x70], + )]; + let mock = i2c::Mock::new(&expectations); + let mut ds4432 = DS4432::with_rfs(mock, None, Some(80_000)).unwrap(); + + // just making sure it doesn't error + ds4432 + .set_status(Output::One, Status::SinkMicroAmp(88.0)) + .unwrap(); let mut mock = ds4432.release(); mock.done();