docs: add README.md to each MCTP service crate
diff --git a/Cargo.lock b/Cargo.lock index 96d40f5..59b3811 100644 --- a/Cargo.lock +++ b/Cargo.lock
@@ -117,6 +117,21 @@ ] [[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] name = "crypto-bigint" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -201,6 +216,12 @@ ] [[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 = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -236,6 +257,21 @@ ] [[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io", +] + +[[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -288,6 +324,16 @@ [[package]] name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heapless" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1edcd5a338e64688fbdcb7531a846cfd3476a54784dcb918a0844682bc7ada5" @@ -342,6 +388,41 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mctp" +version = "0.2.0" +source = "git+https://github.com/OpenPRoT/mctp-rs.git?branch=sync-features#fafe92f6fcadf6e57683b91757fad9933ddd223d" + +[[package]] +name = "mctp-estack" +version = "0.1.0" +source = "git+https://github.com/OpenPRoT/mctp-rs.git?branch=sync-features#fafe92f6fcadf6e57683b91757fad9933ddd223d" +dependencies = [ + "crc", + "embedded-io", + "embedded-io-async", + "heapless 0.8.0", + "log", + "mctp", + "smbus-pec", + "uuid", +] + +[[package]] +name = "mctp-lib" +version = "0.1.0" +source = "git+https://github.com/9elements/mctp-lib.git?branch=buildup#9c53adaeb7ae544cbeab4654530a50d158e33fe5" +dependencies = [ + "mctp", + "mctp-estack", +] + +[[package]] name = "nb" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -402,6 +483,43 @@ ] [[package]] +name = "openprot-mctp-api" +version = "0.1.0" +dependencies = [ + "heapless 0.9.1", + "zerocopy", +] + +[[package]] +name = "openprot-mctp-client" +version = "0.1.0" +dependencies = [ + "openprot-mctp-api", +] + +[[package]] +name = "openprot-mctp-server" +version = "0.1.0" +dependencies = [ + "heapless 0.9.1", + "mctp", + "mctp-lib", + "openprot-mctp-api", +] + +[[package]] +name = "openprot-mctp-transport-i2c" +version = "0.1.0" +dependencies = [ + "embedded-hal 1.0.0", + "heapless 0.9.1", + "mctp", + "mctp-lib", + "openprot-i2c-api", + "openprot-mctp-api", +] + +[[package]] name = "openprot-platform-impl-hubris" version = "0.1.0" dependencies = [ @@ -474,7 +592,7 @@ "aes-gcm", "cipher", "ctr", - "heapless", + "heapless 0.9.1", "hmac", "k256", "openprot-hal-blocking", @@ -627,6 +745,15 @@ ] [[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 = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -672,6 +799,12 @@ ] [[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" + +[[package]] name = "vcell" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml index 7a4c80e..62b29db 100644 --- a/Cargo.toml +++ b/Cargo.toml
@@ -14,6 +14,10 @@ "platform/impls/hubris", "platform/impls/rustcrypto", "services/i2c/api", + "services/mctp/api", + "services/mctp/server", + "services/mctp/transport-i2c", + "services/mctp/client", "services/telemetry", "services/storage", ]
diff --git a/port-mctp-echo.md b/port-mctp-echo.md new file mode 100644 index 0000000..bba4a31 --- /dev/null +++ b/port-mctp-echo.md
@@ -0,0 +1,124 @@ +# Port Hubris `ast1060-mctp-i2c-echo` to OpenPRoT (Pigweed) + +## Context + +**Source:** `hubris/app/ast1060-mctp-i2c-echo/` +A Hubris RTOS application running MCTP-over-I2C echo on an AST1060 RoT. + +| Task | Crate | Role | +|------|-------|------| +| `jefe` | `task-jefe` | Supervisor / fault manager | +| `idle` | `task-idle` | Idle task | +| `mctp_echo` | `mctp-echo` | Listens for MCTP type-1 requests, echoes payload back | +| `mctp_server` | `mctp-server` | MCTP transport layer (features: `serial_log`, `transport_i2c`) | +| `uart_driver` | `drv-ast1060-uart` | UART peripheral driver | +| `i2c_driver` | `drv-mock-i2c` | Mock I2C driver (feature: `mock-only`) | + +**Target:** `openprot/` +Cargo workspace + Bazel, targeting the **Pigweed kernel** (pw_kernel). Existing services follow `api/` + `server/` + `backend-*/` pattern (see `services/i2c/`). Platform integration is Pigweed-only — the MCTP server will run as a **userspace process** under pw_kernel, not a Hubris task or Linux process. + +## Porting Principle + +**Preserve as much of the original Hubris code as possible.** Only change what is OS-dependent. The MCTP protocol logic, packet handling, server state management, transport encoding/decoding, and application logic (echo) should be carried over as-is. The only parts that change are: + +- **IPC mechanism**: Hubris `idol` / `sys_reply` / `Leased` → Pigweed `pw_kernel` IPC/channels +- **Task/process model**: Hubris `task_slot!` / `sys_recv_open` / notifications → Pigweed userspace process event loop +- **Driver APIs**: Hubris `drv-i2c-api` / `ast1060-uart-api` → OpenPRoT `services/i2c/` userspace driver +- **Build system**: Hubris `app.toml` + `build.rs` code generation → Bazel BUILD files + `system.json5` + +Everything else — the `Server` struct, `Router` integration, `Sender` implementations, handle management, timeout logic, MCTP type definitions — should remain structurally identical to the Hubris originals. + +--- + +## Phase 1: MCTP Service API (`services/mctp/api`) — COMPLETE + +Create the platform-independent MCTP types and traits crate. + +- [x] Create `services/mctp/api/` directory structure +- [x] `Cargo.toml` — `openprot-mctp-api` crate +- [x] `src/lib.rs` — `Handle`, `RecvMetadata` types +- [x] `src/error.rs` — `ResponseCode`, `MctpError` (ported from hubris `mctp-api` `ServerError`) +- [x] `src/traits.rs` — `MctpClient`, `MctpListener`, `MctpReqChannel`, `MctpRespChannel` +- [x] Add to workspace `Cargo.toml` +- [x] Verify `cargo check` passes + +## Phase 2: MCTP Server Core (`services/mctp/server`) — COMPLETE + +Create the platform-independent server logic crate. + +- [x] Create `services/mctp/server/` directory structure +- [x] `Cargo.toml` — `openprot-mctp-server` crate +- [x] `src/lib.rs` + `src/server.rs` — `Server` struct with EID mgmt, pending recv tracking, timeouts +- [x] Add to workspace `Cargo.toml` +- [x] Verify `cargo check` passes +- [x] Integrate `mctp-stack` (`mctp-lib`) `Router` as the packet processing engine +- [x] Re-export `mctp_stack::Sender` trait for transport bindings +- [x] Wire up inbound packet → Router → pending recv fulfillment via `Server::inbound()` + `Server::update()` + +## Phase 3: I2C Transport Binding (`services/mctp/transport-i2c`) — COMPLETE + +Port the I2C transport from hubris `mctp-server/src/i2c.rs`, using the I2C userspace driver at `services/i2c/` as the underlying transport. + +- [x] Create `services/mctp/transport-i2c/` crate +- [x] Implement `Sender` for I2C using the `services/i2c/` userspace driver (client API + IPC to I2C server) +- [x] Implement inbound I2C → MCTP packet decoding (using `mctp-stack::i2c::MctpI2cHandler`) +- [x] Use `I2cTargetClient` from `services/i2c/api` for slave/target mode receive +- [x] Echo integration test with client/server partition (`DirectClient` implementing `MctpClient`, 2 tests passing) + +## Phase 4: Serial Transport Binding (`services/mctp/transport-serial`) — NOT STARTED + +Port the serial transport from hubris `mctp-server/src/serial.rs`. (Lower priority than I2C.) + +- [ ] Create `services/mctp/transport-serial/` crate +- [ ] Implement `Sender` for serial (using `embedded-io::Write`, not hubris `ast1060-uart-api`) +- [ ] Implement inbound serial → MCTP packet decoding (using `mctp-stack::serial::MctpSerialHandler`) + +## Phase 5: MCTP Echo Application — IN PROGRESS + +Port the echo task from hubris `task/mctp-echo/`. + +- [x] IPC wire protocol (`services/mctp/api/src/wire.rs`) — request/response encoding for all MCTP operations +- [x] IPC client (`services/mctp/client/`) — `IpcMctpClient` implementing `MctpClient` via wire protocol + IPC +- [x] Server-side IPC dispatch (`services/mctp/server/src/dispatch.rs`) — decodes wire requests, calls `Server` +- [x] Wire-protocol dispatch integration test (`tests/dispatch.rs`) — full round-trip through wire encoding +- [ ] Echo binary as Pigweed userspace process (needs Phase 6 platform wiring) +- [ ] Wire up with server + I2C transport for an end-to-end demo + +## Phase 6: Pigweed Platform Integration — NOT STARTED + +Wire up the MCTP server as a Pigweed userspace process on the AST1060-EVB (`target/ast1060-evb/`). + +- [ ] MCTP server `main.rs`: event loop driven by pw_kernel IPC/channels + - Replaces hubris `sys_recv_open` / notifications / `idol` IPC dispatch + - Uses `dispatch_mctp_op` for IPC request handling + - Follows the pattern established by `services/i2c/server/` +- [ ] Connect `IpcMctpClient::send_recv` to `syscall::channel_transact` +- [ ] Bazel BUILD files for each new crate (following `services/i2c/` pattern) +- [ ] `system.json5` entry for MCTP server + echo processes +- [ ] Integration with `target/ast1060-evb/` platform definition + +## Phase 7: Testing & Documentation — PARTIALLY DONE + +- [x] Wire protocol unit tests (7 tests in `api/src/wire.rs`) +- [x] Echo integration tests with client/server partition (2 tests in `server/tests/echo.rs`) +- [x] Wire-protocol dispatch integration tests (2 tests in `server/tests/dispatch.rs`) +- [ ] QEMU-based end-to-end test (following `services/i2c/` test pattern) +- [ ] Update `docs/src/specification/middleware/mctp.md` with implementation status +- [ ] README for `services/mctp/` + +--- + +## Current Status + +**Phases 1–3 complete, Phase 5 mostly done.** All library code is written: wire protocol, IPC client, server dispatch, and transport bindings. 11 tests pass. The remaining work is Phase 6 (Pigweed platform integration): server `main.rs`, Bazel BUILD files, and `system.json5`. + +## Key Dependencies + +| Crate | Source | Role | +|-------|--------|------| +| `mctp` | workspace (types crate) | `Eid`, `MsgType`, `Tag`, `Error` etc. | +| `mctp-stack` / `mctp-lib` | `github.com/9elements/mctp-lib` branch `buildup` | `Router`, `Sender`, fragmentation, serial/I2C handlers | +| `services/i2c/` | I2C userspace driver | I2C client/target/server — MCTP transport-i2c uses this as its underlying I2C transport | +| `heapless` | workspace | `no_std` collections | +| `zerocopy` | workspace | Zero-copy serialization | +| Pigweed (`pw_kernel`) | Bazel via `MODULE.bazel` | Userspace processes, IPC channels, system image |
diff --git a/services/mctp/api/Cargo.toml b/services/mctp/api/Cargo.toml new file mode 100644 index 0000000..7f3d297 --- /dev/null +++ b/services/mctp/api/Cargo.toml
@@ -0,0 +1,12 @@ +# Licensed under the Apache-2.0 license + +[package] +name = "openprot-mctp-api" +version = "0.1.0" +edition = "2021" +description = "MCTP service API types and traits for OpenPRoT" +license = "Apache-2.0" + +[dependencies] +zerocopy = { workspace = true } +heapless = { workspace = true }
diff --git a/services/mctp/api/README.md b/services/mctp/api/README.md new file mode 100644 index 0000000..b034290 --- /dev/null +++ b/services/mctp/api/README.md
@@ -0,0 +1,31 @@ +# openprot-mctp-api + +Platform-independent MCTP types and traits crate. + +## Overview + +This crate defines the core API contract between MCTP clients and the MCTP server. It provides traits for client operations, listener management, and request/response channels, as well as the binary IPC wire protocol used for inter-process communication. + +## Key Types + +- `Handle` — opaque handle for listeners, requests, or response channels +- `RecvMetadata` — metadata from a successful receive (msg_type, tag, remote_eid, payload_size, etc.) +- `MctpError` / `ResponseCode` — error types (Success, InternalError, NoSpace, AddrInUse, TimedOut, BadArgument, ServerRestarted) + +## Traits + +- `MctpClient` — main client interface (req, listener, get/set EID, recv, send, drop_handle) +- `MctpListener` — receiving incoming MCTP messages of a specific type +- `MctpReqChannel` — request/response channels +- `MctpRespChannel` — response channels + +## Wire Protocol + +The `wire` module implements binary request/response encoding for IPC communication between userspace processes and the MCTP server. + +## Dependencies + +- `zerocopy` — zero-copy serialization +- `heapless` — `no_std` collections + +This crate is `no_std` compatible.
diff --git a/services/mctp/api/src/error.rs b/services/mctp/api/src/error.rs new file mode 100644 index 0000000..24cb1d1 --- /dev/null +++ b/services/mctp/api/src/error.rs
@@ -0,0 +1,107 @@ +// Licensed under the Apache-2.0 license + +//! MCTP error types +//! +//! This module defines error types for MCTP operations, providing both +//! transport-level errors and higher-level service response codes. + +use core::fmt; + +/// Response codes from the MCTP service. +/// +/// These codes indicate the result of an MCTP operation and are designed +/// to be compatible with IPC/RPC response handling. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum ResponseCode { + /// Operation completed successfully. + Success = 0, + /// Internal server error. + InternalError = 1, + /// No space available (e.g., message buffers full). + NoSpace = 2, + /// Address/handle already in use. + AddrInUse = 3, + /// Operation timed out. + TimedOut = 4, + /// Invalid argument provided. + BadArgument = 5, + /// Server restarted, state lost. + ServerRestarted = 6, +} + +impl ResponseCode { + /// Returns `true` if this represents a successful operation. + #[inline] + pub const fn is_success(self) -> bool { + matches!(self, ResponseCode::Success) + } + + /// Returns `true` if this represents an error condition. + #[inline] + pub const fn is_error(self) -> bool { + !self.is_success() + } + + /// Converts from a raw u8 value. + pub const fn from_u8(value: u8) -> Option<Self> { + match value { + 0 => Some(ResponseCode::Success), + 1 => Some(ResponseCode::InternalError), + 2 => Some(ResponseCode::NoSpace), + 3 => Some(ResponseCode::AddrInUse), + 4 => Some(ResponseCode::TimedOut), + 5 => Some(ResponseCode::BadArgument), + 6 => Some(ResponseCode::ServerRestarted), + _ => None, + } + } +} + +impl fmt::Display for ResponseCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ResponseCode::Success => write!(f, "success"), + ResponseCode::InternalError => write!(f, "internal error"), + ResponseCode::NoSpace => write!(f, "no space"), + ResponseCode::AddrInUse => write!(f, "address in use"), + ResponseCode::TimedOut => write!(f, "timed out"), + ResponseCode::BadArgument => write!(f, "bad argument"), + ResponseCode::ServerRestarted => write!(f, "server restarted"), + } + } +} + +/// MCTP operation error. +/// +/// This is the main error type returned by MCTP client operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MctpError { + /// The response code from the service. + pub code: ResponseCode, +} + +impl MctpError { + /// Creates a new error from a response code. + pub const fn from_code(code: ResponseCode) -> Self { + MctpError { code } + } + + /// Returns `true` if this is a timeout error. + #[inline] + pub const fn is_timeout(&self) -> bool { + matches!(self.code, ResponseCode::TimedOut) + } +} + +impl fmt::Display for MctpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "MCTP error: {}", self.code) + } +} + +impl From<ResponseCode> for MctpError { + fn from(code: ResponseCode) -> Self { + MctpError::from_code(code) + } +}
diff --git a/services/mctp/api/src/lib.rs b/services/mctp/api/src/lib.rs new file mode 100644 index 0000000..ffe5b7c --- /dev/null +++ b/services/mctp/api/src/lib.rs
@@ -0,0 +1,66 @@ +// Licensed under the Apache-2.0 license + +//! # MCTP Service API +//! +//! This crate provides the client-side API for interacting with the MCTP service. +//! It defines the types, traits, and error handling for MCTP operations. +//! +//! ## Architecture +//! +//! ```text +//! ┌─────────────────────┐ +//! │ Application │ +//! │ (echo, SPDM, etc.) │ +//! └─────────┬───────────┘ +//! │ uses MctpClient trait +//! ▼ +//! ┌─────────────────────┐ +//! │ mctp-api │◄── This crate +//! │ (types & traits) │ +//! └─────────┬───────────┘ +//! │ IPC (implementation specific) +//! ▼ +//! ┌─────────────────────┐ +//! │ MCTP Server │ +//! │ (transport layer) │ +//! └─────────────────────┘ +//! ``` +//! +//! ## Features +//! +//! - **Listener mode**: Receive incoming MCTP messages by type +//! - **Request mode**: Send requests to a remote EID and receive responses +//! - **Platform independent**: No OS-specific dependencies + +#![no_std] +#![warn(missing_docs)] + +mod error; +mod traits; +pub mod wire; + +pub use error::{MctpError, ResponseCode}; +pub use traits::{MctpClient, MctpListener, MctpReqChannel, MctpRespChannel}; + +/// An opaque handle for a listener, request, or response channel. +/// +/// Handles are allocated by the MCTP server and must be released +/// via `drop` when no longer needed. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Handle(pub u32); + +/// Metadata returned by a successful receive operation. +#[derive(Clone, Copy, Debug)] +pub struct RecvMetadata { + /// MCTP message type. + pub msg_type: u8, + /// Whether an integrity check was present. + pub msg_ic: bool, + /// The tag value for correlating request/response pairs. + pub msg_tag: u8, + /// The source endpoint ID. + pub remote_eid: u8, + /// The size of the payload in bytes. + pub payload_size: usize, +}
diff --git a/services/mctp/api/src/traits.rs b/services/mctp/api/src/traits.rs new file mode 100644 index 0000000..7360c2b --- /dev/null +++ b/services/mctp/api/src/traits.rs
@@ -0,0 +1,93 @@ +// Licensed under the Apache-2.0 license + +//! MCTP client traits +//! +//! Platform-independent traits for interacting with an MCTP server. +//! Implementations are provided per-platform (Hubris IPC, Linux sockets, etc.). + +use crate::{Handle, MctpError, RecvMetadata}; + +/// A client interface to an MCTP stack/server. +/// +/// Provides methods for obtaining listener and request handles, and +/// for managing the endpoint ID (EID). +pub trait MctpClient { + /// Obtain a request handle for sending messages to the given EID. + fn req(&self, eid: u8) -> Result<Handle, MctpError>; + + /// Register a listener for incoming messages of the given MCTP type. + fn listener(&self, msg_type: u8) -> Result<Handle, MctpError>; + + /// Get the local endpoint ID. + fn get_eid(&self) -> u8; + + /// Set the local endpoint ID. + fn set_eid(&self, eid: u8) -> Result<(), MctpError>; + + /// Receive a message on the given handle into `buf`. + /// + /// `timeout_millis` of 0 means no timeout (block indefinitely). + fn recv( + &self, + handle: Handle, + timeout_millis: u32, + buf: &mut [u8], + ) -> Result<RecvMetadata, MctpError>; + + /// Send a message through the given handle. + /// + /// For requests, `handle` is `Some`. For responses, `handle` is `None`. + /// Returns the tag value used. + fn send( + &self, + handle: Option<Handle>, + msg_type: u8, + eid: Option<u8>, + tag: Option<u8>, + integrity_check: bool, + buf: &[u8], + ) -> Result<u8, MctpError>; + + /// Release a handle previously obtained from `req` or `listener`. + fn drop_handle(&self, handle: Handle); +} + +/// A listener that receives incoming MCTP messages of a specific type. +pub trait MctpListener { + /// The response channel type returned when a message is received. + type RespChannel<'a>: MctpRespChannel + where + Self: 'a; + + /// Wait for an incoming message, writing the payload into `buf`. + /// + /// Returns the message metadata, payload slice, and a response channel. + fn recv<'f>( + &mut self, + buf: &'f mut [u8], + ) -> Result<(RecvMetadata, &'f mut [u8], Self::RespChannel<'_>), MctpError>; +} + +/// A request channel for sending MCTP requests and receiving responses. +pub trait MctpReqChannel { + /// Send a request message. + fn send(&mut self, msg_type: u8, buf: &[u8]) -> Result<(), MctpError>; + + /// Receive the response to a previously sent request. + fn recv<'f>( + &mut self, + buf: &'f mut [u8], + ) -> Result<(RecvMetadata, &'f mut [u8]), MctpError>; + + /// The remote endpoint ID this channel targets. + fn remote_eid(&self) -> u8; +} + +/// A response channel for replying to an incoming MCTP request. +pub trait MctpRespChannel { + /// Send a response message. + fn send(&mut self, buf: &[u8]) -> Result<(), MctpError>; + + /// The remote endpoint ID that sent the original request. + fn remote_eid(&self) -> u8; +}
diff --git a/services/mctp/api/src/wire.rs b/services/mctp/api/src/wire.rs new file mode 100644 index 0000000..1f8a8bb --- /dev/null +++ b/services/mctp/api/src/wire.rs
@@ -0,0 +1,670 @@ +// Licensed under the Apache-2.0 license + +//! MCTP IPC Wire Protocol +//! +//! Binary wire protocol for MCTP operations over IPC channels. +//! Uses manual byte encoding for `no_std` compatibility. +//! +//! ## Wire Format +//! +//! ```text +//! Request (12 bytes header + optional payload): +//! ┌────┬───────┬──────────┬─────┬────────┬─────┬──────────┐ +//! │ op │ flags │ msg_type │ eid │ handle │ tag │ reserved │ +//! │ 1B │ 1B │ 1B │ 1B │ 4B LE │ 1B │ 3B │ +//! └────┴───────┴──────────┴─────┴────────┴─────┴──────────┘ +//! +//! Response (12 bytes header + optional payload): +//! ┌──────┬───────┬──────────┬─────┬────────┬────────────┬─────┐ +//! │ code │ flags │ msg_type │ eid │ handle │ payload_len│ tag │ + [payload] +//! │ 1B │ 1B │ 1B │ 1B │ 4B LE │ 2B LE │ 1B │ +//! └──────┴───────┴──────────┴─────┴────────┴────────────┴─────┘ +//! ``` +//! +//! For `Recv` requests, the first 4 bytes of payload contain `timeout_millis` (u32 LE). +//! For `Send` requests, the MCTP payload follows the header. + +use crate::ResponseCode; + +// ============================================================================ +// Wire Error +// ============================================================================ + +/// Error type for wire protocol encoding/decoding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WireError { + /// Output buffer too small. + BufferTooSmall, + /// Payload exceeds maximum size. + PayloadTooLarge, + /// Unrecognized operation code. + InvalidOpcode(u8), + /// Input buffer too short for a complete header. + Truncated, +} + +// ============================================================================ +// Operation Codes +// ============================================================================ + +/// MCTP IPC operation codes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum MctpOp { + /// Set the local endpoint ID. + SetEid = 0, + /// Get the local endpoint ID. + GetEid = 1, + /// Register a listener for a message type. + Listener = 2, + /// Allocate a request handle for a remote EID. + Req = 3, + /// Receive a message on a handle. + Recv = 4, + /// Send a message. + Send = 5, + /// Release a handle. + Unbind = 6, +} + +impl MctpOp { + /// Convert from raw byte. + pub fn from_u8(val: u8) -> Option<Self> { + match val { + 0 => Some(Self::SetEid), + 1 => Some(Self::GetEid), + 2 => Some(Self::Listener), + 3 => Some(Self::Req), + 4 => Some(Self::Recv), + 5 => Some(Self::Send), + 6 => Some(Self::Unbind), + _ => None, + } + } +} + +// ============================================================================ +// Request Flags +// ============================================================================ + +/// Request flag bits. +pub mod flags { + /// Integrity check bit in flags byte. + pub const IC: u8 = 1 << 0; + /// Handle field is valid. + pub const HAS_HANDLE: u8 = 1 << 1; + /// EID field is valid. + pub const HAS_EID: u8 = 1 << 2; + /// Tag field is valid. + pub const HAS_TAG: u8 = 1 << 3; +} + +// ============================================================================ +// Request Header +// ============================================================================ + +/// MCTP request header (12 bytes). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MctpRequestHeader { + /// Operation code. + pub op: u8, + /// Flags (see [`flags`] module). + pub flags: u8, + /// MCTP message type. + pub msg_type: u8, + /// Endpoint ID. + pub eid: u8, + /// Handle value. + pub handle: u32, + /// Tag value. + pub tag: u8, +} + +impl MctpRequestHeader { + /// Header size in bytes. + pub const SIZE: usize = 12; + + /// Encode to bytes (little-endian). + pub fn to_bytes(&self) -> [u8; Self::SIZE] { + let h = self.handle.to_le_bytes(); + [ + self.op, + self.flags, + self.msg_type, + self.eid, + h[0], h[1], h[2], h[3], + self.tag, + 0, 0, 0, // reserved + ] + } + + /// Decode from bytes. + pub fn from_bytes(bytes: &[u8]) -> Option<Self> { + if bytes.len() < Self::SIZE { + return None; + } + Some(Self { + op: bytes[0], + flags: bytes[1], + msg_type: bytes[2], + eid: bytes[3], + handle: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + tag: bytes[8], + }) + } + + /// Get the operation code. + pub fn operation(&self) -> Option<MctpOp> { + MctpOp::from_u8(self.op) + } +} + +// ============================================================================ +// Response Header +// ============================================================================ + +/// MCTP response header (12 bytes). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MctpResponseHeader { + /// Response code. + pub code: u8, + /// Flags (bit 0 = msg_ic). + pub flags: u8, + /// MCTP message type (for Recv responses). + pub msg_type: u8, + /// Remote endpoint ID (for Recv responses). + pub eid: u8, + /// Handle (for Listener/Req) or 0. + pub handle: u32, + /// Payload length (for Recv responses). + pub payload_len: u16, + /// Message tag (for Recv/Send responses). + pub tag: u8, +} + +impl MctpResponseHeader { + /// Header size in bytes. + pub const SIZE: usize = 12; + + /// Create a success response with no data. + pub const fn success() -> Self { + Self { + code: ResponseCode::Success as u8, + flags: 0, + msg_type: 0, + eid: 0, + handle: 0, + payload_len: 0, + tag: 0, + } + } + + /// Create an error response. + pub const fn error(code: ResponseCode) -> Self { + Self { + code: code as u8, + flags: 0, + msg_type: 0, + eid: 0, + handle: 0, + payload_len: 0, + tag: 0, + } + } + + /// Check if the response indicates success. + pub fn is_success(&self) -> bool { + self.code == ResponseCode::Success as u8 + } + + /// Get the response code. + pub fn response_code(&self) -> ResponseCode { + ResponseCode::from_u8(self.code).unwrap_or(ResponseCode::InternalError) + } + + /// Encode to bytes (little-endian). + pub fn to_bytes(&self) -> [u8; Self::SIZE] { + let h = self.handle.to_le_bytes(); + let pl = self.payload_len.to_le_bytes(); + [ + self.code, + self.flags, + self.msg_type, + self.eid, + h[0], h[1], h[2], h[3], + pl[0], pl[1], + self.tag, + 0, // reserved + ] + } + + /// Decode from bytes. + pub fn from_bytes(bytes: &[u8]) -> Option<Self> { + if bytes.len() < Self::SIZE { + return None; + } + Some(Self { + code: bytes[0], + flags: bytes[1], + msg_type: bytes[2], + eid: bytes[3], + handle: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + payload_len: u16::from_le_bytes([bytes[8], bytes[9]]), + tag: bytes[10], + }) + } +} + +// ============================================================================ +// Constants +// ============================================================================ + +/// Maximum MCTP payload size. +pub const MAX_PAYLOAD_SIZE: usize = 1023; + +/// Maximum total request size (header + payload). +pub const MAX_REQUEST_SIZE: usize = MctpRequestHeader::SIZE + MAX_PAYLOAD_SIZE; + +/// Maximum total response size (header + payload). +pub const MAX_RESPONSE_SIZE: usize = MctpResponseHeader::SIZE + MAX_PAYLOAD_SIZE; + +/// Sentinel value for "no handle". +pub const NO_HANDLE: u32 = 0xFFFF_FFFF; + +// ============================================================================ +// Request Encoding +// ============================================================================ + +/// Encode a `SetEid` request. +pub fn encode_set_eid(buf: &mut [u8], eid: u8) -> Result<usize, WireError> { + if buf.len() < MctpRequestHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let header = MctpRequestHeader { + op: MctpOp::SetEid as u8, + flags: 0, + msg_type: 0, + eid, + handle: 0, + tag: 0, + }; + buf[..MctpRequestHeader::SIZE].copy_from_slice(&header.to_bytes()); + Ok(MctpRequestHeader::SIZE) +} + +/// Encode a `GetEid` request. +pub fn encode_get_eid(buf: &mut [u8]) -> Result<usize, WireError> { + if buf.len() < MctpRequestHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let header = MctpRequestHeader { + op: MctpOp::GetEid as u8, + flags: 0, + msg_type: 0, + eid: 0, + handle: 0, + tag: 0, + }; + buf[..MctpRequestHeader::SIZE].copy_from_slice(&header.to_bytes()); + Ok(MctpRequestHeader::SIZE) +} + +/// Encode a `Listener` request. +pub fn encode_listener(buf: &mut [u8], msg_type: u8) -> Result<usize, WireError> { + if buf.len() < MctpRequestHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let header = MctpRequestHeader { + op: MctpOp::Listener as u8, + flags: 0, + msg_type, + eid: 0, + handle: 0, + tag: 0, + }; + buf[..MctpRequestHeader::SIZE].copy_from_slice(&header.to_bytes()); + Ok(MctpRequestHeader::SIZE) +} + +/// Encode a `Req` request. +pub fn encode_req(buf: &mut [u8], eid: u8) -> Result<usize, WireError> { + if buf.len() < MctpRequestHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let header = MctpRequestHeader { + op: MctpOp::Req as u8, + flags: 0, + msg_type: 0, + eid, + handle: 0, + tag: 0, + }; + buf[..MctpRequestHeader::SIZE].copy_from_slice(&header.to_bytes()); + Ok(MctpRequestHeader::SIZE) +} + +/// Encode a `Recv` request. +pub fn encode_recv(buf: &mut [u8], handle: u32, timeout_millis: u32) -> Result<usize, WireError> { + let total = MctpRequestHeader::SIZE + 4; + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + let header = MctpRequestHeader { + op: MctpOp::Recv as u8, + flags: flags::HAS_HANDLE, + msg_type: 0, + eid: 0, + handle, + tag: 0, + }; + buf[..MctpRequestHeader::SIZE].copy_from_slice(&header.to_bytes()); + buf[MctpRequestHeader::SIZE..total].copy_from_slice(&timeout_millis.to_le_bytes()); + Ok(total) +} + +/// Encode a `Send` request. +pub fn encode_send( + buf: &mut [u8], + handle: Option<u32>, + msg_type: u8, + eid: Option<u8>, + tag: Option<u8>, + ic: bool, + payload: &[u8], +) -> Result<usize, WireError> { + if payload.len() > MAX_PAYLOAD_SIZE { + return Err(WireError::PayloadTooLarge); + } + let total = MctpRequestHeader::SIZE + payload.len(); + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + + let mut f: u8 = 0; + if ic { + f |= flags::IC; + } + if handle.is_some() { + f |= flags::HAS_HANDLE; + } + if eid.is_some() { + f |= flags::HAS_EID; + } + if tag.is_some() { + f |= flags::HAS_TAG; + } + + let header = MctpRequestHeader { + op: MctpOp::Send as u8, + flags: f, + msg_type, + eid: eid.unwrap_or(0), + handle: handle.unwrap_or(NO_HANDLE), + tag: tag.unwrap_or(0), + }; + buf[..MctpRequestHeader::SIZE].copy_from_slice(&header.to_bytes()); + buf[MctpRequestHeader::SIZE..total].copy_from_slice(payload); + Ok(total) +} + +/// Encode an `Unbind` request. +pub fn encode_unbind(buf: &mut [u8], handle: u32) -> Result<usize, WireError> { + if buf.len() < MctpRequestHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let header = MctpRequestHeader { + op: MctpOp::Unbind as u8, + flags: flags::HAS_HANDLE, + msg_type: 0, + eid: 0, + handle, + tag: 0, + }; + buf[..MctpRequestHeader::SIZE].copy_from_slice(&header.to_bytes()); + Ok(MctpRequestHeader::SIZE) +} + +// ============================================================================ +// Response Encoding (server side) +// ============================================================================ + +/// Encode a success response for `GetEid`. +pub fn encode_get_eid_response(buf: &mut [u8], eid: u8) -> Result<usize, WireError> { + if buf.len() < MctpResponseHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let mut resp = MctpResponseHeader::success(); + resp.eid = eid; + buf[..MctpResponseHeader::SIZE].copy_from_slice(&resp.to_bytes()); + Ok(MctpResponseHeader::SIZE) +} + +/// Encode a success response for `Listener` or `Req` (returns a handle). +pub fn encode_handle_response(buf: &mut [u8], handle: u32) -> Result<usize, WireError> { + if buf.len() < MctpResponseHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let mut resp = MctpResponseHeader::success(); + resp.handle = handle; + buf[..MctpResponseHeader::SIZE].copy_from_slice(&resp.to_bytes()); + Ok(MctpResponseHeader::SIZE) +} + +/// Encode a success response for `Send` (returns the tag). +pub fn encode_send_response(buf: &mut [u8], tag: u8) -> Result<usize, WireError> { + if buf.len() < MctpResponseHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let mut resp = MctpResponseHeader::success(); + resp.tag = tag; + buf[..MctpResponseHeader::SIZE].copy_from_slice(&resp.to_bytes()); + Ok(MctpResponseHeader::SIZE) +} + +/// Encode a success response for `Recv` (returns metadata + payload). +pub fn encode_recv_response( + buf: &mut [u8], + msg_type: u8, + msg_ic: bool, + eid: u8, + tag: u8, + payload: &[u8], +) -> Result<usize, WireError> { + if payload.len() > MAX_PAYLOAD_SIZE { + return Err(WireError::PayloadTooLarge); + } + let total = MctpResponseHeader::SIZE + payload.len(); + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + let resp = MctpResponseHeader { + code: ResponseCode::Success as u8, + flags: if msg_ic { flags::IC } else { 0 }, + msg_type, + eid, + handle: 0, + payload_len: payload.len() as u16, + tag, + }; + buf[..MctpResponseHeader::SIZE].copy_from_slice(&resp.to_bytes()); + buf[MctpResponseHeader::SIZE..total].copy_from_slice(payload); + Ok(total) +} + +/// Encode a simple success response (no data). +pub fn encode_success_response(buf: &mut [u8]) -> Result<usize, WireError> { + if buf.len() < MctpResponseHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + buf[..MctpResponseHeader::SIZE].copy_from_slice(&MctpResponseHeader::success().to_bytes()); + Ok(MctpResponseHeader::SIZE) +} + +/// Encode an error response. +pub fn encode_error_response(buf: &mut [u8], code: ResponseCode) -> Result<usize, WireError> { + if buf.len() < MctpResponseHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + buf[..MctpResponseHeader::SIZE].copy_from_slice(&MctpResponseHeader::error(code).to_bytes()); + Ok(MctpResponseHeader::SIZE) +} + +// ============================================================================ +// Response Decoding (client side) +// ============================================================================ + +/// Decode a response header. +pub fn decode_response_header(buf: &[u8]) -> Result<MctpResponseHeader, WireError> { + MctpResponseHeader::from_bytes(buf).ok_or(WireError::Truncated) +} + +/// Get response payload data (after header). +pub fn get_response_payload<'a>(buf: &'a [u8], header: &MctpResponseHeader) -> Result<&'a [u8], WireError> { + let end = MctpResponseHeader::SIZE + header.payload_len as usize; + if buf.len() < end { + return Err(WireError::Truncated); + } + Ok(&buf[MctpResponseHeader::SIZE..end]) +} + +/// Decode a request header. +pub fn decode_request_header(buf: &[u8]) -> Result<MctpRequestHeader, WireError> { + MctpRequestHeader::from_bytes(buf).ok_or(WireError::Truncated) +} + +/// Get request payload (after header, for Send operations). +pub fn get_request_payload(buf: &[u8]) -> &[u8] { + if buf.len() > MctpRequestHeader::SIZE { + &buf[MctpRequestHeader::SIZE..] + } else { + &[] + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_header_roundtrip() { + let header = MctpRequestHeader { + op: MctpOp::Send as u8, + flags: flags::HAS_HANDLE | flags::IC, + msg_type: 1, + eid: 42, + handle: 0x1234, + tag: 7, + }; + let bytes = header.to_bytes(); + let decoded = MctpRequestHeader::from_bytes(&bytes).unwrap(); + + assert_eq!(decoded.op, MctpOp::Send as u8); + assert_eq!(decoded.flags, flags::HAS_HANDLE | flags::IC); + assert_eq!(decoded.msg_type, 1); + assert_eq!(decoded.eid, 42); + assert_eq!(decoded.handle, 0x1234); + assert_eq!(decoded.tag, 7); + } + + #[test] + fn response_header_roundtrip() { + let header = MctpResponseHeader { + code: ResponseCode::Success as u8, + flags: flags::IC, + msg_type: 1, + eid: 8, + handle: 0, + payload_len: 16, + tag: 3, + }; + let bytes = header.to_bytes(); + let decoded = MctpResponseHeader::from_bytes(&bytes).unwrap(); + + assert!(decoded.is_success()); + assert_eq!(decoded.flags & flags::IC, flags::IC); + assert_eq!(decoded.msg_type, 1); + assert_eq!(decoded.eid, 8); + assert_eq!(decoded.payload_len, 16); + assert_eq!(decoded.tag, 3); + } + + #[test] + fn encode_send_roundtrip() { + let mut buf = [0u8; 64]; + let payload = b"hello"; + let len = encode_send(&mut buf, Some(5), 1, Some(8), Some(3), true, payload).unwrap(); + + let header = MctpRequestHeader::from_bytes(&buf).unwrap(); + assert_eq!(header.operation(), Some(MctpOp::Send)); + assert_eq!(header.flags & flags::IC, flags::IC); + assert_eq!(header.flags & flags::HAS_HANDLE, flags::HAS_HANDLE); + assert_eq!(header.flags & flags::HAS_EID, flags::HAS_EID); + assert_eq!(header.flags & flags::HAS_TAG, flags::HAS_TAG); + assert_eq!(header.msg_type, 1); + assert_eq!(header.eid, 8); + assert_eq!(header.handle, 5); + assert_eq!(header.tag, 3); + assert_eq!(&buf[MctpRequestHeader::SIZE..len], b"hello"); + } + + #[test] + fn encode_recv_roundtrip() { + let mut buf = [0u8; 32]; + let len = encode_recv(&mut buf, 7, 5000).unwrap(); + assert_eq!(len, MctpRequestHeader::SIZE + 4); + + let header = MctpRequestHeader::from_bytes(&buf).unwrap(); + assert_eq!(header.operation(), Some(MctpOp::Recv)); + assert_eq!(header.handle, 7); + + let timeout = u32::from_le_bytes([ + buf[MctpRequestHeader::SIZE], + buf[MctpRequestHeader::SIZE + 1], + buf[MctpRequestHeader::SIZE + 2], + buf[MctpRequestHeader::SIZE + 3], + ]); + assert_eq!(timeout, 5000); + } + + #[test] + fn error_response() { + let mut buf = [0u8; 16]; + let len = encode_error_response(&mut buf, ResponseCode::NoSpace).unwrap(); + assert_eq!(len, MctpResponseHeader::SIZE); + + let header = decode_response_header(&buf).unwrap(); + assert!(!header.is_success()); + assert_eq!(header.response_code(), ResponseCode::NoSpace); + } + + #[test] + fn recv_response_with_payload() { + let mut buf = [0u8; 64]; + let payload = b"echo data"; + let len = encode_recv_response(&mut buf, 1, false, 42, 5, payload).unwrap(); + + let header = decode_response_header(&buf).unwrap(); + assert!(header.is_success()); + assert_eq!(header.msg_type, 1); + assert_eq!(header.eid, 42); + assert_eq!(header.tag, 5); + assert_eq!(header.payload_len, 9); + + let data = get_response_payload(&buf[..len], &header).unwrap(); + assert_eq!(data, b"echo data"); + } + + #[test] + fn send_payload_too_large() { + let mut buf = [0u8; 2048]; + let oversized = [0u8; MAX_PAYLOAD_SIZE + 1]; + assert_eq!( + encode_send(&mut buf, None, 1, None, None, false, &oversized), + Err(WireError::PayloadTooLarge) + ); + } +}
diff --git a/services/mctp/client/Cargo.toml b/services/mctp/client/Cargo.toml new file mode 100644 index 0000000..7de8400 --- /dev/null +++ b/services/mctp/client/Cargo.toml
@@ -0,0 +1,11 @@ +# Licensed under the Apache-2.0 license + +[package] +name = "openprot-mctp-client" +version = "0.1.0" +edition = "2021" +description = "MCTP IPC client for OpenPRoT (Pigweed userspace)" +license = "Apache-2.0" + +[dependencies] +openprot-mctp-api = { path = "../api" }
diff --git a/services/mctp/client/README.md b/services/mctp/client/README.md new file mode 100644 index 0000000..17c789b --- /dev/null +++ b/services/mctp/client/README.md
@@ -0,0 +1,21 @@ +# openprot-mctp-client + +IPC client for the MCTP server, used by userspace applications under Pigweed (`pw_kernel`). + +## Overview + +This crate provides `IpcMctpClient`, which implements the `MctpClient` trait from `openprot-mctp-api`. It communicates with the MCTP server process over a Pigweed IPC channel using the binary wire protocol defined in `openprot-mctp-api`. + +Applications (such as the MCTP echo task) use this crate to interact with the MCTP server without needing to know about transport details. + +## Key Types + +- `IpcMctpClient` — implements `MctpClient` via Pigweed IPC; uses `RefCell` for interior mutability so trait methods taking `&self` can mutate internal IPC buffers + +## Status + +The `send_recv` method is currently stubbed, pending Phase 6 integration with `syscall::channel_transact`. + +## Dependencies + +- `openprot-mctp-api` — `MctpClient` trait and wire protocol
diff --git a/services/mctp/client/src/lib.rs b/services/mctp/client/src/lib.rs new file mode 100644 index 0000000..4eeaa11 --- /dev/null +++ b/services/mctp/client/src/lib.rs
@@ -0,0 +1,215 @@ +// Licensed under the Apache-2.0 license + +//! MCTP IPC Client +//! +//! Provides an `MctpClient` implementation that communicates with the MCTP +//! server over a Pigweed IPC channel, using the wire protocol from +//! `openprot-mctp-api`. +//! +//! This is the MCTP equivalent of `i2c_client::IpcI2cClient`. +//! +//! ## Usage +//! +//! ```rust,ignore +//! use openprot_mctp_client::IpcMctpClient; +//! use openprot_mctp_api::MctpClient; +//! +//! let client = IpcMctpClient::new(handle::MCTP); +//! +//! client.set_eid(8).unwrap(); +//! let listener = client.listener(1).unwrap(); +//! let meta = client.recv(listener, 0, &mut buf).unwrap(); +//! ``` + +#![no_std] +#![warn(missing_docs)] + +use core::cell::RefCell; + +use openprot_mctp_api::wire::{self, MctpResponseHeader, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE}; +use openprot_mctp_api::{Handle, MctpClient, MctpError, RecvMetadata, ResponseCode}; + +/// Internal mutable state for the IPC client. +struct ClientBuffers { + request_buf: [u8; MAX_REQUEST_SIZE], + response_buf: [u8; MAX_RESPONSE_SIZE], +} + +/// MCTP client that communicates with the MCTP server over Pigweed IPC. +/// +/// Uses `RefCell` for interior mutability so that `MctpClient` trait +/// methods (which take `&self`) can mutate the internal IPC buffers. +/// This matches the Hubris pattern where IPC calls are logically +/// stateless from the caller's perspective. +pub struct IpcMctpClient { + handle: u32, + inner: RefCell<ClientBuffers>, +} + +impl IpcMctpClient { + /// Create a new IPC MCTP client bound to the given channel handle. + /// + /// The handle comes from the application's `app_package`-generated + /// handle module (e.g., `handle::MCTP`). + pub fn new(handle: u32) -> Self { + Self { + handle, + inner: RefCell::new(ClientBuffers { + request_buf: [0u8; MAX_REQUEST_SIZE], + response_buf: [0u8; MAX_RESPONSE_SIZE], + }), + } + } + + /// Get the IPC channel handle. + pub fn channel_handle(&self) -> u32 { + self.handle + } + + /// Encode, send, and decode a transaction. + fn transact(&self, req_len: usize) -> Result<(MctpResponseHeader, usize), MctpError> { + let resp_len = self.send_recv(req_len)?; + let inner = self.inner.borrow(); + + if resp_len < MctpResponseHeader::SIZE { + return Err(MctpError::from_code(ResponseCode::InternalError)); + } + + let header = wire::decode_response_header(&inner.response_buf[..resp_len]) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))?; + + if !header.is_success() { + return Err(MctpError::from_code(header.response_code())); + } + + Ok((header, resp_len)) + } + + /// Platform-specific send/receive. + /// + /// In a Pigweed build, this calls `syscall::channel_transact`. + /// For now, this is a stub that returns an error. + fn send_recv(&self, _req_len: usize) -> Result<usize, MctpError> { + // TODO(Phase 6): Replace with: + // let mut inner = self.inner.borrow_mut(); + // syscall::channel_transact( + // self.handle, + // &inner.request_buf[..req_len], + // &mut inner.response_buf, + // Instant::MAX, + // ).map_err(|_| MctpError::from_code(ResponseCode::InternalError)) + Err(MctpError::from_code(ResponseCode::InternalError)) + } +} + +impl MctpClient for IpcMctpClient { + fn req(&self, eid: u8) -> Result<Handle, MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_req(&mut inner.request_buf, eid) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + let (header, _) = self.transact(req_len)?; + Ok(Handle(header.handle)) + } + + fn listener(&self, msg_type: u8) -> Result<Handle, MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_listener(&mut inner.request_buf, msg_type) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + let (header, _) = self.transact(req_len)?; + Ok(Handle(header.handle)) + } + + fn get_eid(&self) -> u8 { + let req_len = { + let mut inner = self.inner.borrow_mut(); + match wire::encode_get_eid(&mut inner.request_buf) { + Ok(len) => len, + Err(_) => return 0, + } + }; + match self.transact(req_len) { + Ok((header, _)) => header.eid, + Err(_) => 0, + } + } + + fn set_eid(&self, eid: u8) -> Result<(), MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_set_eid(&mut inner.request_buf, eid) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + self.transact(req_len)?; + Ok(()) + } + + fn recv( + &self, + handle: Handle, + timeout_millis: u32, + buf: &mut [u8], + ) -> Result<RecvMetadata, MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_recv(&mut inner.request_buf, handle.0, timeout_millis) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + let (header, resp_len) = self.transact(req_len)?; + + let inner = self.inner.borrow(); + let payload = wire::get_response_payload(&inner.response_buf[..resp_len], &header) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))?; + + let copy_len = core::cmp::min(payload.len(), buf.len()); + buf[..copy_len].copy_from_slice(&payload[..copy_len]); + + Ok(RecvMetadata { + msg_type: header.msg_type, + msg_ic: header.flags & wire::flags::IC != 0, + msg_tag: header.tag, + remote_eid: header.eid, + payload_size: payload.len(), + }) + } + + fn send( + &self, + handle: Option<Handle>, + msg_type: u8, + eid: Option<u8>, + tag: Option<u8>, + integrity_check: bool, + buf: &[u8], + ) -> Result<u8, MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_send( + &mut inner.request_buf, + handle.map(|h| h.0), + msg_type, + eid, + tag, + integrity_check, + buf, + ) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + let (header, _) = self.transact(req_len)?; + Ok(header.tag) + } + + fn drop_handle(&self, handle: Handle) { + let req_len = { + let mut inner = self.inner.borrow_mut(); + match wire::encode_unbind(&mut inner.request_buf, handle.0) { + Ok(len) => len, + Err(_) => return, + } + }; + let _ = self.transact(req_len); + } +}
diff --git a/services/mctp/server/Cargo.toml b/services/mctp/server/Cargo.toml new file mode 100644 index 0000000..73a7acf --- /dev/null +++ b/services/mctp/server/Cargo.toml
@@ -0,0 +1,18 @@ +# Licensed under the Apache-2.0 license + +[package] +name = "openprot-mctp-server" +version = "0.1.0" +edition = "2021" +description = "MCTP server implementation for OpenPRoT" +license = "Apache-2.0" + +[dependencies] +openprot-mctp-api = { path = "../api" } +mctp-stack = { git = "https://github.com/9elements/mctp-lib.git", branch = "buildup", package = "mctp-lib" } +mctp = { git = "https://github.com/OpenPRoT/mctp-rs.git", branch = "sync-features", default-features = false } +heapless = { workspace = true } + +[dev-dependencies] +mctp-stack = { git = "https://github.com/9elements/mctp-lib.git", branch = "buildup", package = "mctp-lib" } +mctp = { git = "https://github.com/OpenPRoT/mctp-rs.git", branch = "sync-features" }
diff --git a/services/mctp/server/README.md b/services/mctp/server/README.md new file mode 100644 index 0000000..fcd534d --- /dev/null +++ b/services/mctp/server/README.md
@@ -0,0 +1,28 @@ +# openprot-mctp-server + +Platform-independent MCTP server core, ported from the Hubris `mctp-server` crate. + +## Overview + +This crate implements the central MCTP server logic: listener and request handle allocation, inbound message routing, outbound message fragmentation/sending, and timeout management for pending receive calls. It is generic over transport bindings via the `mctp_stack::Sender` trait. + +## Key Types + +- `Server<S, N>` — the MCTP server, generic over `Sender` (transport) and `N` (config) +- `ServerConfig` — configuration constants (MAX_REQUESTS: 8, MAX_LISTENERS: 8, MAX_OUTSTANDING: 16, MAX_PAYLOAD: 1023) +- `RecvResult` — result from a receive operation + +## Modules + +- `dispatch` — IPC request dispatcher; decodes wire-protocol requests and calls the corresponding `Server` methods + +## Architecture + +The server wraps the `mctp-lib` `Router` as its packet processing engine. Inbound packets are fed via `Server::inbound()`, and `Server::update()` drives pending-recv fulfillment. The `dispatch` module bridges IPC wire-protocol messages to server operations. + +## Dependencies + +- `openprot-mctp-api` — API traits and wire protocol +- `mctp-stack` (`mctp-lib`) — `Router`, `Sender`, fragmentation, serial/I2C handlers +- `mctp` — core MCTP types (`Eid`, `MsgType`, `Tag`) +- `heapless` — `no_std` collections
diff --git a/services/mctp/server/src/dispatch.rs b/services/mctp/server/src/dispatch.rs new file mode 100644 index 0000000..1fba65a --- /dev/null +++ b/services/mctp/server/src/dispatch.rs
@@ -0,0 +1,127 @@ +// Licensed under the Apache-2.0 license + +//! MCTP IPC request dispatch. +//! +//! Decodes wire-protocol requests and dispatches them to the [`Server`]. +//! This is the server-side counterpart of `openprot-mctp-client`. + +use openprot_mctp_api::wire::{ + self, flags, MctpOp, MctpRequestHeader, +}; +use openprot_mctp_api::ResponseCode; + +use crate::{Sender, Server}; + +/// Dispatch an IPC request to the MCTP server. +/// +/// Decodes the request header, calls the appropriate `Server` method, +/// and encodes the response into `response`. Returns the response length. +/// +/// This is the MCTP equivalent of `dispatch_i2c_op` in the I2C server. +pub fn dispatch_mctp_op<S: Sender, const N: usize>( + request: &[u8], + response: &mut [u8], + server: &mut Server<S, N>, + recv_buf: &mut [u8], +) -> usize { + let header = match MctpRequestHeader::from_bytes(request) { + Some(h) => h, + None => return encode_error(response, ResponseCode::BadArgument), + }; + + let Some(op) = header.operation() else { + return encode_error(response, ResponseCode::BadArgument); + }; + + match op { + MctpOp::SetEid => match server.set_eid(header.eid) { + Ok(()) => encode_success(response), + Err(e) => encode_error(response, e.code), + }, + + MctpOp::GetEid => { + let eid = server.get_eid(); + wire::encode_get_eid_response(response, eid) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)) + } + + MctpOp::Listener => match server.listener(header.msg_type) { + Ok(handle) => wire::encode_handle_response(response, handle.0) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)), + Err(e) => encode_error(response, e.code), + }, + + MctpOp::Req => match server.req(header.eid) { + Ok(handle) => wire::encode_handle_response(response, handle.0) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)), + Err(e) => encode_error(response, e.code), + }, + + MctpOp::Recv => { + let handle = openprot_mctp_api::Handle(header.handle); + + match server.try_recv(handle, recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + wire::encode_recv_response( + response, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)) + } + None => { + // No message available yet. + // In a real Pigweed server, we'd register a pending recv + // and respond later. For now, return TimedOut. + encode_error(response, ResponseCode::TimedOut) + } + } + } + + MctpOp::Send => { + let handle = if header.flags & flags::HAS_HANDLE != 0 { + Some(openprot_mctp_api::Handle(header.handle)) + } else { + None + }; + let eid = if header.flags & flags::HAS_EID != 0 { + Some(header.eid) + } else { + None + }; + let tag = if header.flags & flags::HAS_TAG != 0 { + Some(header.tag) + } else { + None + }; + let ic = header.flags & flags::IC != 0; + let payload = wire::get_request_payload(request); + + match server.send(handle, header.msg_type, eid, tag, ic, payload) { + Ok(tag_val) => wire::encode_send_response(response, tag_val) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)), + Err(e) => encode_error(response, e.code), + } + } + + MctpOp::Unbind => { + let handle = openprot_mctp_api::Handle(header.handle); + match server.unbind(handle) { + Ok(()) => encode_success(response), + Err(e) => encode_error(response, e.code), + } + } + } +} + +fn encode_error(response: &mut [u8], code: ResponseCode) -> usize { + wire::encode_error_response(response, code).unwrap_or(0) +} + +fn encode_success(response: &mut [u8]) -> usize { + wire::encode_success_response(response).unwrap_or(0) +}
diff --git a/services/mctp/server/src/lib.rs b/services/mctp/server/src/lib.rs new file mode 100644 index 0000000..118bfd8 --- /dev/null +++ b/services/mctp/server/src/lib.rs
@@ -0,0 +1,34 @@ +// Licensed under the Apache-2.0 license + +//! # MCTP Server +//! +//! Platform-independent MCTP server implementation for OpenPRoT. +//! +//! This crate provides the core MCTP server logic that manages: +//! - Listener and request handle allocation (via `mctp-lib` [`Router`](mctp_stack::Router)) +//! - Inbound message routing to registered listeners +//! - Outbound message fragmentation and sending +//! - Timeout management for pending receive calls +//! +//! ## Transport Bindings +//! +//! The server is generic over the `mctp-lib` [`Sender`](mctp_stack::Sender) trait +//! for outbound transport. Transport-specific bindings (I2C, serial) implement +//! this trait and feed inbound packets via [`Server::inbound`]. +//! +//! ## Platform Integration +//! +//! The server does not depend on any OS primitives. The platform layer +//! is responsible for: +//! - Driving the event loop (notifications, IPC dispatch) +//! - Providing a time source via [`Server::update`] +//! - Wiring up transport bindings + +#![no_std] +#![warn(missing_docs)] + +pub mod dispatch; +mod server; + +pub use mctp_stack::Sender; +pub use server::{RecvResult, Server, ServerConfig};
diff --git a/services/mctp/server/src/server.rs b/services/mctp/server/src/server.rs new file mode 100644 index 0000000..5f63256 --- /dev/null +++ b/services/mctp/server/src/server.rs
@@ -0,0 +1,293 @@ +// Licensed under the Apache-2.0 license + +//! Core MCTP server logic. +//! +//! This is a direct port of the Hubris `mctp-server/src/server.rs`. +//! The `Router` integration, handle management, timeout logic, and message +//! routing are preserved as-is. Only Hubris IPC primitives (`sys_reply`, +//! `Leased`, `RecvMessage`) have been replaced with platform-independent +//! equivalents. + +use heapless::LinearMap; +use mctp::{Eid, MsgIC, MsgType, Tag, TagValue}; +use mctp_stack::{AppCookie, Router, Sender}; +use openprot_mctp_api::{Handle, MctpError, RecvMetadata, ResponseCode}; + +/// Maximum payload size in bytes. +// TODO: Use configuration from mctp-lib (mctp-estack) +// see https://github.com/OpenPRoT/mctp-lib/issues/4 +const MAX_PAYLOAD: usize = 1023; + +/// Configuration constants for the MCTP server. +pub struct ServerConfig; + +impl ServerConfig { + /// Maximum number of concurrent requests the server can handle. + pub const MAX_REQUESTS: usize = 8; + /// Maximum number of listeners that can be registered concurrently. + pub const MAX_LISTENERS: usize = 8; + /// Maximum number of concurrent outstanding receive calls. + pub const MAX_OUTSTANDING: usize = 16; + /// Maximum payload size in bytes. + pub const MAX_PAYLOAD: usize = MAX_PAYLOAD; +} + +/// A pending receive call waiting for a message or timeout. +#[derive(Debug, Clone, Copy)] +struct PendingRecv { + /// Deadline in milliseconds (0 = no timeout). + deadline: u64, +} + +/// The platform-independent MCTP server. +/// +/// This struct wraps the `mctp-lib` [`Router`] and manages outstanding +/// receive calls with timeout tracking. It is a direct port of the +/// Hubris `Server` struct with OS-specific IPC removed. +/// +/// # Type Parameters +/// +/// * `S` - The [`Sender`] implementation for outbound transport. +/// * `OUTSTANDING` - Maximum number of concurrent pending receive calls. +pub struct Server<S: Sender, const OUTSTANDING: usize> { + /// The underlying MCTP router (from mctp-lib). + pub stack: Router<S, { ServerConfig::MAX_LISTENERS }, { ServerConfig::MAX_REQUESTS }>, + /// Currently outstanding recv calls, keyed by handle value. + /// + /// Maps the handle to a deadline. The platform layer is responsible + /// for storing any additional per-recv state (e.g., reply channels). + outstanding: LinearMap<u32, PendingRecv, OUTSTANDING>, +} + +impl<S: Sender, const OUTSTANDING: usize> Server<S, OUTSTANDING> { + /// Create a new MCTP server instance. + pub fn new(own_eid: Eid, now_millis: u64, outbound: S) -> Self { + let stack = Router::new(own_eid, now_millis, outbound); + Self { + stack, + outstanding: LinearMap::new(), + } + } + + /// Allocate a request handle for sending messages to the given EID. + pub fn req(&mut self, eid: u8) -> Result<Handle, MctpError> { + match self.stack.req(Eid(eid)) { + Ok(cookie) => Ok(Handle(cookie.0 as u32)), + Err(e) => Err(mctp_error_to_server_error(e)), + } + } + + /// Register a listener for incoming messages of the given type. + pub fn listener(&mut self, typ: u8) -> Result<Handle, MctpError> { + match self.stack.listener(MsgType(typ)) { + Ok(cookie) => Ok(Handle(cookie.0 as u32)), + Err(e) => Err(mctp_error_to_server_error(e)), + } + } + + /// Get the currently configured EID. + pub fn get_eid(&self) -> u8 { + self.stack.get_eid().0 + } + + /// Set the EID for this endpoint. + pub fn set_eid(&mut self, eid: u8) -> Result<(), MctpError> { + self.stack + .set_eid(Eid(eid)) + .map_err(mctp_error_to_server_error) + } + + /// Check for an available message on the given handle. + /// + /// If a message is available, returns the metadata and copies the + /// payload into `buf`. Otherwise returns `None` and the caller + /// should register a pending recv via [`register_recv`](Self::register_recv). + pub fn try_recv( + &mut self, + handle: Handle, + buf: &mut [u8], + ) -> Option<RecvMetadata> { + let cookie = AppCookie(handle.0 as usize); + let msg = self.stack.recv(cookie)?; + + let payload_len = msg.payload.len(); + if payload_len <= buf.len() { + buf[..payload_len].copy_from_slice(msg.payload); + } + + Some(RecvMetadata { + msg_type: msg.typ.0, + msg_ic: msg.ic.0, + msg_tag: msg.tag.tag().0, + remote_eid: msg.source.0, + payload_size: payload_len, + }) + } + + /// Register a pending receive call for the given handle. + /// + /// The platform layer should call this when `try_recv` returns `None` + /// and the client wants to block. Returns an error if the outstanding + /// table is full. + pub fn register_recv( + &mut self, + handle: Handle, + timeout_millis: u32, + now_millis: u64, + ) -> Result<(), MctpError> { + let deadline = if timeout_millis != 0 { + now_millis + timeout_millis as u64 + } else { + 0 + }; + + // Don't overwrite existing entries + if self.outstanding.contains_key(&handle.0) { + return Ok(()); + } + + self.outstanding + .insert(handle.0, PendingRecv { deadline }) + .map_err(|_| MctpError::from_code(ResponseCode::NoSpace))?; + Ok(()) + } + + /// Send a message. + /// + /// For requests, `handle` is `Some`. For responses, `handle` is `None`. + /// When responding to a request received by a listener, `eid` and `tag` + /// must be set. Returns the tag value used. + pub fn send( + &mut self, + handle: Option<Handle>, + typ: u8, + eid: Option<u8>, + tag: Option<u8>, + ic: bool, + buf: &[u8], + ) -> Result<u8, MctpError> { + if buf.len() > MAX_PAYLOAD { + return Err(MctpError::from_code(ResponseCode::NoSpace)); + } + + let tag = if handle.is_none() { + // Responses use unowned tags + tag.map(|x| Tag::Unowned(TagValue(x))) + } else { + // Requests use owned tags (or allocate a new one) + tag.map(|x| Tag::Owned(TagValue(x))) + }; + + // Responses need no handle, use 255 as dummy + let cookie = AppCookie(handle.unwrap_or(Handle(255)).0 as usize); + + let result = self.stack.send( + eid.map(Eid), + MsgType(typ), + tag, + MsgIC(ic), + cookie, + buf, + ); + + match result { + Ok(tag) => Ok(tag.tag().0), + Err(e) => Err(mctp_error_to_server_error(e)), + } + } + + /// Update the stack and check for fulfilled receive calls. + /// + /// Should be called on timer events. Returns the interval (ms) until + /// the next required update, and a list of handles that now have + /// messages available (the platform layer should deliver them). + pub fn update( + &mut self, + now_millis: u64, + recv_buf: &mut [u8], + ) -> (u32, heapless::Vec<(Handle, RecvResult), OUTSTANDING>) { + // Update the mctp-stack; get the next timeout interval + let stack_timeout = self + .stack + .update(now_millis) + .unwrap_or(60_000) as u32; + + let mut ready: heapless::Vec<(Handle, RecvResult), OUTSTANDING> = heapless::Vec::new(); + + for (handle_val, pending) in self.outstanding.iter() { + let handle = Handle(*handle_val); + let cookie = AppCookie(*handle_val as usize); + + // Check if a message arrived for this handle + if let Some(mctp_msg) = self.stack.recv(cookie) { + let payload_len = mctp_msg.payload.len(); + if payload_len <= recv_buf.len() { + recv_buf[..payload_len].copy_from_slice(mctp_msg.payload); + } + let metadata = RecvMetadata { + msg_type: mctp_msg.typ.0, + msg_ic: mctp_msg.ic.0, + msg_tag: mctp_msg.tag.tag().0, + remote_eid: mctp_msg.source.0, + payload_size: payload_len, + }; + let _ = ready.push((handle, RecvResult::Message(metadata))); + continue; + } + + // Check for timeout + if pending.deadline != 0 && now_millis >= pending.deadline { + let _ = ready.push((handle, RecvResult::TimedOut)); + } + } + + // Remove fulfilled/timed-out entries + for (handle, _) in &ready { + self.outstanding.remove(&handle.0); + } + + (stack_timeout, ready) + } + + /// Unbind a handle previously allocated by `req` or `listener`. + pub fn unbind(&mut self, handle: Handle) -> Result<(), MctpError> { + let cookie = AppCookie(handle.0 as usize); + let _ = self.stack.unbind(cookie); + self.outstanding.remove(&handle.0); + Ok(()) + } + + /// Feed an inbound MCTP packet to the router. + /// + /// The platform layer calls this when data arrives from a transport + /// binding. The packet should be a raw MCTP packet without transport + /// headers (the transport binding strips those). + pub fn inbound(&mut self, pkt: &[u8]) -> Result<(), MctpError> { + self.stack + .inbound(pkt) + .map_err(mctp_error_to_server_error) + } +} + +/// Result of a pending receive call. +#[derive(Debug, Clone, Copy)] +pub enum RecvResult { + /// A message was received. + Message(RecvMetadata), + /// The receive call timed out. + TimedOut, +} + +/// Map mctp::Error to our MctpError. +fn mctp_error_to_server_error(e: mctp::Error) -> MctpError { + use mctp::Error::*; + let code = match e { + InternalError => ResponseCode::InternalError, + NoSpace => ResponseCode::NoSpace, + AddrInUse => ResponseCode::AddrInUse, + TimedOut => ResponseCode::TimedOut, + BadArgument => ResponseCode::BadArgument, + _ => ResponseCode::InternalError, + }; + MctpError::from_code(code) +}
diff --git a/services/mctp/server/tests/dispatch.rs b/services/mctp/server/tests/dispatch.rs new file mode 100644 index 0000000..1e09dfb --- /dev/null +++ b/services/mctp/server/tests/dispatch.rs
@@ -0,0 +1,174 @@ +// Licensed under the Apache-2.0 license + +//! Wire-protocol dispatch integration test. +//! +//! Exercises the full IPC path: encode request → `dispatch_mctp_op` → decode +//! response. This verifies that the wire protocol + dispatch layer + Server +//! work together correctly, simulating what happens when `IpcMctpClient` +//! talks to the MCTP server process over a Pigweed IPC channel. + +use std::cell::RefCell; + +use mctp::{Eid, Tag}; +use mctp_stack::fragment::{Fragmenter, SendOutput}; +use mctp_stack::Sender; +use openprot_mctp_api::wire; +use openprot_mctp_server::{dispatch::dispatch_mctp_op, Server}; + +// --------------------------------------------------------------------------- +// Mock transport (same as echo.rs) +// --------------------------------------------------------------------------- + +struct BufferSender<'a> { + packets: &'a RefCell<Vec<Vec<u8>>>, +} + +impl Sender for BufferSender<'_> { + fn send_vectored( + &mut self, + mut fragmenter: Fragmenter, + payload: &[&[u8]], + ) -> mctp::Result<Tag> { + loop { + let mut buf = [0u8; 255]; + match fragmenter.fragment_vectored(payload, &mut buf) { + SendOutput::Packet(p) => { + self.packets.borrow_mut().push(p.to_vec()); + } + SendOutput::Complete { tag, .. } => return Ok(tag), + SendOutput::Error { err, .. } => return Err(err), + } + } + } + + fn get_mtu(&self) -> usize { + 255 + } +} + +fn transfer<S: Sender, const N: usize>( + packets: &RefCell<Vec<Vec<u8>>>, + dest: &mut Server<S, N>, +) { + for pkt in packets.borrow().iter() { + dest.inbound(pkt).unwrap(); + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// Test SetEid + GetEid via dispatch. +#[test] +fn dispatch_set_get_eid() { + let buf = RefCell::new(Vec::new()); + let sender = BufferSender { packets: &buf }; + let mut server: Server<_, 16> = Server::new(Eid(0), 0, sender); + + let mut req = [0u8; 64]; + let mut resp = [0u8; 64]; + let mut recv_buf = [0u8; 255]; + + // SetEid(42) + let req_len = wire::encode_set_eid(&mut req, 42).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + + // GetEid → 42 + let req_len = wire::encode_get_eid(&mut req).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + assert_eq!(header.eid, 42); +} + +/// Test Listener + Req + Send + Recv via dispatch (full echo round-trip). +#[test] +fn dispatch_echo_roundtrip() { + // Server A (echo responder, EID 8) + let buf_a = RefCell::new(Vec::new()); + let sender_a = BufferSender { packets: &buf_a }; + let mut server_a: Server<_, 16> = Server::new(Eid(8), 0, sender_a); + + // Server B (requester, EID 42) + let buf_b = RefCell::new(Vec::new()); + let sender_b = BufferSender { packets: &buf_b }; + let mut server_b: Server<_, 16> = Server::new(Eid(42), 0, sender_b); + + let mut req = [0u8; 128]; + let mut resp = [0u8; 128]; + let mut recv_buf = [0u8; 255]; + + // Register listener on A for MsgType(1) + let req_len = wire::encode_listener(&mut req, 1).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_a, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + let listener_handle = header.handle; + + // Register req on B targeting EID 8 + let req_len = wire::encode_req(&mut req, 8).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_b, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + let req_handle = header.handle; + + // B sends a message via dispatch + let payload = b"dispatch echo!"; + let req_len = wire::encode_send( + &mut req, + Some(req_handle), + 1, + None, + None, + false, + payload, + ) + .unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_b, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + + // Transfer B → A + transfer(&buf_b, &mut server_a); + + // A receives via dispatch + let req_len = wire::encode_recv(&mut req, listener_handle, 0).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_a, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + assert_eq!(header.msg_type, 1); + assert_eq!(header.eid, 42); // remote EID + let recv_payload = wire::get_response_payload(&resp[..resp_len], &header).unwrap(); + assert_eq!(recv_payload, payload); + + // A echoes back via dispatch (response: no handle, set eid + tag) + let req_len = wire::encode_send( + &mut req, + None, + header.msg_type, + Some(header.eid), + Some(header.tag), + false, + recv_payload, + ) + .unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_a, &mut recv_buf); + let send_header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(send_header.is_success()); + + // Transfer A → B + transfer(&buf_a, &mut server_b); + + // B receives the echo via dispatch + let req_len = wire::encode_recv(&mut req, req_handle, 0).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_b, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + assert_eq!(header.msg_type, 1); + assert_eq!(header.eid, 8); // from server A + let echo_payload = wire::get_response_payload(&resp[..resp_len], &header).unwrap(); + assert_eq!(echo_payload, payload); +}
diff --git a/services/mctp/server/tests/echo.rs b/services/mctp/server/tests/echo.rs new file mode 100644 index 0000000..183b8ea --- /dev/null +++ b/services/mctp/server/tests/echo.rs
@@ -0,0 +1,272 @@ +// Licensed under the Apache-2.0 license + +//! MCTP echo integration test. +//! +//! This test exercises the full MCTP server stack with a mock transport, +//! replicating the Hubris `mctp-echo` task behavior: +//! +//! 1. Server A (echo responder): listens for MCTP type-1 messages, echoes payload back +//! 2. Server B (requester): sends a request to A and verifies the echo response +//! +//! The test uses a **client/server partition**: the echo application logic +//! interacts exclusively through the `MctpClient` trait (client side), while +//! the `Server` + transport plumbing is the server side. This mirrors the +//! real deployment where the echo task is an IPC client of the MCTP server. + +use std::cell::RefCell; + +use mctp::{Eid, Tag}; +use mctp_stack::fragment::{Fragmenter, SendOutput}; +use mctp_stack::Sender; +use openprot_mctp_api::{Handle, MctpClient, MctpError, RecvMetadata, ResponseCode}; +use openprot_mctp_server::Server; + +// --------------------------------------------------------------------------- +// Mock transport +// --------------------------------------------------------------------------- + +/// A mock sender that captures outbound packets in a buffer. +struct BufferSender<'a> { + packets: &'a RefCell<Vec<Vec<u8>>>, +} + +impl Sender for BufferSender<'_> { + fn send_vectored( + &mut self, + mut fragmenter: Fragmenter, + payload: &[&[u8]], + ) -> mctp::Result<Tag> { + loop { + let mut buf = [0u8; 255]; + match fragmenter.fragment_vectored(payload, &mut buf) { + SendOutput::Packet(p) => { + self.packets.borrow_mut().push(p.to_vec()); + } + SendOutput::Complete { tag, .. } => return Ok(tag), + SendOutput::Error { err, .. } => return Err(err), + } + } + } + + fn get_mtu(&self) -> usize { + 255 + } +} + +/// Transfer packets from one server's outbound buffer to another server. +fn transfer<S: Sender, const N: usize>( + packets: &RefCell<Vec<Vec<u8>>>, + dest: &mut Server<S, N>, +) { + let pkts = packets.borrow(); + for pkt in pkts.iter() { + dest.inbound(pkt).unwrap(); + } +} + +// --------------------------------------------------------------------------- +// Client-side wrapper (simulates IPC client) +// --------------------------------------------------------------------------- + +/// A direct (in-process) MCTP client that wraps a `Server` via `RefCell`. +/// +/// This plays the same role as the Hubris `mctp_api::Stack` — it provides the +/// `MctpClient` trait interface to application code. In production, IPC would +/// sit between client and server; here we call `Server` methods directly. +struct DirectClient<'a, S: Sender, const N: usize> { + server: &'a RefCell<Server<S, N>>, +} + +impl<'a, S: Sender, const N: usize> DirectClient<'a, S, N> { + fn new(server: &'a RefCell<Server<S, N>>) -> Self { + Self { server } + } +} + +impl<S: Sender, const N: usize> MctpClient for DirectClient<'_, S, N> { + fn req(&self, eid: u8) -> Result<Handle, MctpError> { + self.server.borrow_mut().req(eid) + } + + fn listener(&self, msg_type: u8) -> Result<Handle, MctpError> { + self.server.borrow_mut().listener(msg_type) + } + + fn get_eid(&self) -> u8 { + self.server.borrow().get_eid() + } + + fn set_eid(&self, eid: u8) -> Result<(), MctpError> { + self.server.borrow_mut().set_eid(eid) + } + + fn recv( + &self, + handle: Handle, + _timeout_millis: u32, + buf: &mut [u8], + ) -> Result<RecvMetadata, MctpError> { + self.server + .borrow_mut() + .try_recv(handle, buf) + .ok_or(MctpError::from_code(ResponseCode::TimedOut)) + } + + fn send( + &self, + handle: Option<Handle>, + msg_type: u8, + eid: Option<u8>, + tag: Option<u8>, + integrity_check: bool, + buf: &[u8], + ) -> Result<u8, MctpError> { + self.server + .borrow_mut() + .send(handle, msg_type, eid, tag, integrity_check, buf) + } + + fn drop_handle(&self, handle: Handle) { + let _ = self.server.borrow_mut().unbind(handle); + } +} + +// --------------------------------------------------------------------------- +// Echo application logic (client side — mirrors Hubris mctp-echo task) +// --------------------------------------------------------------------------- + +/// Echo one message: receive on the listener, send the payload back. +/// +/// This is the same logic as `hubris/task/mctp-echo/src/main.rs`: +/// ```ignore +/// let (_, _, msg, mut resp) = listener.recv(&mut recv_buf).unwrap_lite(); +/// resp.send(msg).unwrap(); +/// ``` +/// but expressed through the `MctpClient` trait instead of Hubris IPC types. +fn echo_once(client: &impl MctpClient, listener_handle: Handle) { + let mut recv_buf = [0u8; 255]; + let meta = client + .recv(listener_handle, 0, &mut recv_buf) + .expect("echo: should receive a message"); + + let payload = &recv_buf[..meta.payload_size]; + client + .send( + None, + meta.msg_type, + Some(meta.remote_eid), + Some(meta.msg_tag), + false, + payload, + ) + .expect("echo: should send response"); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// MCTP echo: send a request, receive it on a listener, echo back, verify. +/// +/// This replicates the Hubris mctp-echo task behavior: +/// - EID 8 listens for MsgType(1) and echoes the payload +/// - EID 42 sends a request and checks the response matches +#[test] +fn mctp_echo_roundtrip() { + // -- Server side: set up two MCTP server instances with mock transport -- + let buf_a = RefCell::new(Vec::new()); + let sender_a = BufferSender { packets: &buf_a }; + let server_a: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(8), 0, sender_a)); + + let buf_b = RefCell::new(Vec::new()); + let sender_b = BufferSender { packets: &buf_b }; + let server_b: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(42), 0, sender_b)); + + // -- Client side: wrap servers in DirectClient to use MctpClient trait -- + let client_a = DirectClient::new(&server_a); + let client_b = DirectClient::new(&server_b); + + // Client A: register listener for MsgType(1) — same as echo task + let listener_handle = client_a.listener(1).unwrap(); + + // Client B: get a request handle targeting EID 8 + let req_handle = client_b.req(8).unwrap(); + + // Client B: send a request with MsgType(1) + let payload = b"Hello MCTP echo!"; + let _tag = client_b + .send(Some(req_handle), 1, None, None, false, payload) + .unwrap(); + + // Server side: transfer B's outbound packets to A + transfer(&buf_b, &mut server_a.borrow_mut()); + + // Client A: echo the message back (uses MctpClient trait) + echo_once(&client_a, listener_handle); + + // Server side: transfer A's outbound packets to B + transfer(&buf_a, &mut server_b.borrow_mut()); + + // Client B: receive the echo response (uses MctpClient trait) + let mut resp_buf = [0u8; 255]; + let resp_meta = client_b + .recv(req_handle, 0, &mut resp_buf) + .expect("Client B should have received the echo response"); + + let response = &resp_buf[..resp_meta.payload_size]; + assert_eq!(response, payload, "Echo response should match original payload"); + assert_eq!(resp_meta.msg_type, 1); + assert_eq!(resp_meta.remote_eid, 8); + + // Clean up + client_a.drop_handle(listener_handle); + client_b.drop_handle(req_handle); +} + +/// Test that multiple messages can be echoed in sequence. +#[test] +fn mctp_echo_multiple() { + let buf_a = RefCell::new(Vec::new()); + let sender_a = BufferSender { packets: &buf_a }; + let server_a: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(8), 0, sender_a)); + + let buf_b = RefCell::new(Vec::new()); + let sender_b = BufferSender { packets: &buf_b }; + let server_b: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(42), 0, sender_b)); + + let client_a = DirectClient::new(&server_a); + let client_b = DirectClient::new(&server_b); + + let listener = client_a.listener(1).unwrap(); + let req = client_b.req(8).unwrap(); + + for i in 0..5u8 { + let msg = [i; 32]; + + // Client B: send request + client_b + .send(Some(req), 1, None, None, false, &msg) + .unwrap(); + transfer(&buf_b, &mut server_a.borrow_mut()); + buf_b.borrow_mut().clear(); + + // Client A: echo (uses MctpClient trait) + echo_once(&client_a, listener); + transfer(&buf_a, &mut server_b.borrow_mut()); + buf_a.borrow_mut().clear(); + + // Client B: verify echo response + let mut resp_buf = [0u8; 255]; + let resp = client_b + .recv(req, 0, &mut resp_buf) + .unwrap_or_else(|_| panic!("iteration {i}: no response")); + assert_eq!(&resp_buf[..resp.payload_size], &msg); + } + + client_a.drop_handle(listener); + client_b.drop_handle(req); +}
diff --git a/services/mctp/transport-i2c/Cargo.toml b/services/mctp/transport-i2c/Cargo.toml new file mode 100644 index 0000000..2894858 --- /dev/null +++ b/services/mctp/transport-i2c/Cargo.toml
@@ -0,0 +1,16 @@ +# Licensed under the Apache-2.0 license + +[package] +name = "openprot-mctp-transport-i2c" +version = "0.1.0" +edition = "2021" +description = "MCTP over I2C transport binding for OpenPRoT" +license = "Apache-2.0" + +[dependencies] +openprot-mctp-api = { path = "../api" } +openprot-i2c-api = { path = "../../i2c/api" } +mctp-stack = { git = "https://github.com/9elements/mctp-lib.git", branch = "buildup", package = "mctp-lib" } +mctp = { git = "https://github.com/OpenPRoT/mctp-rs.git", branch = "sync-features", default-features = false } +heapless = { workspace = true } +embedded-hal = { workspace = true }
diff --git a/services/mctp/transport-i2c/README.md b/services/mctp/transport-i2c/README.md new file mode 100644 index 0000000..73da720 --- /dev/null +++ b/services/mctp/transport-i2c/README.md
@@ -0,0 +1,21 @@ +# openprot-mctp-transport-i2c + +I2C transport binding for the MCTP server, ported from Hubris `mctp-server/src/i2c.rs`. + +## Overview + +This crate implements MCTP-over-I2C transport. It provides the `Sender` implementation for outbound packets and a receiver/decoder for inbound I2C target messages. It uses the OpenPRoT `services/i2c/` userspace driver as the underlying I2C transport, replacing the Hubris `drv_i2c_api::I2cDevice`. + +## Key Types + +- `I2cSender<C>` — implements `mctp_stack::Sender` for I2C; handles fragmentation, encoding, and PEC (Packet Error Checking) via `mctp_stack::i2c::MctpI2cEncap` +- `MctpI2cReceiver` — decodes inbound I2C target-mode messages into MCTP packets + +## Dependencies + +- `openprot-mctp-api` — API traits +- `openprot-i2c-api` — `I2cClientBlocking` trait for I2C bus access +- `mctp-stack` — `Sender` trait, I2C encapsulation/decapsulation +- `mctp` — core MCTP types +- `embedded-hal` — hardware abstraction +- `heapless` — `no_std` collections
diff --git a/services/mctp/transport-i2c/src/lib.rs b/services/mctp/transport-i2c/src/lib.rs new file mode 100644 index 0000000..d8a1448 --- /dev/null +++ b/services/mctp/transport-i2c/src/lib.rs
@@ -0,0 +1,26 @@ +// Licensed under the Apache-2.0 license + +//! # MCTP over I2C Transport Binding +//! +//! This crate provides the I2C transport binding for the MCTP server, +//! ported from the Hubris `mctp-server/src/i2c.rs`. +//! +//! It implements [`mctp_stack::Sender`] for outbound MCTP-over-I2C packets +//! and provides [`MctpI2cReceiver`] for decoding inbound I2C target messages +//! into MCTP packets. +//! +//! ## Changes from Hubris +//! +//! - Hubris `drv_i2c_api::I2cDevice` → OpenPRoT `I2cClientBlocking` trait +//! - Hubris `TaskId` → generic `I2cClientBlocking` implementor +//! - All MCTP protocol logic (encoding, fragmentation, PEC) preserved as-is +//! via `mctp_stack::i2c::MctpI2cEncap` + +#![no_std] +#![warn(missing_docs)] + +mod receiver; +mod sender; + +pub use receiver::MctpI2cReceiver; +pub use sender::I2cSender;
diff --git a/services/mctp/transport-i2c/src/receiver.rs b/services/mctp/transport-i2c/src/receiver.rs new file mode 100644 index 0000000..d7107e8 --- /dev/null +++ b/services/mctp/transport-i2c/src/receiver.rs
@@ -0,0 +1,48 @@ +// Licensed under the Apache-2.0 license + +//! I2C MCTP receiver — inbound transport binding. +//! +//! Decodes incoming I2C target-mode messages into raw MCTP packets +//! that can be fed to `Server::inbound()`. +//! +//! This corresponds to the `handle_i2c_transport` function in Hubris +//! `mctp-server/src/main.rs`, using `mctp_stack::i2c::MctpI2cEncap` +//! for decoding (same as Hubris). + +use mctp_stack::i2c::MctpI2cEncap; +use openprot_i2c_api::TargetMessage; + +/// Decodes I2C target messages into raw MCTP packets. +/// +/// Wraps the `mctp_stack::i2c::MctpI2cEncap` decoder. One instance +/// should exist per I2C bus carrying MCTP traffic. +pub struct MctpI2cReceiver { + encap: MctpI2cEncap, +} + +impl MctpI2cReceiver { + /// Create a new receiver for the given own I2C address. + pub fn new(own_addr: u8) -> Self { + Self { + encap: MctpI2cEncap::new(own_addr), + } + } + + /// Decode an I2C target message into a raw MCTP packet. + /// + /// Strips the MCTP-I2C transport header and validates PEC. + /// Returns the raw MCTP packet bytes (suitable for `Server::inbound()`) + /// and the I2C source address, or an error if decoding fails. + /// + /// This is the same decode path as Hubris `handle_i2c_transport`: + /// `i2c_reader.recv(data)` → `server.stack.inbound(pkt)`. + pub fn decode<'a>( + &self, + msg: &'a TargetMessage, + ) -> Result<(&'a [u8], u8), mctp::Error> { + let data = msg.data(); + // MctpI2cEncap::decode strips the I2C header, validates PEC, + // and returns the raw MCTP packet + source I2C address. + self.encap.decode(data, true) + } +}
diff --git a/services/mctp/transport-i2c/src/sender.rs b/services/mctp/transport-i2c/src/sender.rs new file mode 100644 index 0000000..90d01fc --- /dev/null +++ b/services/mctp/transport-i2c/src/sender.rs
@@ -0,0 +1,86 @@ +// Licensed under the Apache-2.0 license + +//! I2C MCTP sender — outbound transport binding. +//! +//! Direct port of Hubris `mctp-server/src/i2c.rs` `I2cSender`. +//! Only the I2C driver API is replaced: `drv_i2c_api::I2cDevice` → +//! `openprot_i2c_api::I2cClientBlocking`. + +use mctp::Result; +use mctp_stack::i2c::{MctpI2cEncap, MCTP_I2C_MAXMTU}; +use openprot_i2c_api::{BusIndex, I2cAddress, I2cClientBlocking}; + +/// I2C MCTP sender. +/// +/// Implements `mctp_stack::Sender` to fragment and send MCTP packets +/// over I2C using the OpenPRoT I2C client API. +/// +/// This is a direct port of the Hubris `I2cSender`. The fragmentation +/// loop, I2C encoding via `MctpI2cEncap`, and error mapping are preserved +/// as-is. Only the I2C write call is changed from `drv_i2c_api::I2cDevice::write` +/// to `I2cClientBlocking::write`. +pub struct I2cSender<C: I2cClientBlocking> { + i2c: C, + bus: BusIndex, + own_addr: u8, + // TODO: neighbor table mapping EID → I2C address + // see https://github.com/OpenPRoT/mctp-lib/issues/4 + // For now, destination address is hardcoded (same as Hubris). +} + +impl<C: I2cClientBlocking> I2cSender<C> { + /// Create a new I2C sender. + /// + /// * `i2c` - I2C client for bus writes + /// * `bus` - I2C bus index to use + /// * `own_addr` - Own I2C address (7-bit, used in MCTP-I2C header) + pub fn new(i2c: C, bus: BusIndex, own_addr: u8) -> Self { + Self { + i2c, + bus, + own_addr, + } + } +} + +impl<C: I2cClientBlocking> mctp_stack::Sender for I2cSender<C> { + fn send_vectored( + &mut self, + mut fragmenter: mctp_stack::fragment::Fragmenter, + payload: &[&[u8]], + ) -> Result<mctp::Tag> { + // TODO The stack needs to provide the destination EID to the sender + // let addr = self + // .neighbor_table + // .get(eid) + // .ok_or(mctp::Error::Unreachable)?; + let addr = 0x42; + let dest_address = I2cAddress::new_unchecked(addr); + let encoder = MctpI2cEncap::new(self.own_addr); + + loop { + let mut pkt = [0u8; mctp_stack::serial::MTU_MAX]; + let r = fragmenter.fragment_vectored(payload, &mut pkt); + + match r { + mctp_stack::fragment::SendOutput::Packet(p) => { + let mut out = [0; MCTP_I2C_MAXMTU + 8]; // max MTU + I2C header size + let _packet = encoder.encode(addr, p, &mut out, true)?; + self.i2c + .write(self.bus, dest_address, &out) + .map_err(|_| mctp::Error::TxFailure)?; + } + mctp_stack::fragment::SendOutput::Complete { tag, .. } => { + break Ok(tag); + } + mctp_stack::fragment::SendOutput::Error { err, .. } => { + break Err(err); + } + } + } + } + + fn get_mtu(&self) -> usize { + MCTP_I2C_MAXMTU + } +}