From 3c0e9667c9476993913839dc81c6614197f10552 Mon Sep 17 00:00:00 2001 From: matteotullo Date: Mon, 24 Nov 2025 02:03:21 -0800 Subject: [PATCH 1/5] Add SMBus HAL, dependent on embedded-hal-async::I2c --- Cargo.lock | 2 +- embedded-mcu-hal/Cargo.toml | 3 +- embedded-mcu-hal/src/lib.rs | 1 + embedded-mcu-hal/src/smbus/bus/asynch.rs | 766 +++++++++++++++++++++++ embedded-mcu-hal/src/smbus/bus/mod.rs | 94 +++ embedded-mcu-hal/src/smbus/mod.rs | 1 + supply-chain/audits.toml | 6 + supply-chain/imports.lock | 17 +- 8 files changed, 883 insertions(+), 7 deletions(-) create mode 100644 embedded-mcu-hal/src/smbus/bus/asynch.rs create mode 100644 embedded-mcu-hal/src/smbus/bus/mod.rs create mode 100644 embedded-mcu-hal/src/smbus/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 729aa13..935a31b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -481,4 +481,4 @@ dependencies = [ "proc-macro2", "quote", "syn", -] +] \ No newline at end of file diff --git a/embedded-mcu-hal/Cargo.toml b/embedded-mcu-hal/Cargo.toml index be43739..6e75d45 100644 --- a/embedded-mcu-hal/Cargo.toml +++ b/embedded-mcu-hal/Cargo.toml @@ -17,6 +17,7 @@ chrono = { version = "^0.4", default-features = false, optional = true } defmt = { version = "1.0", optional = true } embedded-hal = { workspace = true } num_enum = { version = "0.7.5", default-features = false } +embedded-hal-async = "1.0.0" [dev-dependencies] proptest = "1.0.0" @@ -26,7 +27,7 @@ tokio = { version = "1", features = ["macros", "rt", "time"] } default = [] chrono = ["dep:chrono"] -defmt = ["dep:defmt"] +defmt = ["dep:defmt", "embedded-hal-async/defmt-03"] [lints] workspace = true diff --git a/embedded-mcu-hal/src/lib.rs b/embedded-mcu-hal/src/lib.rs index fa11d5b..c05ae9c 100644 --- a/embedded-mcu-hal/src/lib.rs +++ b/embedded-mcu-hal/src/lib.rs @@ -70,5 +70,6 @@ pub mod i2c; pub mod nvram; +pub mod smbus; pub mod time; pub mod watchdog; diff --git a/embedded-mcu-hal/src/smbus/bus/asynch.rs b/embedded-mcu-hal/src/smbus/bus/asynch.rs new file mode 100644 index 0000000..328bd83 --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/asynch.rs @@ -0,0 +1,766 @@ +use core::hash::Hasher; + +use crate::smbus::bus::Error as SMBusError; +use embedded_hal_async::i2c::{Error as I2cError, Operation}; + +/// SMBus helper trait built on top of an async I2C implementation. +/// +/// This trait provides higher-level SMBus protocol operations (quick command, +/// send/receive byte, byte/word/block read/write, process calls, and PEC +/// handling) using an underlying asynchronous I2C implementation that +/// implements `embedded_hal_async::i2c::I2c`. +/// +/// # Example Implementation +/// +/// To implement the `Smbus` trait, you need to: +/// 1. Define an error type that implements both the crate's `ErrorType` trait +/// and converts from `embedded_hal_async::i2c::ErrorKind`. +/// 2. Define a PEC calculator type that implements `core::hash::Hasher`. +/// 3. Implement `crate::smbus::bus::ErrorType` to provide error conversions. +/// 4. Implement `embedded_hal_async::i2c::I2c` for I2C operations. +/// 5. Implement `Smbus` itself with a `get_pec_calc()` method. +/// +/// ```ignore +/// // Error type implementing both SMBus and I2C error traits +/// #[derive(Debug, Clone, Copy)] +/// pub enum Error { +/// I2c(embedded_hal::i2c::ErrorKind), +/// Pec, +/// TooLargeBlockTransaction, +/// } +/// +/// impl From for Error { +/// fn from(kind: embedded_hal::i2c::ErrorKind) -> Self { +/// Self::I2c(kind) +/// } +/// } +/// +/// impl crate::smbus::bus::Error for Error { +/// fn kind(&self) -> crate::smbus::bus::ErrorKind { +/// match self { +/// Self::I2c(e) => crate::smbus::bus::ErrorKind::I2c(*e), +/// Self::Pec => crate::smbus::bus::ErrorKind::Pec, +/// Self::TooLargeBlockTransaction => crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, +/// } +/// } +/// +/// fn to_kind(kind: crate::smbus::bus::ErrorKind) -> Self { +/// match kind { +/// crate::smbus::bus::ErrorKind::I2c(e) => Self::I2c(e), +/// crate::smbus::bus::ErrorKind::Pec => Self::Pec, +/// crate::smbus::bus::ErrorKind::TooLargeBlockTransaction => Self::TooLargeBlockTransaction, +/// _ => Self::I2c(embedded_hal::i2c::ErrorKind::Other), +/// } +/// } +/// } +/// +/// // PEC calculator type (example using a simple CRC-8 hasher) +/// pub struct PecCalc(u8); +/// +/// impl core::hash::Hasher for PecCalc { +/// fn write(&mut self, bytes: &[u8]) { +/// for &byte in bytes { +/// self.0 = self.0.wrapping_add(byte); +/// } +/// } +/// +/// fn finish(&self) -> u64 { +/// self.0 as u64 +/// } +/// } +/// +/// // I2C master struct implementing both I2c and Smbus +/// pub struct I2cMaster { +/// // I2C hardware handle +/// } +/// +/// impl embedded_hal_async::i2c::I2c for I2cMaster { +/// // Implement required I2C methods... +/// } +/// +/// impl crate::smbus::bus::ErrorType for I2cMaster { +/// type Error = Error; +/// } +/// +/// impl Smbus for I2cMaster { +/// type PecCalc = PecCalc; +/// +/// fn get_pec_calc() -> Option { +/// Some(PecCalc(0)) // Return PEC calculator if available +/// } +/// } +/// ``` +pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { + /// PEC (Packet Error Code) calculator type. + /// + /// When a SMBus operation requests PEC verification (`use_pec = true`), + /// implementations should return a `PecCalc` instance from `get_pec_calc()` + /// that is then fed the transmitted/received bytes in bus order. The calculator + /// should expose the checksum through `finish()`; this crate treats the + /// resulting value as a single-byte PEC. + /// + /// The type must implement `core::hash::Hasher`. PEC calculators are obtained + /// via the `get_pec_calc()` method, which returns `Option`. If + /// `get_pec_calc()` returns `None`, any operation with `use_pec = true` will + /// return an error of kind `ErrorKind::Pec`. + type PecCalc: core::hash::Hasher; + + /// Obtain a PEC calculator instance if PEC support is available. + /// + /// This method is called by SMBus operations that request PEC verification + /// (`use_pec = true`). Implementations should return `Some(calculator)` if PEC + /// support is available, or `None` if not. When `None` is returned, any + /// operation with `use_pec = true` will fail with an error of kind + /// `ErrorKind::Pec`. + /// + /// The returned calculator should be a fresh instance ready to hash bytes + /// in bus order using the `core::hash::Hasher` interface. + /// + /// Returns `Some(PecCalc)` if PEC is available, or `None` if PEC support + /// is not implemented or unavailable. + fn get_pec_calc() -> Option; + + /// Check PEC (Packet Error Code) validity. + /// + /// Compares a received PEC byte against a computed PEC value to verify data + /// integrity. This is a helper method used internally by read operations that + /// perform PEC verification. + /// + /// Parameters: + /// - `received_pec`: The PEC byte received from the bus. + /// - `computed_pec`: The PEC value computed locally via the `PecCalc` hasher's + /// `finish()` method. Only the low byte is used for comparison. + /// + /// Returns `Ok(())` if the received PEC matches the computed PEC (after + /// truncating to a single byte), or an error of kind `ErrorKind::Pec` if + /// the values do not match, indicating a data integrity error. + fn check_pec(received_pec: u8, computed_pec: u64) -> Result<(), ::Error> { + computed_pec + .eq(&received_pec.into()) + .then_some(()) + .ok_or_else(|| ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec)) + } + + /// Write a buffer of data with optional PEC computation and verification. + /// + /// This is a low-level helper method that performs I2C write operations with + /// optional PEC (Packet Error Code) computation. When `use_pec` is false, the + /// data is written as-is. When `use_pec` is true, a PEC byte is computed over + /// the address and data payload, and the caller-provided buffer must have space + /// for the PEC byte at the end (i.e., the buffer should be sized to + /// `payload_len + 1` to accommodate the computed PEC). + /// + /// Parameters: + /// - `address`: 7-bit target device address (used in PEC calculation). + /// - `use_pec`: When true, compute PEC and append it to the transfer. + /// When false, write the buffer without PEC. + /// - `operations`: Mutable buffer containing the data to write. When `use_pec` + /// is true, the last byte of this buffer will be overwritten with the computed + /// PEC value. The caller must ensure sufficient space. + /// + /// Returns `Ok(())` on success, or an error if: + /// - The underlying I2C write fails (converted from `I2cError`) + /// - PEC is requested but unavailable (returns `ErrorKind::Pec`) + /// - PEC computation fails or overflows (returns `ErrorKind::Pec`) + fn write_buf( + &mut self, + address: u8, + use_pec: bool, + operations: &mut [u8], + ) -> impl core::future::Future::Error>> { + async move { + if use_pec { + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + + pec.write_u8(address << 1); + let (pec_elem, rest) = operations.split_last_mut().ok_or( + ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec), + )?; + pec.write(rest); + *pec_elem = pec.finish().try_into().map_err(|_| { + ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec) + })?; + + self.write(address, operations) + .await + .map_err(|i2c_err| i2c_err.kind())?; + } else { + self.write(address, operations) + .await + .map_err(|i2c_err| i2c_err.kind())?; + } + Ok(()) + } + } + + /// Read a buffer of data with optional PEC verification. + /// + /// This is a low-level helper method that performs I2C read operations with + /// optional PEC (Packet Error Code) verification. When `use_pec` is false, + /// the data is read as-is. When `use_pec` is true, the data (excluding the + /// PEC byte) is hashed using the `PecCalc` calculator, and the final PEC byte + /// in the buffer is verified against the locally computed PEC. The caller must + /// ensure the buffer has space for the PEC byte (i.e., for a single data byte + /// with PEC, provide a 2-byte buffer). + /// + /// Parameters: + /// - `address`: 7-bit target device address (used in PEC calculation). + /// - `use_pec`: When true, verify the PEC byte at the end of the buffer. + /// When false, read the buffer without PEC verification. + /// - `read`: Mutable buffer to store the received data. The last byte should + /// contain the PEC byte if `use_pec` is true. All other bytes contain the + /// actual payload data. + /// + /// Returns `Ok(())` on success, or an error if: + /// - The underlying I2C read fails (converted from `I2cError`) + /// - PEC is requested but unavailable (returns `ErrorKind::Pec`) + /// - PEC verification fails due to mismatch (returns `ErrorKind::Pec`) + fn read_buf( + &mut self, + address: u8, + use_pec: bool, + read: &mut [u8], + ) -> impl core::future::Future::Error>> { + async move { + if use_pec { + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write_u8(address << 1); + self.read(address, read).await.map_err(|i2c_err| i2c_err.kind())?; + let (pec_byte, rest) = read.split_last().ok_or( + ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec), + )?; + pec.write(rest); + + Self::check_pec(*pec_byte, pec.finish())?; + } else { + self.read(address, read).await.map_err(|i2c_err| i2c_err.kind())?; + } + + Ok(()) + } + } + + /// Quick Command + /// + /// Perform an SMBus Quick Command which uses the R/W bit of the 7-bit address + /// to indicate the command (no data payload is transferred). + /// + /// Parameters: + /// - `address`: 7-bit target device address. + /// - `read`: when true, the R/W bit denotes a read (controller issues a read); + /// otherwise it denotes a write. + /// + /// Returns `Ok(())` on success or an error converted from the underlying I2C + /// implementation on failure. + #[inline] + fn quick_command( + &mut self, + address: u8, + read: bool, + ) -> impl core::future::Future::Error>> { + async move { + self.transaction( + address, + &mut if read { + [Operation::Read(&mut [])] + } else { + [Operation::Write(&[])] + }, + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(()) + } + } + + /// Send Byte + /// + /// Sends a single command byte to the target device. + /// + /// Parameters: + /// - `address`: 7-bit target device address. + /// - `byte`: command byte to send. + /// - `use_pec`: when true, compute a PEC byte over the address and command + /// and append it to the transfer. If PEC support is unavailable or PEC + /// computation fails, an error of kind `ErrorKind::Pec` is returned. + /// + /// Returns `Ok(())` on success or an error converted from the underlying I2C + /// implementation on failure. + fn send_byte( + &mut self, + address: u8, + byte: u8, + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if use_pec { + self.write_buf(address, true, &mut [byte, 0]).await + } else { + self.write_buf(address, true, &mut [byte]).await + } + } + } + + /// Receive Byte + /// + /// Read a single byte from the target device. + /// + /// Parameters: + /// - `address`: 7-bit target device address. + /// - `use_pec`: when true, expect an extra PEC byte after the data and + /// verify it against a locally computed PEC. If PEC support is unavailable, + /// or on PEC mismatch, an error of kind `ErrorKind::Pec` is returned. + /// + /// Returns the received byte on success or an error converted from the + /// underlying I2C implementation on failure. + fn receive_byte( + &mut self, + address: u8, + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if use_pec { + let mut buf = [0u8, 2]; + self.read_buf(address, use_pec, &mut buf).await?; + Ok(buf[0]) + } else { + let mut buf = [0u8]; + self.read_buf(address, use_pec, &mut buf).await?; + Ok(buf[0]) + } + } + } + + /// Write Byte + /// + /// Write a single data byte to a command/register on the target device. + /// + /// Parameters: + /// - `address`: 7-bit target device address. + /// - `register`: command/register code to write to. + /// - `byte`: data byte to write. + /// - `use_pec`: when true, compute and append a PEC byte that covers the + /// address, register and data. If PEC support is unavailable or PEC + /// computation fails, an error of kind `ErrorKind::Pec` is returned. + /// + /// Returns `Ok(())` on success or an error converted from the underlying I2C + /// implementation on failure. + fn write_byte( + &mut self, + address: u8, + register: u8, + byte: u8, + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if use_pec { + self.write_buf(address, use_pec, &mut [register, byte, 0]).await + } else { + self.write_buf(address, use_pec, &mut [register, byte]).await + } + } + } + + /// Write Word + /// + /// Write a 16-bit word to a command/register on the target device. The word + /// is transmitted as little-endian (low byte first) on the bus. + /// + /// Parameters: + /// - `address`: 7-bit target device address. + /// - `register`: command/register code to write to. + /// - `word`: 16-bit value to send (little-endian on the wire). + /// - `use_pec`: when true, compute and append a PEC byte that covers the + /// address, register and word bytes. If PEC support is unavailable or PEC + /// computation fails, an error of kind `ErrorKind::Pec` is returned. + /// + /// Returns `Ok(())` on success or an error converted from the underlying I2C + /// implementation on failure. + fn write_word( + &mut self, + address: u8, + register: u8, + word: u16, + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + let word_bytestream = u16::to_le_bytes(word); + if use_pec { + self.write_buf( + address, + use_pec, + &mut [register, word_bytestream[0], word_bytestream[1], 0], + ) + .await + } else { + self.write_buf( + address, + use_pec, + &mut [register, word_bytestream[0], word_bytestream[1]], + ) + .await + } + } + } + + /// Read Byte + /// + /// Write a command/register and then read a single byte from the target + /// device using a repeated START (no intervening STOP). + /// + /// Parameters: + /// - `address`: 7-bit target device address. + /// - `register`: command/register code to request. + /// - `use_pec`: when true, expect an extra PEC byte after the data and + /// verify it against a locally computed PEC. If PEC support is unavailable + /// or on mismatch, an error of kind `ErrorKind::Pec` is returned. + /// + /// Returns the received byte on success or an error converted from the + /// underlying I2C implementation on failure. + fn read_byte( + &mut self, + address: u8, + register: u8, + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if use_pec { + let mut buf = [0u8; 2]; + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write_u8(address << 1); + pec.write_u8(register); + pec.write_u8((address << 1) | 0x01); + self.transaction(address, &mut [Operation::Write(&[register]), Operation::Read(&mut buf)]) + .await + .map_err(|i2c_err| i2c_err.kind())?; + pec.write_u8(buf[0]); + Self::check_pec(buf[1], pec.finish())?; + Ok(buf[0]) + } else { + let mut buf = [0u8]; + self.transaction(address, &mut [Operation::Write(&[register]), Operation::Read(&mut buf)]) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(buf[0]) + } + } + } + + /// Read Word + /// + /// Write a command/register and then read a 16-bit word from the target + /// device using a repeated START (no intervening STOP). The two bytes are + /// interpreted as little-endian (low byte first). + /// + /// Parameters: + /// - `address`: 7-bit target device address. + /// - `register`: command/register code to request. + /// - `use_pec`: when true, expect an extra PEC byte after the two data + /// bytes and verify it against a locally computed PEC. If PEC support + /// is unavailable or on mismatch, an error of kind `ErrorKind::Pec` + /// is returned. + /// + /// Returns the received 16-bit word on success or an error converted from + /// the underlying I2C implementation on failure. + fn read_word( + &mut self, + address: u8, + register: u8, + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if use_pec { + let mut buf = [0u8; 3]; + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write_u8(address << 1); + pec.write_u8(register); + pec.write_u8((address << 1) | 0x01); + self.transaction(address, &mut [Operation::Write(&[register]), Operation::Read(&mut buf)]) + .await + .map_err(|i2c_err| i2c_err.kind())?; + pec.write(&buf[..2]); + Self::check_pec(buf[1], pec.finish())?; + Ok(u16::from_le_bytes([buf[0], buf[1]])) + } else { + let mut buf = [0u8; 2]; + self.transaction(address, &mut [Operation::Write(&[register]), Operation::Read(&mut buf)]) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(u16::from_le_bytes(buf)) + } + } + } + + /// Process Call + /// + /// Performs a combined write of a 16-bit word to the given `register`, + /// followed by a read of a 16-bit response from the device. + /// + /// Parameters: + /// - `address`: 7-bit target address of the slave device. + /// - `register`: command/register code to send. + /// - `word`: 16-bit parameter sent to the device (little-endian on the bus). + /// - `use_pec`: when true, a PEC (Packet Error Code) is calculated and + /// verified for the returned data. If PEC support is unavailable or + /// verification fails, an error with kind `ErrorKind::Pec` is returned. + /// + /// Returns the 16-bit response from the device on success. + fn process_call( + &mut self, + address: u8, + register: u8, + word: u16, + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if use_pec { + let mut buf = [0u8; 3]; + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write_u8(address << 1); + pec.write_u8(register); + pec.write_u16(word); + pec.write_u8((address << 1) | 0x01); + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + pec.write(&buf[..2]); + Self::check_pec(buf[2], pec.finish())?; + Ok(u16::from_le_bytes([buf[0], buf[1]])) + } else { + let mut buf = [0u8; 2]; + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(u16::from_le_bytes(buf)) + } + } + } + + /// Block Write + /// + /// Sends a block write to `register`. The transfer format is: + /// - write `register` + /// - write `length` (1 byte) + /// - write `length` data bytes + /// - if `use_pec` is true, append PEC (1 byte) + /// + /// `data.len()` must be <= 255. When `use_pec` is true a PEC byte is + /// computed over the same sequence of bytes that appear on the bus and + /// appended to the transaction. If PEC support is unavailable, an error + /// of kind `ErrorKind::Pec` is returned. + fn block_write( + &mut self, + address: u8, + register: u8, + data: &[u8], + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if data.len() > 255 { + return Err(::Error::to_kind( + crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, + )); + } + if use_pec { + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write_u8(address << 1); + pec.write_u8(register); + pec.write_u8(data.len() as u8); + pec.write(data); + let pec: u8 = pec.finish().try_into().map_err(|_| { + ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec) + })?; + Ok(self + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[data.len() as u8]), + Operation::Write(data), + Operation::Write(&[pec]), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?) + } else { + Ok(self + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[data.len() as u8]), + Operation::Write(data), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?) + } + } + } + + /// Block Read + /// + /// Reads a block from `register`. The expected transfer sequence is: + /// - write `register` + /// - read `length` (1 byte) + /// - read `length` data bytes into `data` + /// - if `use_pec` is true, read one PEC byte and verify it + /// + /// The provided `data` buffer should be sized to hold the expected + /// incoming block payload (max 255). If `use_pec` is true, the PEC + /// byte is validated against a locally computed PEC. If PEC support + /// is unavailable or on mismatch, an error with kind `ErrorKind::Pec` + /// is returned. + fn block_read( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if data.len() > 255 { + return Err(::Error::to_kind( + crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, + )); + } + let mut msg_size = [0u8]; + if use_pec { + let mut pec_buf = [0u8]; + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write_u8(address << 1); + pec.write_u8(register); + pec.write_u8((address << 1) | 0x01); + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + pec.write(&msg_size); + pec.write(data); + Self::check_pec(pec_buf[0], pec.finish())?; + Ok(()) + } else { + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(()) + } + } + } + + /// Block Write / Block Read / Process Call + /// + /// Performs a combined transaction that first writes a block payload, + /// then reads back a block response. The semantics are analogous to a + /// block write followed by a block read in a single transaction; when + /// `use_pec` is true the PEC is verified for the entire exchange. + /// + /// Parameters: + /// - `write_data`: data to send as the write block payload. + /// - `read_data`: buffer where the incoming block payload is stored. + /// - The sum of `write_data.len()` and `read_data.len()` must be <= 255. + /// - `use_pec`: when true, a PEC byte is read after the response and + /// validated. If PEC support is unavailable or on mismatch, an + /// `ErrorKind::Pec` is returned. + fn block_write_block_read_process_call( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + use_pec: bool, + ) -> impl core::future::Future::Error>> { + async move { + if write_data.len() + read_data.len() > 255 { + return Err(::Error::to_kind( + crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, + )); + } + let mut read_msg_size = [0u8]; + if use_pec { + let mut pec_buf = [0u8]; + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write_u8(address << 1); + pec.write_u8(register); + pec.write_u8(write_data.len() as u8); + pec.write(write_data); + pec.write_u8((address << 1) | 0x01); + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + pec.write(&read_msg_size); + pec.write(read_data); + Self::check_pec(pec_buf[0], pec.finish())?; + Ok(()) + } else { + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(()) + } + } + } +} diff --git a/embedded-mcu-hal/src/smbus/bus/mod.rs b/embedded-mcu-hal/src/smbus/bus/mod.rs new file mode 100644 index 0000000..707f335 --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/mod.rs @@ -0,0 +1,94 @@ +pub mod asynch; + +/// SMBus error. +pub trait Error: core::fmt::Debug { + /// Convert error to a generic SMBus error kind. + /// + /// By using this method, SMBus errors freely defined by HAL implementations + /// can be converted to a set of generic I2C errors upon which generic + /// code can act. + fn kind(&self) -> ErrorKind; + /// Convert error to a generic SMBus error kind. + fn to_kind(kind: ErrorKind) -> Self; +} + +impl Error for core::convert::Infallible { + #[inline] + fn kind(&self) -> ErrorKind { + match *self {} + } + #[inline] + #[allow(clippy::unreachable)] + fn to_kind(_kind: ErrorKind) -> Self { + unreachable!() + } +} + +/// SMBus error kind. +/// +/// This represents a common set of SMBus operation errors. HAL implementations are +/// free to define more specific or additional error types. However, by providing +/// a mapping to these common SMBus errors, generic code can still react to them. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum ErrorKind { + /// Error shared with I2C. + I2c(embedded_hal_async::i2c::ErrorKind), + /// Bus timeout, SMBus defines slave timeout as 35ms. + Timeout, + /// Packet Error Checking (PEC) byte incorrect. + Pec, + /// Block read/write too large transfer, at most 255 bytes can be read/written at once. + TooLargeBlockTransaction, + /// A different error occurred. The original error may contain more information. + Other, +} + +impl From for ErrorKind { + fn from(value: embedded_hal_async::i2c::ErrorKind) -> Self { + Self::I2c(value) + } +} + +impl Error for ErrorKind { + #[inline] + fn kind(&self) -> ErrorKind { + *self + } + #[inline] + fn to_kind(kind: ErrorKind) -> Self { + kind + } +} + +impl core::fmt::Display for ErrorKind { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::I2c(e) => e.fmt(f), + Self::Timeout => write!(f, "Bus timeout, SMBus defines slave timeout as 35ms"), + Self::Pec => write!(f, "Packet Error Checking (PEC) byte incorrect."), + Self::TooLargeBlockTransaction => write!( + f, + "Block read/write transfer size too large, at most 255 bytes can be read/written at once." + ), + Self::Other => write!( + f, + "A different error occurred. The original error may contain more information" + ), + } + } +} + +/// I2C error type trait. +/// +/// This just defines the error type, to be used by the other traits. +pub trait ErrorType { + /// Error type + type Error: Error + From; +} + +impl ErrorType for &mut T { + type Error = T::Error; +} diff --git a/embedded-mcu-hal/src/smbus/mod.rs b/embedded-mcu-hal/src/smbus/mod.rs new file mode 100644 index 0000000..6440547 --- /dev/null +++ b/embedded-mcu-hal/src/smbus/mod.rs @@ -0,0 +1 @@ +pub mod bus; diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 8bd31a2..2920974 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -11,6 +11,12 @@ who = "Felipe Balbi " criteria = "safe-to-run" delta = "1.0.0 -> 1.0.4" +[[audits.defmt]] +who = "matteotullo " +criteria = "safe-to-deploy" +version = "0.3.100" +notes = "defmt-rtt is used for all our logging purposes." + [[audits.embedded-mcu-hal]] who = "Felipe Balbi " criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index a04850a..ff87d42 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -32,6 +32,13 @@ delta = "0.2.7 -> 1.0.0" notes = "Pure no_std trait crate. Complete API redesign for 1.0: removed nb-based traits, CAN module, all unsafe code. Only defines traits/enums/types for digital, I2C, SPI, PWM, delay. No build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embedded-hal-async]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "no_std async HAL trait definitions. No unsafe in library. Build script only runs rustc --version. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.proc-macro-error-attr2]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -260,8 +267,8 @@ who = "Lukasz Anforowicz " criteria = "safe-to-deploy" version = "1.0.78" notes = """ -Grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits -(except for a benign \"fs\" hit in a doc comment) +Grepped for "crypt", "cipher", "fs", "net" - there were no hits +(except for a benign "fs" hit in a doc comment) Notes from the `unsafe` review can be found in https://crrev.com/c/5385745. """ @@ -379,8 +386,8 @@ who = "Lukasz Anforowicz " criteria = "safe-to-deploy" version = "1.0.35" notes = """ -Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits -(except for benign \"net\" hit in tests and \"fs\" hit in README.md) +Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits +(except for benign "net" hit in tests and "fs" hit in README.md) """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" @@ -485,7 +492,7 @@ and there were no hits except for: * Using `unsafe` in a string: ``` - src/constfn.rs: \"unsafe\" => Qualifiers::Unsafe, + src/constfn.rs: "unsafe" => Qualifiers::Unsafe, ``` * Using `std::fs` in `build/build.rs` to write `${OUT_DIR}/version.expr` From 0dad7cd7fc6552ac3ba65696cd846b81113f3225 Mon Sep 17 00:00:00 2001 From: matteotullo Date: Tue, 12 May 2026 17:03:12 -0700 Subject: [PATCH 2/5] Added unit tests --- embedded-mcu-hal/Cargo.toml | 5 + embedded-mcu-hal/src/smbus/bus/asynch.rs | 1365 ++++++++++++++++------ embedded-mcu-hal/src/smbus/bus/mod.rs | 70 ++ embedded-mcu-hal/src/smbus/mod.rs | 2 + supply-chain/audits.toml | 5 + supply-chain/imports.lock | 37 + 6 files changed, 1154 insertions(+), 330 deletions(-) diff --git a/embedded-mcu-hal/Cargo.toml b/embedded-mcu-hal/Cargo.toml index 6e75d45..8a6127c 100644 --- a/embedded-mcu-hal/Cargo.toml +++ b/embedded-mcu-hal/Cargo.toml @@ -22,6 +22,11 @@ embedded-hal-async = "1.0.0" [dev-dependencies] proptest = "1.0.0" tokio = { version = "1", features = ["macros", "rt", "time"] } +embedded-hal-mock = { version = "0.11", default-features = false, features = [ + "eh1", + "embedded-hal-async", +] } +smbus-pec = { version = "1.0", default-features = false } [features] default = [] diff --git a/embedded-mcu-hal/src/smbus/bus/asynch.rs b/embedded-mcu-hal/src/smbus/bus/asynch.rs index 328bd83..0e04b3f 100644 --- a/embedded-mcu-hal/src/smbus/bus/asynch.rs +++ b/embedded-mcu-hal/src/smbus/bus/asynch.rs @@ -1,3 +1,8 @@ +//! Async SMBus controller trait. +//! +//! See the [parent module](super) for the protocol overview, PEC handling, +//! and driver/HAL guidance. + use core::hash::Hasher; use crate::smbus::bus::Error as SMBusError; @@ -90,6 +95,7 @@ use embedded_hal_async::i2c::{Error as I2cError, Operation}; /// } /// } /// ``` +#[allow(async_fn_in_trait)] pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// PEC (Packet Error Code) calculator type. /// @@ -141,6 +147,27 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { .ok_or_else(|| ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec)) } + /// Obtain a fresh PEC calculator pre-fed with the write-address byte. + /// + /// Returns an [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) error + /// if [`get_pec_calc`](Self::get_pec_calc) returns `None`. + fn pec_calc_with_write_addr(address: u8) -> Result::Error> { + let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write_u8(crate::smbus::bus::write_address_byte(address)); + Ok(pec) + } + + /// Truncate a finished PEC value to its low byte. + /// + /// Returns an [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) error + /// if the value does not fit in a byte. + fn finalize_pec_byte(pec: u64) -> Result::Error> { + pec.try_into() + .map_err(|_| ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec)) + } + /// Write a buffer of data with optional PEC computation and verification. /// /// This is a low-level helper method that performs I2C write operations with @@ -162,37 +189,27 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// - The underlying I2C write fails (converted from `I2cError`) /// - PEC is requested but unavailable (returns `ErrorKind::Pec`) /// - PEC computation fails or overflows (returns `ErrorKind::Pec`) - fn write_buf( + async fn write_buf( &mut self, address: u8, use_pec: bool, operations: &mut [u8], - ) -> impl core::future::Future::Error>> { - async move { - if use_pec { - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; - - pec.write_u8(address << 1); - let (pec_elem, rest) = operations.split_last_mut().ok_or( - ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec), - )?; - pec.write(rest); - *pec_elem = pec.finish().try_into().map_err(|_| { - ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec) - })?; - - self.write(address, operations) - .await - .map_err(|i2c_err| i2c_err.kind())?; - } else { - self.write(address, operations) - .await - .map_err(|i2c_err| i2c_err.kind())?; - } - Ok(()) + ) -> Result<(), ::Error> { + if use_pec { + let mut pec = Self::pec_calc_with_write_addr(address)?; + let (pec_elem, rest) = + operations + .split_last_mut() + .ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write(rest); + *pec_elem = Self::finalize_pec_byte(pec.finish())?; } + self.write(address, operations) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(()) } /// Read a buffer of data with optional PEC verification. @@ -217,31 +234,72 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// - The underlying I2C read fails (converted from `I2cError`) /// - PEC is requested but unavailable (returns `ErrorKind::Pec`) /// - PEC verification fails due to mismatch (returns `ErrorKind::Pec`) - fn read_buf( + async fn read_buf( &mut self, address: u8, use_pec: bool, read: &mut [u8], - ) -> impl core::future::Future::Error>> { - async move { - if use_pec { - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + ) -> Result<(), ::Error> { + if use_pec { + let mut pec = Self::pec_calc_with_write_addr(address)?; + self.read(address, read).await.map_err(|i2c_err| i2c_err.kind())?; + let (pec_byte, rest) = read + .split_last() + .ok_or(::Error::to_kind( crate::smbus::bus::ErrorKind::Pec, ))?; - pec.write_u8(address << 1); - self.read(address, read).await.map_err(|i2c_err| i2c_err.kind())?; - let (pec_byte, rest) = read.split_last().ok_or( - ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec), - )?; - pec.write(rest); - - Self::check_pec(*pec_byte, pec.finish())?; - } else { - self.read(address, read).await.map_err(|i2c_err| i2c_err.kind())?; - } + pec.write(rest); - Ok(()) + Self::check_pec(*pec_byte, pec.finish())?; + } else { + self.read(address, read).await.map_err(|i2c_err| i2c_err.kind())?; + } + + Ok(()) + } + + /// Write a buffer and then read a buffer, with optional PEC verification. + /// + /// Performs a single I²C transaction consisting of a `Write(write)` + /// followed by a `Read(read)`. When `use_pec` is true, the caller must + /// size `read` to include one extra trailing byte for the PEC; that + /// byte is then verified against a locally computed PEC that covers + /// (in bus order) the write-address byte, `write`, the read-address + /// byte and the data portion of `read` (everything except the trailing + /// PEC byte). + /// + /// Returns an [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) + /// error if PEC support is unavailable or the received PEC does not + /// match. + async fn write_read_buf( + &mut self, + address: u8, + use_pec: bool, + write: &[u8], + read: &mut [u8], + ) -> Result<(), ::Error> { + // When PEC is requested, fail fast without touching the bus if no + // PEC calculator is available. + let mut pec = if use_pec { + Some(Self::pec_calc_with_write_addr(address)?) + } else { + None + }; + self.transaction(address, &mut [Operation::Write(write), Operation::Read(read)]) + .await + .map_err(|i2c_err| i2c_err.kind())?; + if let Some(pec) = pec.as_mut() { + pec.write(write); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + let (pec_byte, rest) = read + .split_last() + .ok_or(::Error::to_kind( + crate::smbus::bus::ErrorKind::Pec, + ))?; + pec.write(rest); + Self::check_pec(*pec_byte, pec.finish())?; } + Ok(()) } /// Quick Command @@ -257,24 +315,22 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// Returns `Ok(())` on success or an error converted from the underlying I2C /// implementation on failure. #[inline] - fn quick_command( + async fn quick_command( &mut self, address: u8, read: bool, - ) -> impl core::future::Future::Error>> { - async move { - self.transaction( - address, - &mut if read { - [Operation::Read(&mut [])] - } else { - [Operation::Write(&[])] - }, - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - Ok(()) - } + ) -> Result<(), ::Error> { + self.transaction( + address, + &mut if read { + [Operation::Read(&mut [])] + } else { + [Operation::Write(&[])] + }, + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(()) } /// Send Byte @@ -290,18 +346,16 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// /// Returns `Ok(())` on success or an error converted from the underlying I2C /// implementation on failure. - fn send_byte( + async fn send_byte( &mut self, address: u8, byte: u8, use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if use_pec { - self.write_buf(address, true, &mut [byte, 0]).await - } else { - self.write_buf(address, true, &mut [byte]).await - } + ) -> Result<(), ::Error> { + if use_pec { + self.write_buf(address, true, &mut [byte, 0]).await + } else { + self.write_buf(address, false, &mut [byte]).await } } @@ -317,21 +371,19 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// /// Returns the received byte on success or an error converted from the /// underlying I2C implementation on failure. - fn receive_byte( + async fn receive_byte( &mut self, address: u8, use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if use_pec { - let mut buf = [0u8, 2]; - self.read_buf(address, use_pec, &mut buf).await?; - Ok(buf[0]) - } else { - let mut buf = [0u8]; - self.read_buf(address, use_pec, &mut buf).await?; - Ok(buf[0]) - } + ) -> Result::Error> { + if use_pec { + let mut buf = [0u8; 2]; + self.read_buf(address, use_pec, &mut buf).await?; + Ok(buf[0]) + } else { + let mut buf = [0u8]; + self.read_buf(address, use_pec, &mut buf).await?; + Ok(buf[0]) } } @@ -349,19 +401,17 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// /// Returns `Ok(())` on success or an error converted from the underlying I2C /// implementation on failure. - fn write_byte( + async fn write_byte( &mut self, address: u8, register: u8, byte: u8, use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if use_pec { - self.write_buf(address, use_pec, &mut [register, byte, 0]).await - } else { - self.write_buf(address, use_pec, &mut [register, byte]).await - } + ) -> Result<(), ::Error> { + if use_pec { + self.write_buf(address, use_pec, &mut [register, byte, 0]).await + } else { + self.write_buf(address, use_pec, &mut [register, byte]).await } } @@ -380,30 +430,28 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// /// Returns `Ok(())` on success or an error converted from the underlying I2C /// implementation on failure. - fn write_word( + async fn write_word( &mut self, address: u8, register: u8, word: u16, use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - let word_bytestream = u16::to_le_bytes(word); - if use_pec { - self.write_buf( - address, - use_pec, - &mut [register, word_bytestream[0], word_bytestream[1], 0], - ) - .await - } else { - self.write_buf( - address, - use_pec, - &mut [register, word_bytestream[0], word_bytestream[1]], - ) - .await - } + ) -> Result<(), ::Error> { + let word_bytestream = u16::to_le_bytes(word); + if use_pec { + self.write_buf( + address, + use_pec, + &mut [register, word_bytestream[0], word_bytestream[1], 0], + ) + .await + } else { + self.write_buf( + address, + use_pec, + &mut [register, word_bytestream[0], word_bytestream[1]], + ) + .await } } @@ -421,34 +469,20 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// /// Returns the received byte on success or an error converted from the /// underlying I2C implementation on failure. - fn read_byte( + async fn read_byte( &mut self, address: u8, register: u8, use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if use_pec { - let mut buf = [0u8; 2]; - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; - pec.write_u8(address << 1); - pec.write_u8(register); - pec.write_u8((address << 1) | 0x01); - self.transaction(address, &mut [Operation::Write(&[register]), Operation::Read(&mut buf)]) - .await - .map_err(|i2c_err| i2c_err.kind())?; - pec.write_u8(buf[0]); - Self::check_pec(buf[1], pec.finish())?; - Ok(buf[0]) - } else { - let mut buf = [0u8]; - self.transaction(address, &mut [Operation::Write(&[register]), Operation::Read(&mut buf)]) - .await - .map_err(|i2c_err| i2c_err.kind())?; - Ok(buf[0]) - } + ) -> Result::Error> { + if use_pec { + let mut buf = [0u8; 2]; + self.write_read_buf(address, true, &[register], &mut buf).await?; + Ok(buf[0]) + } else { + let mut buf = [0u8; 1]; + self.write_read_buf(address, false, &[register], &mut buf).await?; + Ok(buf[0]) } } @@ -468,34 +502,20 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// /// Returns the received 16-bit word on success or an error converted from /// the underlying I2C implementation on failure. - fn read_word( + async fn read_word( &mut self, address: u8, register: u8, use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if use_pec { - let mut buf = [0u8; 3]; - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; - pec.write_u8(address << 1); - pec.write_u8(register); - pec.write_u8((address << 1) | 0x01); - self.transaction(address, &mut [Operation::Write(&[register]), Operation::Read(&mut buf)]) - .await - .map_err(|i2c_err| i2c_err.kind())?; - pec.write(&buf[..2]); - Self::check_pec(buf[1], pec.finish())?; - Ok(u16::from_le_bytes([buf[0], buf[1]])) - } else { - let mut buf = [0u8; 2]; - self.transaction(address, &mut [Operation::Write(&[register]), Operation::Read(&mut buf)]) - .await - .map_err(|i2c_err| i2c_err.kind())?; - Ok(u16::from_le_bytes(buf)) - } + ) -> Result::Error> { + if use_pec { + let mut buf = [0u8; 3]; + self.write_read_buf(address, true, &[register], &mut buf).await?; + Ok(u16::from_le_bytes([buf[0], buf[1]])) + } else { + let mut buf = [0u8; 2]; + self.write_read_buf(address, false, &[register], &mut buf).await?; + Ok(u16::from_le_bytes(buf)) } } @@ -513,50 +533,50 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// verification fails, an error with kind `ErrorKind::Pec` is returned. /// /// Returns the 16-bit response from the device on success. - fn process_call( + async fn process_call( &mut self, address: u8, register: u8, word: u16, use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if use_pec { - let mut buf = [0u8; 3]; - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( + ) -> Result::Error> { + if use_pec { + let mut buf = [0u8; 3]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u16(word); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + let (recvd_pec, data) = buf + .split_last() + .ok_or(::Error::to_kind( crate::smbus::bus::ErrorKind::Pec, ))?; - pec.write_u8(address << 1); - pec.write_u8(register); - pec.write_u16(word); - pec.write_u8((address << 1) | 0x01); - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&word.to_le_bytes()), - Operation::Read(&mut buf), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - pec.write(&buf[..2]); - Self::check_pec(buf[2], pec.finish())?; - Ok(u16::from_le_bytes([buf[0], buf[1]])) - } else { - let mut buf = [0u8; 2]; - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&word.to_le_bytes()), - Operation::Read(&mut buf), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - Ok(u16::from_le_bytes(buf)) - } + pec.write(data); + Self::check_pec(*recvd_pec, pec.finish())?; + Ok(u16::from_le_bytes([buf[0], buf[1]])) + } else { + let mut buf = [0u8; 2]; + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(u16::from_le_bytes(buf)) } } @@ -572,55 +592,48 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// computed over the same sequence of bytes that appear on the bus and /// appended to the transaction. If PEC support is unavailable, an error /// of kind `ErrorKind::Pec` is returned. - fn block_write( + async fn block_write( &mut self, address: u8, register: u8, data: &[u8], use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if data.len() > 255 { - return Err(::Error::to_kind( - crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, - )); - } - if use_pec { - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; - pec.write_u8(address << 1); - pec.write_u8(register); - pec.write_u8(data.len() as u8); - pec.write(data); - let pec: u8 = pec.finish().try_into().map_err(|_| { - ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec) - })?; - Ok(self - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[data.len() as u8]), - Operation::Write(data), - Operation::Write(&[pec]), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?) - } else { - Ok(self - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[data.len() as u8]), - Operation::Write(data), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?) - } + ) -> Result<(), ::Error> { + if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(::Error::to_kind( + crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, + )); + } + if use_pec { + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(data.len() as u8); + pec.write(data); + let pec: u8 = Self::finalize_pec_byte(pec.finish())?; + Ok(self + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[data.len() as u8]), + Operation::Write(data), + Operation::Write(&[pec]), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?) + } else { + Ok(self + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[data.len() as u8]), + Operation::Write(data), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?) } } @@ -637,56 +650,51 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// byte is validated against a locally computed PEC. If PEC support /// is unavailable or on mismatch, an error with kind `ErrorKind::Pec` /// is returned. - fn block_read( + async fn block_read( &mut self, address: u8, register: u8, data: &mut [u8], use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if data.len() > 255 { - return Err(::Error::to_kind( - crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, - )); - } - let mut msg_size = [0u8]; - if use_pec { - let mut pec_buf = [0u8]; - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; - pec.write_u8(address << 1); - pec.write_u8(register); - pec.write_u8((address << 1) | 0x01); - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Read(&mut msg_size), - Operation::Read(data), - Operation::Read(&mut pec_buf), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - pec.write(&msg_size); - pec.write(data); - Self::check_pec(pec_buf[0], pec.finish())?; - Ok(()) - } else { - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Read(&mut msg_size), - Operation::Read(data), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - Ok(()) - } + ) -> Result<(), ::Error> { + if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(::Error::to_kind( + crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, + )); + } + let mut msg_size = [0u8]; + if use_pec { + let mut pec_buf = [0u8]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + pec.write(&msg_size); + pec.write(data); + Self::check_pec(pec_buf[0], pec.finish())?; + Ok(()) + } else { + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(()) } } @@ -704,63 +712,760 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// - `use_pec`: when true, a PEC byte is read after the response and /// validated. If PEC support is unavailable or on mismatch, an /// `ErrorKind::Pec` is returned. - fn block_write_block_read_process_call( + async fn block_write_block_read_process_call( &mut self, address: u8, register: u8, write_data: &[u8], read_data: &mut [u8], use_pec: bool, - ) -> impl core::future::Future::Error>> { - async move { - if write_data.len() + read_data.len() > 255 { - return Err(::Error::to_kind( - crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, - )); - } - let mut read_msg_size = [0u8]; - if use_pec { - let mut pec_buf = [0u8]; - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; - pec.write_u8(address << 1); - pec.write_u8(register); - pec.write_u8(write_data.len() as u8); - pec.write(write_data); - pec.write_u8((address << 1) | 0x01); - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[write_data.len() as u8]), - Operation::Write(write_data), - Operation::Read(&mut read_msg_size), - Operation::Read(read_data), - Operation::Read(&mut pec_buf), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - pec.write(&read_msg_size); - pec.write(read_data); - Self::check_pec(pec_buf[0], pec.finish())?; - Ok(()) - } else { - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[write_data.len() as u8]), - Operation::Write(write_data), - Operation::Read(&mut read_msg_size), - Operation::Read(read_data), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - Ok(()) - } + ) -> Result<(), ::Error> { + if write_data.len() + read_data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(::Error::to_kind( + crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, + )); + } + let mut read_msg_size = [0u8]; + if use_pec { + let mut pec_buf = [0u8]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(write_data.len() as u8); + pec.write(write_data); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + pec.write(&read_msg_size); + pec.write(read_data); + Self::check_pec(pec_buf[0], pec.finish())?; + Ok(()) + } else { + self.transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + ], + ) + .await + .map_err(|i2c_err| i2c_err.kind())?; + Ok(()) + } + } +} + +impl Smbus for &mut T { + type PecCalc = T::PecCalc; + + #[inline] + fn get_pec_calc() -> Option { + T::get_pec_calc() + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::cast_possible_truncation)] +mod tests { + use super::Smbus; + use crate::smbus::bus::{ + read_address_byte, write_address_byte, Error as SmbusError, ErrorKind, ErrorType, MAX_BLOCK_SIZE, READ_BIT, + }; + use core::hash::Hasher; + use embedded_hal_async::i2c::{ErrorKind as I2cErrorKind, I2c, Operation}; + use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as Tx}; + use smbus_pec::{pec, Pec}; + + const ADDR: u8 = 0x42; + const REG: u8 = 0x07; + + /// Test SMBus error type. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct TestError(ErrorKind); + + impl From for TestError { + fn from(k: I2cErrorKind) -> Self { + Self(ErrorKind::I2c(k)) + } + } + + impl SmbusError for TestError { + fn kind(&self) -> ErrorKind { + self.0 + } + fn to_kind(kind: ErrorKind) -> Self { + Self(kind) + } + } + + /// Compute the expected SMBus PEC byte over a flat concatenation of byte + /// slices, using the `smbus-pec` crate as the reference implementation. + fn expected_pec(parts: &[&[u8]]) -> u8 { + let mut buf: std::vec::Vec = std::vec::Vec::new(); + for p in parts { + buf.extend_from_slice(p); + } + pec(&buf) + } + + /// Bus that wires `embedded_hal_mock::eh1::i2c::Mock` to the `Smbus` trait. + struct TestBus { + i2c: I2cMock, + } + + impl embedded_hal_async::i2c::ErrorType for TestBus { + type Error = I2cErrorKind; + } + + impl I2c for TestBus { + async fn transaction(&mut self, address: u8, ops: &mut [Operation<'_>]) -> Result<(), Self::Error> { + ::transaction(&mut self.i2c, address, ops).await + } + async fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> { + ::read(&mut self.i2c, address, read).await + } + async fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> { + ::write(&mut self.i2c, address, write).await + } + } + + impl ErrorType for TestBus { + type Error = TestError; + } + + impl Smbus for TestBus { + type PecCalc = Pec; + fn get_pec_calc() -> Option { + Some(Pec::new()) + } + } + + /// Bus without PEC support, used to validate the unavailable-PEC error path. + struct NoPecBus { + i2c: I2cMock, + } + + impl embedded_hal_async::i2c::ErrorType for NoPecBus { + type Error = I2cErrorKind; + } + + impl I2c for NoPecBus { + async fn transaction(&mut self, address: u8, ops: &mut [Operation<'_>]) -> Result<(), Self::Error> { + ::transaction(&mut self.i2c, address, ops).await + } + async fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> { + ::read(&mut self.i2c, address, read).await + } + async fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> { + ::write(&mut self.i2c, address, write).await + } + } + + impl ErrorType for NoPecBus { + type Error = TestError; + } + + impl Smbus for NoPecBus { + type PecCalc = Pec; + fn get_pec_calc() -> Option { + None + } + } + + fn new_bus(expectations: &[Tx]) -> TestBus { + TestBus { + i2c: I2cMock::new(expectations), + } + } + + fn done(mut bus: TestBus) { + bus.i2c.done(); + } + + // ---------- constants / helpers ---------- + + #[test] + fn constants() { + assert_eq!(MAX_BLOCK_SIZE, 255); + assert_eq!(READ_BIT, 0x01); + assert_eq!(write_address_byte(0x42), 0x84); + assert_eq!(read_address_byte(0x42), 0x85); + } + + #[test] + fn error_kind_display_and_kind() { + let k = ErrorKind::Timeout; + assert_eq!(k.kind(), ErrorKind::Timeout); + // Display impls cover all branches. + for k in [ + ErrorKind::I2c(I2cErrorKind::Bus), + ErrorKind::Timeout, + ErrorKind::Pec, + ErrorKind::TooLargeBlockTransaction, + ErrorKind::Other, + ] { + let s = std::format!("{}", k); + assert!(!s.is_empty()); + } + } + + #[test] + fn error_kind_from_i2c_error_kind() { + let k: ErrorKind = I2cErrorKind::Bus.into(); + assert_eq!(k, ErrorKind::I2c(I2cErrorKind::Bus)); + } + + #[test] + fn infallible_error_to_kind_round_trip() { + // Infallible cannot be constructed; we only check the trait wires up. + fn _accepts(_e: &E) {} + let k = ErrorKind::Pec; + _accepts(&k); + } + + #[tokio::test] + async fn check_pec_match() { + let bus = new_bus(&[]); + TestBus::check_pec(0x42, 0x42).unwrap(); + TestBus::check_pec(0x00, 0x00).unwrap(); + done(bus); + } + + #[tokio::test] + async fn check_pec_mismatch() { + let bus = new_bus(&[]); + let err = TestBus::check_pec(0x42, 0x43).unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); + } + + // ---------- write_buf / read_buf (low-level) ---------- + + #[tokio::test] + async fn write_buf_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0xAB, 0xCD])]); + bus.write_buf(ADDR, false, &mut [0xAB, 0xCD]).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn write_buf_pec() { + let payload = [0xAB, 0xCD]; + let pec = expected_pec(&[&[write_address_byte(ADDR)], &payload]); + let mut buf = [0xAB, 0xCD, 0x00]; + let mut wire = std::vec![0xAB, 0xCD, pec]; + let mut bus = new_bus(&[Tx::write(ADDR, wire.clone())]); + bus.write_buf(ADDR, true, &mut buf).await.unwrap(); + // Last byte should now be the PEC. + assert_eq!(buf[2], pec); + wire.clear(); + done(bus); + } + + #[tokio::test] + async fn read_buf_no_pec() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x11, 0x22])]); + let mut buf = [0u8; 2]; + bus.read_buf(ADDR, false, &mut buf).await.unwrap(); + assert_eq!(buf, [0x11, 0x22]); + done(bus); + } + + #[tokio::test] + async fn read_buf_pec() { + let data = 0x11u8; + let pec = expected_pec(&[&[write_address_byte(ADDR)], &[data]]); + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); + let mut buf = [0u8; 2]; + bus.read_buf(ADDR, true, &mut buf).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn read_buf_pec_mismatch() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x11, 0xFF])]); // wrong PEC + let mut buf = [0u8; 2]; + let err = bus.read_buf(ADDR, true, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); + } + + // ---------- quick_command ---------- + + #[tokio::test] + async fn quick_command_write() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![]), + Tx::transaction_end(ADDR), + ]); + bus.quick_command(ADDR, false).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn quick_command_read() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::read(ADDR, std::vec![]), + Tx::transaction_end(ADDR), + ]); + bus.quick_command(ADDR, true).await.unwrap(); + done(bus); + } + + // ---------- send_byte / receive_byte ---------- + + #[tokio::test] + async fn send_byte_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); + bus.send_byte(ADDR, 0x55, false).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn send_byte_pec() { + let pec = expected_pec(&[&[write_address_byte(ADDR), 0x55]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55, pec])]); + bus.send_byte(ADDR, 0x55, true).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn receive_byte_no_pec() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99])]); + let b = bus.receive_byte(ADDR, false).await.unwrap(); + assert_eq!(b, 0x99); + done(bus); + } + + #[tokio::test] + async fn receive_byte_pec() { + let data = 0x99u8; + let pec = expected_pec(&[&[write_address_byte(ADDR), data]]); + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); + let b = bus.receive_byte(ADDR, true).await.unwrap(); + assert_eq!(b, data); + done(bus); + } + + #[tokio::test] + async fn receive_byte_pec_mismatch() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99, 0xFF])]); + let err = bus.receive_byte(ADDR, true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); + } + + // ---------- write_byte / write_word ---------- + + #[tokio::test] + async fn write_byte_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33])]); + bus.write_byte(ADDR, REG, 0x33, false).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn write_byte_pec() { + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, 0x33]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33, pec])]); + bus.write_byte(ADDR, REG, 0x33, true).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn write_word_no_pec() { + let word: u16 = 0xBEEF; + let bytes = word.to_le_bytes(); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1]])]); + bus.write_word(ADDR, REG, word, false).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn write_word_pec() { + let word: u16 = 0xBEEF; + let bytes = word.to_le_bytes(); + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, bytes[0], bytes[1]]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1], pec])]); + bus.write_word(ADDR, REG, word, true).await.unwrap(); + done(bus); + } + + // ---------- read_byte / read_word ---------- + + #[tokio::test] + async fn read_byte_no_pec() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77]), + Tx::transaction_end(ADDR), + ]); + let b = bus.read_byte(ADDR, REG, false).await.unwrap(); + assert_eq!(b, 0x77); + done(bus); + } + + #[tokio::test] + async fn read_byte_pec() { + let data = 0x77u8; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, read_address_byte(ADDR), data]]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![data, pec]), + Tx::transaction_end(ADDR), + ]); + let b = bus.read_byte(ADDR, REG, true).await.unwrap(); + assert_eq!(b, data); + done(bus); + } + + #[tokio::test] + async fn read_byte_pec_mismatch() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77, 0xFF]), + Tx::transaction_end(ADDR), + ]); + let err = bus.read_byte(ADDR, REG, true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); + } + + #[tokio::test] + async fn read_word_no_pec() { + let lo = 0x12u8; + let hi = 0x34u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![lo, hi]), + Tx::transaction_end(ADDR), + ]); + let w = bus.read_word(ADDR, REG, false).await.unwrap(); + assert_eq!(w, u16::from_le_bytes([lo, hi])); + done(bus); + } + + #[tokio::test] + async fn read_word_pec() { + let lo = 0x12u8; + let hi = 0x34u8; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, read_address_byte(ADDR), lo, hi]]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![lo, hi, pec]), + Tx::transaction_end(ADDR), + ]); + let w = bus.read_word(ADDR, REG, true).await.unwrap(); + assert_eq!(w, u16::from_le_bytes([lo, hi])); + done(bus); + } + + // ---------- process_call ---------- + + #[tokio::test] + async fn process_call_no_pec() { + let word: u16 = 0x0102; + let resp_lo = 0xAAu8; + let resp_hi = 0xBBu8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, word.to_le_bytes().to_vec()), + Tx::read(ADDR, std::vec![resp_lo, resp_hi]), + Tx::transaction_end(ADDR), + ]); + let r = bus.process_call(ADDR, REG, word, false).await.unwrap(); + assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); + done(bus); + } + + #[tokio::test] + async fn process_call_pec() { + let word: u16 = 0x0102; + let resp_lo = 0xAAu8; + let resp_hi = 0xBBu8; + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write_u16(word); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[resp_lo, resp_hi]); + let pec = hasher.finish() as u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, word.to_le_bytes().to_vec()), + Tx::read(ADDR, std::vec![resp_lo, resp_hi, pec]), + Tx::transaction_end(ADDR), + ]); + let r = bus.process_call(ADDR, REG, word, true).await.unwrap(); + assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); + done(bus); + } + + // ---------- block_write ---------- + + #[tokio::test] + async fn block_write_no_pec() { + let data = [0xDE, 0xAD, 0xBE, 0xEF]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![data.len() as u8]), + Tx::write(ADDR, data.to_vec()), + Tx::transaction_end(ADDR), + ]); + bus.block_write(ADDR, REG, &data, false).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn block_write_pec() { + let data = [0xDE, 0xAD, 0xBE, 0xEF]; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, data.len() as u8], &data]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![data.len() as u8]), + Tx::write(ADDR, data.to_vec()), + Tx::write(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_write(ADDR, REG, &data, true).await.unwrap(); + done(bus); + } + + #[tokio::test] + async fn block_write_too_large() { + let mut bus = new_bus(&[]); + let data = std::vec![0u8; MAX_BLOCK_SIZE + 1]; + let err = bus.block_write(ADDR, REG, &data, false).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); + } + + // ---------- block_read ---------- + + #[tokio::test] + async fn block_read_no_pec() { + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![3]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::transaction_end(ADDR), + ]); + bus.block_read(ADDR, REG, &mut buf, false).await.unwrap(); + assert_eq!(buf, [0x10, 0x20, 0x30]); + done(bus); + } + + #[tokio::test] + async fn block_read_pec() { + let payload = [0x10u8, 0x20, 0x30]; + let len = payload.len() as u8; + // PEC source matches the implementation: addr+W, reg, addr+R, then msg_size, then data. + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[len]); + hasher.write(&payload); + let pec = hasher.finish() as u8; + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![len]), + Tx::read(ADDR, payload.to_vec()), + Tx::read(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_read(ADDR, REG, &mut buf, true).await.unwrap(); + assert_eq!(buf, payload); + done(bus); + } + + #[tokio::test] + async fn block_read_too_large() { + let mut bus = new_bus(&[]); + let mut buf = std::vec![0u8; MAX_BLOCK_SIZE + 1]; + let err = bus.block_read(ADDR, REG, &mut buf, false).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); + } + + // ---------- block_write_block_read_process_call ---------- + + #[tokio::test] + async fn bwbr_no_pec() { + let write_data = [0x01u8, 0x02]; + let read_payload = [0xAAu8, 0xBB]; + let mut read_buf = [0u8; 2]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![read_payload.len() as u8]), + Tx::read(ADDR, read_payload.to_vec()), + Tx::transaction_end(ADDR), + ]); + bus.block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf, false) + .await + .unwrap(); + assert_eq!(read_buf, read_payload); + done(bus); + } + + #[tokio::test] + async fn bwbr_pec() { + let write_data = [0x01u8, 0x02]; + let read_payload = [0xAAu8, 0xBB]; + let mut read_buf = [0u8; 2]; + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write_u8(write_data.len() as u8); + hasher.write(&write_data); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[read_payload.len() as u8]); + hasher.write(&read_payload); + let pec = hasher.finish() as u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![read_payload.len() as u8]), + Tx::read(ADDR, read_payload.to_vec()), + Tx::read(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf, true) + .await + .unwrap(); + assert_eq!(read_buf, read_payload); + done(bus); + } + + #[tokio::test] + async fn bwbr_too_large() { + let mut bus = new_bus(&[]); + let write_data = std::vec![0u8; 200]; + let mut read_buf = std::vec![0u8; 60]; // 200 + 60 > 255 + let err = bus + .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf, false) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); + } + + // ---------- PEC unavailable ---------- + + fn new_no_pec_bus(expectations: &[Tx]) -> NoPecBus { + NoPecBus { + i2c: I2cMock::new(expectations), } } + + fn done_no_pec(mut bus: NoPecBus) { + bus.i2c.done(); + } + + #[test] + fn no_pec_bus_get_pec_calc_returns_none() { + assert!(NoPecBus::get_pec_calc().is_none()); + } + + #[tokio::test] + async fn no_pec_bus_non_pec_ops_still_work() { + // All `use_pec = false` paths must succeed even though `get_pec_calc` + // returns `None`: the trait must not consult the PEC calculator unless + // PEC was actually requested. + let mut bus = new_no_pec_bus(&[ + Tx::write(ADDR, std::vec![0x55]), + Tx::read(ADDR, std::vec![0x99]), + Tx::write(ADDR, std::vec![REG, 0x33]), + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77]), + Tx::transaction_end(ADDR), + ]); + bus.send_byte(ADDR, 0x55, false).await.unwrap(); + assert_eq!(bus.receive_byte(ADDR, false).await.unwrap(), 0x99); + bus.write_byte(ADDR, REG, 0x33, false).await.unwrap(); + assert_eq!(bus.read_byte(ADDR, REG, false).await.unwrap(), 0x77); + done_no_pec(bus); + } + + #[tokio::test] + async fn pec_unavailable_returns_pec_error() { + let mut bus = NoPecBus { i2c: I2cMock::new(&[]) }; + // Any PEC-requiring path should fail without touching the bus. + let err = bus.send_byte(ADDR, 0x55, true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.receive_byte(ADDR, true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.read_byte(ADDR, REG, true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.read_word(ADDR, REG, true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.process_call(ADDR, REG, 0, true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.block_write(ADDR, REG, &[1, 2], true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let mut rb = [0u8; 2]; + let err = bus.block_read(ADDR, REG, &mut rb, true).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus + .block_write_block_read_process_call(ADDR, REG, &[1], &mut rb, true) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + bus.i2c.done(); + } + + // ---------- &mut T forwarding ---------- + + #[tokio::test] + async fn mut_ref_smbus_forwards() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); + let r: &mut TestBus = &mut bus; + r.send_byte(ADDR, 0x55, false).await.unwrap(); + assert!(<&mut TestBus as Smbus>::get_pec_calc().is_some()); + done(bus); + } + + // ---------- error propagation from underlying I2C ---------- + + #[tokio::test] + async fn i2c_error_propagates() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55]).with_error(I2cErrorKind::Bus)]); + let err = bus.send_byte(ADDR, 0x55, false).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::I2c(I2cErrorKind::Bus)); + done(bus); + } } diff --git a/embedded-mcu-hal/src/smbus/bus/mod.rs b/embedded-mcu-hal/src/smbus/bus/mod.rs index 707f335..d09025a 100644 --- a/embedded-mcu-hal/src/smbus/bus/mod.rs +++ b/embedded-mcu-hal/src/smbus/bus/mod.rs @@ -1,5 +1,75 @@ +//! SMBus controller API. +//! +//! This module hosts SMBus controller-side traits built on top of the +//! controller traits from [`embedded_hal_async::i2c`]. Where the underlying +//! I²C controller traits move arbitrary byte streams across the bus, the +//! SMBus traits encode the higher-level SMBus protocol transactions +//! (quick command, send/receive byte, byte/word/block read/write, process +//! calls) together with optional Packet Error Code (PEC) computation and +//! verification. +//! +//! # PEC handling +//! +//! When an SMBus operation is invoked with `use_pec = true`, the +//! implementation obtains a fresh PEC calculator from +//! [`asynch::Smbus::get_pec_calc`] and feeds it the bytes that appear on +//! the wire (address, register, payload, …) in bus order. The truncated +//! low byte of [`core::hash::Hasher::finish`] is treated as the PEC. +//! +//! Implementations that do not support PEC return `None` from +//! `get_pec_calc()`; any operation with `use_pec = true` then fails with +//! [`ErrorKind::Pec`]. +//! +//! # For driver authors +//! +//! Drivers should take an `Smbus` instance by value, not by `&mut`. The +//! blanket impl for `&mut T` lets the user pass either, but owning the +//! instance keeps the driver's API symmetric with the controller-side +//! traits in [`embedded_hal_async::i2c`]. +//! +//! # For HAL authors +//! +//! - Bus configuration (clocking, addressing, SMBus role) is a peripheral +//! concern handled at construction time. These traits deliberately +//! expose none of that — they only describe the protocol-level +//! transactions. +//! +//! - Block transfers are capped at 255 bytes per the SMBus specification; +//! exceeding this returns [`ErrorKind::TooLargeBlockTransaction`]. +//! +//! - The SMBus slave timeout (35 ms) is reported as [`ErrorKind::Timeout`]. +//! +//! [`embedded_hal_async::i2c`]: +//! https://docs.rs/embedded-hal-async/1.0.0/embedded_hal_async/i2c/index.html + pub mod asynch; +/// Maximum payload size, in bytes, of a single SMBus block transfer. +/// +/// The SMBus specification caps the `length` field of a block read or +/// block write at one byte, so a single block transaction can carry at +/// most 255 data bytes. +pub(crate) const MAX_BLOCK_SIZE: usize = 255; + +/// Read-bit value OR-ed into the shifted address byte to mark a read. +/// +/// The 8-bit address byte placed on the wire is `(address << 1) | rw`, +/// where `rw` is `0` for a write and [`READ_BIT`] (`1`) for a read. +pub(crate) const READ_BIT: u8 = 0x01; + +/// Compute the 8-bit write-address byte (`address << 1`) used on the wire. +#[inline] +pub(crate) const fn write_address_byte(address: u8) -> u8 { + address << 1 +} + +/// Compute the 8-bit read-address byte (`(address << 1) | READ_BIT`) used +/// on the wire. +#[inline] +pub(crate) const fn read_address_byte(address: u8) -> u8 { + (address << 1) | READ_BIT +} + /// SMBus error. pub trait Error: core::fmt::Debug { /// Convert error to a generic SMBus error kind. diff --git a/embedded-mcu-hal/src/smbus/mod.rs b/embedded-mcu-hal/src/smbus/mod.rs index 6440547..c734dcd 100644 --- a/embedded-mcu-hal/src/smbus/mod.rs +++ b/embedded-mcu-hal/src/smbus/mod.rs @@ -1 +1,3 @@ +//! Traits for interacting with SMBus controllers and targets. + pub mod bus; diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 2920974..cdeed2a 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -17,6 +17,11 @@ criteria = "safe-to-deploy" version = "0.3.100" notes = "defmt-rtt is used for all our logging purposes." +[[audits.embedded-hal-mock]] +who = "matteotullo " +criteria = "safe-to-run" +delta = "0.8.0 -> 0.11.1" + [[audits.embedded-mcu-hal]] who = "Felipe Balbi " criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index ff87d42..8431550 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -19,6 +19,12 @@ criteria = "safe-to-deploy" version = "1.0.0" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embedded-crc-macros]] +who = "Matteo Tullo " +criteria = "safe-to-deploy" +version = "1.0.0" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.embedded-hal]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -39,6 +45,13 @@ version = "1.0.0" notes = "no_std async HAL trait definitions. No unsafe in library. Build script only runs rustc --version. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embedded-hal-nb]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "no_std trait-only crate. No unsafe, no build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.proc-macro-error-attr2]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -51,6 +64,12 @@ criteria = "safe-to-deploy" version = "2.0.1" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.smbus-pec]] +who = "Matteo Tullo " +criteria = "safe-to-deploy" +version = "1.0.1" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.tokio]] who = "Robert Zieba " criteria = "safe-to-run" @@ -124,6 +143,12 @@ criteria = "safe-to-deploy" version = "1.0.0" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.embedded-hal-mock]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.8.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.errno]] who = "Ying Hsu " criteria = "safe-to-run" @@ -206,6 +231,18 @@ criteria = "safe-to-run" version = "0.6.5" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" +[[audits.google.audits.nb]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.nb]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +delta = "1.0.0 -> 1.1.0" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.num-traits]] who = "Manish Goregaokar " criteria = "safe-to-deploy" From a10c7327f0eee02c4d3d97cdde9422a057848f12 Mon Sep 17 00:00:00 2001 From: matteotullo Date: Tue, 12 May 2026 18:16:54 -0700 Subject: [PATCH 3/5] Refactor SMBus async trait into Smbus + SwSmbusI2c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the async SMBus surface into an abstract protocol trait and a concrete software implementation. The `Smbus` trait now declares only the SMBus protocol operations plus the PEC associated items (`PecCalc`, `get_pec_calc`, `check_pec`). All default implementations that bit-bang the protocol on top of an I²C bus have moved into a new `SwSmbusI2c` wrapper struct that owns an `embedded_hal_async::i2c::I2c` bus and delegates PEC calculator construction to a new `PecProvider` trait via the `P` parameter. HALs with a hardware SMBus peripheral can implement `Smbus` directly without inheriting any I²C-specific machinery. `write_buf`, `read_buf`, and `write_read_buf` are no longer trait methods; they are inherent helpers on `SwSmbusI2c` that the other protocol methods reuse, eliminating duplicated PEC setup and I²C error mapping across the byte/word/block operations. PEC calculator priming with the write-address byte and the truncation of the finished hash to a single byte are likewise factored into private helpers (`pec_calc_with_write_addr`, `finalize_pec_byte`). The blanket `impl Smbus for &mut T` forwarding impl is preserved, and `SwSmbusI2c::Error` is `ErrorKind` directly so callers do not need to define a wrapper error type. Tests are reworked to drive `SwSmbusI2c` through small `PecProvider` impls (`TestPec`, `NoPec`). Assisted-by: GitHub Copilot:claude-opus-4.7 --- embedded-mcu-hal/src/smbus/bus/asynch.rs | 1085 ++++++++++------------ 1 file changed, 494 insertions(+), 591 deletions(-) diff --git a/embedded-mcu-hal/src/smbus/bus/asynch.rs b/embedded-mcu-hal/src/smbus/bus/asynch.rs index 0e04b3f..17572c8 100644 --- a/embedded-mcu-hal/src/smbus/bus/asynch.rs +++ b/embedded-mcu-hal/src/smbus/bus/asynch.rs @@ -1,102 +1,49 @@ -//! Async SMBus controller trait. +//! Async SMBus controller trait and software implementation. +//! +//! This module defines the [`Smbus`] async controller trait describing the +//! SMBus protocol surface. The trait declares the protocol-level +//! operations as required methods plus two associated items — +//! [`Smbus::PecCalc`] and [`Smbus::get_pec_calc`] — and a +//! default-implemented helper [`Smbus::check_pec`]. +//! +//! Concrete bit-banging of the protocol on top of an +//! [`embedded_hal_async::i2c::I2c`] bus is provided by [`SwSmbusI2c`]. +//! HAL authors with a hardware SMBus peripheral may instead implement +//! [`Smbus`] directly. //! //! See the [parent module](super) for the protocol overview, PEC handling, //! and driver/HAL guidance. use core::hash::Hasher; +use core::marker::PhantomData; use crate::smbus::bus::Error as SMBusError; -use embedded_hal_async::i2c::{Error as I2cError, Operation}; +use embedded_hal_async::i2c::{Error as I2cError, I2c, Operation}; -/// SMBus helper trait built on top of an async I2C implementation. -/// -/// This trait provides higher-level SMBus protocol operations (quick command, -/// send/receive byte, byte/word/block read/write, process calls, and PEC -/// handling) using an underlying asynchronous I2C implementation that -/// implements `embedded_hal_async::i2c::I2c`. -/// -/// # Example Implementation -/// -/// To implement the `Smbus` trait, you need to: -/// 1. Define an error type that implements both the crate's `ErrorType` trait -/// and converts from `embedded_hal_async::i2c::ErrorKind`. -/// 2. Define a PEC calculator type that implements `core::hash::Hasher`. -/// 3. Implement `crate::smbus::bus::ErrorType` to provide error conversions. -/// 4. Implement `embedded_hal_async::i2c::I2c` for I2C operations. -/// 5. Implement `Smbus` itself with a `get_pec_calc()` method. -/// -/// ```ignore -/// // Error type implementing both SMBus and I2C error traits -/// #[derive(Debug, Clone, Copy)] -/// pub enum Error { -/// I2c(embedded_hal::i2c::ErrorKind), -/// Pec, -/// TooLargeBlockTransaction, -/// } -/// -/// impl From for Error { -/// fn from(kind: embedded_hal::i2c::ErrorKind) -> Self { -/// Self::I2c(kind) -/// } -/// } -/// -/// impl crate::smbus::bus::Error for Error { -/// fn kind(&self) -> crate::smbus::bus::ErrorKind { -/// match self { -/// Self::I2c(e) => crate::smbus::bus::ErrorKind::I2c(*e), -/// Self::Pec => crate::smbus::bus::ErrorKind::Pec, -/// Self::TooLargeBlockTransaction => crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, -/// } -/// } -/// -/// fn to_kind(kind: crate::smbus::bus::ErrorKind) -> Self { -/// match kind { -/// crate::smbus::bus::ErrorKind::I2c(e) => Self::I2c(e), -/// crate::smbus::bus::ErrorKind::Pec => Self::Pec, -/// crate::smbus::bus::ErrorKind::TooLargeBlockTransaction => Self::TooLargeBlockTransaction, -/// _ => Self::I2c(embedded_hal::i2c::ErrorKind::Other), -/// } -/// } -/// } -/// -/// // PEC calculator type (example using a simple CRC-8 hasher) -/// pub struct PecCalc(u8); -/// -/// impl core::hash::Hasher for PecCalc { -/// fn write(&mut self, bytes: &[u8]) { -/// for &byte in bytes { -/// self.0 = self.0.wrapping_add(byte); -/// } -/// } -/// -/// fn finish(&self) -> u64 { -/// self.0 as u64 -/// } -/// } -/// -/// // I2C master struct implementing both I2c and Smbus -/// pub struct I2cMaster { -/// // I2C hardware handle -/// } +/// PEC calculator factory for [`SwSmbusI2c`]. /// -/// impl embedded_hal_async::i2c::I2c for I2cMaster { -/// // Implement required I2C methods... -/// } -/// -/// impl crate::smbus::bus::ErrorType for I2cMaster { -/// type Error = Error; -/// } -/// -/// impl Smbus for I2cMaster { -/// type PecCalc = PecCalc; +/// Decouples [`SwSmbusI2c`] from a particular PEC implementation. Provide +/// a type implementing this trait as the `P` type parameter of +/// [`SwSmbusI2c`] to describe what PEC calculator (if any) the bus should +/// use. +pub trait PecProvider { + /// PEC calculator type. + type Calc: Hasher; + + /// Construct a fresh PEC calculator, or return `None` when PEC is + /// unsupported on this bus. When `None` is returned, any operation + /// invoked with `use_pec = true` fails with + /// [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec). + fn new_calc() -> Option; +} + +/// Async SMBus controller trait. /// -/// fn get_pec_calc() -> Option { -/// Some(PecCalc(0)) // Return PEC calculator if available -/// } -/// } -/// ``` +/// Declares the SMBus protocol surface. Implementations may either be a +/// software protocol-basher over a generic I²C bus (see [`SwSmbusI2c`]) +/// or a HAL-level wrapper around a hardware SMBus peripheral. #[allow(async_fn_in_trait)] -pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { +pub trait Smbus: crate::smbus::bus::ErrorType { /// PEC (Packet Error Code) calculator type. /// /// When a SMBus operation requests PEC verification (`use_pec = true`), @@ -113,33 +60,16 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { /// Obtain a PEC calculator instance if PEC support is available. /// - /// This method is called by SMBus operations that request PEC verification - /// (`use_pec = true`). Implementations should return `Some(calculator)` if PEC - /// support is available, or `None` if not. When `None` is returned, any - /// operation with `use_pec = true` will fail with an error of kind - /// `ErrorKind::Pec`. - /// - /// The returned calculator should be a fresh instance ready to hash bytes - /// in bus order using the `core::hash::Hasher` interface. - /// - /// Returns `Some(PecCalc)` if PEC is available, or `None` if PEC support - /// is not implemented or unavailable. + /// Returns `Some(calculator)` if PEC support is available, or `None` if + /// not. When `None` is returned, any operation with `use_pec = true` + /// fails with an error of kind + /// [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec). fn get_pec_calc() -> Option; /// Check PEC (Packet Error Code) validity. /// - /// Compares a received PEC byte against a computed PEC value to verify data - /// integrity. This is a helper method used internally by read operations that - /// perform PEC verification. - /// - /// Parameters: - /// - `received_pec`: The PEC byte received from the bus. - /// - `computed_pec`: The PEC value computed locally via the `PecCalc` hasher's - /// `finish()` method. Only the low byte is used for comparison. - /// - /// Returns `Ok(())` if the received PEC matches the computed PEC (after - /// truncating to a single byte), or an error of kind `ErrorKind::Pec` if - /// the values do not match, indicating a data integrity error. + /// Compares a received PEC byte against a computed PEC value. Only the + /// low byte of `computed_pec` is used. fn check_pec(received_pec: u8, computed_pec: u64) -> Result<(), ::Error> { computed_pec .eq(&received_pec.into()) @@ -147,137 +77,352 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { .ok_or_else(|| ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec)) } - /// Obtain a fresh PEC calculator pre-fed with the write-address byte. - /// - /// Returns an [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) error - /// if [`get_pec_calc`](Self::get_pec_calc) returns `None`. - fn pec_calc_with_write_addr(address: u8) -> Result::Error> { - let mut pec = Self::get_pec_calc().ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; + /// Quick Command. + async fn quick_command( + &mut self, + address: u8, + read: bool, + ) -> Result<(), ::Error>; + + /// Send Byte. + async fn send_byte( + &mut self, + address: u8, + byte: u8, + use_pec: bool, + ) -> Result<(), ::Error>; + + /// Receive Byte. + async fn receive_byte( + &mut self, + address: u8, + use_pec: bool, + ) -> Result::Error>; + + /// Write Byte. + async fn write_byte( + &mut self, + address: u8, + register: u8, + byte: u8, + use_pec: bool, + ) -> Result<(), ::Error>; + + /// Write Word (little-endian on the wire). + async fn write_word( + &mut self, + address: u8, + register: u8, + word: u16, + use_pec: bool, + ) -> Result<(), ::Error>; + + /// Read Byte. + async fn read_byte( + &mut self, + address: u8, + register: u8, + use_pec: bool, + ) -> Result::Error>; + + /// Read Word (little-endian on the wire). + async fn read_word( + &mut self, + address: u8, + register: u8, + use_pec: bool, + ) -> Result::Error>; + + /// Process Call. + async fn process_call( + &mut self, + address: u8, + register: u8, + word: u16, + use_pec: bool, + ) -> Result::Error>; + + /// Block Write. + async fn block_write( + &mut self, + address: u8, + register: u8, + data: &[u8], + use_pec: bool, + ) -> Result<(), ::Error>; + + /// Block Read. + async fn block_read( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + use_pec: bool, + ) -> Result<(), ::Error>; + + /// Block Write / Block Read / Process Call. + async fn block_write_block_read_process_call( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + use_pec: bool, + ) -> Result<(), ::Error>; +} + +impl Smbus for &mut T { + type PecCalc = T::PecCalc; + + #[inline] + fn get_pec_calc() -> Option { + T::get_pec_calc() + } + + #[inline] + async fn quick_command( + &mut self, + address: u8, + read: bool, + ) -> Result<(), ::Error> { + T::quick_command(*self, address, read).await + } + + #[inline] + async fn send_byte( + &mut self, + address: u8, + byte: u8, + use_pec: bool, + ) -> Result<(), ::Error> { + T::send_byte(*self, address, byte, use_pec).await + } + + #[inline] + async fn receive_byte( + &mut self, + address: u8, + use_pec: bool, + ) -> Result::Error> { + T::receive_byte(*self, address, use_pec).await + } + + #[inline] + async fn write_byte( + &mut self, + address: u8, + register: u8, + byte: u8, + use_pec: bool, + ) -> Result<(), ::Error> { + T::write_byte(*self, address, register, byte, use_pec).await + } + + #[inline] + async fn write_word( + &mut self, + address: u8, + register: u8, + word: u16, + use_pec: bool, + ) -> Result<(), ::Error> { + T::write_word(*self, address, register, word, use_pec).await + } + + #[inline] + async fn read_byte( + &mut self, + address: u8, + register: u8, + use_pec: bool, + ) -> Result::Error> { + T::read_byte(*self, address, register, use_pec).await + } + + #[inline] + async fn read_word( + &mut self, + address: u8, + register: u8, + use_pec: bool, + ) -> Result::Error> { + T::read_word(*self, address, register, use_pec).await + } + + #[inline] + async fn process_call( + &mut self, + address: u8, + register: u8, + word: u16, + use_pec: bool, + ) -> Result::Error> { + T::process_call(*self, address, register, word, use_pec).await + } + + #[inline] + async fn block_write( + &mut self, + address: u8, + register: u8, + data: &[u8], + use_pec: bool, + ) -> Result<(), ::Error> { + T::block_write(*self, address, register, data, use_pec).await + } + + #[inline] + async fn block_read( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + use_pec: bool, + ) -> Result<(), ::Error> { + T::block_read(*self, address, register, data, use_pec).await + } + + #[inline] + async fn block_write_block_read_process_call( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + use_pec: bool, + ) -> Result<(), ::Error> { + T::block_write_block_read_process_call(*self, address, register, write_data, read_data, use_pec).await + } +} + +/// Software SMBus controller built on top of an async I²C bus. +/// +/// `SwSmbusI2c` implements [`Smbus`] by bit-banging the SMBus +/// protocol on top of an [`embedded_hal_async::i2c::I2c`] bus `I`. PEC +/// support is delegated to the [`PecProvider`] `P`. +pub struct SwSmbusI2c { + i2c: I, + _pec: PhantomData

