blob: 3eeafea1be0147ff6313919f7a7f23223e20ba96 [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! Client for the i2c userspace driver.
//!
//! `I2cClient<T>` exposes itself **only** as an implementation of
//! `embedded_hal::i2c::I2c` — consumers depend on that abstract seam, never on
//! the transport. All wire marshalling lives here and is generic over
//! [`Transport`]: the *same* encode/decode code runs in production
//! (`IpcTransport`, cross-process) and in host tests (`LoopbackTransport`,
//! in-process against a mock bus). One `transaction()` call serializes the
//! whole address + operation list, performs exactly one `Transport::transact`,
//! and scatters the read results back into the caller's slices: one seam call
//! ⇒ one round-trip ⇒ one server-side run-to-completion.
//!
//! This crate has **no kernel/IPC dependency** and builds on the host — that
//! is what makes the encoders/decoders testable without a kernel.
#![no_std]
use i2c_api::seam::{error_kind, ErrorKind, ErrorType, I2c, Operation, SevenBitAddress};
use i2c_api::{
I2cError, I2cOp, I2cOpDesc, I2cOpKind, I2cRequestHeader, I2cResponseHeader, SlaveEvent,
Transport, TransportError, MAX_OPS, MAX_PAYLOAD_SIZE,
};
// One IPC message fits in a single 512-byte channel buffer on the server side.
// Raising this requires a matching change to the server's receive buffer.
const MAX_BUF_SIZE: usize = 512;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientError {
Transport(TransportError),
ServerError(I2cError),
InvalidResponse,
/// Request or response would exceed `MAX_BUF_SIZE` / `MAX_PAYLOAD_SIZE`.
/// The whole transaction must fit one round-trip — it is never fragmented.
BufferTooSmall,
TooManyOperations,
}
/// Result of a slave-receive operation with event metadata.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlaveReceiveEvent {
/// Kind of event that triggered this receive (DataReceived, ReadRequest, Stop).
pub kind: SlaveEvent,
/// Source I2C address (7-bit) of the master that wrote to us.
/// `None` if the hardware did not capture it.
pub source_address: Option<SevenBitAddress>,
/// Number of data bytes in the buffer.
pub data_len: usize,
/// True if the latched buffer exceeded `buf` and was truncated.
pub truncated: bool,
}
impl core::fmt::Display for ClientError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Transport(e) => write!(f, "i2c transport error: {e}"),
Self::ServerError(e) => write!(f, "i2c server error: {e}"),
Self::InvalidResponse => f.write_str("malformed i2c response"),
Self::BufferTooSmall => f.write_str("transaction exceeds one round-trip buffer"),
Self::TooManyOperations => f.write_str("too many operations in one transaction"),
}
}
}
impl core::error::Error for ClientError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Transport(e) => Some(e),
Self::ServerError(e) => Some(e),
_ => None,
}
}
}
impl From<TransportError> for ClientError {
fn from(e: TransportError) -> Self {
Self::Transport(e)
}
}
// Lets a consumer treat `ClientError` through the embedded-hal error taxonomy
// without knowing it came from a userspace driver client.
impl i2c_api::seam::I2cBusError for ClientError {
fn kind(&self) -> ErrorKind {
match self {
ClientError::ServerError(e) => error_kind(*e),
_ => ErrorKind::Other,
}
}
}
/// An i2c client bound to one bus, speaking the `i2c_api` wire protocol over
/// any [`Transport`]. Implements `embedded_hal::i2c::I2c` and nothing else
/// publicly — the transport is invisible to consumers.
pub struct I2cClient<T: Transport> {
transport: T,
}
impl<T: Transport> I2cClient<T> {
/// Create a client bound to `transport`.
pub const fn new(transport: T) -> Self {
Self { transport }
}
// ---- Target/slave mode (thin notification slice) ----
//
// One IPC channel per bus, so none of these carry a bus argument — the
// bus is the channel this client is bound to. Same Transport, same
// marshalling discipline as master: one whole request, one round-trip.
/// Send a header-only slave-control op; returns the response payload
/// length (0 for ack-only ops). `out` receives `SlaveReceive` data.
fn slave_cmd(
&mut self,
op: I2cOp,
address: u16,
max_len: u16,
out: Option<&mut [u8]>,
) -> Result<usize, ClientError> {
let hdr = I2cRequestHeader::new(op, address, max_len, 0);
let mut req = [0u8; I2cRequestHeader::SIZE];
req.copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr));
let mut resp = [0u8; I2cResponseHeader::SIZE + MAX_PAYLOAD_SIZE];
let resp_len = self.transport.transact(&req, &mut resp)?;
if resp_len < I2cResponseHeader::SIZE {
return Err(ClientError::InvalidResponse);
}
let Some(rhdr) =
zerocopy::Ref::<_, I2cResponseHeader>::from_bytes(&resp[..I2cResponseHeader::SIZE])
.ok()
else {
return Err(ClientError::InvalidResponse);
};
if !rhdr.is_success() {
return Err(ClientError::ServerError(
rhdr.error_code().unwrap_or(I2cError::InternalError),
));
}
let n = rhdr.payload_length();
if resp_len < I2cResponseHeader::SIZE + n {
return Err(ClientError::InvalidResponse);
}
if let Some(buf) = out {
let copy = n.min(buf.len());
buf[..copy]
.copy_from_slice(&resp[I2cResponseHeader::SIZE..I2cResponseHeader::SIZE + copy]);
return Ok(copy);
}
Ok(n)
}
/// Set this bus's slave (target) address.
///
/// # Errors
/// - [`ClientError::Transport`] — the IPC round-trip failed.
/// - [`ClientError::ServerError`] — the server rejected the address.
/// - [`ClientError::InvalidResponse`] — the response was malformed.
pub fn configure_slave(&mut self, address: SevenBitAddress) -> Result<(), ClientError> {
self.slave_cmd(I2cOp::ConfigureSlave, address as u16, 0, None)
.map(|_| ())
}
/// Enter slave mode (start ACKing the configured address).
///
/// # Errors
/// - [`ClientError::Transport`] — the IPC round-trip failed.
/// - [`ClientError::ServerError`] — the server could not enable slave mode.
/// - [`ClientError::InvalidResponse`] — the response was malformed.
pub fn enable_slave(&mut self) -> Result<(), ClientError> {
self.slave_cmd(I2cOp::EnableSlave, 0, 0, None).map(|_| ())
}
/// Leave slave mode.
///
/// # Errors
/// - [`ClientError::Transport`] — the IPC round-trip failed.
/// - [`ClientError::ServerError`] — the server could not disable slave mode.
/// - [`ClientError::InvalidResponse`] — the response was malformed.
pub fn disable_slave(&mut self) -> Result<(), ClientError> {
self.slave_cmd(I2cOp::DisableSlave, 0, 0, None).map(|_| ())
}
/// Arm interrupt-driven slave-RX notification. After this the server
/// raises `Signals::USER` on this bus's channel when data is latched;
/// the consumer then calls [`slave_receive`](Self::slave_receive).
///
/// # Errors
/// - [`ClientError::Transport`] — the IPC round-trip failed.
/// - [`ClientError::ServerError`] — the server could not arm the notification.
/// - [`ClientError::InvalidResponse`] — the response was malformed.
pub fn enable_notification(&mut self) -> Result<(), ClientError> {
self.slave_cmd(I2cOp::EnableSlaveNotification, 0, 0, None)
.map(|_| ())
}
/// Disarm slave-RX notification (also drops any latched buffer).
///
/// # Errors
/// - [`ClientError::Transport`] — the IPC round-trip failed.
/// - [`ClientError::ServerError`] — the server could not disarm the notification.
/// - [`ClientError::InvalidResponse`] — the response was malformed.
pub fn disable_notification(&mut self) -> Result<(), ClientError> {
self.slave_cmd(I2cOp::DisableSlaveNotification, 0, 0, None)
.map(|_| ())
}
/// Fetch the latched slave-RX bytes and metadata into `buf` (non-blocking).
/// Returns event kind, source address, and data length.
/// Call this after a `Signals::USER` wake on the channel.
///
/// Response payload format: [kind (1), source_addr (1), data (0..)]
///
/// # Errors
/// - [`ClientError::Transport`] — the IPC round-trip failed.
/// - [`ClientError::ServerError`]`(`[`I2cError::NoData`]`)` — nothing is latched yet.
/// - [`ClientError::InvalidResponse`] — the response was malformed.
pub fn slave_receive(&mut self, buf: &mut [u8]) -> Result<SlaveReceiveEvent, ClientError> {
let max = (buf.len().saturating_sub(2)).min(MAX_PAYLOAD_SIZE) as u16;
let mut resp = [0u8; I2cResponseHeader::SIZE + MAX_PAYLOAD_SIZE];
let hdr = I2cRequestHeader::new(I2cOp::SlaveReceive, 0, max, 0);
let mut req = [0u8; I2cRequestHeader::SIZE];
req.copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr));
let resp_len = self.transport.transact(&req, &mut resp)?;
if resp_len < I2cResponseHeader::SIZE {
return Err(ClientError::InvalidResponse);
}
let Some(rhdr) =
zerocopy::Ref::<_, I2cResponseHeader>::from_bytes(&resp[..I2cResponseHeader::SIZE])
.ok()
else {
return Err(ClientError::InvalidResponse);
};
if !rhdr.is_success() {
return Err(ClientError::ServerError(
rhdr.error_code().unwrap_or(I2cError::InternalError),
));
}
let payload_len = rhdr.payload_length();
if resp_len < I2cResponseHeader::SIZE + payload_len {
return Err(ClientError::InvalidResponse);
}
// Payload format: [kind (1), source (1), data (0..)]
if payload_len < 2 {
return Err(ClientError::InvalidResponse);
}
let payload_offset = I2cResponseHeader::SIZE;
let kind_byte = resp[payload_offset];
let source_addr = resp[payload_offset + 1];
let data_len = payload_len - 2;
let kind = SlaveEvent::try_from(kind_byte).map_err(|_| ClientError::InvalidResponse)?;
// Copy data into the caller's buffer.
let copy = data_len.min(buf.len());
if copy > 0 {
buf[..copy].copy_from_slice(&resp[payload_offset + 2..payload_offset + 2 + copy]);
}
Ok(SlaveReceiveEvent {
kind,
source_address: if source_addr <= 0x7F {
Some(source_addr)
} else {
None
},
data_len: copy,
truncated: data_len > copy,
})
}
/// Pre-load the slave TX buffer so the hardware can respond immediately
/// when the master reads from our slave address.
///
/// NOTE: not required for MCTP-over-I2C. Provided for testing slave-TX
/// and register-echo patterns only.
///
/// # Errors
/// - [`ClientError::BufferTooSmall`] — `data` exceeds the one round-trip buffer.
/// - [`ClientError::Transport`] — the IPC round-trip failed.
/// - [`ClientError::ServerError`] — the server rejected the TX buffer.
/// - [`ClientError::InvalidResponse`] — the response was malformed.
pub fn slave_set_response(&mut self, data: &[u8]) -> Result<(), ClientError> {
let hdr = I2cRequestHeader::new(I2cOp::SlaveSetResponse, 0, 0, data.len() as u16);
let req_len = I2cRequestHeader::SIZE + data.len();
if req_len > MAX_BUF_SIZE {
return Err(ClientError::BufferTooSmall);
}
let mut req = [0u8; MAX_BUF_SIZE];
req[..I2cRequestHeader::SIZE].copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr));
req[I2cRequestHeader::SIZE..req_len].copy_from_slice(data);
let mut resp = [0u8; I2cResponseHeader::SIZE];
let resp_len = self.transport.transact(&req[..req_len], &mut resp)?;
if resp_len < I2cResponseHeader::SIZE {
return Err(ClientError::InvalidResponse);
}
let Some(rhdr) =
zerocopy::Ref::<_, I2cResponseHeader>::from_bytes(&resp[..I2cResponseHeader::SIZE])
.ok()
else {
return Err(ClientError::InvalidResponse);
};
if rhdr.is_success() {
Ok(())
} else {
Err(ClientError::ServerError(
rhdr.error_code().unwrap_or(I2cError::InternalError),
))
}
}
fn transact(
&mut self,
address: SevenBitAddress,
operations: &mut [Operation<'_>],
) -> Result<(), ClientError> {
if operations.len() > MAX_OPS {
return Err(ClientError::TooManyOperations);
}
// Size the request: header + one descriptor per op + inline write data.
// Bound the read side too — the whole transaction must fit one buffer.
let mut write_total = 0usize;
let mut read_total = 0usize;
for op in operations.iter() {
match op {
Operation::Write(buf) => write_total += buf.len(),
Operation::Read(buf) => read_total += buf.len(),
}
}
let desc_bytes = operations.len() * I2cOpDesc::SIZE;
let req_len = I2cRequestHeader::SIZE + desc_bytes + write_total;
if req_len > MAX_BUF_SIZE
|| write_total > MAX_PAYLOAD_SIZE
|| read_total > MAX_PAYLOAD_SIZE
|| I2cResponseHeader::SIZE + read_total > MAX_BUF_SIZE
{
return Err(ClientError::BufferTooSmall);
}
let mut req = [0u8; MAX_BUF_SIZE];
let mut resp = [0u8; MAX_BUF_SIZE];
let hdr = I2cRequestHeader::new(
I2cOp::Transaction,
address as u16,
operations.len() as u16,
(desc_bytes + write_total) as u16,
);
req[..I2cRequestHeader::SIZE].copy_from_slice(zerocopy::IntoBytes::as_bytes(&hdr));
// Descriptor array, in operation order.
let mut off = I2cRequestHeader::SIZE;
for op in operations.iter() {
let desc = match op {
Operation::Write(buf) => I2cOpDesc::new(I2cOpKind::Write, buf.len() as u16),
Operation::Read(buf) => I2cOpDesc::new(I2cOpKind::Read, buf.len() as u16),
};
req[off..off + I2cOpDesc::SIZE].copy_from_slice(zerocopy::IntoBytes::as_bytes(&desc));
off += I2cOpDesc::SIZE;
}
// Inline write payloads, concatenated in operation order.
for op in operations.iter() {
if let Operation::Write(buf) = op {
req[off..off + buf.len()].copy_from_slice(buf);
off += buf.len();
}
}
let resp_len = self.transport.transact(&req[..off], &mut resp)?;
if resp_len < I2cResponseHeader::SIZE {
return Err(ClientError::InvalidResponse);
}
let rhdr_bytes = &resp[..I2cResponseHeader::SIZE];
let Some(rhdr) = zerocopy::Ref::<_, I2cResponseHeader>::from_bytes(rhdr_bytes).ok() else {
return Err(ClientError::InvalidResponse);
};
if !rhdr.is_success() {
return Err(ClientError::ServerError(
rhdr.error_code().unwrap_or(I2cError::InternalError),
));
}
let payload_len = rhdr.payload_length();
if payload_len != read_total || resp_len < I2cResponseHeader::SIZE + payload_len {
return Err(ClientError::InvalidResponse);
}
// Scatter read results back into the caller's slices, in order.
let mut rp = I2cResponseHeader::SIZE;
for op in operations.iter_mut() {
if let Operation::Read(buf) = op {
let n = buf.len();
buf.copy_from_slice(&resp[rp..rp + n]);
rp += n;
}
}
Ok(())
}
}
impl<T: Transport> ErrorType for I2cClient<T> {
type Error = ClientError;
}
impl<T: Transport> I2c<SevenBitAddress> for I2cClient<T> {
/// Execute one atomic I2C transaction (address + ordered read/write ops).
///
/// # Errors
/// - [`ClientError::TooManyOperations`] — more than `MAX_OPS` ops supplied.
/// - [`ClientError::BufferTooSmall`] — total read or write payload exceeds one round-trip buffer.
/// - [`ClientError::Transport`] — the IPC round-trip failed.
/// - [`ClientError::ServerError`] — the server reported a bus-level error (NACK, timeout, …).
/// - [`ClientError::InvalidResponse`] — the response was malformed or payload length mismatch.
fn transaction(
&mut self,
address: SevenBitAddress,
operations: &mut [Operation<'_>],
) -> Result<(), Self::Error> {
self.transact(address, operations)
}
}