diff --git a/Cargo.toml b/Cargo.toml index 4dd6e9b..f857661 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,10 +22,13 @@ device-driver = { version = "1.0.7", default-features = false, features = ["yaml defmt = { version = "1.0", optional = true } embedded-hal = "1.0.0" embedded-hal-async = "1.0.0" +embassy-sync = "0.7.2" [dev-dependencies] embedded-hal-mock = { version = "0.11.1", features = ["embedded-hal-async"] } tokio = { version = "1.42.0", features = ["rt", "macros"] } +embassy-sync = { version = "0.7.2", features = ["std"] } +critical-section = { version = "1.2", features = ["std"] } [lints.rust] unsafe_code = "forbid" @@ -40,3 +43,7 @@ pedantic = "deny" [features] defmt = ["dep:defmt", "device-driver/defmt-03"] + +[patch.crates-io] +embedded-hal = {git = "https://github.com/rust-embedded/embedded-hal.git"} +embedded-hal-async = {git = "https://github.com/rust-embedded/embedded-hal.git"} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index dd556dd..b903658 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,13 @@ pub enum Pcal6416aError { /// I2C bus error I2c(E), } + +impl embedded_hal::digital::Error for Pcal6416aError { + fn kind(&self) -> embedded_hal::digital::ErrorKind { + embedded_hal::digital::ErrorKind::Other + } +} + const IOEXP_ADDR_LOW: u8 = 0x20; const IOEXP_ADDR_HIGH: u8 = 0x21; const LARGEST_REG_SIZE_BYTES: usize = 2; @@ -48,6 +55,16 @@ device_driver::create_device!( manifest: "device.yaml" ); +/// A shared, mutex-protected wrapper around a [`Device`] for concurrent access. +/// +/// This type wraps a [`Device`] inside an [`embassy_sync::mutex::Mutex`], allowing +/// multiple async tasks to access the same PCAL6416A device concurrently with +/// synchronized I2C register access. Use [`SharedDevice::split`] to obtain +/// individual [`IoPin`] instances that can be passed to different tasks. +pub struct SharedDevice { + device: embassy_sync::mutex::Mutex>>, +} + impl device_driver::AsyncRegisterInterface for Pcal6416aDevice { type Error = Pcal6416aError; type AddressType = u8; @@ -119,6 +136,620 @@ impl device_driver::RegisterInterface for Pcal6416a } } +/// Port number for the PCAL6416A device +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Port { + /// Port 0 (pins 0-7) + Port0, + /// Port 1 (pins 0-7) + Port1, +} + +/// Pin number within a port (0-7) for the PCAL6416A device +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Pin { + /// Pin 0 + Pin0, + /// Pin 1 + Pin1, + /// Pin 2 + Pin2, + /// Pin 3 + Pin3, + /// Pin 4 + Pin4, + /// Pin 5 + Pin5, + /// Pin 6 + Pin6, + /// Pin 7 + Pin7, +} + +impl Pin { + /// Get the bit position within the port (0-7) + #[must_use] + pub const fn bit(self) -> u8 { + match self { + Self::Pin0 => 0, + Self::Pin1 => 1, + Self::Pin2 => 2, + Self::Pin3 => 3, + Self::Pin4 => 4, + Self::Pin5 => 5, + Self::Pin6 => 6, + Self::Pin7 => 7, + } + } + + /// Get the pin number within the port (0-7) + #[must_use] + pub const fn number(&self) -> u8 { + self.bit() + } +} + +/// Individual pin instance that provides GPIO operations for a single pin +/// +/// This struct is created by calling `split()` on a `SharedDevice` instance. +/// It provides methods to read and write the state of a single pin without +/// requiring mutable access to the entire device. +/// +/// Note: This uses a shared mutex to provide safe concurrent access to the device. +/// All pin operations acquire the mutex lock before performing I2C operations. +pub struct IoPin<'a, I2c: embedded_hal_async::i2c::I2c, M: embassy_sync::blocking_mutex::raw::RawMutex> { + port: Port, + pin: Pin, + device: &'a embassy_sync::mutex::Mutex>>, +} + +impl<'a, I2c: embedded_hal_async::i2c::I2c, M: embassy_sync::blocking_mutex::raw::RawMutex> IoPin<'a, I2c, M> { + const fn new( + port: Port, + pin: Pin, + device: &'a embassy_sync::mutex::Mutex>>, + ) -> Self { + Self { port, pin, device } + } + + /// Get the pin number within the port (0-7) + #[must_use] + pub const fn number(&self) -> u8 { + self.pin.number() + } + + /// Get the Pin enum for this pin + #[must_use] + pub const fn pin(&self) -> Pin { + self.pin + } + + /// Get the Port enum for this pin + #[must_use] + pub const fn port(&self) -> Port { + self.port + } +} + +impl IoPin<'_, I2c, M> { + /// Read the state of this input pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_high_async(&self) -> Result> { + self.device.lock().await.is_pin_high_async(self.port, self.pin).await + } + + /// Read the state of this input pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_low_async(&self) -> Result> { + Ok(!self.is_high_async().await?) + } + + /// Set this output pin to high state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn set_high_async(&self) -> Result<(), Pcal6416aError> { + self.device.lock().await.set_pin_high_async(self.port, self.pin).await + } + + /// Set this output pin to low state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn set_low_async(&self) -> Result<(), Pcal6416aError> { + self.device.lock().await.set_pin_low_async(self.port, self.pin).await + } + + /// Toggle this output pin state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn toggle_async(&self) -> Result<(), Pcal6416aError> { + self.device.lock().await.toggle_pin_async(self.port, self.pin).await + } + + /// Read the current state of this output pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_set_high_async(&self) -> Result> { + self.device + .lock() + .await + .is_pin_set_high_async(self.port, self.pin) + .await + } + + /// Read the current state of this output pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_set_low_async(&self) -> Result> { + Ok(!self.is_set_high_async().await?) + } +} + +// Implement embedded-hal digital traits for IoPin +impl embedded_hal::digital::ErrorType + for IoPin<'_, I2c, M> +{ + type Error = Pcal6416aError; +} + +impl + embedded_hal_async::digital::InputPin for IoPin<'_, I2c, M> +{ + async fn is_high(&mut self) -> Result { + IoPin::is_high_async(self).await + } + + async fn is_low(&mut self) -> Result { + IoPin::is_low_async(self).await + } +} + +impl + embedded_hal_async::digital::OutputPin for IoPin<'_, I2c, M> +{ + async fn set_low(&mut self) -> Result<(), Self::Error> { + IoPin::set_low_async(self).await + } + + async fn set_high(&mut self) -> Result<(), Self::Error> { + IoPin::set_high_async(self).await + } +} + +impl + embedded_hal_async::digital::StatefulOutputPin for IoPin<'_, I2c, M> +{ + async fn is_set_high(&mut self) -> Result { + IoPin::is_set_high_async(self).await + } + + async fn is_set_low(&mut self) -> Result { + IoPin::is_set_low_async(self).await + } + + async fn toggle(&mut self) -> Result<(), Self::Error> { + IoPin::toggle_async(self).await + } +} + +impl Device> { + /// Read the state of an input pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_pin_high(&mut self, port: Port, pin: Pin) -> Result> { + let bit = pin.bit(); + + let value = match port { + Port::Port0 => { + let reg = self.input_port_0().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + Port::Port1 => { + let reg = self.input_port_1().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + }; + + Ok(value) + } + + /// Read the state of an input pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_pin_low(&mut self, port: Port, pin: Pin) -> Result> { + Ok(!self.is_pin_high(port, pin)?) + } + + /// Set an output pin to high state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn set_pin_high(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { + let bit = pin.bit(); + + match port { + Port::Port0 => self.output_port_0().modify(|r| match bit { + 0 => r.set_o_0_0(true), + 1 => r.set_o_0_1(true), + 2 => r.set_o_0_2(true), + 3 => r.set_o_0_3(true), + 4 => r.set_o_0_4(true), + 5 => r.set_o_0_5(true), + 6 => r.set_o_0_6(true), + 7 => r.set_o_0_7(true), + _ => unreachable!(), + }), + Port::Port1 => self.output_port_1().modify(|r| match bit { + 0 => r.set_o_1_0(true), + 1 => r.set_o_1_1(true), + 2 => r.set_o_1_2(true), + 3 => r.set_o_1_3(true), + 4 => r.set_o_1_4(true), + 5 => r.set_o_1_5(true), + 6 => r.set_o_1_6(true), + 7 => r.set_o_1_7(true), + _ => unreachable!(), + }), + } + } + + /// Set an output pin to low state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn set_pin_low(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { + let bit = pin.bit(); + + match port { + Port::Port0 => self.output_port_0().modify(|r| match bit { + 0 => r.set_o_0_0(false), + 1 => r.set_o_0_1(false), + 2 => r.set_o_0_2(false), + 3 => r.set_o_0_3(false), + 4 => r.set_o_0_4(false), + 5 => r.set_o_0_5(false), + 6 => r.set_o_0_6(false), + 7 => r.set_o_0_7(false), + _ => unreachable!(), + }), + Port::Port1 => self.output_port_1().modify(|r| match bit { + 0 => r.set_o_1_0(false), + 1 => r.set_o_1_1(false), + 2 => r.set_o_1_2(false), + 3 => r.set_o_1_3(false), + 4 => r.set_o_1_4(false), + 5 => r.set_o_1_5(false), + 6 => r.set_o_1_6(false), + 7 => r.set_o_1_7(false), + _ => unreachable!(), + }), + } + } + + /// Toggle an output pin state + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn toggle_pin(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { + let bit = pin.bit(); + + match port { + Port::Port0 => self.output_port_0().modify(|r| match bit { + 0 => r.set_o_0_0(!r.o_0_0()), + 1 => r.set_o_0_1(!r.o_0_1()), + 2 => r.set_o_0_2(!r.o_0_2()), + 3 => r.set_o_0_3(!r.o_0_3()), + 4 => r.set_o_0_4(!r.o_0_4()), + 5 => r.set_o_0_5(!r.o_0_5()), + 6 => r.set_o_0_6(!r.o_0_6()), + 7 => r.set_o_0_7(!r.o_0_7()), + _ => unreachable!(), + }), + Port::Port1 => self.output_port_1().modify(|r| match bit { + 0 => r.set_o_1_0(!r.o_1_0()), + 1 => r.set_o_1_1(!r.o_1_1()), + 2 => r.set_o_1_2(!r.o_1_2()), + 3 => r.set_o_1_3(!r.o_1_3()), + 4 => r.set_o_1_4(!r.o_1_4()), + 5 => r.set_o_1_5(!r.o_1_5()), + 6 => r.set_o_1_6(!r.o_1_6()), + 7 => r.set_o_1_7(!r.o_1_7()), + _ => unreachable!(), + }), + } + } + + /// Read the current state of an output pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_pin_set_high(&mut self, port: Port, pin: Pin) -> Result> { + let bit = pin.bit(); + + let value = match port { + Port::Port0 => { + let reg = self.output_port_0().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + Port::Port1 => { + let reg = self.output_port_1().read()?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + }; + + Ok(value) + } + + /// Read the current state of an output pin + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub fn is_pin_set_low(&mut self, port: Port, pin: Pin) -> Result> { + Ok(!self.is_pin_set_high(port, pin)?) + } +} + +impl Device> { + /// Read the state of an input pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_pin_high_async(&mut self, port: Port, pin: Pin) -> Result> { + let bit = pin.bit(); + + let value = match port { + Port::Port0 => { + let reg = self.input_port_0().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + Port::Port1 => { + let reg = self.input_port_1().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + }; + + Ok(value) + } + + /// Read the state of an input pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_pin_low_async(&mut self, port: Port, pin: Pin) -> Result> { + Ok(!self.is_pin_high_async(port, pin).await?) + } + + /// Set an output pin to high state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn set_pin_high_async(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { + let bit = pin.bit(); + + match port { + Port::Port0 => { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(true), + 1 => r.set_o_0_1(true), + 2 => r.set_o_0_2(true), + 3 => r.set_o_0_3(true), + 4 => r.set_o_0_4(true), + 5 => r.set_o_0_5(true), + 6 => r.set_o_0_6(true), + 7 => r.set_o_0_7(true), + _ => unreachable!(), + }) + .await + } + Port::Port1 => { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(true), + 1 => r.set_o_1_1(true), + 2 => r.set_o_1_2(true), + 3 => r.set_o_1_3(true), + 4 => r.set_o_1_4(true), + 5 => r.set_o_1_5(true), + 6 => r.set_o_1_6(true), + 7 => r.set_o_1_7(true), + _ => unreachable!(), + }) + .await + } + } + } + + /// Set an output pin to low state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn set_pin_low_async(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { + let bit = pin.bit(); + + match port { + Port::Port0 => { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(false), + 1 => r.set_o_0_1(false), + 2 => r.set_o_0_2(false), + 3 => r.set_o_0_3(false), + 4 => r.set_o_0_4(false), + 5 => r.set_o_0_5(false), + 6 => r.set_o_0_6(false), + 7 => r.set_o_0_7(false), + _ => unreachable!(), + }) + .await + } + Port::Port1 => { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(false), + 1 => r.set_o_1_1(false), + 2 => r.set_o_1_2(false), + 3 => r.set_o_1_3(false), + 4 => r.set_o_1_4(false), + 5 => r.set_o_1_5(false), + 6 => r.set_o_1_6(false), + 7 => r.set_o_1_7(false), + _ => unreachable!(), + }) + .await + } + } + } + + /// Toggle an output pin state (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn toggle_pin_async(&mut self, port: Port, pin: Pin) -> Result<(), Pcal6416aError> { + let bit = pin.bit(); + + match port { + Port::Port0 => { + self.output_port_0() + .modify_async(|r| match bit { + 0 => r.set_o_0_0(!r.o_0_0()), + 1 => r.set_o_0_1(!r.o_0_1()), + 2 => r.set_o_0_2(!r.o_0_2()), + 3 => r.set_o_0_3(!r.o_0_3()), + 4 => r.set_o_0_4(!r.o_0_4()), + 5 => r.set_o_0_5(!r.o_0_5()), + 6 => r.set_o_0_6(!r.o_0_6()), + 7 => r.set_o_0_7(!r.o_0_7()), + _ => unreachable!(), + }) + .await + } + Port::Port1 => { + self.output_port_1() + .modify_async(|r| match bit { + 0 => r.set_o_1_0(!r.o_1_0()), + 1 => r.set_o_1_1(!r.o_1_1()), + 2 => r.set_o_1_2(!r.o_1_2()), + 3 => r.set_o_1_3(!r.o_1_3()), + 4 => r.set_o_1_4(!r.o_1_4()), + 5 => r.set_o_1_5(!r.o_1_5()), + 6 => r.set_o_1_6(!r.o_1_6()), + 7 => r.set_o_1_7(!r.o_1_7()), + _ => unreachable!(), + }) + .await + } + } + } + + /// Read the current state of an output pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_pin_set_high_async(&mut self, port: Port, pin: Pin) -> Result> { + let bit = pin.bit(); + + let value = match port { + Port::Port0 => { + let reg = self.output_port_0().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + Port::Port1 => { + let reg = self.output_port_1().read_async().await?; + let reg: [u8; 1] = reg.into(); + reg[0] & (1 << bit) != 0 + } + }; + + Ok(value) + } + + /// Read the current state of an output pin (async version) + /// # Errors + /// + /// Will return `Err` if underlying I2C bus operation fails + pub async fn is_pin_set_low_async(&mut self, port: Port, pin: Pin) -> Result> { + Ok(!self.is_pin_set_high_async(port, pin).await?) + } +} + +impl SharedDevice { + /// Create a new `SharedDevice` from a [`Device`] instance. + /// + /// The device is wrapped in a mutex to enable safe shared access + /// from multiple [`IoPin`] instances. + pub fn new(device: Device>) -> Self { + Self { + device: embassy_sync::mutex::Mutex::new(device), + } + } + + /// Split the driver into an array of individual pin instances + /// + /// This borrows the shared device mutably and returns an array of 16 `IoPin` instances, + /// one for each GPIO pin. The pins can be passed individually to different functions. + /// Each pin uses the shared mutex to safely access the underlying device. + /// + /// # Example + /// ```ignore + /// let device = Device::new(Pcal6416aDevice { addr_pin, i2cbus }); + /// let mut shared = SharedDevice::new(device); + /// let pins = shared.split(); + /// + /// // Pass individual pins to different functions + /// use_led(&pins[0]).await; + /// use_button(&pins[1]).await; + /// + /// // Or access by index + /// pins[2].set_high_async().await?; + /// pins[3].set_low_async().await?; + /// + /// // Iterate over pins + /// for (i, pin) in pins.iter().enumerate() { + /// println!("Pin {} number: {}", i, pin.number()); + /// } + /// ``` + pub fn split(&mut self) -> [IoPin<'_, I2c, M>; 16] { + [ + IoPin::new(Port::Port0, Pin::Pin0, &self.device), + IoPin::new(Port::Port0, Pin::Pin1, &self.device), + IoPin::new(Port::Port0, Pin::Pin2, &self.device), + IoPin::new(Port::Port0, Pin::Pin3, &self.device), + IoPin::new(Port::Port0, Pin::Pin4, &self.device), + IoPin::new(Port::Port0, Pin::Pin5, &self.device), + IoPin::new(Port::Port0, Pin::Pin6, &self.device), + IoPin::new(Port::Port0, Pin::Pin7, &self.device), + IoPin::new(Port::Port1, Pin::Pin0, &self.device), + IoPin::new(Port::Port1, Pin::Pin1, &self.device), + IoPin::new(Port::Port1, Pin::Pin2, &self.device), + IoPin::new(Port::Port1, Pin::Pin3, &self.device), + IoPin::new(Port::Port1, Pin::Pin4, &self.device), + IoPin::new(Port::Port1, Pin::Pin5, &self.device), + IoPin::new(Port::Port1, Pin::Pin6, &self.device), + IoPin::new(Port::Port1, Pin::Pin7, &self.device), + ] + } +} + #[cfg(test)] mod tests { use embedded_hal_mock::eh1::i2c::{Mock, Transaction}; @@ -972,4 +1603,557 @@ mod tests { let _ = dev.config_port_1().read_async().await.unwrap(); dev.interface.i2cbus.done(); } + + #[test] + fn input_pin_is_high() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b1000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + assert!(dev.is_pin_high(Port::Port0, Pin::Pin0).unwrap()); + assert!(dev.is_pin_high(Port::Port1, Pin::Pin7).unwrap()); + dev.interface.i2cbus.done(); + } + + #[test] + fn input_pin_is_low() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + assert!(dev.is_pin_low(Port::Port0, Pin::Pin0).unwrap()); + assert!(dev.is_pin_low(Port::Port1, Pin::Pin7).unwrap()); + dev.interface.i2cbus.done(); + } + + #[test] + fn input_pin_port1() { + let expectations = vec![Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b1000_0000])]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + assert!(dev.is_pin_high(Port::Port1, Pin::Pin7).unwrap()); + dev.interface.i2cbus.done(); + } + + #[test] + fn output_pin_set_high() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b1000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.set_pin_high(Port::Port0, Pin::Pin0).unwrap(); + dev.set_pin_high(Port::Port1, Pin::Pin7).unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn output_pin_set_low() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b1111_1111]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b1111_1110]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1111_1111]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0111_1111]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.set_pin_low(Port::Port0, Pin::Pin0).unwrap(); + dev.set_pin_low(Port::Port1, Pin::Pin7).unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn output_pin_port1() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0111_1111]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b1111_1111]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.set_pin_high(Port::Port1, Pin::Pin7).unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn toggle_pin() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + dev.toggle_pin(Port::Port0, Pin::Pin0).unwrap(); + dev.toggle_pin(Port::Port1, Pin::Pin7).unwrap(); + dev.interface.i2cbus.done(); + } + + #[test] + fn is_pin_set_high() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + assert!(dev.is_pin_set_high(Port::Port0, Pin::Pin0).unwrap()); + assert!(dev.is_pin_set_high(Port::Port1, Pin::Pin7).unwrap()); + dev.interface.i2cbus.done(); + } + + #[test] + fn multiple_pins_at_once() { + let expectations = vec![ + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0011]), + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b1111_1111]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + // Set multiple pins without borrowing conflicts + dev.set_pin_high(Port::Port0, Pin::Pin0).unwrap(); + dev.set_pin_high(Port::Port0, Pin::Pin1).unwrap(); + assert!(dev.is_pin_high(Port::Port0, Pin::Pin7).unwrap()); + dev.interface.i2cbus.done(); + } + + #[tokio::test] + async fn split_pins() { + let expectations = vec![ + // Set pin 0 high + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // Set pin 1 high + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0011]), + // Read pin 0 + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0011]), + // Toggle pin 1 + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0011]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let mut pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + + // Use individual pins independently + pins[0].set_high_async().await.unwrap(); + pins[1].set_high_async().await.unwrap(); + assert!(pins[0].is_high_async().await.unwrap()); + pins[1].toggle_async().await.unwrap(); + } + + // Verify mock expectations + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn split_pin_numbers() { + let expectations = vec![]; + let i2cbus = Mock::new(&expectations); + let dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + + // Verify all 16 pins have correct numbers (0-7 per port) + for i in 0..8 { + assert_eq!(pins[i].number(), i as u8, "Pin at index {} should have number {}", i, i); + assert_eq!(pins[i].port(), Port::Port0, "Pin at index {} should be on Port 0", i); + } + for i in 8..16 { + assert_eq!( + pins[i].number(), + (i - 8) as u8, + "Pin at index {} should have number {}", + i, + i - 8 + ); + assert_eq!(pins[i].port(), Port::Port1, "Pin at index {} should be on Port 1", i); + } + + // Verify Pin enum values for all pins + assert_eq!(pins[0].pin(), Pin::Pin0); + assert_eq!(pins[1].pin(), Pin::Pin1); + assert_eq!(pins[2].pin(), Pin::Pin2); + assert_eq!(pins[3].pin(), Pin::Pin3); + assert_eq!(pins[4].pin(), Pin::Pin4); + assert_eq!(pins[5].pin(), Pin::Pin5); + assert_eq!(pins[6].pin(), Pin::Pin6); + assert_eq!(pins[7].pin(), Pin::Pin7); + assert_eq!(pins[8].pin(), Pin::Pin0); + assert_eq!(pins[9].pin(), Pin::Pin1); + assert_eq!(pins[10].pin(), Pin::Pin2); + assert_eq!(pins[11].pin(), Pin::Pin3); + assert_eq!(pins[12].pin(), Pin::Pin4); + assert_eq!(pins[13].pin(), Pin::Pin5); + assert_eq!(pins[14].pin(), Pin::Pin6); + assert_eq!(pins[15].pin(), Pin::Pin7); + + // Verify Port enum values + assert_eq!(pins[0].port(), Port::Port0); + assert_eq!(pins[7].port(), Port::Port0); + assert_eq!(pins[8].port(), Port::Port1); + assert_eq!(pins[15].port(), Port::Port1); + } + + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_set_high() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b1000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + pins[0].set_high_async().await.unwrap(); + pins[15].set_high_async().await.unwrap(); + } + + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_set_low() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + pins[0].set_low_async().await.unwrap(); + pins[15].set_low_async().await.unwrap(); + } + + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_is_high() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b1000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + assert!(pins[0].is_high_async().await.unwrap()); + assert!(pins[15].is_high_async().await.unwrap()); + } + + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_is_low() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x01], vec![0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + assert!(pins[0].is_low_async().await.unwrap()); + assert!(pins[15].is_low_async().await.unwrap()); + } + + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_toggle() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x03, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + pins[0].toggle_async().await.unwrap(); + pins[15].toggle_async().await.unwrap(); + } + + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_is_set_high() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b1000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + assert!(pins[0].is_set_high_async().await.unwrap()); + assert!(pins[15].is_set_high_async().await.unwrap()); + } + + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn async_pin_is_set_low() { + let expectations = vec![ + // Port 0 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + // Port 1 pin + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x03], vec![0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + assert!(pins[0].is_set_low_async().await.unwrap()); + assert!(pins[15].is_set_low_async().await.unwrap()); + } + + dev.device.lock().await.interface.i2cbus.done(); + } + + #[tokio::test] + async fn embedded_hal_async_traits() { + use embedded_hal_async::digital::{InputPin, OutputPin, StatefulOutputPin}; + + let expectations = vec![ + // OutputPin::set_high + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // OutputPin::set_low + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + // InputPin::is_high (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // InputPin::is_low (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0001]), + // InputPin::is_high (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // InputPin::is_low (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x00], vec![0b0000_0000]), + // StatefulOutputPin::is_set_high (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + // StatefulOutputPin::is_set_low (when low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + // StatefulOutputPin::toggle (low to high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0000]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0001]), + // StatefulOutputPin::is_set_high (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // StatefulOutputPin::is_set_low (when high) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + // StatefulOutputPin::toggle (high to low) + Transaction::write_read(IOEXP_ADDR_LOW, vec![0x02], vec![0b0000_0001]), + Transaction::write(IOEXP_ADDR_LOW, vec![0x02, 0b0000_0000]), + ]; + let i2cbus = Mock::new(&expectations); + let mut dev = Device::new(Pcal6416aDevice { + addr_pin: AddrPinState::Low, + i2cbus, + }); + let mut dev = SharedDevice::new(dev); + + { + let mut pins: [IoPin< + '_, + embedded_hal_mock::common::Generic, + embassy_sync::blocking_mutex::raw::NoopRawMutex, + >; 16] = dev.split(); + + // Test OutputPin trait - set_high + OutputPin::set_high(&mut pins[0]).await.unwrap(); + + // Test OutputPin trait - set_low + OutputPin::set_low(&mut pins[0]).await.unwrap(); + + // Test InputPin trait - is_high when pin is high + assert!(InputPin::is_high(&mut pins[0]).await.unwrap()); + + // Test InputPin trait - is_low when pin is high + assert!(!InputPin::is_low(&mut pins[0]).await.unwrap()); + + // Test InputPin trait - is_high when pin is low + assert!(!InputPin::is_high(&mut pins[0]).await.unwrap()); + + // Test InputPin trait - is_low when pin is low + assert!(InputPin::is_low(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - is_set_high when output is low + assert!(!StatefulOutputPin::is_set_high(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - is_set_low when output is low + assert!(StatefulOutputPin::is_set_low(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - toggle from low to high + StatefulOutputPin::toggle(&mut pins[0]).await.unwrap(); + + // Test StatefulOutputPin trait - is_set_high when output is high + assert!(StatefulOutputPin::is_set_high(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - is_set_low when output is high + assert!(!StatefulOutputPin::is_set_low(&mut pins[0]).await.unwrap()); + + // Test StatefulOutputPin trait - toggle from high to low + StatefulOutputPin::toggle(&mut pins[0]).await.unwrap(); + } + + dev.device.lock().await.interface.i2cbus.done(); + } }