, +} + +impl SwSmbusI2c { + /// Wrap an I²C bus to form a software SMBus controller. + #[inline] + pub const fn new(i2c: I) -> Self { + Self { i2c, _pec: PhantomData } + } + + /// Consume the wrapper and return the underlying I²C bus. + #[inline] + pub fn into_inner(self) -> I { + self.i2c + } + + /// Borrow the underlying I²C bus. + #[inline] + pub fn inner(&self) -> &I { + &self.i2c + } + + /// Mutably borrow the underlying I²C bus. + #[inline] + pub fn inner_mut(&mut self) -> &mut I { + &mut self.i2c + } +} + +impl crate::smbus::bus::ErrorType for SwSmbusI2c { + type Error = crate::smbus::bus::ErrorKind; +} + +impl SwSmbusI2c +where + P: PecProvider, +{ + /// Obtain a fresh PEC calculator pre-fed with the write-address byte, + /// or [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) if the + /// provider returns `None`. + fn pec_calc_with_write_addr(address: u8) -> Result { + let mut pec = P::new_calc().ok_or(crate::smbus::bus::ErrorKind::Pec)?; pec.write_u8(crate::smbus::bus::write_address_byte(address)); Ok(pec) } /// Truncate a finished PEC value to its low byte. - /// - /// Returns an [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) error - /// if the value does not fit in a byte. - fn finalize_pec_byte(pec: u64) -> Result::Error> { - pec.try_into() - .map_err(|_| ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec)) + fn finalize_pec_byte(pec: u64) -> Result { + pec.try_into().map_err(|_| crate::smbus::bus::ErrorKind::Pec) } +} - /// Write a buffer of data with optional PEC computation and verification. - /// - /// This is a low-level helper method that performs I2C write operations with - /// optional PEC (Packet Error Code) computation. When `use_pec` is false, the - /// data is written as-is. When `use_pec` is true, a PEC byte is computed over - /// the address and data payload, and the caller-provided buffer must have space - /// for the PEC byte at the end (i.e., the buffer should be sized to - /// `payload_len + 1` to accommodate the computed PEC). +impl SwSmbusI2c +where + I: I2c, + P: PecProvider, +{ + /// Write a buffer of data with optional PEC computation. /// - /// Parameters: - /// - `address`: 7-bit target device address (used in PEC calculation). - /// - `use_pec`: When true, compute PEC and append it to the transfer. - /// When false, write the buffer without PEC. - /// - `operations`: Mutable buffer containing the data to write. When `use_pec` - /// is true, the last byte of this buffer will be overwritten with the computed - /// PEC value. The caller must ensure sufficient space. - /// - /// Returns `Ok(())` on success, or an error if: - /// - The underlying I2C write fails (converted from `I2cError`) - /// - PEC is requested but unavailable (returns `ErrorKind::Pec`) - /// - PEC computation fails or overflows (returns `ErrorKind::Pec`) + /// When `use_pec` is true, the caller must size `operations` to include + /// one extra trailing byte for the PEC; that byte is filled in with the + /// computed PEC before the I²C write. async fn write_buf( &mut self, address: u8, use_pec: bool, operations: &mut [u8], - ) -> Result<(), ::Error> { + ) -> Result<(), crate::smbus::bus::ErrorKind> { if use_pec { let mut pec = Self::pec_calc_with_write_addr(address)?; - let (pec_elem, rest) = - operations - .split_last_mut() - .ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; + let (pec_elem, rest) = operations.split_last_mut().ok_or(crate::smbus::bus::ErrorKind::Pec)?; pec.write(rest); *pec_elem = Self::finalize_pec_byte(pec.finish())?; } - self.write(address, operations) + self.i2c + .write(address, operations) .await - .map_err(|i2c_err| i2c_err.kind())?; + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; Ok(()) } /// Read a buffer of data with optional PEC verification. /// - /// This is a low-level helper method that performs I2C read operations with - /// optional PEC (Packet Error Code) verification. When `use_pec` is false, - /// the data is read as-is. When `use_pec` is true, the data (excluding the - /// PEC byte) is hashed using the `PecCalc` calculator, and the final PEC byte - /// in the buffer is verified against the locally computed PEC. The caller must - /// ensure the buffer has space for the PEC byte (i.e., for a single data byte - /// with PEC, provide a 2-byte buffer). - /// - /// Parameters: - /// - `address`: 7-bit target device address (used in PEC calculation). - /// - `use_pec`: When true, verify the PEC byte at the end of the buffer. - /// When false, read the buffer without PEC verification. - /// - `read`: Mutable buffer to store the received data. The last byte should - /// contain the PEC byte if `use_pec` is true. All other bytes contain the - /// actual payload data. - /// - /// Returns `Ok(())` on success, or an error if: - /// - The underlying I2C read fails (converted from `I2cError`) - /// - PEC is requested but unavailable (returns `ErrorKind::Pec`) - /// - PEC verification fails due to mismatch (returns `ErrorKind::Pec`) + /// When `use_pec` is true, the caller must size `read` to include one + /// extra trailing byte for the PEC byte; it is verified after the read. async fn read_buf( &mut self, address: u8, use_pec: bool, read: &mut [u8], - ) -> Result<(), ::Error> { + ) -> Result<(), crate::smbus::bus::ErrorKind> { if use_pec { let mut pec = Self::pec_calc_with_write_addr(address)?; - self.read(address, read).await.map_err(|i2c_err| i2c_err.kind())?; - let (pec_byte, rest) = read - .split_last() - .ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; + self.i2c + .read(address, read) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + let (pec_byte, rest) = read.split_last().ok_or(crate::smbus::bus::ErrorKind::Pec)?; pec.write(rest); - - Self::check_pec(*pec_byte, pec.finish())?; + ::check_pec(*pec_byte, pec.finish())?; } else { - self.read(address, read).await.map_err(|i2c_err| i2c_err.kind())?; + self.i2c + .read(address, read) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; } - Ok(()) } /// Write a buffer and then read a buffer, with optional PEC verification. /// - /// Performs a single I²C transaction consisting of a `Write(write)` - /// followed by a `Read(read)`. When `use_pec` is true, the caller must - /// size `read` to include one extra trailing byte for the PEC; that - /// byte is then verified against a locally computed PEC that covers - /// (in bus order) the write-address byte, `write`, the read-address - /// byte and the data portion of `read` (everything except the trailing - /// PEC byte). - /// - /// Returns an [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) - /// error if PEC support is unavailable or the received PEC does not - /// match. + /// When `use_pec` is true, the caller must size `read` to include one + /// extra trailing byte for the PEC byte; it is verified against a + /// locally computed PEC. async fn write_read_buf( &mut self, address: u8, use_pec: bool, write: &[u8], read: &mut [u8], - ) -> Result<(), ::Error> { + ) -> Result<(), crate::smbus::bus::ErrorKind> { // When PEC is requested, fail fast without touching the bus if no // PEC calculator is available. let mut pec = if use_pec { @@ -285,73 +430,50 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { } else { None }; - self.transaction(address, &mut [Operation::Write(write), Operation::Read(read)]) + self.i2c + .transaction(address, &mut [Operation::Write(write), Operation::Read(read)]) .await - .map_err(|i2c_err| i2c_err.kind())?; + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; if let Some(pec) = pec.as_mut() { pec.write(write); pec.write_u8(crate::smbus::bus::read_address_byte(address)); - let (pec_byte, rest) = read - .split_last() - .ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; + let (pec_byte, rest) = read.split_last().ok_or(crate::smbus::bus::ErrorKind::Pec)?; pec.write(rest); - Self::check_pec(*pec_byte, pec.finish())?; + ::check_pec(*pec_byte, pec.finish())?; } Ok(()) } +} + +impl Smbus for SwSmbusI2c +where + I: I2c, + P: PecProvider, +{ + type PecCalc = P::Calc; - /// Quick Command - /// - /// Perform an SMBus Quick Command which uses the R/W bit of the 7-bit address - /// to indicate the command (no data payload is transferred). - /// - /// Parameters: - /// - `address`: 7-bit target device address. - /// - `read`: when true, the R/W bit denotes a read (controller issues a read); - /// otherwise it denotes a write. - /// - /// Returns `Ok(())` on success or an error converted from the underlying I2C - /// implementation on failure. #[inline] - async fn quick_command( - &mut self, - address: u8, - read: bool, - ) -> Result<(), ::Error> { - self.transaction( - address, - &mut if read { - [Operation::Read(&mut [])] - } else { - [Operation::Write(&[])] - }, - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; + fn get_pec_calc() -> Option { + P::new_calc() + } + + #[inline] + async fn quick_command(&mut self, address: u8, read: bool) -> Result<(), crate::smbus::bus::ErrorKind> { + self.i2c + .transaction( + address, + &mut if read { + [Operation::Read(&mut [])] + } else { + [Operation::Write(&[])] + }, + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; Ok(()) } - /// Send Byte - /// - /// Sends a single command byte to the target device. - /// - /// Parameters: - /// - `address`: 7-bit target device address. - /// - `byte`: command byte to send. - /// - `use_pec`: when true, compute a PEC byte over the address and command - /// and append it to the transfer. If PEC support is unavailable or PEC - /// computation fails, an error of kind `ErrorKind::Pec` is returned. - /// - /// Returns `Ok(())` on success or an error converted from the underlying I2C - /// implementation on failure. - async fn send_byte( - &mut self, - address: u8, - byte: u8, - use_pec: bool, - ) -> Result<(), ::Error> { + async fn send_byte(&mut self, address: u8, byte: u8, use_pec: bool) -> Result<(), crate::smbus::bus::ErrorKind> { if use_pec { self.write_buf(address, true, &mut [byte, 0]).await } else { @@ -359,122 +481,53 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { } } - /// Receive Byte - /// - /// Read a single byte from the target device. - /// - /// Parameters: - /// - `address`: 7-bit target device address. - /// - `use_pec`: when true, expect an extra PEC byte after the data and - /// verify it against a locally computed PEC. If PEC support is unavailable, - /// or on PEC mismatch, an error of kind `ErrorKind::Pec` is returned. - /// - /// Returns the received byte on success or an error converted from the - /// underlying I2C implementation on failure. - async fn receive_byte( - &mut self, - address: u8, - use_pec: bool, - ) -> Result::Error> { + async fn receive_byte(&mut self, address: u8, use_pec: bool) -> Result { if use_pec { let mut buf = [0u8; 2]; - self.read_buf(address, use_pec, &mut buf).await?; + self.read_buf(address, true, &mut buf).await?; Ok(buf[0]) } else { - let mut buf = [0u8]; - self.read_buf(address, use_pec, &mut buf).await?; + let mut buf = [0u8; 1]; + self.read_buf(address, false, &mut buf).await?; Ok(buf[0]) } } - /// Write Byte - /// - /// Write a single data byte to a command/register on the target device. - /// - /// Parameters: - /// - `address`: 7-bit target device address. - /// - `register`: command/register code to write to. - /// - `byte`: data byte to write. - /// - `use_pec`: when true, compute and append a PEC byte that covers the - /// address, register and data. If PEC support is unavailable or PEC - /// computation fails, an error of kind `ErrorKind::Pec` is returned. - /// - /// Returns `Ok(())` on success or an error converted from the underlying I2C - /// implementation on failure. async fn write_byte( &mut self, address: u8, register: u8, byte: u8, use_pec: bool, - ) -> Result<(), ::Error> { + ) -> Result<(), crate::smbus::bus::ErrorKind> { if use_pec { - self.write_buf(address, use_pec, &mut [register, byte, 0]).await + self.write_buf(address, true, &mut [register, byte, 0]).await } else { - self.write_buf(address, use_pec, &mut [register, byte]).await + self.write_buf(address, false, &mut [register, byte]).await } } - /// Write Word - /// - /// Write a 16-bit word to a command/register on the target device. The word - /// is transmitted as little-endian (low byte first) on the bus. - /// - /// Parameters: - /// - `address`: 7-bit target device address. - /// - `register`: command/register code to write to. - /// - `word`: 16-bit value to send (little-endian on the wire). - /// - `use_pec`: when true, compute and append a PEC byte that covers the - /// address, register and word bytes. If PEC support is unavailable or PEC - /// computation fails, an error of kind `ErrorKind::Pec` is returned. - /// - /// Returns `Ok(())` on success or an error converted from the underlying I2C - /// implementation on failure. async fn write_word( &mut self, address: u8, register: u8, word: u16, use_pec: bool, - ) -> Result<(), ::Error> { - let word_bytestream = u16::to_le_bytes(word); + ) -> Result<(), crate::smbus::bus::ErrorKind> { + let b = u16::to_le_bytes(word); if use_pec { - self.write_buf( - address, - use_pec, - &mut [register, word_bytestream[0], word_bytestream[1], 0], - ) - .await + self.write_buf(address, true, &mut [register, b[0], b[1], 0]).await } else { - self.write_buf( - address, - use_pec, - &mut [register, word_bytestream[0], word_bytestream[1]], - ) - .await + self.write_buf(address, false, &mut [register, b[0], b[1]]).await } } - /// Read Byte - /// - /// Write a command/register and then read a single byte from the target - /// device using a repeated START (no intervening STOP). - /// - /// Parameters: - /// - `address`: 7-bit target device address. - /// - `register`: command/register code to request. - /// - `use_pec`: when true, expect an extra PEC byte after the data and - /// verify it against a locally computed PEC. If PEC support is unavailable - /// or on mismatch, an error of kind `ErrorKind::Pec` is returned. - /// - /// Returns the received byte on success or an error converted from the - /// underlying I2C implementation on failure. async fn read_byte( &mut self, address: u8, register: u8, use_pec: bool, - ) -> Result::Error> { + ) -> Result { if use_pec { let mut buf = [0u8; 2]; self.write_read_buf(address, true, &[register], &mut buf).await?; @@ -486,28 +539,12 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { } } - /// Read Word - /// - /// Write a command/register and then read a 16-bit word from the target - /// device using a repeated START (no intervening STOP). The two bytes are - /// interpreted as little-endian (low byte first). - /// - /// Parameters: - /// - `address`: 7-bit target device address. - /// - `register`: command/register code to request. - /// - `use_pec`: when true, expect an extra PEC byte after the two data - /// bytes and verify it against a locally computed PEC. If PEC support - /// is unavailable or on mismatch, an error of kind `ErrorKind::Pec` - /// is returned. - /// - /// Returns the received 16-bit word on success or an error converted from - /// the underlying I2C implementation on failure. async fn read_word( &mut self, address: u8, register: u8, use_pec: bool, - ) -> Result::Error> { + ) -> Result { if use_pec { let mut buf = [0u8; 3]; self.write_read_buf(address, true, &[register], &mut buf).await?; @@ -519,90 +556,60 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { } } - /// Process Call - /// - /// Performs a combined write of a 16-bit word to the given `register`, - /// followed by a read of a 16-bit response from the device. - /// - /// Parameters: - /// - `address`: 7-bit target address of the slave device. - /// - `register`: command/register code to send. - /// - `word`: 16-bit parameter sent to the device (little-endian on the bus). - /// - `use_pec`: when true, a PEC (Packet Error Code) is calculated and - /// verified for the returned data. If PEC support is unavailable or - /// verification fails, an error with kind `ErrorKind::Pec` is returned. - /// - /// Returns the 16-bit response from the device on success. async fn process_call( &mut self, address: u8, register: u8, word: u16, use_pec: bool, - ) -> Result::Error> { + ) -> Result { if use_pec { let mut buf = [0u8; 3]; let mut pec = Self::pec_calc_with_write_addr(address)?; pec.write_u8(register); pec.write_u16(word); pec.write_u8(crate::smbus::bus::read_address_byte(address)); - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&word.to_le_bytes()), - Operation::Read(&mut buf), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - let (recvd_pec, data) = buf - .split_last() - .ok_or(::Error::to_kind( - crate::smbus::bus::ErrorKind::Pec, - ))?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + let (recvd_pec, data) = buf.split_last().ok_or(crate::smbus::bus::ErrorKind::Pec)?; pec.write(data); Self::check_pec(*recvd_pec, pec.finish())?; Ok(u16::from_le_bytes([buf[0], buf[1]])) } else { let mut buf = [0u8; 2]; - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&word.to_le_bytes()), - Operation::Read(&mut buf), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; Ok(u16::from_le_bytes(buf)) } } - /// Block Write - /// - /// Sends a block write to `register`. The transfer format is: - /// - write `register` - /// - write `length` (1 byte) - /// - write `length` data bytes - /// - if `use_pec` is true, append PEC (1 byte) - /// - /// `data.len()` must be <= 255. When `use_pec` is true a PEC byte is - /// computed over the same sequence of bytes that appear on the bus and - /// appended to the transaction. If PEC support is unavailable, an error - /// of kind `ErrorKind::Pec` is returned. async fn block_write( &mut self, address: u8, register: u8, data: &[u8], use_pec: bool, - ) -> Result<(), ::Error> { + ) -> Result<(), crate::smbus::bus::ErrorKind> { if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { - return Err(::Error::to_kind( - crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, - )); + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); } if use_pec { let mut pec = Self::pec_calc_with_write_addr(address)?; @@ -610,7 +617,7 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { pec.write_u8(data.len() as u8); pec.write(data); let pec: u8 = Self::finalize_pec_byte(pec.finish())?; - Ok(self + self.i2c .transaction( address, &mut [ @@ -621,9 +628,9 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { ], ) .await - .map_err(|i2c_err| i2c_err.kind())?) + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; } else { - Ok(self + self.i2c .transaction( address, &mut [ @@ -633,34 +640,20 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { ], ) .await - .map_err(|i2c_err| i2c_err.kind())?) + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; } + Ok(()) } - /// Block Read - /// - /// Reads a block from `register`. The expected transfer sequence is: - /// - write `register` - /// - read `length` (1 byte) - /// - read `length` data bytes into `data` - /// - if `use_pec` is true, read one PEC byte and verify it - /// - /// The provided `data` buffer should be sized to hold the expected - /// incoming block payload (max 255). If `use_pec` is true, the PEC - /// byte is validated against a locally computed PEC. If PEC support - /// is unavailable or on mismatch, an error with kind `ErrorKind::Pec` - /// is returned. async fn block_read( &mut self, address: u8, register: u8, data: &mut [u8], use_pec: bool, - ) -> Result<(), ::Error> { + ) -> Result<(), crate::smbus::bus::ErrorKind> { if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { - return Err(::Error::to_kind( - crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, - )); + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); } let mut msg_size = [0u8]; if use_pec { @@ -668,50 +661,37 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { let mut pec = Self::pec_calc_with_write_addr(address)?; pec.write_u8(register); pec.write_u8(crate::smbus::bus::read_address_byte(address)); - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Read(&mut msg_size), - Operation::Read(data), - Operation::Read(&mut pec_buf), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; pec.write(&msg_size); pec.write(data); Self::check_pec(pec_buf[0], pec.finish())?; - Ok(()) } else { - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Read(&mut msg_size), - Operation::Read(data), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - Ok(()) + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; } + Ok(()) } - /// Block Write / Block Read / Process Call - /// - /// Performs a combined transaction that first writes a block payload, - /// then reads back a block response. The semantics are analogous to a - /// block write followed by a block read in a single transaction; when - /// `use_pec` is true the PEC is verified for the entire exchange. - /// - /// Parameters: - /// - `write_data`: data to send as the write block payload. - /// - `read_data`: buffer where the incoming block payload is stored. - /// - The sum of `write_data.len()` and `read_data.len()` must be <= 255. - /// - `use_pec`: when true, a PEC byte is read after the response and - /// validated. If PEC support is unavailable or on mismatch, an - /// `ErrorKind::Pec` is returned. async fn block_write_block_read_process_call( &mut self, address: u8, @@ -719,11 +699,9 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { write_data: &[u8], read_data: &mut [u8], use_pec: bool, - ) -> Result<(), ::Error> { + ) -> Result<(), crate::smbus::bus::ErrorKind> { if write_data.len() + read_data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { - return Err(::Error::to_kind( - crate::smbus::bus::ErrorKind::TooLargeBlockTransaction, - )); + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); } let mut read_msg_size = [0u8]; if use_pec { @@ -733,84 +711,56 @@ pub trait Smbus: crate::smbus::bus::ErrorType + embedded_hal_async::i2c::I2c { pec.write_u8(write_data.len() as u8); pec.write(write_data); pec.write_u8(crate::smbus::bus::read_address_byte(address)); - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[write_data.len() as u8]), - Operation::Write(write_data), - Operation::Read(&mut read_msg_size), - Operation::Read(read_data), - Operation::Read(&mut pec_buf), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; pec.write(&read_msg_size); pec.write(read_data); Self::check_pec(pec_buf[0], pec.finish())?; - Ok(()) } else { - self.transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[write_data.len() as u8]), - Operation::Write(write_data), - Operation::Read(&mut read_msg_size), - Operation::Read(read_data), - ], - ) - .await - .map_err(|i2c_err| i2c_err.kind())?; - Ok(()) + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; } + Ok(()) } } - -impl Smbus for &mut T { - type PecCalc = T::PecCalc; - - #[inline] - fn get_pec_calc() -> Option { - T::get_pec_calc() - } -} - #[cfg(test)] #[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::cast_possible_truncation)] mod tests { use super::Smbus; use crate::smbus::bus::{ - read_address_byte, write_address_byte, Error as SmbusError, ErrorKind, ErrorType, MAX_BLOCK_SIZE, READ_BIT, + read_address_byte, write_address_byte, Error as SmbusError, ErrorKind, MAX_BLOCK_SIZE, READ_BIT, }; use core::hash::Hasher; - use embedded_hal_async::i2c::{ErrorKind as I2cErrorKind, I2c, Operation}; + use embedded_hal_async::i2c::ErrorKind as I2cErrorKind; use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as Tx}; use smbus_pec::{pec, Pec}; const ADDR: u8 = 0x42; const REG: u8 = 0x07; - /// Test SMBus error type. - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - struct TestError(ErrorKind); - - impl From for TestError { - fn from(k: I2cErrorKind) -> Self { - Self(ErrorKind::I2c(k)) - } - } - - impl SmbusError for TestError { - fn kind(&self) -> ErrorKind { - self.0 - } - fn to_kind(kind: ErrorKind) -> Self { - Self(kind) - } - } - /// Compute the expected SMBus PEC byte over a flat concatenation of byte /// slices, using the `smbus-pec` crate as the reference implementation. fn expected_pec(parts: &[&[u8]]) -> u8 { @@ -821,78 +771,33 @@ mod tests { pec(&buf) } - /// Bus that wires `embedded_hal_mock::eh1::i2c::Mock` to the `Smbus` trait. - struct TestBus { - i2c: I2cMock, - } - - impl embedded_hal_async::i2c::ErrorType for TestBus { - type Error = I2cErrorKind; - } - - impl I2c for TestBus { - async fn transaction(&mut self, address: u8, ops: &mut [Operation<'_>]) -> Result<(), Self::Error> { - ::transaction(&mut self.i2c, address, ops).await - } - async fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> { - ::read(&mut self.i2c, address, read).await - } - async fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> { - ::write(&mut self.i2c, address, write).await - } - } - - impl ErrorType for TestBus { - type Error = TestError; - } - - impl Smbus for TestBus { - type PecCalc = Pec; - fn get_pec_calc() -> Option { + /// PEC provider that exposes the `smbus-pec` calculator. + struct TestPec; + impl super::PecProvider for TestPec { + type Calc = Pec; + fn new_calc() -> Option { Some(Pec::new()) } } - /// Bus without PEC support, used to validate the unavailable-PEC error path. - struct NoPecBus { - i2c: I2cMock, - } - - impl embedded_hal_async::i2c::ErrorType for NoPecBus { - type Error = I2cErrorKind; - } - - impl I2c for NoPecBus { - async fn transaction(&mut self, address: u8, ops: &mut [Operation<'_>]) -> Result<(), Self::Error> { - ::transaction(&mut self.i2c, address, ops).await - } - async fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> { - ::read(&mut self.i2c, address, read).await - } - async fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> { - ::write(&mut self.i2c, address, write).await - } - } - - impl ErrorType for NoPecBus { - type Error = TestError; - } - - impl Smbus for NoPecBus { - type PecCalc = Pec; - fn get_pec_calc() -> Option { + /// PEC provider that reports PEC as unavailable. + struct NoPec; + impl super::PecProvider for NoPec { + type Calc = Pec; + fn new_calc() -> Option { None } } + type TestBus = super::SwSmbusI2c; + type NoPecBus = super::SwSmbusI2c; + fn new_bus(expectations: &[Tx]) -> TestBus { - TestBus { - i2c: I2cMock::new(expectations), - } + super::SwSmbusI2c::new(I2cMock::new(expectations)) } fn done(mut bus: TestBus) { - bus.i2c.done(); + bus.inner_mut().done(); } // ---------- constants / helpers ---------- @@ -1386,13 +1291,11 @@ mod tests { // ---------- PEC unavailable ---------- fn new_no_pec_bus(expectations: &[Tx]) -> NoPecBus { - NoPecBus { - i2c: I2cMock::new(expectations), - } + super::SwSmbusI2c::new(I2cMock::new(expectations)) } fn done_no_pec(mut bus: NoPecBus) { - bus.i2c.done(); + bus.inner_mut().done(); } #[test] @@ -1423,7 +1326,7 @@ mod tests { #[tokio::test] async fn pec_unavailable_returns_pec_error() { - let mut bus = NoPecBus { i2c: I2cMock::new(&[]) }; + let mut bus = super::SwSmbusI2c::::new(I2cMock::new(&[])); // Any PEC-requiring path should fail without touching the bus. let err = bus.send_byte(ADDR, 0x55, true).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); @@ -1445,7 +1348,7 @@ mod tests { .await .unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); - bus.i2c.done(); + bus.inner_mut().done(); } // ---------- &mut T forwarding ---------- From 92c5d668cd945446b297930fcf204b7cf6fb0514 Mon Sep 17 00:00:00 2001 From: Matteo Tullo Date: Thu, 14 May 2026 16:38:31 -0700 Subject: [PATCH 4/5] Adress latest review comments. - Renamed `to_kind` method to `from_kind` in the `Error` trait for clarity. - Updated the `ErrorKind` enum to include a new variant `BlockSizeMismatch`. - Made API more explicit by breaking out the pec and non pec SMBUS transactions into their own methods. --- Cargo.lock | 71 +- embedded-mcu-hal/src/lib.rs | 12 +- embedded-mcu-hal/src/smbus/bus/asynch.rs | 818 +++++++++++++++-------- embedded-mcu-hal/src/smbus/bus/mod.rs | 27 +- supply-chain/imports.lock | 10 +- 5 files changed, 642 insertions(+), 296 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 935a31b..0f1acc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.0.1", +] + [[package]] name = "defmt" version = "1.0.1" @@ -82,21 +91,64 @@ dependencies = [ "thiserror", ] +[[package]] +name = "embedded-crc-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1c75747a43b086df1a87fb2a889590bc0725e0abf54bba6d0c4bf7bd9e762c" + [[package]] name = "embedded-hal" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" +dependencies = [ + "defmt 0.3.100", +] + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "defmt 0.3.100", + "embedded-hal", +] + +[[package]] +name = "embedded-hal-mock" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a0f04f8886106faf281c47b6a0e4054a369baedaf63591fdb8da9761f3f379" +dependencies = [ + "embedded-hal", + "embedded-hal-async", + "embedded-hal-nb", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal", + "nb", +] [[package]] name = "embedded-mcu-hal" version = "0.2.0" dependencies = [ "chrono", - "defmt", + "defmt 1.0.1", "embedded-hal", + "embedded-hal-async", + "embedded-hal-mock", "num_enum", "proptest", + "smbus-pec", "tokio", ] @@ -146,6 +198,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + [[package]] name = "num-traits" version = "0.2.19" @@ -343,6 +401,15 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "smbus-pec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0763a680cd5d72b28f7bfc8a054c117d8841380a6ad4f72f05bd2a34217d3e" +dependencies = [ + "embedded-crc-macros", +] + [[package]] name = "storage_bus" version = "0.1.0" @@ -481,4 +548,4 @@ dependencies = [ "proc-macro2", "quote", "syn", -] \ No newline at end of file +] diff --git a/embedded-mcu-hal/src/lib.rs b/embedded-mcu-hal/src/lib.rs index c05ae9c..659f5ec 100644 --- a/embedded-mcu-hal/src/lib.rs +++ b/embedded-mcu-hal/src/lib.rs @@ -17,9 +17,14 @@ //! * **Device-agnostic** — no register addresses, magic values, or //! MCU-specific types appear in any public API. //! -//! * **Trait-only** — the crate ships no runtime code beyond the -//! [`time::Datetime`] value type and its helpers. Keeping implementations -//! out-of-crate means zero overhead: you only pay for what you use. +//! * **Mostly trait-only** — the crate is primarily a contract surface, +//! not a runtime. The only concrete runtime code shipped today is the +//! [`time::Datetime`] value type and its helpers, and a portable software +//! SMBus controller ([`smbus::bus::asynch::SwSmbusI2c`]) that layers the +//! SMBus protocol on top of any [`embedded_hal_async::i2c::I2c`] +//! implementation. Hardware-specific implementations of every trait +//! still belong in board or chip support crates, so generic drivers pay +//! only for what they use. //! //! * **`no_std` first** — every public item is usable in bare-metal firmware //! with no heap allocator. The standard library is only linked during @@ -45,6 +50,7 @@ //! |--------|----------------| //! | [`time`] | Wall-clock date/time types and real-time clock (RTC) traits | //! | [`nvram`] | Non-Volatile RAM storage traits | +//! | [`smbus`] | SMBus controller traits and a portable software implementation atop `embedded-hal-async` I²C | //! | [`watchdog`] | Watchdog timer trait | //! //! # Optional Cargo features diff --git a/embedded-mcu-hal/src/smbus/bus/asynch.rs b/embedded-mcu-hal/src/smbus/bus/asynch.rs index 17572c8..f541437 100644 --- a/embedded-mcu-hal/src/smbus/bus/asynch.rs +++ b/embedded-mcu-hal/src/smbus/bus/asynch.rs @@ -74,7 +74,7 @@ pub trait Smbus: crate::smbus::bus::ErrorType { computed_pec .eq(&received_pec.into()) .then_some(()) - .ok_or_else(|| ::Error::to_kind(crate::smbus::bus::ErrorKind::Pec)) + .ok_or_else(|| ::Error::from_kind(crate::smbus::bus::ErrorKind::Pec)) } /// Quick Command. @@ -85,19 +85,21 @@ pub trait Smbus: crate::smbus::bus::ErrorType { ) -> Result<(), ::Error>; /// Send Byte. - async fn send_byte( + async fn send_byte(&mut self, address: u8, byte: u8) -> Result<(), ::Error>; + + /// Send Byte with PEC. + async fn send_byte_with_pec( &mut self, address: u8, byte: u8, - use_pec: bool, ) -> Result<(), ::Error>; /// Receive Byte. - async fn receive_byte( - &mut self, - address: u8, - use_pec: bool, - ) -> Result::Error>; + async fn receive_byte(&mut self, address: u8) -> Result::Error>; + + /// Receive Byte with PEC. + async fn receive_byte_with_pec(&mut self, address: u8) + -> Result::Error>; /// Write Byte. async fn write_byte( @@ -105,7 +107,14 @@ pub trait Smbus: crate::smbus::bus::ErrorType { address: u8, register: u8, byte: u8, - use_pec: bool, + ) -> Result<(), ::Error>; + + /// Write Byte with PEC. + async fn write_byte_with_pec( + &mut self, + address: u8, + register: u8, + byte: u8, ) -> Result<(), ::Error>; /// Write Word (little-endian on the wire). @@ -114,7 +123,14 @@ pub trait Smbus: crate::smbus::bus::ErrorType { address: u8, register: u8, word: u16, - use_pec: bool, + ) -> Result<(), ::Error>; + + /// Write Word with PEC (little-endian on the wire). + async fn write_word_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, ) -> Result<(), ::Error>; /// Read Byte. @@ -122,7 +138,13 @@ pub trait Smbus: crate::smbus::bus::ErrorType { &mut self, address: u8, register: u8, - use_pec: bool, + ) -> Result::Error>; + + /// Read Byte with PEC. + async fn read_byte_with_pec( + &mut self, + address: u8, + register: u8, ) -> Result::Error>; /// Read Word (little-endian on the wire). @@ -130,7 +152,13 @@ pub trait Smbus: crate::smbus::bus::ErrorType { &mut self, address: u8, register: u8, - use_pec: bool, + ) -> Result::Error>; + + /// Read Word with PEC (little-endian on the wire). + async fn read_word_with_pec( + &mut self, + address: u8, + register: u8, ) -> Result::Error>; /// Process Call. @@ -139,7 +167,14 @@ pub trait Smbus: crate::smbus::bus::ErrorType { address: u8, register: u8, word: u16, - use_pec: bool, + ) -> Result::Error>; + + /// Process Call with PEC. + async fn process_call_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, ) -> Result::Error>; /// Block Write. @@ -148,7 +183,14 @@ pub trait Smbus: crate::smbus::bus::ErrorType { address: u8, register: u8, data: &[u8], - use_pec: bool, + ) -> Result<(), ::Error>; + + /// Block Write with PEC. + async fn block_write_with_pec( + &mut self, + address: u8, + register: u8, + data: &[u8], ) -> Result<(), ::Error>; /// Block Read. @@ -157,7 +199,14 @@ pub trait Smbus: crate::smbus::bus::ErrorType { address: u8, register: u8, data: &mut [u8], - use_pec: bool, + ) -> Result<(), ::Error>; + + /// Block Read with PEC. + async fn block_read_with_pec( + &mut self, + address: u8, + register: u8, + data: &mut [u8], ) -> Result<(), ::Error>; /// Block Write / Block Read / Process Call. @@ -167,7 +216,15 @@ pub trait Smbus: crate::smbus::bus::ErrorType { register: u8, write_data: &[u8], read_data: &mut [u8], - use_pec: bool, + ) -> Result<(), ::Error>; + + /// Block Write / Block Read / Process Call with PEC. + async fn block_write_block_read_process_call_with_pec( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], ) -> Result<(), ::Error>; } @@ -189,22 +246,30 @@ impl Smbus for &mut T { } #[inline] - async fn send_byte( + async fn send_byte(&mut self, address: u8, byte: u8) -> Result<(), ::Error> { + T::send_byte(*self, address, byte).await + } + + #[inline] + async fn send_byte_with_pec( &mut self, address: u8, byte: u8, - use_pec: bool, ) -> Result<(), ::Error> { - T::send_byte(*self, address, byte, use_pec).await + T::send_byte_with_pec(*self, address, byte).await } #[inline] - async fn receive_byte( + async fn receive_byte(&mut self, address: u8) -> Result::Error> { + T::receive_byte(*self, address).await + } + + #[inline] + async fn receive_byte_with_pec( &mut self, address: u8, - use_pec: bool, ) -> Result::Error> { - T::receive_byte(*self, address, use_pec).await + T::receive_byte_with_pec(*self, address).await } #[inline] @@ -213,9 +278,18 @@ impl Smbus for &mut T { address: u8, register: u8, byte: u8, - use_pec: bool, ) -> Result<(), ::Error> { - T::write_byte(*self, address, register, byte, use_pec).await + T::write_byte(*self, address, register, byte).await + } + + #[inline] + async fn write_byte_with_pec( + &mut self, + address: u8, + register: u8, + byte: u8, + ) -> Result<(), ::Error> { + T::write_byte_with_pec(*self, address, register, byte).await } #[inline] @@ -224,9 +298,18 @@ impl Smbus for &mut T { address: u8, register: u8, word: u16, - use_pec: bool, ) -> Result<(), ::Error> { - T::write_word(*self, address, register, word, use_pec).await + T::write_word(*self, address, register, word).await + } + + #[inline] + async fn write_word_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result<(), ::Error> { + T::write_word_with_pec(*self, address, register, word).await } #[inline] @@ -234,9 +317,17 @@ impl Smbus for &mut T { &mut self, address: u8, register: u8, - use_pec: bool, ) -> Result::Error> { - T::read_byte(*self, address, register, use_pec).await + T::read_byte(*self, address, register).await + } + + #[inline] + async fn read_byte_with_pec( + &mut self, + address: u8, + register: u8, + ) -> Result::Error> { + T::read_byte_with_pec(*self, address, register).await } #[inline] @@ -244,9 +335,17 @@ impl Smbus for &mut T { &mut self, address: u8, register: u8, - use_pec: bool, ) -> Result::Error> { - T::read_word(*self, address, register, use_pec).await + T::read_word(*self, address, register).await + } + + #[inline] + async fn read_word_with_pec( + &mut self, + address: u8, + register: u8, + ) -> Result::Error> { + T::read_word_with_pec(*self, address, register).await } #[inline] @@ -255,9 +354,18 @@ impl Smbus for &mut T { address: u8, register: u8, word: u16, - use_pec: bool, ) -> Result::Error> { - T::process_call(*self, address, register, word, use_pec).await + T::process_call(*self, address, register, word).await + } + + #[inline] + async fn process_call_with_pec( + &mut self, + address: u8, + register: u8, + word: u16, + ) -> Result::Error> { + T::process_call_with_pec(*self, address, register, word).await } #[inline] @@ -266,9 +374,18 @@ impl Smbus for &mut T { address: u8, register: u8, data: &[u8], - use_pec: bool, ) -> Result<(), ::Error> { - T::block_write(*self, address, register, data, use_pec).await + T::block_write(*self, address, register, data).await + } + + #[inline] + async fn block_write_with_pec( + &mut self, + address: u8, + register: u8, + data: &[u8], + ) -> Result<(), ::Error> { + T::block_write_with_pec(*self, address, register, data).await } #[inline] @@ -277,9 +394,18 @@ impl Smbus for &mut T { address: u8, register: u8, data: &mut [u8], - use_pec: bool, ) -> Result<(), ::Error> { - T::block_read(*self, address, register, data, use_pec).await + T::block_read(*self, address, register, data).await + } + + #[inline] + async fn block_read_with_pec( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), ::Error> { + T::block_read_with_pec(*self, address, register, data).await } #[inline] @@ -289,9 +415,19 @@ impl Smbus for &mut T { register: u8, write_data: &[u8], read_data: &mut [u8], - use_pec: bool, ) -> Result<(), ::Error> { - T::block_write_block_read_process_call(*self, address, register, write_data, read_data, use_pec).await + T::block_write_block_read_process_call(*self, address, register, write_data, read_data).await + } + + #[inline] + async fn block_write_block_read_process_call_with_pec( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + ) -> Result<(), ::Error> { + T::block_write_block_read_process_call_with_pec(*self, address, register, write_data, read_data).await } } @@ -348,6 +484,16 @@ where Ok(pec) } + /// Obtain a fresh PEC calculator pre-fed with the read-address byte, + /// or [`ErrorKind::Pec`](crate::smbus::bus::ErrorKind::Pec) if the + /// provider returns `None`. Used by pure-read transactions (e.g. + /// Receive Byte) whose first wire byte is the read-direction address. + fn pec_calc_with_read_addr(address: u8) -> Result { + let mut pec = P::new_calc().ok_or(crate::smbus::bus::ErrorKind::Pec)?; + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + Ok(pec) + } + /// Truncate a finished PEC value to its low byte. fn finalize_pec_byte(pec: u64) -> Result { pec.try_into().map_err(|_| crate::smbus::bus::ErrorKind::Pec) @@ -387,6 +533,8 @@ where /// /// When `use_pec` is true, the caller must size `read` to include one /// extra trailing byte for the PEC byte; it is verified after the read. + /// The PEC is seeded with the read-direction address byte because this + /// helper drives a pure-read SMBus transaction (e.g. Receive Byte). async fn read_buf( &mut self, address: u8, @@ -394,7 +542,7 @@ where read: &mut [u8], ) -> Result<(), crate::smbus::bus::ErrorKind> { if use_pec { - let mut pec = Self::pec_calc_with_write_addr(address)?; + let mut pec = Self::pec_calc_with_read_addr(address)?; self.i2c .read(address, read) .await @@ -473,132 +621,125 @@ where Ok(()) } - async fn send_byte(&mut self, address: u8, byte: u8, use_pec: bool) -> Result<(), crate::smbus::bus::ErrorKind> { - if use_pec { - self.write_buf(address, true, &mut [byte, 0]).await - } else { - self.write_buf(address, false, &mut [byte]).await - } + async fn send_byte(&mut self, address: u8, byte: u8) -> Result<(), crate::smbus::bus::ErrorKind> { + self.write_buf(address, false, &mut [byte]).await } - async fn receive_byte(&mut self, address: u8, use_pec: bool) -> Result { - if use_pec { - let mut buf = [0u8; 2]; - self.read_buf(address, true, &mut buf).await?; - Ok(buf[0]) - } else { - let mut buf = [0u8; 1]; - self.read_buf(address, false, &mut buf).await?; - Ok(buf[0]) - } + async fn send_byte_with_pec(&mut self, address: u8, byte: u8) -> Result<(), crate::smbus::bus::ErrorKind> { + self.write_buf(address, true, &mut [byte, 0]).await } - async fn write_byte( + async fn receive_byte(&mut self, address: u8) -> Result { + let mut buf = [0u8; 1]; + self.read_buf(address, false, &mut buf).await?; + Ok(buf[0]) + } + + async fn receive_byte_with_pec(&mut self, address: u8) -> Result { + let mut buf = [0u8; 2]; + self.read_buf(address, true, &mut buf).await?; + Ok(buf[0]) + } + + async fn write_byte(&mut self, address: u8, register: u8, byte: u8) -> Result<(), crate::smbus::bus::ErrorKind> { + self.write_buf(address, false, &mut [register, byte]).await + } + + async fn write_byte_with_pec( &mut self, address: u8, register: u8, byte: u8, - use_pec: bool, ) -> Result<(), crate::smbus::bus::ErrorKind> { - if use_pec { - self.write_buf(address, true, &mut [register, byte, 0]).await - } else { - self.write_buf(address, false, &mut [register, byte]).await - } + self.write_buf(address, true, &mut [register, byte, 0]).await } - async fn write_word( + async fn write_word(&mut self, address: u8, register: u8, word: u16) -> Result<(), crate::smbus::bus::ErrorKind> { + let b = u16::to_le_bytes(word); + self.write_buf(address, false, &mut [register, b[0], b[1]]).await + } + + async fn write_word_with_pec( &mut self, address: u8, register: u8, word: u16, - use_pec: bool, ) -> Result<(), crate::smbus::bus::ErrorKind> { let b = u16::to_le_bytes(word); - if use_pec { - self.write_buf(address, true, &mut [register, b[0], b[1], 0]).await - } else { - self.write_buf(address, false, &mut [register, b[0], b[1]]).await - } + self.write_buf(address, true, &mut [register, b[0], b[1], 0]).await } - async fn read_byte( - &mut self, - address: u8, - register: u8, - use_pec: bool, - ) -> Result { - if use_pec { - let mut buf = [0u8; 2]; - self.write_read_buf(address, true, &[register], &mut buf).await?; - Ok(buf[0]) - } else { - let mut buf = [0u8; 1]; - self.write_read_buf(address, false, &[register], &mut buf).await?; - Ok(buf[0]) - } + async fn read_byte(&mut self, address: u8, register: u8) -> Result { + let mut buf = [0u8; 1]; + self.write_read_buf(address, false, &[register], &mut buf).await?; + Ok(buf[0]) } - async fn read_word( + async fn read_byte_with_pec(&mut self, address: u8, register: u8) -> Result { + let mut buf = [0u8; 2]; + self.write_read_buf(address, true, &[register], &mut buf).await?; + Ok(buf[0]) + } + + async fn read_word(&mut self, address: u8, register: u8) -> Result { + let mut buf = [0u8; 2]; + self.write_read_buf(address, false, &[register], &mut buf).await?; + Ok(u16::from_le_bytes(buf)) + } + + async fn read_word_with_pec(&mut self, address: u8, register: u8) -> Result { + let mut buf = [0u8; 3]; + self.write_read_buf(address, true, &[register], &mut buf).await?; + Ok(u16::from_le_bytes([buf[0], buf[1]])) + } + + async fn process_call( &mut self, address: u8, register: u8, - use_pec: bool, + word: u16, ) -> Result { - if use_pec { - let mut buf = [0u8; 3]; - self.write_read_buf(address, true, &[register], &mut buf).await?; - Ok(u16::from_le_bytes([buf[0], buf[1]])) - } else { - let mut buf = [0u8; 2]; - self.write_read_buf(address, false, &[register], &mut buf).await?; - Ok(u16::from_le_bytes(buf)) - } + let mut buf = [0u8; 2]; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + Ok(u16::from_le_bytes(buf)) } - async fn process_call( + async fn process_call_with_pec( &mut self, address: u8, register: u8, word: u16, - use_pec: bool, ) -> Result { - if use_pec { - let mut buf = [0u8; 3]; - let mut pec = Self::pec_calc_with_write_addr(address)?; - pec.write_u8(register); - pec.write_u16(word); - pec.write_u8(crate::smbus::bus::read_address_byte(address)); - self.i2c - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&word.to_le_bytes()), - Operation::Read(&mut buf), - ], - ) - .await - .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; - let (recvd_pec, data) = buf.split_last().ok_or(crate::smbus::bus::ErrorKind::Pec)?; - pec.write(data); - Self::check_pec(*recvd_pec, pec.finish())?; - Ok(u16::from_le_bytes([buf[0], buf[1]])) - } else { - let mut buf = [0u8; 2]; - self.i2c - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&word.to_le_bytes()), - Operation::Read(&mut buf), - ], - ) - .await - .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; - Ok(u16::from_le_bytes(buf)) - } + let mut buf = [0u8; 3]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write(&word.to_le_bytes()); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&word.to_le_bytes()), + Operation::Read(&mut buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + let (recvd_pec, data) = buf.split_last().ok_or(crate::smbus::bus::ErrorKind::Pec)?; + pec.write(data); + Self::check_pec(*recvd_pec, pec.finish())?; + Ok(u16::from_le_bytes([buf[0], buf[1]])) } async fn block_write( @@ -606,42 +747,50 @@ where address: u8, register: u8, data: &[u8], - use_pec: bool, ) -> Result<(), crate::smbus::bus::ErrorKind> { if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); } - if use_pec { - let mut pec = Self::pec_calc_with_write_addr(address)?; - pec.write_u8(register); - pec.write_u8(data.len() as u8); - pec.write(data); - let pec: u8 = Self::finalize_pec_byte(pec.finish())?; - self.i2c - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[data.len() as u8]), - Operation::Write(data), - Operation::Write(&[pec]), - ], - ) - .await - .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; - } else { - self.i2c - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[data.len() as u8]), - Operation::Write(data), - ], - ) - .await - .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[data.len() as u8]), + Operation::Write(data), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + Ok(()) + } + + async fn block_write_with_pec( + &mut self, + address: u8, + register: u8, + data: &[u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); } + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(data.len() as u8); + pec.write(data); + let pec: u8 = Self::finalize_pec_byte(pec.finish())?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[data.len() as u8]), + Operation::Write(data), + Operation::Write(&[pec]), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; Ok(()) } @@ -650,99 +799,130 @@ where address: u8, register: u8, data: &mut [u8], - use_pec: bool, ) -> Result<(), crate::smbus::bus::ErrorKind> { if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); } let mut msg_size = [0u8]; - if use_pec { - let mut pec_buf = [0u8]; - let mut pec = Self::pec_calc_with_write_addr(address)?; - pec.write_u8(register); - pec.write_u8(crate::smbus::bus::read_address_byte(address)); - self.i2c - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Read(&mut msg_size), - Operation::Read(data), - Operation::Read(&mut pec_buf), - ], - ) - .await - .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; - pec.write(&msg_size); - pec.write(data); - Self::check_pec(pec_buf[0], pec.finish())?; - } else { - self.i2c - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Read(&mut msg_size), - Operation::Read(data), - ], - ) - .await - .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if usize::from(msg_size[0]) != data.len() { + return Err(crate::smbus::bus::ErrorKind::BlockSizeMismatch); } Ok(()) } + async fn block_read_with_pec( + &mut self, + address: u8, + register: u8, + data: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); + } + let mut msg_size = [0u8]; + let mut pec_buf = [0u8]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Read(&mut msg_size), + Operation::Read(data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if usize::from(msg_size[0]) != data.len() { + return Err(crate::smbus::bus::ErrorKind::BlockSizeMismatch); + } + pec.write(&msg_size); + pec.write(data); + Self::check_pec(pec_buf[0], pec.finish())?; + Ok(()) + } + async fn block_write_block_read_process_call( &mut self, address: u8, register: u8, write_data: &[u8], read_data: &mut [u8], - use_pec: bool, ) -> Result<(), crate::smbus::bus::ErrorKind> { if write_data.len() + read_data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); } let mut read_msg_size = [0u8]; - if use_pec { - let mut pec_buf = [0u8]; - let mut pec = Self::pec_calc_with_write_addr(address)?; - pec.write_u8(register); - pec.write_u8(write_data.len() as u8); - pec.write(write_data); - pec.write_u8(crate::smbus::bus::read_address_byte(address)); - self.i2c - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[write_data.len() as u8]), - Operation::Write(write_data), - Operation::Read(&mut read_msg_size), - Operation::Read(read_data), - Operation::Read(&mut pec_buf), - ], - ) - .await - .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; - pec.write(&read_msg_size); - pec.write(read_data); - Self::check_pec(pec_buf[0], pec.finish())?; - } else { - self.i2c - .transaction( - address, - &mut [ - Operation::Write(&[register]), - Operation::Write(&[write_data.len() as u8]), - Operation::Write(write_data), - Operation::Read(&mut read_msg_size), - Operation::Read(read_data), - ], - ) - .await - .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if usize::from(read_msg_size[0]) != read_data.len() { + return Err(crate::smbus::bus::ErrorKind::BlockSizeMismatch); + } + Ok(()) + } + + async fn block_write_block_read_process_call_with_pec( + &mut self, + address: u8, + register: u8, + write_data: &[u8], + read_data: &mut [u8], + ) -> Result<(), crate::smbus::bus::ErrorKind> { + if write_data.len() + read_data.len() > crate::smbus::bus::MAX_BLOCK_SIZE { + return Err(crate::smbus::bus::ErrorKind::TooLargeBlockTransaction); + } + let mut read_msg_size = [0u8]; + let mut pec_buf = [0u8]; + let mut pec = Self::pec_calc_with_write_addr(address)?; + pec.write_u8(register); + pec.write_u8(write_data.len() as u8); + pec.write(write_data); + pec.write_u8(crate::smbus::bus::read_address_byte(address)); + self.i2c + .transaction( + address, + &mut [ + Operation::Write(&[register]), + Operation::Write(&[write_data.len() as u8]), + Operation::Write(write_data), + Operation::Read(&mut read_msg_size), + Operation::Read(read_data), + Operation::Read(&mut pec_buf), + ], + ) + .await + .map_err(|e| crate::smbus::bus::ErrorKind::from(e.kind()))?; + if usize::from(read_msg_size[0]) != read_data.len() { + return Err(crate::smbus::bus::ErrorKind::BlockSizeMismatch); } + pec.write(&read_msg_size); + pec.write(read_data); + Self::check_pec(pec_buf[0], pec.finish())?; Ok(()) } } @@ -820,6 +1000,7 @@ mod tests { ErrorKind::Timeout, ErrorKind::Pec, ErrorKind::TooLargeBlockTransaction, + ErrorKind::BlockSizeMismatch, ErrorKind::Other, ] { let s = std::format!("{}", k); @@ -892,7 +1073,7 @@ mod tests { #[tokio::test] async fn read_buf_pec() { let data = 0x11u8; - let pec = expected_pec(&[&[write_address_byte(ADDR)], &[data]]); + let pec = expected_pec(&[&[read_address_byte(ADDR)], &[data]]); let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); let mut buf = [0u8; 2]; bus.read_buf(ADDR, true, &mut buf).await.unwrap(); @@ -937,7 +1118,7 @@ mod tests { #[tokio::test] async fn send_byte_no_pec() { let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); - bus.send_byte(ADDR, 0x55, false).await.unwrap(); + bus.send_byte(ADDR, 0x55).await.unwrap(); done(bus); } @@ -945,14 +1126,14 @@ mod tests { async fn send_byte_pec() { let pec = expected_pec(&[&[write_address_byte(ADDR), 0x55]]); let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55, pec])]); - bus.send_byte(ADDR, 0x55, true).await.unwrap(); + bus.send_byte_with_pec(ADDR, 0x55).await.unwrap(); done(bus); } #[tokio::test] async fn receive_byte_no_pec() { let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99])]); - let b = bus.receive_byte(ADDR, false).await.unwrap(); + let b = bus.receive_byte(ADDR).await.unwrap(); assert_eq!(b, 0x99); done(bus); } @@ -960,9 +1141,9 @@ mod tests { #[tokio::test] async fn receive_byte_pec() { let data = 0x99u8; - let pec = expected_pec(&[&[write_address_byte(ADDR), data]]); + let pec = expected_pec(&[&[read_address_byte(ADDR), data]]); let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); - let b = bus.receive_byte(ADDR, true).await.unwrap(); + let b = bus.receive_byte_with_pec(ADDR).await.unwrap(); assert_eq!(b, data); done(bus); } @@ -970,7 +1151,7 @@ mod tests { #[tokio::test] async fn receive_byte_pec_mismatch() { let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99, 0xFF])]); - let err = bus.receive_byte(ADDR, true).await.unwrap_err(); + let err = bus.receive_byte_with_pec(ADDR).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); done(bus); } @@ -980,7 +1161,7 @@ mod tests { #[tokio::test] async fn write_byte_no_pec() { let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33])]); - bus.write_byte(ADDR, REG, 0x33, false).await.unwrap(); + bus.write_byte(ADDR, REG, 0x33).await.unwrap(); done(bus); } @@ -988,7 +1169,7 @@ mod tests { async fn write_byte_pec() { let pec = expected_pec(&[&[write_address_byte(ADDR), REG, 0x33]]); let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33, pec])]); - bus.write_byte(ADDR, REG, 0x33, true).await.unwrap(); + bus.write_byte_with_pec(ADDR, REG, 0x33).await.unwrap(); done(bus); } @@ -997,7 +1178,7 @@ mod tests { let word: u16 = 0xBEEF; let bytes = word.to_le_bytes(); let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1]])]); - bus.write_word(ADDR, REG, word, false).await.unwrap(); + bus.write_word(ADDR, REG, word).await.unwrap(); done(bus); } @@ -1007,7 +1188,7 @@ mod tests { let bytes = word.to_le_bytes(); let pec = expected_pec(&[&[write_address_byte(ADDR), REG, bytes[0], bytes[1]]]); let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1], pec])]); - bus.write_word(ADDR, REG, word, true).await.unwrap(); + bus.write_word_with_pec(ADDR, REG, word).await.unwrap(); done(bus); } @@ -1021,7 +1202,7 @@ mod tests { Tx::read(ADDR, std::vec![0x77]), Tx::transaction_end(ADDR), ]); - let b = bus.read_byte(ADDR, REG, false).await.unwrap(); + let b = bus.read_byte(ADDR, REG).await.unwrap(); assert_eq!(b, 0x77); done(bus); } @@ -1036,7 +1217,7 @@ mod tests { Tx::read(ADDR, std::vec![data, pec]), Tx::transaction_end(ADDR), ]); - let b = bus.read_byte(ADDR, REG, true).await.unwrap(); + let b = bus.read_byte_with_pec(ADDR, REG).await.unwrap(); assert_eq!(b, data); done(bus); } @@ -1049,7 +1230,7 @@ mod tests { Tx::read(ADDR, std::vec![0x77, 0xFF]), Tx::transaction_end(ADDR), ]); - let err = bus.read_byte(ADDR, REG, true).await.unwrap_err(); + let err = bus.read_byte_with_pec(ADDR, REG).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); done(bus); } @@ -1064,7 +1245,7 @@ mod tests { Tx::read(ADDR, std::vec![lo, hi]), Tx::transaction_end(ADDR), ]); - let w = bus.read_word(ADDR, REG, false).await.unwrap(); + let w = bus.read_word(ADDR, REG).await.unwrap(); assert_eq!(w, u16::from_le_bytes([lo, hi])); done(bus); } @@ -1080,7 +1261,7 @@ mod tests { Tx::read(ADDR, std::vec![lo, hi, pec]), Tx::transaction_end(ADDR), ]); - let w = bus.read_word(ADDR, REG, true).await.unwrap(); + let w = bus.read_word_with_pec(ADDR, REG).await.unwrap(); assert_eq!(w, u16::from_le_bytes([lo, hi])); done(bus); } @@ -1099,7 +1280,7 @@ mod tests { Tx::read(ADDR, std::vec![resp_lo, resp_hi]), Tx::transaction_end(ADDR), ]); - let r = bus.process_call(ADDR, REG, word, false).await.unwrap(); + let r = bus.process_call(ADDR, REG, word).await.unwrap(); assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); done(bus); } @@ -1112,7 +1293,7 @@ mod tests { let mut hasher = Pec::new(); hasher.write_u8(write_address_byte(ADDR)); hasher.write_u8(REG); - hasher.write_u16(word); + hasher.write(&word.to_le_bytes()); hasher.write_u8(read_address_byte(ADDR)); hasher.write(&[resp_lo, resp_hi]); let pec = hasher.finish() as u8; @@ -1123,7 +1304,7 @@ mod tests { Tx::read(ADDR, std::vec![resp_lo, resp_hi, pec]), Tx::transaction_end(ADDR), ]); - let r = bus.process_call(ADDR, REG, word, true).await.unwrap(); + let r = bus.process_call_with_pec(ADDR, REG, word).await.unwrap(); assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); done(bus); } @@ -1140,7 +1321,7 @@ mod tests { Tx::write(ADDR, data.to_vec()), Tx::transaction_end(ADDR), ]); - bus.block_write(ADDR, REG, &data, false).await.unwrap(); + bus.block_write(ADDR, REG, &data).await.unwrap(); done(bus); } @@ -1156,7 +1337,7 @@ mod tests { Tx::write(ADDR, std::vec![pec]), Tx::transaction_end(ADDR), ]); - bus.block_write(ADDR, REG, &data, true).await.unwrap(); + bus.block_write_with_pec(ADDR, REG, &data).await.unwrap(); done(bus); } @@ -1164,7 +1345,7 @@ mod tests { async fn block_write_too_large() { let mut bus = new_bus(&[]); let data = std::vec![0u8; MAX_BLOCK_SIZE + 1]; - let err = bus.block_write(ADDR, REG, &data, false).await.unwrap_err(); + let err = bus.block_write(ADDR, REG, &data).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); done(bus); } @@ -1181,7 +1362,7 @@ mod tests { Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), Tx::transaction_end(ADDR), ]); - bus.block_read(ADDR, REG, &mut buf, false).await.unwrap(); + bus.block_read(ADDR, REG, &mut buf).await.unwrap(); assert_eq!(buf, [0x10, 0x20, 0x30]); done(bus); } @@ -1207,7 +1388,7 @@ mod tests { Tx::read(ADDR, std::vec![pec]), Tx::transaction_end(ADDR), ]); - bus.block_read(ADDR, REG, &mut buf, true).await.unwrap(); + bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap(); assert_eq!(buf, payload); done(bus); } @@ -1216,11 +1397,46 @@ mod tests { async fn block_read_too_large() { let mut bus = new_bus(&[]); let mut buf = std::vec![0u8; MAX_BLOCK_SIZE + 1]; - let err = bus.block_read(ADDR, REG, &mut buf, false).await.unwrap_err(); + let err = bus.block_read(ADDR, REG, &mut buf).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); done(bus); } + #[tokio::test] + async fn block_read_size_mismatch_no_pec() { + // Device reports `2` but the caller expected `3`. + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![2]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::transaction_end(ADDR), + ]); + let err = bus.block_read(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); + } + + #[tokio::test] + async fn block_read_size_mismatch_pec() { + // Device reports `2` but the caller expected `3`. The mismatch must be + // reported as `BlockSizeMismatch` rather than `Pec`, even though the + // received PEC byte would not match either. + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![2]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::read(ADDR, std::vec![0x00]), + Tx::transaction_end(ADDR), + ]); + let err = bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); + } + // ---------- block_write_block_read_process_call ---------- #[tokio::test] @@ -1237,7 +1453,7 @@ mod tests { Tx::read(ADDR, read_payload.to_vec()), Tx::transaction_end(ADDR), ]); - bus.block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf, false) + bus.block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) .await .unwrap(); assert_eq!(read_buf, read_payload); @@ -1268,7 +1484,7 @@ mod tests { Tx::read(ADDR, std::vec![pec]), Tx::transaction_end(ADDR), ]); - bus.block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf, true) + bus.block_write_block_read_process_call_with_pec(ADDR, REG, &write_data, &mut read_buf) .await .unwrap(); assert_eq!(read_buf, read_payload); @@ -1281,13 +1497,59 @@ mod tests { let write_data = std::vec![0u8; 200]; let mut read_buf = std::vec![0u8; 60]; // 200 + 60 > 255 let err = bus - .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf, false) + .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) .await .unwrap_err(); assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); done(bus); } + #[tokio::test] + async fn bwbr_size_mismatch_no_pec() { + let write_data = [0x01u8, 0x02]; + let mut read_buf = [0u8; 2]; + // Device returns count `1` but caller expected `2`. + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![1]), + Tx::read(ADDR, std::vec![0xAA, 0xBB]), + Tx::transaction_end(ADDR), + ]); + let err = bus + .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); + } + + #[tokio::test] + async fn bwbr_size_mismatch_pec() { + let write_data = [0x01u8, 0x02]; + let mut read_buf = [0u8; 2]; + // Device returns count `1` but caller expected `2`. Must be reported + // as `BlockSizeMismatch` rather than `Pec`. + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![1]), + Tx::read(ADDR, std::vec![0xAA, 0xBB]), + Tx::read(ADDR, std::vec![0x00]), + Tx::transaction_end(ADDR), + ]); + let err = bus + .block_write_block_read_process_call_with_pec(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); + } + // ---------- PEC unavailable ---------- fn new_no_pec_bus(expectations: &[Tx]) -> NoPecBus { @@ -1317,10 +1579,10 @@ mod tests { Tx::read(ADDR, std::vec![0x77]), Tx::transaction_end(ADDR), ]); - bus.send_byte(ADDR, 0x55, false).await.unwrap(); - assert_eq!(bus.receive_byte(ADDR, false).await.unwrap(), 0x99); - bus.write_byte(ADDR, REG, 0x33, false).await.unwrap(); - assert_eq!(bus.read_byte(ADDR, REG, false).await.unwrap(), 0x77); + bus.send_byte(ADDR, 0x55).await.unwrap(); + assert_eq!(bus.receive_byte(ADDR).await.unwrap(), 0x99); + bus.write_byte(ADDR, REG, 0x33).await.unwrap(); + assert_eq!(bus.read_byte(ADDR, REG).await.unwrap(), 0x77); done_no_pec(bus); } @@ -1328,23 +1590,23 @@ mod tests { async fn pec_unavailable_returns_pec_error() { let mut bus = super::SwSmbusI2c::::new(I2cMock::new(&[])); // Any PEC-requiring path should fail without touching the bus. - let err = bus.send_byte(ADDR, 0x55, true).await.unwrap_err(); + let err = bus.send_byte_with_pec(ADDR, 0x55).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.receive_byte(ADDR, true).await.unwrap_err(); + let err = bus.receive_byte_with_pec(ADDR).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.read_byte(ADDR, REG, true).await.unwrap_err(); + let err = bus.read_byte_with_pec(ADDR, REG).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.read_word(ADDR, REG, true).await.unwrap_err(); + let err = bus.read_word_with_pec(ADDR, REG).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.process_call(ADDR, REG, 0, true).await.unwrap_err(); + let err = bus.process_call_with_pec(ADDR, REG, 0).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.block_write(ADDR, REG, &[1, 2], true).await.unwrap_err(); + let err = bus.block_write_with_pec(ADDR, REG, &[1, 2]).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); let mut rb = [0u8; 2]; - let err = bus.block_read(ADDR, REG, &mut rb, true).await.unwrap_err(); + let err = bus.block_read_with_pec(ADDR, REG, &mut rb).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); let err = bus - .block_write_block_read_process_call(ADDR, REG, &[1], &mut rb, true) + .block_write_block_read_process_call_with_pec(ADDR, REG, &[1], &mut rb) .await .unwrap_err(); assert_eq!(err.kind(), ErrorKind::Pec); @@ -1357,7 +1619,7 @@ mod tests { async fn mut_ref_smbus_forwards() { let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); let r: &mut TestBus = &mut bus; - r.send_byte(ADDR, 0x55, false).await.unwrap(); + r.send_byte(ADDR, 0x55).await.unwrap(); assert!(<&mut TestBus as Smbus>::get_pec_calc().is_some()); done(bus); } @@ -1367,7 +1629,7 @@ mod tests { #[tokio::test] async fn i2c_error_propagates() { let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55]).with_error(I2cErrorKind::Bus)]); - let err = bus.send_byte(ADDR, 0x55, false).await.unwrap_err(); + let err = bus.send_byte(ADDR, 0x55).await.unwrap_err(); assert_eq!(err.kind(), ErrorKind::I2c(I2cErrorKind::Bus)); done(bus); } diff --git a/embedded-mcu-hal/src/smbus/bus/mod.rs b/embedded-mcu-hal/src/smbus/bus/mod.rs index d09025a..6af5ec7 100644 --- a/embedded-mcu-hal/src/smbus/bus/mod.rs +++ b/embedded-mcu-hal/src/smbus/bus/mod.rs @@ -75,11 +75,11 @@ pub trait Error: core::fmt::Debug { /// Convert error to a generic SMBus error kind. /// /// By using this method, SMBus errors freely defined by HAL implementations - /// can be converted to a set of generic I2C errors upon which generic + /// can be converted to a common set of SMBus errors upon which generic /// code can act. fn kind(&self) -> ErrorKind; - /// Convert error to a generic SMBus error kind. - fn to_kind(kind: ErrorKind) -> Self; + /// Construct an error from a generic SMBus error kind. + fn from_kind(kind: ErrorKind) -> Self; } impl Error for core::convert::Infallible { @@ -88,9 +88,13 @@ impl Error for core::convert::Infallible { match *self {} } #[inline] - #[allow(clippy::unreachable)] - fn to_kind(_kind: ErrorKind) -> Self { - unreachable!() + fn from_kind(_kind: ErrorKind) -> Self { + // `Infallible` is uninhabited, so this function can never actually + // be called + #[allow(clippy::unreachable)] + { + unreachable!() + } } } @@ -111,6 +115,9 @@ pub enum ErrorKind { Pec, /// Block read/write too large transfer, at most 255 bytes can be read/written at once. TooLargeBlockTransaction, + /// Block read returned a byte count that did not match the caller's + /// expected buffer length. + BlockSizeMismatch, /// A different error occurred. The original error may contain more information. Other, } @@ -127,7 +134,7 @@ impl Error for ErrorKind { *self } #[inline] - fn to_kind(kind: ErrorKind) -> Self { + fn from_kind(kind: ErrorKind) -> Self { kind } } @@ -143,6 +150,10 @@ impl core::fmt::Display for ErrorKind { f, "Block read/write transfer size too large, at most 255 bytes can be read/written at once." ), + Self::BlockSizeMismatch => write!( + f, + "Block read returned a byte count that did not match the caller's expected buffer length." + ), Self::Other => write!( f, "A different error occurred. The original error may contain more information" @@ -151,7 +162,7 @@ impl core::fmt::Display for ErrorKind { } } -/// I2C error type trait. +/// SMBus error type trait. /// /// This just defines the error type, to be used by the other traits. pub trait ErrorType { diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 8431550..04fa752 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -304,8 +304,8 @@ who = "Lukasz Anforowicz " criteria = "safe-to-deploy" version = "1.0.78" notes = """ -Grepped for "crypt", "cipher", "fs", "net" - there were no hits -(except for a benign "fs" hit in a doc comment) +Grepped for \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits +(except for a benign \"fs\" hit in a doc comment) Notes from the `unsafe` review can be found in https://crrev.com/c/5385745. """ @@ -423,8 +423,8 @@ who = "Lukasz Anforowicz " criteria = "safe-to-deploy" version = "1.0.35" notes = """ -Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits -(except for benign "net" hit in tests and "fs" hit in README.md) +Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits +(except for benign \"net\" hit in tests and \"fs\" hit in README.md) """ aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" @@ -529,7 +529,7 @@ and there were no hits except for: * Using `unsafe` in a string: ``` - src/constfn.rs: "unsafe" => Qualifiers::Unsafe, + src/constfn.rs: \"unsafe\" => Qualifiers::Unsafe, ``` * Using `std::fs` in `build/build.rs` to write `${OUT_DIR}/version.expr` From 16ffef6bec5c50a5b031e8c3086222bcc9dad213 Mon Sep 17 00:00:00 2001 From: Matteo Tullo Date: Thu, 14 May 2026 17:06:31 -0700 Subject: [PATCH 5/5] Move tests into standalone file --- .../smbus/bus/{asynch.rs => asynch/mod.rs} | 710 +----------------- .../src/smbus/bus/asynch/tests.rs | 704 +++++++++++++++++ 2 files changed, 707 insertions(+), 707 deletions(-) rename embedded-mcu-hal/src/smbus/bus/{asynch.rs => asynch/mod.rs} (55%) create mode 100644 embedded-mcu-hal/src/smbus/bus/asynch/tests.rs diff --git a/embedded-mcu-hal/src/smbus/bus/asynch.rs b/embedded-mcu-hal/src/smbus/bus/asynch/mod.rs similarity index 55% rename from embedded-mcu-hal/src/smbus/bus/asynch.rs rename to embedded-mcu-hal/src/smbus/bus/asynch/mod.rs index f541437..d4a14ba 100644 --- a/embedded-mcu-hal/src/smbus/bus/asynch.rs +++ b/embedded-mcu-hal/src/smbus/bus/asynch/mod.rs @@ -1,4 +1,4 @@ -//! Async SMBus controller trait and software implementation. +//! Async SMBus controller trait and software implementation. //! //! This module defines the [`Smbus`] async controller trait describing the //! SMBus protocol surface. The trait declares the protocol-level @@ -926,711 +926,7 @@ where Ok(()) } } + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::cast_possible_truncation)] -mod tests { - use super::Smbus; - use crate::smbus::bus::{ - read_address_byte, write_address_byte, Error as SmbusError, ErrorKind, MAX_BLOCK_SIZE, READ_BIT, - }; - use core::hash::Hasher; - use embedded_hal_async::i2c::ErrorKind as I2cErrorKind; - use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as Tx}; - use smbus_pec::{pec, Pec}; - - const ADDR: u8 = 0x42; - const REG: u8 = 0x07; - - /// Compute the expected SMBus PEC byte over a flat concatenation of byte - /// slices, using the `smbus-pec` crate as the reference implementation. - fn expected_pec(parts: &[&[u8]]) -> u8 { - let mut buf: std::vec::Vec = std::vec::Vec::new(); - for p in parts { - buf.extend_from_slice(p); - } - pec(&buf) - } - - /// PEC provider that exposes the `smbus-pec` calculator. - struct TestPec; - impl super::PecProvider for TestPec { - type Calc = Pec; - fn new_calc() -> Option { - Some(Pec::new()) - } - } - - /// PEC provider that reports PEC as unavailable. - struct NoPec; - impl super::PecProvider for NoPec { - type Calc = Pec; - fn new_calc() -> Option { - None - } - } - - type TestBus = super::SwSmbusI2c; - type NoPecBus = super::SwSmbusI2c; - - fn new_bus(expectations: &[Tx]) -> TestBus { - super::SwSmbusI2c::new(I2cMock::new(expectations)) - } - - fn done(mut bus: TestBus) { - bus.inner_mut().done(); - } - - // ---------- constants / helpers ---------- - - #[test] - fn constants() { - assert_eq!(MAX_BLOCK_SIZE, 255); - assert_eq!(READ_BIT, 0x01); - assert_eq!(write_address_byte(0x42), 0x84); - assert_eq!(read_address_byte(0x42), 0x85); - } - - #[test] - fn error_kind_display_and_kind() { - let k = ErrorKind::Timeout; - assert_eq!(k.kind(), ErrorKind::Timeout); - // Display impls cover all branches. - for k in [ - ErrorKind::I2c(I2cErrorKind::Bus), - ErrorKind::Timeout, - ErrorKind::Pec, - ErrorKind::TooLargeBlockTransaction, - ErrorKind::BlockSizeMismatch, - ErrorKind::Other, - ] { - let s = std::format!("{}", k); - assert!(!s.is_empty()); - } - } - - #[test] - fn error_kind_from_i2c_error_kind() { - let k: ErrorKind = I2cErrorKind::Bus.into(); - assert_eq!(k, ErrorKind::I2c(I2cErrorKind::Bus)); - } - - #[test] - fn infallible_error_to_kind_round_trip() { - // Infallible cannot be constructed; we only check the trait wires up. - fn _accepts(_e: &E) {} - let k = ErrorKind::Pec; - _accepts(&k); - } - - #[tokio::test] - async fn check_pec_match() { - let bus = new_bus(&[]); - TestBus::check_pec(0x42, 0x42).unwrap(); - TestBus::check_pec(0x00, 0x00).unwrap(); - done(bus); - } - - #[tokio::test] - async fn check_pec_mismatch() { - let bus = new_bus(&[]); - let err = TestBus::check_pec(0x42, 0x43).unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - done(bus); - } - - // ---------- write_buf / read_buf (low-level) ---------- - - #[tokio::test] - async fn write_buf_no_pec() { - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0xAB, 0xCD])]); - bus.write_buf(ADDR, false, &mut [0xAB, 0xCD]).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn write_buf_pec() { - let payload = [0xAB, 0xCD]; - let pec = expected_pec(&[&[write_address_byte(ADDR)], &payload]); - let mut buf = [0xAB, 0xCD, 0x00]; - let mut wire = std::vec![0xAB, 0xCD, pec]; - let mut bus = new_bus(&[Tx::write(ADDR, wire.clone())]); - bus.write_buf(ADDR, true, &mut buf).await.unwrap(); - // Last byte should now be the PEC. - assert_eq!(buf[2], pec); - wire.clear(); - done(bus); - } - - #[tokio::test] - async fn read_buf_no_pec() { - let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x11, 0x22])]); - let mut buf = [0u8; 2]; - bus.read_buf(ADDR, false, &mut buf).await.unwrap(); - assert_eq!(buf, [0x11, 0x22]); - done(bus); - } - - #[tokio::test] - async fn read_buf_pec() { - let data = 0x11u8; - let pec = expected_pec(&[&[read_address_byte(ADDR)], &[data]]); - let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); - let mut buf = [0u8; 2]; - bus.read_buf(ADDR, true, &mut buf).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn read_buf_pec_mismatch() { - let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x11, 0xFF])]); // wrong PEC - let mut buf = [0u8; 2]; - let err = bus.read_buf(ADDR, true, &mut buf).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - done(bus); - } - - // ---------- quick_command ---------- - - #[tokio::test] - async fn quick_command_write() { - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![]), - Tx::transaction_end(ADDR), - ]); - bus.quick_command(ADDR, false).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn quick_command_read() { - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::read(ADDR, std::vec![]), - Tx::transaction_end(ADDR), - ]); - bus.quick_command(ADDR, true).await.unwrap(); - done(bus); - } - - // ---------- send_byte / receive_byte ---------- - - #[tokio::test] - async fn send_byte_no_pec() { - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); - bus.send_byte(ADDR, 0x55).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn send_byte_pec() { - let pec = expected_pec(&[&[write_address_byte(ADDR), 0x55]]); - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55, pec])]); - bus.send_byte_with_pec(ADDR, 0x55).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn receive_byte_no_pec() { - let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99])]); - let b = bus.receive_byte(ADDR).await.unwrap(); - assert_eq!(b, 0x99); - done(bus); - } - - #[tokio::test] - async fn receive_byte_pec() { - let data = 0x99u8; - let pec = expected_pec(&[&[read_address_byte(ADDR), data]]); - let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); - let b = bus.receive_byte_with_pec(ADDR).await.unwrap(); - assert_eq!(b, data); - done(bus); - } - - #[tokio::test] - async fn receive_byte_pec_mismatch() { - let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99, 0xFF])]); - let err = bus.receive_byte_with_pec(ADDR).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - done(bus); - } - - // ---------- write_byte / write_word ---------- - - #[tokio::test] - async fn write_byte_no_pec() { - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33])]); - bus.write_byte(ADDR, REG, 0x33).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn write_byte_pec() { - let pec = expected_pec(&[&[write_address_byte(ADDR), REG, 0x33]]); - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33, pec])]); - bus.write_byte_with_pec(ADDR, REG, 0x33).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn write_word_no_pec() { - let word: u16 = 0xBEEF; - let bytes = word.to_le_bytes(); - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1]])]); - bus.write_word(ADDR, REG, word).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn write_word_pec() { - let word: u16 = 0xBEEF; - let bytes = word.to_le_bytes(); - let pec = expected_pec(&[&[write_address_byte(ADDR), REG, bytes[0], bytes[1]]]); - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1], pec])]); - bus.write_word_with_pec(ADDR, REG, word).await.unwrap(); - done(bus); - } - - // ---------- read_byte / read_word ---------- - - #[tokio::test] - async fn read_byte_no_pec() { - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![0x77]), - Tx::transaction_end(ADDR), - ]); - let b = bus.read_byte(ADDR, REG).await.unwrap(); - assert_eq!(b, 0x77); - done(bus); - } - - #[tokio::test] - async fn read_byte_pec() { - let data = 0x77u8; - let pec = expected_pec(&[&[write_address_byte(ADDR), REG, read_address_byte(ADDR), data]]); - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![data, pec]), - Tx::transaction_end(ADDR), - ]); - let b = bus.read_byte_with_pec(ADDR, REG).await.unwrap(); - assert_eq!(b, data); - done(bus); - } - - #[tokio::test] - async fn read_byte_pec_mismatch() { - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![0x77, 0xFF]), - Tx::transaction_end(ADDR), - ]); - let err = bus.read_byte_with_pec(ADDR, REG).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - done(bus); - } - - #[tokio::test] - async fn read_word_no_pec() { - let lo = 0x12u8; - let hi = 0x34u8; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![lo, hi]), - Tx::transaction_end(ADDR), - ]); - let w = bus.read_word(ADDR, REG).await.unwrap(); - assert_eq!(w, u16::from_le_bytes([lo, hi])); - done(bus); - } - - #[tokio::test] - async fn read_word_pec() { - let lo = 0x12u8; - let hi = 0x34u8; - let pec = expected_pec(&[&[write_address_byte(ADDR), REG, read_address_byte(ADDR), lo, hi]]); - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![lo, hi, pec]), - Tx::transaction_end(ADDR), - ]); - let w = bus.read_word_with_pec(ADDR, REG).await.unwrap(); - assert_eq!(w, u16::from_le_bytes([lo, hi])); - done(bus); - } - - // ---------- process_call ---------- - - #[tokio::test] - async fn process_call_no_pec() { - let word: u16 = 0x0102; - let resp_lo = 0xAAu8; - let resp_hi = 0xBBu8; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::write(ADDR, word.to_le_bytes().to_vec()), - Tx::read(ADDR, std::vec![resp_lo, resp_hi]), - Tx::transaction_end(ADDR), - ]); - let r = bus.process_call(ADDR, REG, word).await.unwrap(); - assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); - done(bus); - } - - #[tokio::test] - async fn process_call_pec() { - let word: u16 = 0x0102; - let resp_lo = 0xAAu8; - let resp_hi = 0xBBu8; - let mut hasher = Pec::new(); - hasher.write_u8(write_address_byte(ADDR)); - hasher.write_u8(REG); - hasher.write(&word.to_le_bytes()); - hasher.write_u8(read_address_byte(ADDR)); - hasher.write(&[resp_lo, resp_hi]); - let pec = hasher.finish() as u8; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::write(ADDR, word.to_le_bytes().to_vec()), - Tx::read(ADDR, std::vec![resp_lo, resp_hi, pec]), - Tx::transaction_end(ADDR), - ]); - let r = bus.process_call_with_pec(ADDR, REG, word).await.unwrap(); - assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); - done(bus); - } - - // ---------- block_write ---------- - - #[tokio::test] - async fn block_write_no_pec() { - let data = [0xDE, 0xAD, 0xBE, 0xEF]; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::write(ADDR, std::vec![data.len() as u8]), - Tx::write(ADDR, data.to_vec()), - Tx::transaction_end(ADDR), - ]); - bus.block_write(ADDR, REG, &data).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn block_write_pec() { - let data = [0xDE, 0xAD, 0xBE, 0xEF]; - let pec = expected_pec(&[&[write_address_byte(ADDR), REG, data.len() as u8], &data]); - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::write(ADDR, std::vec![data.len() as u8]), - Tx::write(ADDR, data.to_vec()), - Tx::write(ADDR, std::vec![pec]), - Tx::transaction_end(ADDR), - ]); - bus.block_write_with_pec(ADDR, REG, &data).await.unwrap(); - done(bus); - } - - #[tokio::test] - async fn block_write_too_large() { - let mut bus = new_bus(&[]); - let data = std::vec![0u8; MAX_BLOCK_SIZE + 1]; - let err = bus.block_write(ADDR, REG, &data).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); - done(bus); - } - - // ---------- block_read ---------- - - #[tokio::test] - async fn block_read_no_pec() { - let mut buf = [0u8; 3]; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![3]), - Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), - Tx::transaction_end(ADDR), - ]); - bus.block_read(ADDR, REG, &mut buf).await.unwrap(); - assert_eq!(buf, [0x10, 0x20, 0x30]); - done(bus); - } - - #[tokio::test] - async fn block_read_pec() { - let payload = [0x10u8, 0x20, 0x30]; - let len = payload.len() as u8; - // PEC source matches the implementation: addr+W, reg, addr+R, then msg_size, then data. - let mut hasher = Pec::new(); - hasher.write_u8(write_address_byte(ADDR)); - hasher.write_u8(REG); - hasher.write_u8(read_address_byte(ADDR)); - hasher.write(&[len]); - hasher.write(&payload); - let pec = hasher.finish() as u8; - let mut buf = [0u8; 3]; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![len]), - Tx::read(ADDR, payload.to_vec()), - Tx::read(ADDR, std::vec![pec]), - Tx::transaction_end(ADDR), - ]); - bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap(); - assert_eq!(buf, payload); - done(bus); - } - - #[tokio::test] - async fn block_read_too_large() { - let mut bus = new_bus(&[]); - let mut buf = std::vec![0u8; MAX_BLOCK_SIZE + 1]; - let err = bus.block_read(ADDR, REG, &mut buf).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); - done(bus); - } - - #[tokio::test] - async fn block_read_size_mismatch_no_pec() { - // Device reports `2` but the caller expected `3`. - let mut buf = [0u8; 3]; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![2]), - Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), - Tx::transaction_end(ADDR), - ]); - let err = bus.block_read(ADDR, REG, &mut buf).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); - done(bus); - } - - #[tokio::test] - async fn block_read_size_mismatch_pec() { - // Device reports `2` but the caller expected `3`. The mismatch must be - // reported as `BlockSizeMismatch` rather than `Pec`, even though the - // received PEC byte would not match either. - let mut buf = [0u8; 3]; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![2]), - Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), - Tx::read(ADDR, std::vec![0x00]), - Tx::transaction_end(ADDR), - ]); - let err = bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); - done(bus); - } - - // ---------- block_write_block_read_process_call ---------- - - #[tokio::test] - async fn bwbr_no_pec() { - let write_data = [0x01u8, 0x02]; - let read_payload = [0xAAu8, 0xBB]; - let mut read_buf = [0u8; 2]; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::write(ADDR, std::vec![write_data.len() as u8]), - Tx::write(ADDR, write_data.to_vec()), - Tx::read(ADDR, std::vec![read_payload.len() as u8]), - Tx::read(ADDR, read_payload.to_vec()), - Tx::transaction_end(ADDR), - ]); - bus.block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) - .await - .unwrap(); - assert_eq!(read_buf, read_payload); - done(bus); - } - - #[tokio::test] - async fn bwbr_pec() { - let write_data = [0x01u8, 0x02]; - let read_payload = [0xAAu8, 0xBB]; - let mut read_buf = [0u8; 2]; - let mut hasher = Pec::new(); - hasher.write_u8(write_address_byte(ADDR)); - hasher.write_u8(REG); - hasher.write_u8(write_data.len() as u8); - hasher.write(&write_data); - hasher.write_u8(read_address_byte(ADDR)); - hasher.write(&[read_payload.len() as u8]); - hasher.write(&read_payload); - let pec = hasher.finish() as u8; - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::write(ADDR, std::vec![write_data.len() as u8]), - Tx::write(ADDR, write_data.to_vec()), - Tx::read(ADDR, std::vec![read_payload.len() as u8]), - Tx::read(ADDR, read_payload.to_vec()), - Tx::read(ADDR, std::vec![pec]), - Tx::transaction_end(ADDR), - ]); - bus.block_write_block_read_process_call_with_pec(ADDR, REG, &write_data, &mut read_buf) - .await - .unwrap(); - assert_eq!(read_buf, read_payload); - done(bus); - } - - #[tokio::test] - async fn bwbr_too_large() { - let mut bus = new_bus(&[]); - let write_data = std::vec![0u8; 200]; - let mut read_buf = std::vec![0u8; 60]; // 200 + 60 > 255 - let err = bus - .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) - .await - .unwrap_err(); - assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); - done(bus); - } - - #[tokio::test] - async fn bwbr_size_mismatch_no_pec() { - let write_data = [0x01u8, 0x02]; - let mut read_buf = [0u8; 2]; - // Device returns count `1` but caller expected `2`. - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::write(ADDR, std::vec![write_data.len() as u8]), - Tx::write(ADDR, write_data.to_vec()), - Tx::read(ADDR, std::vec![1]), - Tx::read(ADDR, std::vec![0xAA, 0xBB]), - Tx::transaction_end(ADDR), - ]); - let err = bus - .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) - .await - .unwrap_err(); - assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); - done(bus); - } - - #[tokio::test] - async fn bwbr_size_mismatch_pec() { - let write_data = [0x01u8, 0x02]; - let mut read_buf = [0u8; 2]; - // Device returns count `1` but caller expected `2`. Must be reported - // as `BlockSizeMismatch` rather than `Pec`. - let mut bus = new_bus(&[ - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::write(ADDR, std::vec![write_data.len() as u8]), - Tx::write(ADDR, write_data.to_vec()), - Tx::read(ADDR, std::vec![1]), - Tx::read(ADDR, std::vec![0xAA, 0xBB]), - Tx::read(ADDR, std::vec![0x00]), - Tx::transaction_end(ADDR), - ]); - let err = bus - .block_write_block_read_process_call_with_pec(ADDR, REG, &write_data, &mut read_buf) - .await - .unwrap_err(); - assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); - done(bus); - } - - // ---------- PEC unavailable ---------- - - fn new_no_pec_bus(expectations: &[Tx]) -> NoPecBus { - super::SwSmbusI2c::new(I2cMock::new(expectations)) - } - - fn done_no_pec(mut bus: NoPecBus) { - bus.inner_mut().done(); - } - - #[test] - fn no_pec_bus_get_pec_calc_returns_none() { - assert!(NoPecBus::get_pec_calc().is_none()); - } - - #[tokio::test] - async fn no_pec_bus_non_pec_ops_still_work() { - // All `use_pec = false` paths must succeed even though `get_pec_calc` - // returns `None`: the trait must not consult the PEC calculator unless - // PEC was actually requested. - let mut bus = new_no_pec_bus(&[ - Tx::write(ADDR, std::vec![0x55]), - Tx::read(ADDR, std::vec![0x99]), - Tx::write(ADDR, std::vec![REG, 0x33]), - Tx::transaction_start(ADDR), - Tx::write(ADDR, std::vec![REG]), - Tx::read(ADDR, std::vec![0x77]), - Tx::transaction_end(ADDR), - ]); - bus.send_byte(ADDR, 0x55).await.unwrap(); - assert_eq!(bus.receive_byte(ADDR).await.unwrap(), 0x99); - bus.write_byte(ADDR, REG, 0x33).await.unwrap(); - assert_eq!(bus.read_byte(ADDR, REG).await.unwrap(), 0x77); - done_no_pec(bus); - } - - #[tokio::test] - async fn pec_unavailable_returns_pec_error() { - let mut bus = super::SwSmbusI2c::::new(I2cMock::new(&[])); - // Any PEC-requiring path should fail without touching the bus. - let err = bus.send_byte_with_pec(ADDR, 0x55).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.receive_byte_with_pec(ADDR).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.read_byte_with_pec(ADDR, REG).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.read_word_with_pec(ADDR, REG).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.process_call_with_pec(ADDR, REG, 0).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus.block_write_with_pec(ADDR, REG, &[1, 2]).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - let mut rb = [0u8; 2]; - let err = bus.block_read_with_pec(ADDR, REG, &mut rb).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - let err = bus - .block_write_block_read_process_call_with_pec(ADDR, REG, &[1], &mut rb) - .await - .unwrap_err(); - assert_eq!(err.kind(), ErrorKind::Pec); - bus.inner_mut().done(); - } - - // ---------- &mut T forwarding ---------- - - #[tokio::test] - async fn mut_ref_smbus_forwards() { - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); - let r: &mut TestBus = &mut bus; - r.send_byte(ADDR, 0x55).await.unwrap(); - assert!(<&mut TestBus as Smbus>::get_pec_calc().is_some()); - done(bus); - } - - // ---------- error propagation from underlying I2C ---------- - - #[tokio::test] - async fn i2c_error_propagates() { - let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55]).with_error(I2cErrorKind::Bus)]); - let err = bus.send_byte(ADDR, 0x55).await.unwrap_err(); - assert_eq!(err.kind(), ErrorKind::I2c(I2cErrorKind::Bus)); - done(bus); - } -} +mod tests; diff --git a/embedded-mcu-hal/src/smbus/bus/asynch/tests.rs b/embedded-mcu-hal/src/smbus/bus/asynch/tests.rs new file mode 100644 index 0000000..001a8a1 --- /dev/null +++ b/embedded-mcu-hal/src/smbus/bus/asynch/tests.rs @@ -0,0 +1,704 @@ +use super::Smbus; +use crate::smbus::bus::{ + read_address_byte, write_address_byte, Error as SmbusError, ErrorKind, MAX_BLOCK_SIZE, READ_BIT, +}; +use core::hash::Hasher; +use embedded_hal_async::i2c::ErrorKind as I2cErrorKind; +use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as Tx}; +use smbus_pec::{pec, Pec}; + +const ADDR: u8 = 0x42; +const REG: u8 = 0x07; + +/// Compute the expected SMBus PEC byte over a flat concatenation of byte +/// slices, using the `smbus-pec` crate as the reference implementation. +fn expected_pec(parts: &[&[u8]]) -> u8 { + let mut buf: std::vec::Vec = std::vec::Vec::new(); + for p in parts { + buf.extend_from_slice(p); + } + pec(&buf) +} + +/// PEC provider that exposes the `smbus-pec` calculator. +struct TestPec; +impl super::PecProvider for TestPec { + type Calc = Pec; + fn new_calc() -> Option { + Some(Pec::new()) + } +} + +/// PEC provider that reports PEC as unavailable. +struct NoPec; +impl super::PecProvider for NoPec { + type Calc = Pec; + fn new_calc() -> Option { + None + } +} + +type TestBus = super::SwSmbusI2c; +type NoPecBus = super::SwSmbusI2c; + +fn new_bus(expectations: &[Tx]) -> TestBus { + super::SwSmbusI2c::new(I2cMock::new(expectations)) +} + +fn done(mut bus: TestBus) { + bus.inner_mut().done(); +} + +// ---------- constants / helpers ---------- + +#[test] +fn constants() { + assert_eq!(MAX_BLOCK_SIZE, 255); + assert_eq!(READ_BIT, 0x01); + assert_eq!(write_address_byte(0x42), 0x84); + assert_eq!(read_address_byte(0x42), 0x85); +} + +#[test] +fn error_kind_display_and_kind() { + let k = ErrorKind::Timeout; + assert_eq!(k.kind(), ErrorKind::Timeout); + // Display impls cover all branches. + for k in [ + ErrorKind::I2c(I2cErrorKind::Bus), + ErrorKind::Timeout, + ErrorKind::Pec, + ErrorKind::TooLargeBlockTransaction, + ErrorKind::BlockSizeMismatch, + ErrorKind::Other, + ] { + let s = std::format!("{}", k); + assert!(!s.is_empty()); + } +} + +#[test] +fn error_kind_from_i2c_error_kind() { + let k: ErrorKind = I2cErrorKind::Bus.into(); + assert_eq!(k, ErrorKind::I2c(I2cErrorKind::Bus)); +} + +#[test] +fn infallible_error_to_kind_round_trip() { + // Infallible cannot be constructed; we only check the trait wires up. + fn _accepts(_e: &E) {} + let k = ErrorKind::Pec; + _accepts(&k); +} + +#[tokio::test] +async fn check_pec_match() { + let bus = new_bus(&[]); + TestBus::check_pec(0x42, 0x42).unwrap(); + TestBus::check_pec(0x00, 0x00).unwrap(); + done(bus); +} + +#[tokio::test] +async fn check_pec_mismatch() { + let bus = new_bus(&[]); + let err = TestBus::check_pec(0x42, 0x43).unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); +} + +// ---------- write_buf / read_buf (low-level) ---------- + +#[tokio::test] +async fn write_buf_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0xAB, 0xCD])]); + bus.write_buf(ADDR, false, &mut [0xAB, 0xCD]).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn write_buf_pec() { + let payload = [0xAB, 0xCD]; + let pec = expected_pec(&[&[write_address_byte(ADDR)], &payload]); + let mut buf = [0xAB, 0xCD, 0x00]; + let mut wire = std::vec![0xAB, 0xCD, pec]; + let mut bus = new_bus(&[Tx::write(ADDR, wire.clone())]); + bus.write_buf(ADDR, true, &mut buf).await.unwrap(); + // Last byte should now be the PEC. + assert_eq!(buf[2], pec); + wire.clear(); + done(bus); +} + +#[tokio::test] +async fn read_buf_no_pec() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x11, 0x22])]); + let mut buf = [0u8; 2]; + bus.read_buf(ADDR, false, &mut buf).await.unwrap(); + assert_eq!(buf, [0x11, 0x22]); + done(bus); +} + +#[tokio::test] +async fn read_buf_pec() { + let data = 0x11u8; + let pec = expected_pec(&[&[read_address_byte(ADDR)], &[data]]); + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); + let mut buf = [0u8; 2]; + bus.read_buf(ADDR, true, &mut buf).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn read_buf_pec_mismatch() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x11, 0xFF])]); // wrong PEC + let mut buf = [0u8; 2]; + let err = bus.read_buf(ADDR, true, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); +} + +// ---------- quick_command ---------- + +#[tokio::test] +async fn quick_command_write() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![]), + Tx::transaction_end(ADDR), + ]); + bus.quick_command(ADDR, false).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn quick_command_read() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::read(ADDR, std::vec![]), + Tx::transaction_end(ADDR), + ]); + bus.quick_command(ADDR, true).await.unwrap(); + done(bus); +} + +// ---------- send_byte / receive_byte ---------- + +#[tokio::test] +async fn send_byte_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); + bus.send_byte(ADDR, 0x55).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn send_byte_pec() { + let pec = expected_pec(&[&[write_address_byte(ADDR), 0x55]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55, pec])]); + bus.send_byte_with_pec(ADDR, 0x55).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn receive_byte_no_pec() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99])]); + let b = bus.receive_byte(ADDR).await.unwrap(); + assert_eq!(b, 0x99); + done(bus); +} + +#[tokio::test] +async fn receive_byte_pec() { + let data = 0x99u8; + let pec = expected_pec(&[&[read_address_byte(ADDR), data]]); + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![data, pec])]); + let b = bus.receive_byte_with_pec(ADDR).await.unwrap(); + assert_eq!(b, data); + done(bus); +} + +#[tokio::test] +async fn receive_byte_pec_mismatch() { + let mut bus = new_bus(&[Tx::read(ADDR, std::vec![0x99, 0xFF])]); + let err = bus.receive_byte_with_pec(ADDR).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); +} + +// ---------- write_byte / write_word ---------- + +#[tokio::test] +async fn write_byte_no_pec() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33])]); + bus.write_byte(ADDR, REG, 0x33).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn write_byte_pec() { + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, 0x33]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, 0x33, pec])]); + bus.write_byte_with_pec(ADDR, REG, 0x33).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn write_word_no_pec() { + let word: u16 = 0xBEEF; + let bytes = word.to_le_bytes(); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1]])]); + bus.write_word(ADDR, REG, word).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn write_word_pec() { + let word: u16 = 0xBEEF; + let bytes = word.to_le_bytes(); + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, bytes[0], bytes[1]]]); + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![REG, bytes[0], bytes[1], pec])]); + bus.write_word_with_pec(ADDR, REG, word).await.unwrap(); + done(bus); +} + +// ---------- read_byte / read_word ---------- + +#[tokio::test] +async fn read_byte_no_pec() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77]), + Tx::transaction_end(ADDR), + ]); + let b = bus.read_byte(ADDR, REG).await.unwrap(); + assert_eq!(b, 0x77); + done(bus); +} + +#[tokio::test] +async fn read_byte_pec() { + let data = 0x77u8; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, read_address_byte(ADDR), data]]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![data, pec]), + Tx::transaction_end(ADDR), + ]); + let b = bus.read_byte_with_pec(ADDR, REG).await.unwrap(); + assert_eq!(b, data); + done(bus); +} + +#[tokio::test] +async fn read_byte_pec_mismatch() { + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77, 0xFF]), + Tx::transaction_end(ADDR), + ]); + let err = bus.read_byte_with_pec(ADDR, REG).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + done(bus); +} + +#[tokio::test] +async fn read_word_no_pec() { + let lo = 0x12u8; + let hi = 0x34u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![lo, hi]), + Tx::transaction_end(ADDR), + ]); + let w = bus.read_word(ADDR, REG).await.unwrap(); + assert_eq!(w, u16::from_le_bytes([lo, hi])); + done(bus); +} + +#[tokio::test] +async fn read_word_pec() { + let lo = 0x12u8; + let hi = 0x34u8; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, read_address_byte(ADDR), lo, hi]]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![lo, hi, pec]), + Tx::transaction_end(ADDR), + ]); + let w = bus.read_word_with_pec(ADDR, REG).await.unwrap(); + assert_eq!(w, u16::from_le_bytes([lo, hi])); + done(bus); +} + +// ---------- process_call ---------- + +#[tokio::test] +async fn process_call_no_pec() { + let word: u16 = 0x0102; + let resp_lo = 0xAAu8; + let resp_hi = 0xBBu8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, word.to_le_bytes().to_vec()), + Tx::read(ADDR, std::vec![resp_lo, resp_hi]), + Tx::transaction_end(ADDR), + ]); + let r = bus.process_call(ADDR, REG, word).await.unwrap(); + assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); + done(bus); +} + +#[tokio::test] +async fn process_call_pec() { + let word: u16 = 0x0102; + let resp_lo = 0xAAu8; + let resp_hi = 0xBBu8; + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write(&word.to_le_bytes()); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[resp_lo, resp_hi]); + let pec = hasher.finish() as u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, word.to_le_bytes().to_vec()), + Tx::read(ADDR, std::vec![resp_lo, resp_hi, pec]), + Tx::transaction_end(ADDR), + ]); + let r = bus.process_call_with_pec(ADDR, REG, word).await.unwrap(); + assert_eq!(r, u16::from_le_bytes([resp_lo, resp_hi])); + done(bus); +} + +// ---------- block_write ---------- + +#[tokio::test] +async fn block_write_no_pec() { + let data = [0xDE, 0xAD, 0xBE, 0xEF]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![data.len() as u8]), + Tx::write(ADDR, data.to_vec()), + Tx::transaction_end(ADDR), + ]); + bus.block_write(ADDR, REG, &data).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn block_write_pec() { + let data = [0xDE, 0xAD, 0xBE, 0xEF]; + let pec = expected_pec(&[&[write_address_byte(ADDR), REG, data.len() as u8], &data]); + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![data.len() as u8]), + Tx::write(ADDR, data.to_vec()), + Tx::write(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_write_with_pec(ADDR, REG, &data).await.unwrap(); + done(bus); +} + +#[tokio::test] +async fn block_write_too_large() { + let mut bus = new_bus(&[]); + let data = std::vec![0u8; MAX_BLOCK_SIZE + 1]; + let err = bus.block_write(ADDR, REG, &data).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +// ---------- block_read ---------- + +#[tokio::test] +async fn block_read_no_pec() { + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![3]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::transaction_end(ADDR), + ]); + bus.block_read(ADDR, REG, &mut buf).await.unwrap(); + assert_eq!(buf, [0x10, 0x20, 0x30]); + done(bus); +} + +#[tokio::test] +async fn block_read_pec() { + let payload = [0x10u8, 0x20, 0x30]; + let len = payload.len() as u8; + // PEC source matches the implementation: addr+W, reg, addr+R, then msg_size, then data. + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[len]); + hasher.write(&payload); + let pec = hasher.finish() as u8; + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![len]), + Tx::read(ADDR, payload.to_vec()), + Tx::read(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap(); + assert_eq!(buf, payload); + done(bus); +} + +#[tokio::test] +async fn block_read_too_large() { + let mut bus = new_bus(&[]); + let mut buf = std::vec![0u8; MAX_BLOCK_SIZE + 1]; + let err = bus.block_read(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +#[tokio::test] +async fn block_read_size_mismatch_no_pec() { + // Device reports `2` but the caller expected `3`. + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![2]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::transaction_end(ADDR), + ]); + let err = bus.block_read(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); +} + +#[tokio::test] +async fn block_read_size_mismatch_pec() { + // Device reports `2` but the caller expected `3`. The mismatch must be + // reported as `BlockSizeMismatch` rather than `Pec`, even though the + // received PEC byte would not match either. + let mut buf = [0u8; 3]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![2]), + Tx::read(ADDR, std::vec![0x10, 0x20, 0x30]), + Tx::read(ADDR, std::vec![0x00]), + Tx::transaction_end(ADDR), + ]); + let err = bus.block_read_with_pec(ADDR, REG, &mut buf).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); +} + +// ---------- block_write_block_read_process_call ---------- + +#[tokio::test] +async fn bwbr_no_pec() { + let write_data = [0x01u8, 0x02]; + let read_payload = [0xAAu8, 0xBB]; + let mut read_buf = [0u8; 2]; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![read_payload.len() as u8]), + Tx::read(ADDR, read_payload.to_vec()), + Tx::transaction_end(ADDR), + ]); + bus.block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap(); + assert_eq!(read_buf, read_payload); + done(bus); +} + +#[tokio::test] +async fn bwbr_pec() { + let write_data = [0x01u8, 0x02]; + let read_payload = [0xAAu8, 0xBB]; + let mut read_buf = [0u8; 2]; + let mut hasher = Pec::new(); + hasher.write_u8(write_address_byte(ADDR)); + hasher.write_u8(REG); + hasher.write_u8(write_data.len() as u8); + hasher.write(&write_data); + hasher.write_u8(read_address_byte(ADDR)); + hasher.write(&[read_payload.len() as u8]); + hasher.write(&read_payload); + let pec = hasher.finish() as u8; + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![read_payload.len() as u8]), + Tx::read(ADDR, read_payload.to_vec()), + Tx::read(ADDR, std::vec![pec]), + Tx::transaction_end(ADDR), + ]); + bus.block_write_block_read_process_call_with_pec(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap(); + assert_eq!(read_buf, read_payload); + done(bus); +} + +#[tokio::test] +async fn bwbr_too_large() { + let mut bus = new_bus(&[]); + let write_data = std::vec![0u8; 200]; + let mut read_buf = std::vec![0u8; 60]; // 200 + 60 > 255 + let err = bus + .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::TooLargeBlockTransaction); + done(bus); +} + +#[tokio::test] +async fn bwbr_size_mismatch_no_pec() { + let write_data = [0x01u8, 0x02]; + let mut read_buf = [0u8; 2]; + // Device returns count `1` but caller expected `2`. + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![1]), + Tx::read(ADDR, std::vec![0xAA, 0xBB]), + Tx::transaction_end(ADDR), + ]); + let err = bus + .block_write_block_read_process_call(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); +} + +#[tokio::test] +async fn bwbr_size_mismatch_pec() { + let write_data = [0x01u8, 0x02]; + let mut read_buf = [0u8; 2]; + // Device returns count `1` but caller expected `2`. Must be reported + // as `BlockSizeMismatch` rather than `Pec`. + let mut bus = new_bus(&[ + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::write(ADDR, std::vec![write_data.len() as u8]), + Tx::write(ADDR, write_data.to_vec()), + Tx::read(ADDR, std::vec![1]), + Tx::read(ADDR, std::vec![0xAA, 0xBB]), + Tx::read(ADDR, std::vec![0x00]), + Tx::transaction_end(ADDR), + ]); + let err = bus + .block_write_block_read_process_call_with_pec(ADDR, REG, &write_data, &mut read_buf) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::BlockSizeMismatch); + done(bus); +} + +// ---------- PEC unavailable ---------- + +fn new_no_pec_bus(expectations: &[Tx]) -> NoPecBus { + super::SwSmbusI2c::new(I2cMock::new(expectations)) +} + +fn done_no_pec(mut bus: NoPecBus) { + bus.inner_mut().done(); +} + +#[test] +fn no_pec_bus_get_pec_calc_returns_none() { + assert!(NoPecBus::get_pec_calc().is_none()); +} + +#[tokio::test] +async fn no_pec_bus_non_pec_ops_still_work() { + // All `use_pec = false` paths must succeed even though `get_pec_calc` + // returns `None`: the trait must not consult the PEC calculator unless + // PEC was actually requested. + let mut bus = new_no_pec_bus(&[ + Tx::write(ADDR, std::vec![0x55]), + Tx::read(ADDR, std::vec![0x99]), + Tx::write(ADDR, std::vec![REG, 0x33]), + Tx::transaction_start(ADDR), + Tx::write(ADDR, std::vec![REG]), + Tx::read(ADDR, std::vec![0x77]), + Tx::transaction_end(ADDR), + ]); + bus.send_byte(ADDR, 0x55).await.unwrap(); + assert_eq!(bus.receive_byte(ADDR).await.unwrap(), 0x99); + bus.write_byte(ADDR, REG, 0x33).await.unwrap(); + assert_eq!(bus.read_byte(ADDR, REG).await.unwrap(), 0x77); + done_no_pec(bus); +} + +#[tokio::test] +async fn pec_unavailable_returns_pec_error() { + let mut bus = super::SwSmbusI2c::::new(I2cMock::new(&[])); + // Any PEC-requiring path should fail without touching the bus. + let err = bus.send_byte_with_pec(ADDR, 0x55).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.receive_byte_with_pec(ADDR).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.read_byte_with_pec(ADDR, REG).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.read_word_with_pec(ADDR, REG).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.process_call_with_pec(ADDR, REG, 0).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus.block_write_with_pec(ADDR, REG, &[1, 2]).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let mut rb = [0u8; 2]; + let err = bus.block_read_with_pec(ADDR, REG, &mut rb).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + let err = bus + .block_write_block_read_process_call_with_pec(ADDR, REG, &[1], &mut rb) + .await + .unwrap_err(); + assert_eq!(err.kind(), ErrorKind::Pec); + bus.inner_mut().done(); +} + +// ---------- &mut T forwarding ---------- + +#[tokio::test] +async fn mut_ref_smbus_forwards() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55])]); + let r: &mut TestBus = &mut bus; + r.send_byte(ADDR, 0x55).await.unwrap(); + assert!(<&mut TestBus as Smbus>::get_pec_calc().is_some()); + done(bus); +} + +// ---------- error propagation from underlying I2C ---------- + +#[tokio::test] +async fn i2c_error_propagates() { + let mut bus = new_bus(&[Tx::write(ADDR, std::vec![0x55]).with_error(I2cErrorKind::Bus)]); + let err = bus.send_byte(ADDR, 0x55).await.unwrap_err(); + assert_eq!(err.kind(), ErrorKind::I2c(I2cErrorKind::Bus)); + done(bus); +}