Version: 0.1.0
Date: February 15, 2026
Status: Planning
This document outlines the implementation plan for the I2C Client API as specified in i2c-client-api.md.
Files: services/i2c/api/src/address.rs, services/i2c/api/src/error.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AddressError { OutOfRange(u8), Reserved(u8), }
Display impl for error messagesDebug, Clone, Copy, PartialEq, Eq derives#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct I2cAddress(u8);
Methods:
new(addr: u8) -> Result<Self, AddressError> — validate 0x08-0x77new_unchecked(addr: u8) -> Self — for reserved addressesvalue(self) -> u8 — raw accessorwrite_address(self) -> u8 — (addr << 1) | 0read_address(self) -> u8 — (addr << 1) | 1TryFrom<u8> implUnit tests:
#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct I2cError { pub code: ResponseCode, pub kind: Option<I2cErrorKind>, }
impl embedded_hal::i2c::Error with kind() mappingimpl Display for human-readable messagesfrom_code(), from_kind()#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum ResponseCode { Success = 0, NoDevice = 1, NackData = 2, ArbitrationLost = 3, BusStuck = 4, Timeout = 5, InvalidBus = 6, InvalidAddress = 7, BufferTooSmall = 8, BufferTooLarge = 9, NotInitialized = 10, Busy = 11, Unauthorized = 12, IoError = 13, ServerError = 14, }
from_u8(u8) -> Option<Self> for wire decoding#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum I2cErrorKind { Bus, ArbitrationLoss, NoAcknowledge(NoAcknowledgeSource), Overrun, Other, }
embedded_hal::i2c::NoAcknowledgeSourceFiles: services/i2c/api/src/client.rs, services/i2c/api/src/operation.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct BusIndex(u8);
new(index: u8) -> Selfvalue(self) -> u8BUS_0, BUS_1, BUS_2#[derive(Debug)] pub enum Operation<'a> { Write(&'a [u8]), Read(&'a mut [u8]), }
use embedded_hal::i2c::ErrorType; pub trait I2cClient: ErrorType { fn write_read( &mut self, bus: BusIndex, address: I2cAddress, write: &[u8], read: &mut [u8], ) -> Result<usize, Self::Error>; fn transaction( &mut self, bus: BusIndex, address: I2cAddress, operations: &mut [Operation<'_>], ) -> Result<(), Self::Error>; }
pub trait I2cClientBlocking: I2cClient { fn write(...) -> Result<(), Self::Error>; fn read(...) -> Result<usize, Self::Error>; fn read_register(register: u8, buffer: &mut [u8]) -> Result<usize, Self::Error>; fn write_register(register: u8, value: &[u8]) -> Result<(), Self::Error>; fn probe(...) -> Result<bool, Self::Error>; } impl<T: I2cClient> I2cClientBlocking for T {}
Note: write_register concatenates register + value into single contiguous write.
File: services/i2c/api/src/target.rs
#[derive(Debug, Clone)] pub struct TargetMessage { pub source_address: I2cAddress, data: [u8; 255], len: u8, }
Methods:
data() -> &[u8]len() -> usizeis_empty() -> boolDefault impluse embedded_hal::i2c::ErrorType; pub trait I2cTargetClient: ErrorType { fn configure_target_address( &mut self, bus: BusIndex, address: I2cAddress, ) -> Result<(), Self::Error>; fn enable_receive(&mut self, bus: BusIndex) -> Result<(), Self::Error>; fn disable_receive(&mut self, bus: BusIndex) -> Result<(), Self::Error>; fn wait_for_messages( &mut self, bus: BusIndex, messages: &mut [TargetMessage], timeout: Option<Duration>, ) -> Result<usize, Self::Error>; fn register_notification( &mut self, bus: BusIndex, notification_mask: u32, ) -> Result<(), Self::Error>; fn get_pending_messages( &mut self, bus: BusIndex, messages: &mut [TargetMessage], ) -> Result<usize, Self::Error>; }
Files: services/i2c/api/src/lib.rs, services/i2c/api/Cargo.toml
#![no_std] mod address; mod client; mod error; mod operation; mod target; pub use address::{AddressError, I2cAddress}; pub use client::{BusIndex, I2cClient, I2cClientBlocking}; pub use error::{I2cError, I2cErrorKind, ResponseCode}; pub use operation::Operation; pub use target::{I2cTargetClient, TargetMessage}; // Re-export for convenience pub use embedded_hal::i2c::{ErrorType, NoAcknowledgeSource};
[package] name = "i2c-api" version = "0.1.0" edition = "2021" license = "Apache-2.0" description = "I2C client API traits for OpenPRoT" [dependencies] embedded-hal = "1.0" [features] default = [] std = [] mock = ["std"] [dev-dependencies]
File: services/i2c/api/BUILD.bazel
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") rust_library( name = "i2c_api", srcs = glob(["src/**/*.rs"]), crate_name = "i2c_api", deps = [ "@crates//:embedded-hal", ], visibility = ["//visibility:public"], ) rust_test( name = "i2c_api_test", crate = ":i2c_api", )
File: services/i2c/api/src/mock.rs (feature-gated with mock)
pub struct ExpectedCall { pub bus: BusIndex, pub address: I2cAddress, pub write_data: Vec<u8>, pub response: Vec<u8>, pub result: Result<usize, I2cError>, }
pub struct MockI2cClient { expected_calls: Vec<ExpectedCall>, call_index: usize, } impl embedded_hal::i2c::ErrorType for MockI2cClient { type Error = I2cError; } impl I2cClient for MockI2cClient { // Verify calls match expectations }
Builder API:
new() -> Selfexpect_write_read(...) -> &mut Selfexpect_transaction(...) -> &mut Selfverify(&self) — assert all expected calls were madeDirectory: services/i2c/client/
services/i2c/client/
├── Cargo.toml
├── BUILD.bazel
└── src/
└── lib.rs
pub struct I2cChannelClient { server_channel: pw_channel::Channel, } impl embedded_hal::i2c::ErrorType for I2cChannelClient { type Error = I2cError; } impl I2cClient for I2cChannelClient { // Wire protocol implementation }
Wire protocol:
[op, bus, addr, write_len, write_data..., read_len][status, data...]Dependencies:
i2c-api cratepw_channel (when available)| # | Task | File | Est. | Status |
|---|---|---|---|---|
| 1 | Create address.rs with AddressError, I2cAddress | src/address.rs | 1h | ☐ |
| 2 | Create error.rs with I2cError, ResponseCode, I2cErrorKind | src/error.rs | 1h | ☐ |
| 3 | Create operation.rs with Operation enum | src/operation.rs | 15m | ☐ |
| 4 | Create client.rs with BusIndex, I2cClient, I2cClientBlocking | src/client.rs | 2h | ☐ |
| 5 | Create target.rs with TargetMessage, I2cTargetClient | src/target.rs | 1.5h | ☐ |
| 6 | Create lib.rs with re-exports | src/lib.rs | 15m | ☐ |
| 7 | Create Cargo.toml | Cargo.toml | 15m | ☐ |
| 8 | Create BUILD.bazel | BUILD.bazel | 30m | ☐ |
| 9 | Add unit tests for address validation | src/address.rs | 30m | ☐ |
| 10 | Add unit tests for error conversions | src/error.rs | 30m | ☐ |
| 11 | Create mock.rs with MockI2cClient | src/mock.rs | 1h | ☐ |
| 12 | Create IPC client crate stub | services/i2c/client/ | 1h | ☐ |
Total estimate: ~9 hours
embedded-hal (external)
│
▼
┌─────────┐
│ i2c-api │ ◄── Phases 1-6
└─────────┘
│
▼
┌──────────────┐
│ i2c-client │ ◄── Phase 7 (IPC implementation)
│ (pw_channel) │
└──────────────┘
│
▼
┌──────────────┐
│ Application │ (sensor drivers, MCTP, etc.)
└──────────────┘
cargo build --no-default-features succeeds (no_std)cargo test passes all unit testsbazel build //services/i2c/api:i2c_api succeedsI2cError implements embedded_hal::i2c::ErrorI2cClient uses embedded_hal::i2c::ErrorType as supertraitwrite_register sends single contiguous wire write (verified by mock)Vec, String, or heap allocation in core crate (except mock feature)| Risk | Mitigation |
|---|---|
pw_channel API not finalized | Abstract behind trait; stub implementation |
embedded-hal 1.x breaking changes | Pin version in Cargo.toml, test on update |
| Target mode notification design | Align with existing hal/blocking patterns |
| IPC buffer size limits | Document 255-byte limit; provide direct write/transaction for larger |