From 5269fc43ca5241f91419fd494d289c4b94e4ea1d Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Thu, 28 May 2026 09:25:07 -0700 Subject: [PATCH 1/4] feat(mctp-rs): add canonical ODP wire format under `odp` feature Adds an `odp` module gated behind the `odp` cargo feature, containing: - `ODP_MESSAGE_TYPE = 0x7D` constant - `OdpService` enum with `from_u8` decoder - `OdpHeader` as a 4-byte BE bitfield with round-trip serde, unknown- service rejection, and an `impl MctpMessageHeaderTrait` - `OdpMessage` envelope with body and `serialize_with_header` helper - Snapshot tests asserting the new types produce byte-identical wire output to the legacy hand-rolled encoders in battery-service-relay, thermal-service-relay, and time-alarm-service-relay This is the canonical wire-format crate for ODP. Consumers downstream in `odp-client` and the relay service crates will migrate to it, removing three duplicated copies of the same logic. --- mctp-rs/Cargo.toml | 3 +- mctp-rs/src/message_type/mod.rs | 2 + mctp-rs/src/message_type/odp.rs | 332 ++++++++++++++++++++++++++++++++ 3 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 mctp-rs/src/message_type/odp.rs diff --git a/mctp-rs/Cargo.toml b/mctp-rs/Cargo.toml index 7d16b1c5..6cca8407 100644 --- a/mctp-rs/Cargo.toml +++ b/mctp-rs/Cargo.toml @@ -10,8 +10,9 @@ ignored = ["embedded-batteries", "espi-device", "uuid", "crc"] [features] default = [] -espi = ["dep:espi-device"] defmt = ["dep:defmt", "embedded-batteries/defmt"] +espi = ["dep:espi-device"] +odp = [] serial = ["dep:crc"] [dependencies] diff --git a/mctp-rs/src/message_type/mod.rs b/mctp-rs/src/message_type/mod.rs index bb356dc1..28e3c0fc 100644 --- a/mctp-rs/src/message_type/mod.rs +++ b/mctp-rs/src/message_type/mod.rs @@ -1,4 +1,6 @@ mod mctp_control; +#[cfg(feature = "odp")] +pub mod odp; mod vendor_defined_pci; pub use mctp_control::*; diff --git a/mctp-rs/src/message_type/odp.rs b/mctp-rs/src/message_type/odp.rs new file mode 100644 index 00000000..40ab0d12 --- /dev/null +++ b/mctp-rs/src/message_type/odp.rs @@ -0,0 +1,332 @@ +//! ODP (Open Device Partnership) MCTP message type. +//! +//! Wire format defined by the ODP project. Message type byte `0x7D`. +//! Header layout: 32-bit bitfield, big-endian on the wire. + +use crate::{MctpMedium, MctpMessageHeaderTrait, MctpMessageTrait, MctpPacketError, error::MctpPacketResult}; +use bit_register::bit_register; + +/// MCTP message type byte assigned to ODP traffic. +pub const ODP_MESSAGE_TYPE: u8 = 0x7D; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum OdpService { + Battery = 0x08, + Thermal = 0x09, + TimeAlarm = 0x0B, +} + +impl OdpService { + pub fn from_u8(raw: u8) -> Option { + match raw { + 0x08 => Some(Self::Battery), + 0x09 => Some(Self::Thermal), + 0x0B => Some(Self::TimeAlarm), + _ => None, + } + } +} + +bit_register! { + #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] + pub struct OdpHeaderWireFormat: little_endian u32 { + pub message_id: u16 => [0:14], + pub is_error: u8 => [15], + pub service_id: u8 => [16:23], + pub _reserved_b24: u8 => [24], + pub is_request: u8 => [25], + pub _reserved_b26_31: u8 => [26:31], + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct OdpHeader { + pub is_request: bool, + pub service: OdpService, + pub is_error: bool, + /// Caller-supplied 15-bit message identifier. Values above `0x7FFF` + /// are silently masked on serialize (matching legacy behavior in + /// `odp-platform-common::ec-test-lib::serial.rs:106`). + pub message_id: u16, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct UnknownService(pub u8); + +impl OdpHeader { + pub const HEADER_LEN: usize = 4; + + pub fn to_be_bytes(self) -> [u8; 4] { + let w = OdpHeaderWireFormat { + is_request: self.is_request as u8, + service_id: self.service as u8, + is_error: self.is_error as u8, + message_id: self.message_id & 0x7FFF, + ..Default::default() + }; + let raw: u32 = w.try_into().expect("reserved fields are zero; cannot overflow"); + raw.to_be_bytes() + } + + pub fn from_be_bytes(bytes: [u8; 4]) -> Result { + let raw = u32::from_be_bytes(bytes); + let w: OdpHeaderWireFormat = raw + .try_into() + .expect("primitive fields u8/u16 always parse from any u32"); + let raw_service = w.service_id; + let service = OdpService::from_u8(raw_service).ok_or(UnknownService(raw_service))?; + Ok(Self { + is_request: w.is_request != 0, + service, + is_error: w.is_error != 0, + message_id: w.message_id, + }) + } +} + +impl MctpMessageHeaderTrait for OdpHeader { + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + if buffer.len() < Self::HEADER_LEN { + return Err(MctpPacketError::SerializeError("buffer too small for ODP header")); + } + buffer[..Self::HEADER_LEN].copy_from_slice(&self.to_be_bytes()); + Ok(Self::HEADER_LEN) + } + + fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M> { + if buffer.len() < Self::HEADER_LEN { + return Err(MctpPacketError::HeaderParseError("buffer too small for ODP header")); + } + let mut b = [0u8; 4]; + b.copy_from_slice(&buffer[..Self::HEADER_LEN]); + let header = Self::from_be_bytes(b).map_err(|_| MctpPacketError::HeaderParseError("unknown ODP service id"))?; + Ok((header, &buffer[Self::HEADER_LEN..])) + } +} + +/// An ODP message: a typed header followed by an opaque body payload. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct OdpMessage<'a> { + pub header: OdpHeader, + pub body: &'a [u8], +} + +impl<'a> OdpMessage<'a> { + /// Serialize the full ODP wire packet (header + body) into `out`. + /// + /// Returns the total number of bytes written (`OdpHeader::HEADER_LEN + + /// self.body.len()`) on success, or `MctpPacketError::SerializeError` if + /// the output buffer is too small. + /// + /// This is the convenience entry point for callers that hold a complete + /// `OdpMessage` and want a single call to produce on-wire bytes. The + /// framework-level trait method, `::serialize`, + /// is body-only — the header is serialized separately by the packet framing + /// layer when using the MCTP trait infrastructure. See `MctpMessageTrait` + /// in `mctp-rs/src/message_type/mod.rs` for the framework pattern. + pub fn serialize_with_header(&self, out: &mut [u8]) -> MctpPacketResult { + let needed = OdpHeader::HEADER_LEN + self.body.len(); + if out.len() < needed { + return Err(MctpPacketError::SerializeError("buffer too small for ODP message")); + } + out[..OdpHeader::HEADER_LEN].copy_from_slice(&self.header.to_be_bytes()); + out[OdpHeader::HEADER_LEN..needed].copy_from_slice(self.body); + Ok(needed) + } +} + +/// Body-only serialization per the `MctpMessageTrait` framework contract. +/// The header is serialized separately at the packet framing layer. +/// Use [`OdpMessage::serialize_with_header`] for a single-call full-packet +/// serialization. +impl<'buf> MctpMessageTrait<'buf> for OdpMessage<'buf> { + type Header = OdpHeader; + const MESSAGE_TYPE: u8 = ODP_MESSAGE_TYPE; + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + if buffer.len() < self.body.len() { + return Err(MctpPacketError::SerializeError("buffer too small for ODP message body")); + } + buffer[..self.body.len()].copy_from_slice(self.body); + Ok(self.body.len()) + } + + fn deserialize(header: &Self::Header, buffer: &'buf [u8]) -> MctpPacketResult { + Ok(OdpMessage { + header: *header, + body: buffer, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn message_type_constant_is_0x7d() { + assert_eq!(ODP_MESSAGE_TYPE, 0x7D); + } + + #[test] + fn odp_service_known_codes_round_trip() { + for (raw, service) in [ + (0x08, OdpService::Battery), + (0x09, OdpService::Thermal), + (0x0B, OdpService::TimeAlarm), + ] { + assert_eq!(OdpService::from_u8(raw), Some(service)); + assert_eq!(service as u8, raw); + } + } + + #[test] + fn odp_service_unknown_code_is_none() { + assert_eq!(OdpService::from_u8(0xFF), None); + } + + #[test] + fn odp_header_round_trip_via_wire_bytes() { + let original = OdpHeader { + is_request: true, + service: OdpService::Battery, + is_error: false, + message_id: 0x1234, + }; + let bytes = original.to_be_bytes(); + let parsed = OdpHeader::from_be_bytes(bytes).expect("known service"); + assert_eq!(parsed, original); + } + + #[test] + fn odp_header_bit_layout_is_stable() { + // Snapshot matches odp-platform-common::ec-test-lib::serial.rs:509: + // build_odp_header(is_request=true, service=Battery=0x08, msg_id=2) + // => [0x02, 0x08, 0x00, 0x02] + let h = OdpHeader { + is_request: true, + service: OdpService::Battery, + is_error: false, + message_id: 2, + }; + assert_eq!(h.to_be_bytes(), [0x02, 0x08, 0x00, 0x02]); + } + + #[test] + fn odp_header_response_round_trip() { + // is_request=false (response), is_error=false, msg_id=2 — mirrors serial.rs:530. + let h = OdpHeader { + is_request: false, + service: OdpService::Battery, + is_error: false, + message_id: 2, + }; + let parsed = OdpHeader::from_be_bytes(h.to_be_bytes()).expect("known service"); + assert_eq!(parsed, h); + } + + #[test] + fn odp_header_rejects_unknown_service() { + // service_id 0xFF is not in the known set. + let raw_u32: u32 = (1 << 25) | (0xFF << 16) | 1; + let bytes = raw_u32.to_be_bytes(); + assert!(matches!(OdpHeader::from_be_bytes(bytes), Err(UnknownService(0xFF)))); + } + + #[test] + fn odp_message_serializes_header_then_body() { + use crate::test_util::TestMedium; + let msg = OdpMessage { + header: OdpHeader { + service: OdpService::Battery, + is_error: false, + message_id: 7, + is_request: true, + }, + body: &[0xDE, 0xAD, 0xBE, 0xEF], + }; + let mut out = [0u8; 32]; + let n = msg.serialize_with_header::(&mut out).unwrap(); + assert_eq!(n, OdpHeader::HEADER_LEN + 4); + assert_eq!(&out[..OdpHeader::HEADER_LEN], &msg.header.to_be_bytes()); + assert_eq!( + &out[OdpHeader::HEADER_LEN..OdpHeader::HEADER_LEN + 4], + &[0xDE, 0xAD, 0xBE, 0xEF] + ); + } + + #[test] + fn odp_header_implements_mctp_message_header_trait() { + use crate::MctpMessageHeaderTrait; + use crate::test_util::TestMedium; + + let h = OdpHeader { + is_request: true, + service: OdpService::Battery, + is_error: false, + message_id: 0x42, + }; + let mut buf = [0u8; 4]; + let written = h.serialize::(&mut buf).unwrap(); + assert_eq!(written, 4); + let (parsed, rest) = OdpHeader::deserialize::(&buf).unwrap(); + assert_eq!(rest.len(), 0); + assert_eq!(parsed, h); + } + + #[test] + fn wire_snapshot_matches_legacy_impls_battery_get_bst() { + // Source of truth: odp-platform-common/ec/test-lib/src/serial.rs:505-510 + // build_odp_header(is_request=true, service=Battery=0x08, msg_id=2) + // produces [0x02, 0x08, 0x00, 0x02]. + let h = OdpHeader { + is_request: true, + service: OdpService::Battery, + is_error: false, + message_id: 2, + }; + assert_eq!(h.to_be_bytes(), [0x02, 0x08, 0x00, 0x02]); + } + + #[test] + fn wire_snapshot_matches_legacy_impls_thermal_request() { + // Source: serial.rs:513-516. + let h = OdpHeader { + is_request: true, + service: OdpService::Thermal, + is_error: false, + message_id: 1, + }; + assert_eq!(h.to_be_bytes(), [0x02, 0x09, 0x00, 0x01]); + } + + #[test] + fn wire_snapshot_matches_legacy_impls_time_alarm_15bit_id() { + // Source: serial.rs:520-526. service_id=0x0B, msg_id=0x1234. + let h = OdpHeader { + is_request: true, + service: OdpService::TimeAlarm, + is_error: false, + message_id: 0x1234, + }; + let bytes = h.to_be_bytes(); + let parsed = OdpHeader::from_be_bytes(bytes).expect("known service"); + assert_eq!(parsed, h); + // raw_u32 = (1<<25) | (0x0B<<16) | 0x1234 = 0x020B1234 → [0x02, 0x0B, 0x12, 0x34] + assert_eq!(bytes, [0x02, 0x0B, 0x12, 0x34]); + } + + #[test] + fn wire_snapshot_battery_response() { + // Source: serial.rs:530-538. is_request=false, service=Battery, msg_id=2. + let h = OdpHeader { + is_request: false, + service: OdpService::Battery, + is_error: false, + message_id: 2, + }; + // raw_u32 = (0<<25) | (0x08<<16) | 2 = 0x00080002 → [0x00, 0x08, 0x00, 0x02] + assert_eq!(h.to_be_bytes(), [0x00, 0x08, 0x00, 0x02]); + } +} From 0b25ce11844ecbf5f0c7775a01345f14f6637360 Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Thu, 28 May 2026 09:25:12 -0700 Subject: [PATCH 2/4] feat(odp-client): add transport-blind sync ODP client crate New `odp-client/` crate exposing: - `OdpTransport` trait: byte-in / byte-out, no mctp-rs in the public surface - `OdpClient` + `Relay` trait: encode an ODP request, send it through the transport, receive the response, validate `message_id` round-trip, return decoded body - `MctpSerialTransport`: production transport that wraps mctp-rs's serial framing - `LoopbackTransport`: in-memory test double - `OdpError`: structured error enum (Copy, with a typed `UnexpectedMessageId { expected, got }` variant) Per-service code can now depend on `odp-client` and `R: Relay` instead of touching `mctp-rs` or the transport directly. Swapping the underlying medium does not affect call sites. --- Cargo.lock | 10 ++ Cargo.toml | 2 + odp-client/Cargo.toml | 16 +++ odp-client/src/client.rs | 98 ++++++++++++++++++ odp-client/src/error.rs | 36 +++++++ odp-client/src/lib.rs | 19 ++++ odp-client/src/loopback.rs | 55 ++++++++++ odp-client/src/mctp_serial.rs | 167 ++++++++++++++++++++++++++++++ odp-client/src/server.rs | 1 + odp-client/src/transport.rs | 21 ++++ odp-client/tests/client_invoke.rs | 95 +++++++++++++++++ odp-client/tests/error.rs | 26 +++++ odp-client/tests/loopback.rs | 49 +++++++++ odp-client/tests/mctp_serial.rs | 74 +++++++++++++ odp-client/tests/transport.rs | 44 ++++++++ 15 files changed, 713 insertions(+) create mode 100644 odp-client/Cargo.toml create mode 100644 odp-client/src/client.rs create mode 100644 odp-client/src/error.rs create mode 100644 odp-client/src/lib.rs create mode 100644 odp-client/src/loopback.rs create mode 100644 odp-client/src/mctp_serial.rs create mode 100644 odp-client/src/server.rs create mode 100644 odp-client/src/transport.rs create mode 100644 odp-client/tests/client_invoke.rs create mode 100644 odp-client/tests/error.rs create mode 100644 odp-client/tests/loopback.rs create mode 100644 odp-client/tests/mctp_serial.rs create mode 100644 odp-client/tests/transport.rs diff --git a/Cargo.lock b/Cargo.lock index 1671e7da..f69972e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1394,6 +1394,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "odp-client" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embedded-io 0.7.1", + "heapless 0.9.3", + "mctp-rs", +] + [[package]] name = "odp-service-common" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 404f79a3..6ba386ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "debug-service-messages", "keyboard-service", "power-policy-interface", + "odp-client", "odp-service-common", "type-c-interface", "fw-update-interface", @@ -79,6 +80,7 @@ debug-service-messages = { path = "./debug-service-messages" } embassy-executor = "0.10.0" cfu-service = { path = "./cfu-service" } embedded-hal-nb = "1.0" +embedded-io = "0.7" embedded-io-async = "0.7.0" embedded-mcu-hal = "0.2.0" embassy-futures = "0.1.2" diff --git a/odp-client/Cargo.toml b/odp-client/Cargo.toml new file mode 100644 index 00000000..76b82cb9 --- /dev/null +++ b/odp-client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "odp-client" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Sync ODP MCTP client + async server macro. Hides mctp-rs from consumers." + +[features] +default = [] +defmt = ["mctp-rs/defmt", "dep:defmt"] + +[dependencies] +defmt = { workspace = true, optional = true } +embedded-io = { workspace = true } +heapless = { workspace = true } +mctp-rs = { workspace = true, features = ["odp", "serial"] } diff --git a/odp-client/src/client.rs b/odp-client/src/client.rs new file mode 100644 index 00000000..977ffaf4 --- /dev/null +++ b/odp-client/src/client.rs @@ -0,0 +1,98 @@ +use crate::{OdpError, OdpHeader, OdpTransport}; + +/// Decoded ODP response handed to the parse closure inside [`Relay::invoke`]. +/// +/// The body slice borrows from the client's internal assembly buffer; only +/// owned values can escape the closure. +pub struct OdpResponse<'a> { + pub header: OdpHeader, + pub body: &'a [u8], +} + +/// Transport-blind abstraction over "issue a request to the EC and parse the +/// response". Per-service code depends on `R: Relay`, never on a concrete +/// `OdpClient`. +/// +/// The closure pattern is required because the response body slice borrows +/// from the client's internal buffer; parsing inside the closure guarantees +/// only owned types escape. +/// +/// # Object safety +/// +/// `invoke` is generic over `R` (return type) and `F` (closure type), so +/// `Relay` is **not** object-safe. Use it as a bound (`impl Relay` / +/// `R: Relay`), not as `dyn Relay`. +pub trait Relay { + /// Encode `header` + `body` as an ODP wire frame, hand them to the + /// underlying transport, receive the response, validate the + /// `message_id` round-trip, and pass an [`OdpResponse`] to `parse`. + /// + /// The parse closure runs inside `invoke` so that the borrow of the + /// client's internal buffer is confined to the closure scope; any + /// owned value returned from the closure escapes cleanly. + fn invoke( + &mut self, + header: OdpHeader, + body: &[u8], + parse: F, + ) -> Result + where + F: FnOnce(OdpResponse<'_>) -> Result; +} + +/// Sync ODP-over-transport client. +/// +/// Owns the transport and a 256-byte assembly buffer used for both request +/// encoding and response decoding (sequentially — the phases never overlap +/// because send completes before recv begins). +pub struct OdpClient { + transport: T, + buf: [u8; 256], +} + +impl OdpClient { + /// Create a new client, consuming `transport` by value. + pub fn new(transport: T) -> Self { + Self { transport, buf: [0u8; 256] } + } +} + +impl Relay for OdpClient { + fn invoke( + &mut self, + header: OdpHeader, + body: &[u8], + parse: F, + ) -> Result + where + F: FnOnce(OdpResponse<'_>) -> Result, + { + let need = 4 + body.len(); + if need > self.buf.len() { + return Err(OdpError::BufferTooSmall); + } + + // Compose request: 4-byte BE header + body. + self.buf[..4].copy_from_slice(&header.to_be_bytes()); + self.buf[4..need].copy_from_slice(body); + self.transport.send_message(&self.buf[..need])?; + + // Receive response into the same buffer (send is complete). + let n = self.transport.recv_message(&mut self.buf)?; + if n < 4 { + return Err(OdpError::Decode); + } + let mut hb = [0u8; 4]; + hb.copy_from_slice(&self.buf[..4]); + let resp_header = OdpHeader::from_be_bytes(hb).map_err(|_| OdpError::Decode)?; + + if resp_header.message_id != header.message_id { + return Err(OdpError::UnexpectedMessageId { + expected: header.message_id, + got: resp_header.message_id, + }); + } + + parse(OdpResponse { header: resp_header, body: &self.buf[4..n] }) + } +} diff --git a/odp-client/src/error.rs b/odp-client/src/error.rs new file mode 100644 index 00000000..edeed259 --- /dev/null +++ b/odp-client/src/error.rs @@ -0,0 +1,36 @@ +/// Errors surfaced by the `odp-client` API. +/// +/// The transport layer maps lower-level MCTP / framing errors into these +/// variants so that callers see one consistent error surface and don't have +/// to depend on `mctp-rs` directly. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum OdpError { + /// Underlying transport (serial / MCTP framing) reported a failure. + Transport, + /// Timed out waiting for a response from the peer. + Timeout, + /// Caller-supplied buffer was too small to hold the message. + BufferTooSmall, + /// Peer responded with a different `OdpService` than the request. + UnexpectedResponseKind, + /// Peer responded with a different `message_id` than the request. + UnexpectedMessageId { expected: u16, got: u16 }, + /// Failed to decode an incoming ODP message (malformed header / body). + Decode, +} + +#[cfg(feature = "defmt")] +impl defmt::Format for OdpError { + fn format(&self, f: defmt::Formatter) { + match self { + OdpError::Transport => defmt::write!(f, "OdpError::Transport"), + OdpError::Timeout => defmt::write!(f, "OdpError::Timeout"), + OdpError::BufferTooSmall => defmt::write!(f, "OdpError::BufferTooSmall"), + OdpError::UnexpectedResponseKind => defmt::write!(f, "OdpError::UnexpectedResponseKind"), + OdpError::UnexpectedMessageId { expected, got } => { + defmt::write!(f, "OdpError::UnexpectedMessageId {{ expected: {}, got: {} }}", expected, got) + } + OdpError::Decode => defmt::write!(f, "OdpError::Decode"), + } + } +} diff --git a/odp-client/src/lib.rs b/odp-client/src/lib.rs new file mode 100644 index 00000000..e8fbc7a4 --- /dev/null +++ b/odp-client/src/lib.rs @@ -0,0 +1,19 @@ +#![no_std] +//! Sync ODP client + transport abstraction. +//! +//! Consumers depend on this crate, NOT on `mctp-rs` directly. The wire format +//! lives in `mctp_rs::odp` and is re-exported here for convenience. + +pub use mctp_rs::odp::{ODP_MESSAGE_TYPE, OdpHeader, OdpMessage, OdpService}; +pub use error::OdpError; +pub use transport::OdpTransport; +pub use mctp_serial::MctpSerialTransport; +pub use loopback::LoopbackTransport; +pub use client::{OdpClient, OdpResponse, Relay}; + +mod client; +mod error; +mod loopback; +mod mctp_serial; +mod server; +mod transport; diff --git a/odp-client/src/loopback.rs b/odp-client/src/loopback.rs new file mode 100644 index 00000000..9714d116 --- /dev/null +++ b/odp-client/src/loopback.rs @@ -0,0 +1,55 @@ +use crate::{OdpError, OdpTransport}; +use heapless::Vec; + +/// In-memory test double for [`OdpTransport`]. Echoes what was sent on +/// the next [`OdpTransport::recv_message`] call. +/// +/// Useful for unit testing per-service client logic without standing +/// up a real UART. Capacity is fixed at 256 bytes — large enough for +/// any single ODP message we expect to round-trip in tests. +/// +/// # Lifecycle +/// +/// - `send_message(payload)` replaces any previously buffered bytes +/// with `payload`. +/// - `recv_message(buf)` copies the buffered bytes into `buf` and +/// clears the internal buffer. A subsequent `recv_message` without +/// an intervening `send_message` returns [`OdpError::Transport`]. +pub struct LoopbackTransport { + queued: Vec, +} + +impl LoopbackTransport { + pub fn new() -> Self { + Self { queued: Vec::new() } + } +} + +impl Default for LoopbackTransport { + fn default() -> Self { + Self::new() + } +} + +impl OdpTransport for LoopbackTransport { + fn send_message(&mut self, payload: &[u8]) -> Result<(), OdpError> { + self.queued.clear(); + self.queued + .extend_from_slice(payload) + .map_err(|_| OdpError::BufferTooSmall)?; + Ok(()) + } + + fn recv_message(&mut self, buf: &mut [u8]) -> Result { + if self.queued.is_empty() { + return Err(OdpError::Transport); + } + if buf.len() < self.queued.len() { + return Err(OdpError::BufferTooSmall); + } + let n = self.queued.len(); + buf[..n].copy_from_slice(&self.queued); + self.queued.clear(); + Ok(n) + } +} diff --git a/odp-client/src/mctp_serial.rs b/odp-client/src/mctp_serial.rs new file mode 100644 index 00000000..868460d5 --- /dev/null +++ b/odp-client/src/mctp_serial.rs @@ -0,0 +1,167 @@ +use crate::{OdpError, OdpTransport, ODP_MESSAGE_TYPE}; +use embedded_io::{Read, ReadExactError, Write}; +use mctp_rs::{ + EndpointId, MctpMedium, MctpMessageHeaderTrait, MctpMessageTag, MctpMessageTrait, + MctpPacketContext, MctpPacketError, MctpPacketResult, MctpReplyContext, MctpSequenceNumber, + MctpSerialMedium, +}; + +/// MCTP serial framing END flag (DSP0253 0x7E). +const SERIAL_END_FLAG: u8 = 0x7E; + +/// Inner buffer sizes — match the legacy `ec_relay.rs` which has been +/// proven to fit single-packet ODP messages. +const ASSEMBLY_BUF_LEN: usize = 128; +const RX_FRAME_BUF_LEN: usize = 128; + +struct RawHeader([u8; 4]); + +impl MctpMessageHeaderTrait for RawHeader { + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + if buffer.len() < 4 { + return Err(MctpPacketError::SerializeError("buffer too small for odp header")); + } + buffer[..4].copy_from_slice(&self.0); + Ok(4) + } + + fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M> { + if buffer.len() < 4 { + return Err(MctpPacketError::HeaderParseError("buffer too small for odp header")); + } + let mut h = [0u8; 4]; + h.copy_from_slice(&buffer[..4]); + Ok((RawHeader(h), &buffer[4..])) + } +} + +struct RawBody<'b>(&'b [u8]); + +impl<'buf> MctpMessageTrait<'buf> for RawBody<'buf> { + type Header = RawHeader; + const MESSAGE_TYPE: u8 = ODP_MESSAGE_TYPE; + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + let n = self.0.len(); + if buffer.len() < n { + return Err(MctpPacketError::SerializeError("buffer too small for odp body")); + } + buffer[..n].copy_from_slice(self.0); + Ok(n) + } + + fn deserialize(_h: &RawHeader, buffer: &'buf [u8]) -> MctpPacketResult { + Ok(RawBody(buffer)) + } +} + +fn reply_context(src: EndpointId, dst: EndpointId) -> MctpReplyContext { + MctpReplyContext { + source_endpoint_id: src, + destination_endpoint_id: dst, + packet_sequence_number: MctpSequenceNumber::new(0), + message_tag: MctpMessageTag::try_from(0).expect("tag 0 always valid"), + medium_context: (), + } +} + +/// MCTP-over-serial implementation of [`OdpTransport`]. +/// +/// Owns the UART and an internal MCTP packet-assembly buffer. The MCTP +/// framing (per DSP0253: revision byte, byte_count, body, FCS-16, end +/// flag 0x7E) is encapsulated entirely inside this type — callers hand +/// in raw ODP wire bytes (4-byte header + body) and receive raw ODP +/// wire bytes back. +/// +/// # Endpoint IDs +/// +/// The transport is configured at construction time with `src_eid` and +/// `dst_eid` used in the MCTP header of outbound frames. Inbound frames +/// are MCTP-decoded but the EIDs are not validated against expectation +/// (the serial medium has no addressing of its own; the loopback in +/// tests and production callers tolerate whichever EID pair the peer +/// returns). +pub struct MctpSerialTransport { + uart: U, + src_eid: EndpointId, + dst_eid: EndpointId, + rx_frame_buf: [u8; RX_FRAME_BUF_LEN], + assembly_buf: [u8; ASSEMBLY_BUF_LEN], +} + +impl MctpSerialTransport { + /// Create a new transport wrapping `uart`, using `src_eid` / `dst_eid` + /// in the MCTP header of every outbound frame. All MCTP framing + /// (DSP0253 serial encapsulation) is handled internally; callers only + /// see raw ODP wire bytes. + pub fn new(uart: U, src_eid: EndpointId, dst_eid: EndpointId) -> Self { + Self { + uart, + src_eid, + dst_eid, + rx_frame_buf: [0u8; RX_FRAME_BUF_LEN], + assembly_buf: [0u8; ASSEMBLY_BUF_LEN], + } + } +} + +impl OdpTransport for MctpSerialTransport { + fn send_message(&mut self, payload: &[u8]) -> Result<(), OdpError> { + if payload.len() < 4 { + return Err(OdpError::BufferTooSmall); + } + let mut header = [0u8; 4]; + header.copy_from_slice(&payload[..4]); + let body = &payload[4..]; + + // Use a local tx buffer so `assembly_buf` stays free for recv. + let mut tx_buf = [0u8; ASSEMBLY_BUF_LEN]; + let mut tx_ctx = MctpPacketContext::::new(MctpSerialMedium, &mut tx_buf); + let reply_ctx = reply_context(self.src_eid, self.dst_eid); + let mut state = tx_ctx + .serialize_packet(reply_ctx, (RawHeader(header), RawBody(body))) + .map_err(|_| OdpError::Transport)?; + while let Some(pkt) = state.next() { + let pkt = pkt.map_err(|_| OdpError::Transport)?; + self.uart.write_all(pkt).map_err(|_| OdpError::Transport)?; + } + Ok(()) + } + + fn recv_message(&mut self, buf: &mut [u8]) -> Result { + // 1. Read serial frame (bytes until 0x7E) into rx_frame_buf. + let mut filled = 0usize; + loop { + if filled >= self.rx_frame_buf.len() { + return Err(OdpError::BufferTooSmall); + } + let mut byte = [0u8; 1]; + match self.uart.read_exact(&mut byte) { + Ok(()) => {} + Err(ReadExactError::UnexpectedEof) => return Err(OdpError::Transport), + Err(ReadExactError::Other(_)) => return Err(OdpError::Transport), + } + self.rx_frame_buf[filled] = byte[0]; + filled += 1; + if byte[0] == SERIAL_END_FLAG { + break; + } + } + + // 2. MCTP-strip via a fresh PacketContext borrowing assembly_buf. + let mut rx_ctx = + MctpPacketContext::::new(MctpSerialMedium, &mut self.assembly_buf); + let message = rx_ctx + .deserialize_packet(&self.rx_frame_buf[..filled]) + .map_err(|_| OdpError::Decode)? + .ok_or(OdpError::Decode)?; + + // 3. Copy out the MCTP body (= ODP header+body wire bytes). + let body = message.message_buffer.body(); + if buf.len() < body.len() { + return Err(OdpError::BufferTooSmall); + } + buf[..body.len()].copy_from_slice(body); + Ok(body.len()) + } +} diff --git a/odp-client/src/server.rs b/odp-client/src/server.rs new file mode 100644 index 00000000..02e7ed1d --- /dev/null +++ b/odp-client/src/server.rs @@ -0,0 +1 @@ +// Stub module — populated by subsequent PLAN-006 tasks. diff --git a/odp-client/src/transport.rs b/odp-client/src/transport.rs new file mode 100644 index 00000000..d2079d0d --- /dev/null +++ b/odp-client/src/transport.rs @@ -0,0 +1,21 @@ +use crate::OdpError; + +/// Carries a single fully-framed ODP message over the wire. +/// +/// Implementations encapsulate any MCTP framing internally. The public +/// surface is bytes only — callers do not need to know whether the +/// underlying transport is MCTP-over-serial, MCTP-over-eSPI, or anything +/// else. This is the seam that lets us swap transports without rewriting +/// callers. +pub trait OdpTransport { + /// Send one fully-framed ODP message (header + body, wire bytes). + /// + /// Returns once the message has been handed to the transport. + fn send_message(&mut self, payload: &[u8]) -> Result<(), OdpError>; + + /// Receive one fully-framed ODP message into `buf`. + /// + /// Returns the number of bytes written. Returns + /// `OdpError::BufferTooSmall` if `buf` cannot hold the entire message. + fn recv_message(&mut self, buf: &mut [u8]) -> Result; +} diff --git a/odp-client/tests/client_invoke.rs b/odp-client/tests/client_invoke.rs new file mode 100644 index 00000000..d261b152 --- /dev/null +++ b/odp-client/tests/client_invoke.rs @@ -0,0 +1,95 @@ +use odp_client::{LoopbackTransport, OdpClient, OdpError, OdpHeader, OdpResponse, OdpService, Relay}; + +#[test] +fn invoke_round_trips_request_and_parses_response() { + // LoopbackTransport echoes the request bytes back as the response. + let transport = LoopbackTransport::new(); + let mut client = OdpClient::new(transport); + + let req_header = OdpHeader { + is_request: true, + service: OdpService::Battery, + is_error: false, + message_id: 0x11, + }; + + let parsed: u8 = client + .invoke(req_header, &[0xAA, 0xBB], |resp: OdpResponse<'_>| { + assert_eq!(resp.header.service, OdpService::Battery); + assert_eq!(resp.header.message_id, 0x11); + Ok(resp.body[0]) + }) + .unwrap(); + assert_eq!(parsed, 0xAA); +} + +#[test] +fn invoke_with_oversized_body_returns_buffer_too_small() { + // Internal buf is 256 bytes; 4-byte header + 253-byte body = 257 → overflow. + let mut client = OdpClient::new(LoopbackTransport::new()); + let req_header = OdpHeader { + is_request: true, + service: OdpService::Battery, + is_error: false, + message_id: 1, + }; + let huge = [0u8; 253]; + let err = client + .invoke(req_header, &huge, |_| Ok::<(), OdpError>(())) + .unwrap_err(); + assert_eq!(err, OdpError::BufferTooSmall); +} + +/// Inline stub transport that allows pre-loading recv bytes independently +/// of what was sent. Used to test message_id mismatch detection. +struct StubTransport { + recv_bytes: [u8; 64], + recv_len: usize, +} + +impl StubTransport { + fn with_recv(bytes: &[u8]) -> Self { + let mut buf = [0u8; 64]; + buf[..bytes.len()].copy_from_slice(bytes); + Self { recv_bytes: buf, recv_len: bytes.len() } + } +} + +impl odp_client::OdpTransport for StubTransport { + fn send_message(&mut self, _data: &[u8]) -> Result<(), OdpError> { + Ok(()) + } + fn recv_message(&mut self, buf: &mut [u8]) -> Result { + let n = self.recv_len; + buf[..n].copy_from_slice(&self.recv_bytes[..n]); + Ok(n) + } +} + +#[test] +fn invoke_with_mismatched_response_message_id_returns_unexpected() { + // Pre-load a response with message_id=0x99; request uses message_id=0x11. + let stale_header = OdpHeader { + is_request: false, + service: OdpService::Battery, + is_error: false, + message_id: 0x99, + }; + let recv_bytes = stale_header.to_be_bytes(); + let transport = StubTransport::with_recv(&recv_bytes); + let mut client = OdpClient::new(transport); + + let req_header = OdpHeader { + is_request: true, + service: OdpService::Battery, + is_error: false, + message_id: 0x11, + }; + let err = client + .invoke(req_header, &[], |_| Ok::<(), OdpError>(())) + .unwrap_err(); + assert_eq!( + err, + OdpError::UnexpectedMessageId { expected: 0x11, got: 0x99 } + ); +} diff --git a/odp-client/tests/error.rs b/odp-client/tests/error.rs new file mode 100644 index 00000000..3b033fbc --- /dev/null +++ b/odp-client/tests/error.rs @@ -0,0 +1,26 @@ +use odp_client::OdpError; + +#[test] +fn odp_error_is_copy_eq_and_debug() { + let e = OdpError::Transport; + // Copy + Clone + let _ = e; + let _ = e; + // Eq + assert_eq!(OdpError::Transport, OdpError::Transport); + assert_ne!(OdpError::Transport, OdpError::Timeout); + // Debug (just confirm it doesn't panic) + let _ = format!("{e:?}"); +} + +#[test] +fn odp_error_unexpected_message_id_carries_expected_and_got() { + let e = OdpError::UnexpectedMessageId { expected: 0x1234, got: 0x5678 }; + match e { + OdpError::UnexpectedMessageId { expected, got } => { + assert_eq!(expected, 0x1234); + assert_eq!(got, 0x5678); + } + _ => panic!("wrong variant"), + } +} diff --git a/odp-client/tests/loopback.rs b/odp-client/tests/loopback.rs new file mode 100644 index 00000000..ac3c7920 --- /dev/null +++ b/odp-client/tests/loopback.rs @@ -0,0 +1,49 @@ +use odp_client::{LoopbackTransport, OdpError, OdpTransport}; + +#[test] +fn loopback_echoes_what_you_send() { + let mut t = LoopbackTransport::new(); + t.send_message(&[9, 8, 7]).unwrap(); + let mut buf = [0u8; 8]; + let n = t.recv_message(&mut buf).unwrap(); + assert_eq!(&buf[..n], &[9, 8, 7]); +} + +#[test] +fn loopback_recv_with_no_pending_message_returns_transport_error() { + let mut t = LoopbackTransport::new(); + let mut buf = [0u8; 8]; + let err = t.recv_message(&mut buf).unwrap_err(); + assert_eq!(err, OdpError::Transport); +} + +#[test] +fn loopback_recv_with_too_small_buffer_returns_buffer_too_small() { + let mut t = LoopbackTransport::new(); + t.send_message(&[1, 2, 3, 4, 5]).unwrap(); + let mut buf = [0u8; 2]; + let err = t.recv_message(&mut buf).unwrap_err(); + assert_eq!(err, OdpError::BufferTooSmall); +} + +#[test] +fn loopback_send_overflow_returns_buffer_too_small() { + let mut t = LoopbackTransport::new(); + let huge = [0u8; 512]; // larger than the 256-byte internal buffer + let err = t.send_message(&huge).unwrap_err(); + assert_eq!(err, OdpError::BufferTooSmall); +} + +#[test] +fn loopback_send_then_recv_is_idempotent_across_messages() { + let mut t = LoopbackTransport::new(); + // Round 1 + t.send_message(&[1, 2]).unwrap(); + let mut buf = [0u8; 8]; + let n = t.recv_message(&mut buf).unwrap(); + assert_eq!(&buf[..n], &[1, 2]); + // Round 2 — should work after the previous recv drained the buffer + t.send_message(&[3, 4, 5]).unwrap(); + let n = t.recv_message(&mut buf).unwrap(); + assert_eq!(&buf[..n], &[3, 4, 5]); +} diff --git a/odp-client/tests/mctp_serial.rs b/odp-client/tests/mctp_serial.rs new file mode 100644 index 00000000..965a95a9 --- /dev/null +++ b/odp-client/tests/mctp_serial.rs @@ -0,0 +1,74 @@ +use mctp_rs::{EC_EID, SP_EID}; +use odp_client::{MctpSerialTransport, OdpTransport}; + +struct LoopbackUart { + buf: Vec, + cursor: usize, +} + +impl LoopbackUart { + fn new() -> Self { + Self { buf: Vec::new(), cursor: 0 } + } +} + +impl embedded_io::ErrorType for LoopbackUart { + type Error = core::convert::Infallible; +} + +impl embedded_io::Read for LoopbackUart { + fn read(&mut self, out: &mut [u8]) -> Result { + let avail = self.buf.len() - self.cursor; + if avail == 0 { + return Ok(0); + } + let n = avail.min(out.len()); + out[..n].copy_from_slice(&self.buf[self.cursor..self.cursor + n]); + self.cursor += n; + Ok(n) + } +} + +impl embedded_io::Write for LoopbackUart { + fn write(&mut self, data: &[u8]) -> Result { + self.buf.extend_from_slice(data); + Ok(data.len()) + } + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[test] +fn mctp_serial_transport_round_trips_one_message() { + // The transport encapsulates MCTP framing. We hand in ODP wire + // bytes (4-byte header + body), the transport MCTP-frames them + // before writing to the UART. On read, the transport reads the + // serial frame, MCTP-strips, and returns the original ODP bytes. + let uart = LoopbackUart::new(); + let mut t = MctpSerialTransport::new(uart, SP_EID, EC_EID); + + // Arbitrary 6 bytes: first 4 are the OdpHeader byte pattern, last 2 + // are the body. The transport does NOT semantically validate the + // header — it just blits bytes — so we can use a service-id (0x55) + // that isn't a real OdpService variant. + let payload = [0x01u8, 0x55, 0x00, 0x00, 0xDE, 0xAD]; + t.send_message(&payload).unwrap(); + + let mut out = [0u8; 64]; + let n = t.recv_message(&mut out).unwrap(); + assert_eq!(&out[..n], &payload); +} + +#[test] +fn mctp_serial_transport_send_with_too_short_payload_errors() { + // Header is 4 bytes; payload < 4 has no valid header bytes to + // wrap. The transport should reject it. + let uart = LoopbackUart::new(); + let mut t = MctpSerialTransport::new(uart, SP_EID, EC_EID); + let err = t.send_message(&[0x01, 0x02]).unwrap_err(); + // Any OdpError variant — we just want to prove it doesn't panic + // and doesn't silently produce malformed framing. We use BufferTooSmall + // semantically (the input doesn't carry a full header). + assert_eq!(err, odp_client::OdpError::BufferTooSmall); +} diff --git a/odp-client/tests/transport.rs b/odp-client/tests/transport.rs new file mode 100644 index 00000000..281063da --- /dev/null +++ b/odp-client/tests/transport.rs @@ -0,0 +1,44 @@ +use odp_client::{OdpError, OdpTransport}; + +struct DummyTransport(Option>); + +impl OdpTransport for DummyTransport { + fn send_message(&mut self, payload: &[u8]) -> Result<(), OdpError> { + self.0 = Some(payload.to_vec()); + Ok(()) + } + fn recv_message(&mut self, buf: &mut [u8]) -> Result { + let v = self.0.take().ok_or(OdpError::Transport)?; + if buf.len() < v.len() { + return Err(OdpError::BufferTooSmall); + } + buf[..v.len()].copy_from_slice(&v); + Ok(v.len()) + } +} + +#[test] +fn dummy_transport_satisfies_the_trait() { + let mut t = DummyTransport(None); + t.send_message(&[1, 2, 3]).unwrap(); + let mut buf = [0u8; 8]; + let n = t.recv_message(&mut buf).unwrap(); + assert_eq!(&buf[..n], &[1, 2, 3]); +} + +#[test] +fn dummy_transport_recv_with_no_pending_message_returns_transport_error() { + let mut t = DummyTransport(None); + let mut buf = [0u8; 8]; + let err = t.recv_message(&mut buf).unwrap_err(); + assert_eq!(err, OdpError::Transport); +} + +#[test] +fn dummy_transport_recv_with_too_small_buffer_returns_buffer_too_small() { + let mut t = DummyTransport(None); + t.send_message(&[1, 2, 3, 4, 5]).unwrap(); + let mut buf = [0u8; 2]; + let err = t.recv_message(&mut buf).unwrap_err(); + assert_eq!(err, OdpError::BufferTooSmall); +} From 21e96437761154a8227e50c36cef4cc477c90dc2 Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Thu, 28 May 2026 09:25:18 -0700 Subject: [PATCH 3/4] refactor: move odp relay macro into odp-client; delete embedded-service relay module The `impl_odp_relay_handler!` macro (previously `impl_odp_mctp_relay_handler!` in `embedded-service::relay`) and its supporting traits move into the new `odp-client/` crate as `odp_client::server::*`. Migrates all 11 consumer source files and 7 consumer Cargo.toml manifests across espi-service, uart-service, thermal-service-relay, battery-service-relay, time-alarm-service-relay, debug-service, debug-service-messages, and examples/rt685s-evk. The `embedded-service/src/relay/` directory is deleted along with the `embedded-service` re-exports it required (bitfield, paste, mctp-rs). Updates `.github/copilot-instructions.md` to point at the new `odp_client::server` path. CI lockfile fallout: - `cargo-machete` flagged three service-relay crates with orphaned `embedded-services` deps; dropped them. - `time-alarm-service-relay` was propagating the `embedded-services/log` feature even after dropping the dep; cleaned up. - Regenerated nested example lockfiles (`examples/pico-de-gallo`, `examples/std`, `examples/rt685s-evk`) after the dependency shifts. --- .github/copilot-instructions.md | 2 +- Cargo.lock | 15 +- Cargo.toml | 1 + battery-service-relay/Cargo.toml | 2 + battery-service-relay/src/lib.rs | 4 +- battery-service-relay/src/serialization.rs | 2 +- debug-service-messages/Cargo.toml | 4 +- debug-service-messages/src/lib.rs | 2 +- debug-service/Cargo.toml | 3 +- debug-service/src/debug_service.rs | 4 +- embedded-service/Cargo.toml | 8 +- embedded-service/src/lib.rs | 10 - embedded-service/src/relay/mod.rs | 503 ------------------ espi-service/Cargo.toml | 2 + espi-service/src/espi_service.rs | 26 +- examples/pico-de-gallo/Cargo.lock | 88 --- examples/rt685s-evk/Cargo.lock | 19 +- examples/rt685s-evk/Cargo.toml | 1 + examples/rt685s-evk/src/bin/time_alarm.rs | 4 +- examples/std/Cargo.lock | 33 +- odp-client/Cargo.toml | 4 +- odp-client/src/client.rs | 24 +- odp-client/src/error.rs | 7 +- odp-client/src/lib.rs | 22 +- odp-client/src/mctp_serial.rs | 10 +- odp-client/src/serializable.rs | 97 ++++ odp-client/src/server.rs | 407 +++++++++++++- odp-client/tests/client_invoke.rs | 14 +- odp-client/tests/error.rs | 5 +- odp-client/tests/mctp_serial.rs | 5 +- thermal-service-relay/Cargo.toml | 4 +- thermal-service-relay/src/lib.rs | 4 +- thermal-service-relay/src/serialization.rs | 2 +- time-alarm-service-relay/Cargo.toml | 5 +- time-alarm-service-relay/src/lib.rs | 4 +- time-alarm-service-relay/src/serialization.rs | 2 +- uart-service/Cargo.toml | 2 + uart-service/src/lib.rs | 2 +- uart-service/src/task.rs | 2 +- 39 files changed, 661 insertions(+), 694 deletions(-) delete mode 100644 embedded-service/src/relay/mod.rs create mode 100644 odp-client/src/serializable.rs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 27556de9..eede3ac1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -108,7 +108,7 @@ Services use a variety of async IPC mechanisms from `embassy-sync` and `embedded - **`embassy_sync::signal::Signal`** — single-value async notifications - **`embedded_services::ipc::deferred`** — request/response channels where the caller awaits a reply - **`embedded_services::broadcaster`** — publish/subscribe pattern for event fan-out -- **`embedded_services::relay`** — relay service pattern for MCTP-based request/response dispatch with direct async calls +- **`odp_client::server`** — relay service pattern for MCTP-based request/response dispatch with direct async calls ### Composition diff --git a/Cargo.lock b/Cargo.lock index f69972e4..eb607a1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "embedded-services", "log", "num_enum", + "odp-client", ] [[package]] @@ -419,6 +420,7 @@ dependencies = [ "embassy-sync", "embedded-services", "log", + "odp-client", "rtt-target", ] @@ -427,8 +429,8 @@ name = "debug-service-messages" version = "0.1.0" dependencies = [ "defmt 0.3.100", - "embedded-services", "num_enum", + "odp-client", ] [[package]] @@ -811,15 +813,12 @@ dependencies = [ name = "embedded-services" version = "0.1.0" dependencies = [ - "bitfield 0.17.0", "cortex-m", "critical-section", "defmt 0.3.100", "embassy-futures", "embassy-sync", "log", - "mctp-rs", - "paste", "portable-atomic", "serde", "static_cell", @@ -908,6 +907,7 @@ dependencies = [ "embedded-services", "log", "mctp-rs", + "odp-client", "odp-service-common", ] @@ -1398,10 +1398,12 @@ dependencies = [ name = "odp-client" version = "0.1.0" dependencies = [ + "bitfield 0.17.0", "defmt 0.3.100", "embedded-io 0.7.1", "heapless 0.9.3", "mctp-rs", + "paste", ] [[package]] @@ -1944,8 +1946,8 @@ name = "thermal-service-relay" version = "0.1.0" dependencies = [ "defmt 0.3.100", - "embedded-services", "num_enum", + "odp-client", "thermal-service-interface", "uuid", ] @@ -2016,9 +2018,9 @@ version = "0.1.0" dependencies = [ "defmt 0.3.100", "embedded-mcu-hal", - "embedded-services", "log", "num_enum", + "odp-client", "time-alarm-service-interface", ] @@ -2254,6 +2256,7 @@ dependencies = [ "embedded-services", "log", "mctp-rs", + "odp-client", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6ba386ed..ae089f81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ fw-update-interface = { path = "./fw-update-interface" } fw-update-interface-mocks = { path = "./fw-update-interface-mocks" } mctp-rs = { path = "./mctp-rs" } num_enum = { version = "0.7.5", default-features = false } +odp-client = { path = "./odp-client" } portable-atomic = { version = "1.11", default-features = false } power-policy-interface = { path = "./power-policy-interface" } paste = "1.0.15" diff --git a/battery-service-relay/Cargo.toml b/battery-service-relay/Cargo.toml index 7d53c854..6da40f4d 100644 --- a/battery-service-relay/Cargo.toml +++ b/battery-service-relay/Cargo.toml @@ -12,6 +12,7 @@ ignored = ["log"] defmt = { workspace = true, optional = true } log = { workspace = true, optional = true } embedded-services.workspace = true +odp-client.workspace = true num_enum.workspace = true battery-service-interface.workspace = true @@ -21,6 +22,7 @@ defmt = [ "dep:defmt", "embedded-services/defmt", "battery-service-interface/defmt", + "odp-client/defmt", ] log = ["dep:log", "embedded-services/log", "battery-service-interface/log"] diff --git a/battery-service-relay/src/lib.rs b/battery-service-relay/src/lib.rs index 92988ae2..7694a6a5 100644 --- a/battery-service-relay/src/lib.rs +++ b/battery-service-relay/src/lib.rs @@ -18,14 +18,14 @@ impl BatteryServiceRelayHandler } } -impl embedded_services::relay::mctp::RelayServiceHandlerTypes +impl odp_client::server::RelayServiceHandlerTypes for BatteryServiceRelayHandler { type RequestType = serialization::AcpiBatteryRequest; type ResultType = serialization::AcpiBatteryResult; } -impl embedded_services::relay::mctp::RelayServiceHandler +impl odp_client::server::RelayServiceHandler for BatteryServiceRelayHandler { async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { diff --git a/battery-service-relay/src/serialization.rs b/battery-service-relay/src/serialization.rs index 39f411a4..e2300023 100644 --- a/battery-service-relay/src/serialization.rs +++ b/battery-service-relay/src/serialization.rs @@ -1,5 +1,5 @@ use battery_service_interface::*; -use embedded_services::relay::{MessageSerializationError, SerializableMessage}; +use odp_client::{MessageSerializationError, SerializableMessage}; #[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] #[repr(u16)] diff --git a/debug-service-messages/Cargo.toml b/debug-service-messages/Cargo.toml index 5127a757..edd290ab 100644 --- a/debug-service-messages/Cargo.toml +++ b/debug-service-messages/Cargo.toml @@ -7,11 +7,11 @@ repository.workspace = true [dependencies] defmt = { workspace = true, optional = true } -embedded-services.workspace = true num_enum.workspace = true +odp-client.workspace = true [lints] workspace = true [features] -defmt = ["dep:defmt"] +defmt = ["dep:defmt", "odp-client/defmt"] diff --git a/debug-service-messages/src/lib.rs b/debug-service-messages/src/lib.rs index a4dafc67..44621dde 100644 --- a/debug-service-messages/src/lib.rs +++ b/debug-service-messages/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -use embedded_services::relay::{MessageSerializationError, SerializableMessage}; +use odp_client::{MessageSerializationError, SerializableMessage}; /// Standard Debug Service Log Buffer Size pub const STD_DEBUG_BUF_SIZE: usize = 128; diff --git a/debug-service/Cargo.toml b/debug-service/Cargo.toml index 5381d626..1eb328ba 100644 --- a/debug-service/Cargo.toml +++ b/debug-service/Cargo.toml @@ -16,10 +16,11 @@ debug-service-messages.workspace = true embassy-sync.workspace = true embedded-services.workspace = true log.workspace = true +odp-client.workspace = true rtt-target = "0.6.1" [features] -defmt = ["debug-service-messages/defmt"] +defmt = ["debug-service-messages/defmt", "odp-client/defmt"] [lints] workspace = true diff --git a/debug-service/src/debug_service.rs b/debug-service/src/debug_service.rs index ef976230..0fc7b141 100644 --- a/debug-service/src/debug_service.rs +++ b/debug-service/src/debug_service.rs @@ -34,12 +34,12 @@ impl Service { } } -impl embedded_services::relay::mctp::RelayServiceHandlerTypes for Service { +impl odp_client::server::RelayServiceHandlerTypes for Service { type RequestType = DebugRequest; type ResultType = DebugResult; } -impl embedded_services::relay::mctp::RelayServiceHandler for Service { +impl odp_client::server::RelayServiceHandler for Service { async fn process_request(&self, _request: Self::RequestType) -> Self::ResultType { // Host sent an ACPI/MCTP request (e.g. GetDebugBuffer). Treat this as the // trigger to send the staged debug buffer back to the host. diff --git a/embedded-service/Cargo.toml b/embedded-service/Cargo.toml index 9774188c..35543035 100644 --- a/embedded-service/Cargo.toml +++ b/embedded-service/Cargo.toml @@ -11,17 +11,11 @@ rust-version.workspace = true workspace = true [dependencies] -bitfield.workspace = true critical-section.workspace = true defmt = { workspace = true, optional = true } embassy-sync.workspace = true embassy-futures.workspace = true log = { workspace = true, optional = true } -paste.workspace = true - -[dependencies.mctp-rs] -workspace = true -features = ["espi"] [dependencies.serde] workspace = true @@ -41,5 +35,5 @@ tokio = { workspace = true, features = ["rt", "macros", "time"] } [features] default = [] -defmt = ["dep:defmt", "embassy-sync/defmt", "mctp-rs/defmt"] +defmt = ["dep:defmt", "embassy-sync/defmt"] log = ["dep:log", "embassy-sync/log"] diff --git a/embedded-service/src/lib.rs b/embedded-service/src/lib.rs index e62562e3..7b1cb55a 100644 --- a/embedded-service/src/lib.rs +++ b/embedded-service/src/lib.rs @@ -22,18 +22,8 @@ pub mod init; pub mod ipc; pub mod keyboard; pub mod named; -pub mod relay; pub mod sync; -/// Hidden re-exports used by macros defined in this crate. -/// Not part of the public API — do not depend on these directly. -#[doc(hidden)] -pub mod _macro_internal { - pub use bitfield; - pub use mctp_rs; - pub use paste; -} - /// Global Mutex type, ThreadModeRawMutex is used in a microcontroller context, whereas CriticalSectionRawMutex is used /// in a standard context for unit testing. /// diff --git a/embedded-service/src/relay/mod.rs b/embedded-service/src/relay/mod.rs deleted file mode 100644 index d92cfb97..00000000 --- a/embedded-service/src/relay/mod.rs +++ /dev/null @@ -1,503 +0,0 @@ -//! Helper code for serialization/deserialization of arbitrary messages to/from the embedded controller via a relay service, e.g. the eSPI service. - -/// Error type for serializing/deserializing messages -#[derive(Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum MessageSerializationError { - /// The message payload does not represent a valid message - InvalidPayload(&'static str), - - /// The message discriminant does not represent a known message type - UnknownMessageDiscriminant(u16), - - /// The provided buffer is too small to serialize the message - BufferTooSmall, - - /// Unspecified error - Other(&'static str), -} - -/// Trait for serializing and deserializing messages -pub trait SerializableMessage: Sized { - /// Serializes the message into the provided buffer. - /// On success, returns the number of bytes written - fn serialize(self, buffer: &mut [u8]) -> Result; - - /// Returns the discriminant needed to deserialize this type of message. - fn discriminant(&self) -> u16; - - /// Deserializes the message from the provided buffer. - fn deserialize(discriminant: u16, buffer: &[u8]) -> Result; -} - -// Prevent other types from implementing SerializableResult - they should instead use SerializableMessage on a Response type and an Error type -#[doc(hidden)] -mod private { - pub trait Sealed {} - - impl Sealed for Result {} -} - -/// Responses sent over MCTP are called "Results" and are of type Result where T and E both implement SerializableMessage -pub trait SerializableResult: private::Sealed + Sized { - /// The type of the result when the operation being responded to succeeded - type SuccessType: SerializableMessage; - - /// The type of the result when the operation being responded to failed - type ErrorType: SerializableMessage; - - /// Returns true if the result represents a successful operation, false otherwise - fn is_ok(&self) -> bool; - - /// Returns a unique discriminant that can be used to deserialize the specific type of result. - /// Discriminants can be reused for success and error messages. - fn discriminant(&self) -> u16; - - /// Writes the result into the provided buffer. - /// On success, returns the number of bytes written - fn serialize(self, buffer: &mut [u8]) -> Result; - - /// Attempts to deserialize the result from the provided buffer. - fn deserialize(is_error: bool, discriminant: u16, buffer: &[u8]) -> Result; -} - -impl SerializableResult for Result -where - T: SerializableMessage, - E: SerializableMessage, -{ - type SuccessType = T; - type ErrorType = E; - - fn is_ok(&self) -> bool { - Result::::is_ok(self) - } - - fn discriminant(&self) -> u16 { - match self { - Ok(success_value) => success_value.discriminant(), - Err(error_value) => error_value.discriminant(), - } - } - - fn serialize(self, buffer: &mut [u8]) -> Result { - match self { - Ok(success_value) => success_value.serialize(buffer), - Err(error_value) => error_value.serialize(buffer), - } - } - - fn deserialize(is_error: bool, discriminant: u16, buffer: &[u8]) -> Result { - if is_error { - Ok(Err(E::deserialize(discriminant, buffer)?)) - } else { - Ok(Ok(T::deserialize(discriminant, buffer)?)) - } - } -} - -pub mod mctp { - //! Contains helper functions for services that relay comms messages over MCTP - - /// Error type for MCTP relay operations - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub enum MctpError { - /// The endpoint ID does not correspond to a known service - UnknownEndpointId, - } - - /// Trait for types that are used by a relay service to relay messages from your service over the wire. - /// If you are implementing this trait, you should also implement RelayServiceHandler. - /// - pub trait RelayServiceHandlerTypes { - /// The request type that this service handler processes - type RequestType: super::SerializableMessage; - - /// The result type that this service handler processes - type ResultType: super::SerializableResult; - } - - /// Trait for a service that can be relayed over an external bus (e.g. battery service, thermal service, time-alarm service) - /// - pub trait RelayServiceHandler: RelayServiceHandlerTypes { - /// Process the provided request and yield a result. - fn process_request<'a>( - &'a self, - request: Self::RequestType, - ) -> impl core::future::Future + 'a; - } - - // Traits below this point are intended for consumption by relay services (e.g. the eSPI service), not individual services that want their messages relayed. - // In general, you should not implement these yourself; rather, you should leverage the `impl_odp_mctp_relay_handler` macro to do that for you. - - /// Contains additional methods that must be implemented on the relay header type. - /// Do not implement this yourself - rather, rely on the `impl_odp_mctp_relay_handler` macro to implement this. - #[doc(hidden)] - pub trait RelayHeader { - /// Return the ID of the service associated with the request - fn get_service_id(&self) -> ServiceIdType; - } - - /// Contains additional methods that must be implemented on the relay response type. - /// Do not implement this yourself - rather, rely on the `impl_odp_mctp_relay_handler` macro to implement this. - #[doc(hidden)] - pub trait RelayResponse { - /// Construct an MCTP header suitable for representing the result based on the provided service handler ID and result - fn create_header(&self, service_id: &ServiceIdType) -> HeaderType; - } - - /// Trait for aggregating collections of services that can be relayed over an external bus. - /// Do not implement this yourself - rather, rely on the `impl_odp_mctp_relay_handler` macro to implement this. - /// - pub trait RelayHandler { - /// The type that uniquely identifies individual services. Generally expected to be a C-style enum. - type ServiceIdType: Into + TryFrom + Copy; - - /// The header type used by request and result enums - type HeaderType: mctp_rs::MctpMessageHeaderTrait + RelayHeader; - - /// An enum over all possible request types - type RequestEnumType: for<'buf> mctp_rs::MctpMessageTrait<'buf, Header = Self::HeaderType>; - - /// An enum over all possible result types - type ResultEnumType: for<'buf> mctp_rs::MctpMessageTrait<'buf, Header = Self::HeaderType> - + RelayResponse; - - /// Process the provided request and yield a result. - fn process_request<'a>( - &'a self, - message: Self::RequestEnumType, - ) -> impl core::future::Future + 'a; - } - - /// This macro generates a relay type over a collection of message types, which can be used by a relay service to - /// receive messages over the wire and translate them into calls to a particular service on the EC. - /// - /// This is the recommended way to implement a relay handler - you should not implement the RelayHandler trait yourself. - /// - /// This macro will emit a type with the name you specify that is generic over a lifetime for the hardware (probably 'static in production code), - /// implements the `RelayHandler` trait, and has a single constructor method `new` that takes as arguments references to the service handler - /// types that you specify that have the 'hardware lifetime'. - /// - /// The macro takes the following inputs once: - /// relay_type_name: The name of the relay type to generate. This is arbitrary. The macro will emit a type with this name. - /// - /// Followed by a list of any number of service entries, which are specified by the following inputs: - /// service_name: A name to assign to generated identifiers associated with the service, e.g. "Battery". - /// This can be arbitrary. - /// service_id: A unique u8 that addresses that service on the EC. - /// service_handler_type: A type that implements the RelayServiceHandler trait, which will be used to process messages - /// for this service. - /// - /// Example usage: - /// - /// ```ignore - /// - /// impl_odp_mctp_relay_handler!( - /// MyRelayHandlerType; - /// Battery, 0x9, battery_service_relay::RelayHandler>; - /// TimeAlarm, 0xB, time_alarm_service_relay::RelayHandler>; - /// ); - /// - /// let relay_handler = MyRelayHandlerType::new(battery_service_instance, time_alarm_service_instance); - /// - /// // Then, pass relay_handler to your relay service (e.g. eSPI service), which should be generic over an `impl RelayHandler`. - /// - /// ``` - /// - #[macro_export] - macro_rules! impl_odp_mctp_relay_handler { - ( - $relay_type_name:ident; - $( - $service_name:ident, - $service_id:expr, - $service_handler_type:ty; - )+ - ) => { - $crate::_macro_internal::paste::paste! { - mod [< _odp_impl_ $relay_type_name:snake >] { - use $crate::_macro_internal::bitfield::bitfield; - use core::convert::Infallible; - use $crate::_macro_internal::mctp_rs::smbus_espi::SmbusEspiMedium; - use $crate::_macro_internal::mctp_rs::{MctpMedium, MctpMessageHeaderTrait, MctpMessageTrait, MctpPacketError, MctpPacketResult}; - use $crate::relay::{SerializableMessage, SerializableResult}; - use $crate::relay::mctp::RelayServiceHandler; - - #[derive(Debug, PartialEq, Eq, Clone, Copy)] - #[repr(u8)] - pub enum OdpService { - $( - $service_name = $service_id, - )+ - } - - impl From for u8 { - fn from(val: OdpService) -> u8 { - val as u8 - } - } - - impl TryFrom for OdpService { - type Error = u8; - fn try_from(value: u8) -> Result { - match value { - $( - $service_id => Ok(OdpService::$service_name), - )+ - other => Err(other), - } - } - } - - pub enum HostRequest { - $( - $service_name(<$service_handler_type as $crate::relay::mctp::RelayServiceHandlerTypes>::RequestType), - )+ - } - - impl MctpMessageTrait<'_> for HostRequest { - type Header = OdpHeader; - const MESSAGE_TYPE: u8 = 0x7D; // ODP message type - - fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { - match self { - $( - HostRequest::$service_name(request) => SerializableMessage::serialize(request, buffer) - .map_err(|_| MctpPacketError::SerializeError(concat!("Failed to serialize ", stringify!($service_name), " request"))), - )+ - } - } - - fn deserialize(header: &Self::Header, buffer: &'_ [u8]) -> MctpPacketResult { - Ok(match header.service { - $( - OdpService::$service_name => Self::$service_name( - <$service_handler_type as $crate::relay::mctp::RelayServiceHandlerTypes>::RequestType::deserialize(header.message_id, buffer) - .map_err(|_| MctpPacketError::CommandParseError(concat!("Could not parse ", stringify!($service_name), " request")))?, - ), - )+ - }) - } - } - - bitfield! { - /// Wire format for ODP MCTP headers. Not user-facing - use OdpHeader instead. - #[derive(Copy, Clone, PartialEq, Eq)] - struct OdpHeaderWireFormat(u32); - impl Debug; - impl new; - /// If true, represents a request; otherwise, represents a result - is_request, set_is_request: 25; - - /// The service ID that this message is related to - /// Note: Error checking is done when you access the field, not when you construct the OdpHeader. Take care when constructing a header. - u8, service_id, set_service_id: 23, 16; - - /// On results, indicates if the result message is an error. Unused on requests. - is_error, set_is_error: 15; - - /// The message type/discriminant - u16, message_id, set_message_id: 14, 0; - } - - #[derive(Copy, Clone, PartialEq, Eq)] - pub enum OdpMessageType { - Request, - Result { is_error: bool }, - } - - #[derive(Copy, Clone, PartialEq, Eq)] - pub struct OdpHeader { - pub message_type: OdpMessageType, - pub service: OdpService, - pub message_id: u16, - } - - impl From for OdpHeaderWireFormat { - fn from(src: OdpHeader) -> Self { - Self::new( - matches!(src.message_type, OdpMessageType::Request), - src.service.into(), - match src.message_type { - OdpMessageType::Request => false, // unused on requests - OdpMessageType::Result { is_error } => is_error, - }, - src.message_id, - ) - } - } - - impl TryFrom for OdpHeader { - type Error = MctpPacketError; - - fn try_from(src: OdpHeaderWireFormat) -> Result { - let service = OdpService::try_from(src.service_id()) - .map_err(|_| MctpPacketError::HeaderParseError("invalid odp service in odp header"))?; - - let message_type = if src.is_request() { - OdpMessageType::Request - } else { - OdpMessageType::Result { - is_error: src.is_error(), - } - }; - - Ok(OdpHeader { - message_type, - service, - message_id: src.message_id(), - }) - } - } - - impl MctpMessageHeaderTrait for OdpHeader { - fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { - let wire_format = OdpHeaderWireFormat::from(self); - let bytes = wire_format.0.to_be_bytes(); - buffer - .get_mut(0..bytes.len()) - .ok_or(MctpPacketError::SerializeError("buffer too small for odp header"))? - .copy_from_slice(&bytes); - - Ok(bytes.len()) - } - - fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M> { - let bytes = buffer - .get(0..core::mem::size_of::()) - .ok_or(MctpPacketError::HeaderParseError("buffer too small for odp header"))?; - let raw = u32::from_be_bytes( - bytes - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("buffer too small for odp header"))?, - ); - - let parsed_wire_format = OdpHeaderWireFormat(raw); - let header = OdpHeader::try_from(parsed_wire_format) - .map_err(|_| MctpPacketError::HeaderParseError("invalid odp header received"))?; - - Ok(( - header, - buffer - .get(core::mem::size_of::()..) - .ok_or(MctpPacketError::HeaderParseError("buffer too small for odp header"))?, - )) - } - } - - impl $crate::relay::mctp::RelayHeader for OdpHeader { - fn get_service_id(&self) -> OdpService { - self.service - } - } - - #[derive(Clone)] - pub enum HostResult { - $( - $service_name(<$service_handler_type as $crate::relay::mctp::RelayServiceHandlerTypes>::ResultType), - )+ - } - - impl $crate::relay::mctp::RelayResponse for HostResult { - fn create_header(&self, service_id: &OdpService) -> OdpHeader { - match (self) { - $( - (HostResult::$service_name(result)) => OdpHeader { - message_type: OdpMessageType::Result { is_error: !result.is_ok() }, - service: *service_id, - message_id: result.discriminant(), - }, - )+ - } - } - } - - impl MctpMessageTrait<'_> for HostResult { - const MESSAGE_TYPE: u8 = 0x7D; // ODP message type - type Header = OdpHeader; - - fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { - match self { - $( - HostResult::$service_name(result) => result - .serialize(buffer) - .map_err(|_| MctpPacketError::SerializeError(concat!("Failed to serialize ", stringify!($service_name), " result"))), - )+ - } - } - - fn deserialize(header: &Self::Header, buffer: &'_ [u8]) -> MctpPacketResult { - match header.service { - $( - OdpService::$service_name => { - match header.message_type { - OdpMessageType::Request => { - Err(MctpPacketError::CommandParseError(concat!("Received ", stringify!($service_name), " request when expecting result"))) - } - OdpMessageType::Result { is_error } => { - Ok(HostResult::$service_name(<$service_handler_type as $crate::relay::mctp::RelayServiceHandlerTypes>::ResultType::deserialize(is_error, header.message_id, buffer) - .map_err(|_| MctpPacketError::CommandParseError(concat!("Could not parse ", stringify!($service_name), " result")))?)) - } - } - }, - )+ - } - } - } - - - pub struct $relay_type_name { - $( - [<$service_name:snake _handler>]: $service_handler_type, - )+ - } - - impl $relay_type_name { - pub fn new( - $( - [<$service_name:snake _handler>]: $service_handler_type, - )+ - ) -> Self { - Self { - $( - [<$service_name:snake _handler>], - )+ - } - } - } - - impl $crate::relay::mctp::RelayHandler for $relay_type_name { - type ServiceIdType = OdpService; - type HeaderType = OdpHeader; - type RequestEnumType = HostRequest; - type ResultEnumType = HostResult; - - fn process_request<'a>( - &'a self, - message: HostRequest, - ) -> impl core::future::Future + 'a { - async move { - match message { - $( - HostRequest::$service_name(request) => { - let result = self.[<$service_name:snake _handler>].process_request(request).await; - HostResult::$service_name(result) - } - )+ - } - } - } - } - } // end mod __odp_impl - - // Allows this generated relay type to be publicly re-exported - pub use [< _odp_impl_ $relay_type_name:snake >]::$relay_type_name; - - } // end paste! - }; // end macro arm - } // end macro - - pub use impl_odp_mctp_relay_handler; -} diff --git a/espi-service/Cargo.toml b/espi-service/Cargo.toml index 350adc4a..446886d0 100644 --- a/espi-service/Cargo.toml +++ b/espi-service/Cargo.toml @@ -21,6 +21,7 @@ embassy-sync.workspace = true embassy-imxrt = { workspace = true, features = ["mimxrt633s"] } embassy-futures.workspace = true mctp-rs = { workspace = true, features = ["espi"] } +odp-client.workspace = true odp-service-common.workspace = true [target.'cfg(target_os = "none")'.dependencies] @@ -43,6 +44,7 @@ defmt = [ "embassy-sync/defmt", "embassy-imxrt/defmt", "mctp-rs/defmt", + "odp-client/defmt", ] log = ["dep:log", "embedded-services/log"] diff --git a/espi-service/src/espi_service.rs b/espi-service/src/espi_service.rs index f47ab3fd..e69854e2 100644 --- a/espi-service/src/espi_service.rs +++ b/espi-service/src/espi_service.rs @@ -19,7 +19,7 @@ const ASSEMBLY_BUF_SIZE: usize = 256; #[derive(Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -struct HostResultMessage { +struct HostResultMessage { pub handler_service_id: RelayHandler::ServiceIdType, pub message: RelayHandler::ResultEnumType, } @@ -32,23 +32,23 @@ pub enum Error { } /// The memory required by the eSPI service to run -pub struct Resources<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { +pub struct Resources<'hw, RelayHandler: odp_client::server::RelayHandler> { inner: Option>, } -impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> Default for Resources<'hw, RelayHandler> { +impl<'hw, RelayHandler: odp_client::server::RelayHandler> Default for Resources<'hw, RelayHandler> { fn default() -> Self { Self { inner: None } } } /// Service runner for the eSPI service. Users must call the run() method on the runner for the service to start processing events. -pub struct Runner<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { +pub struct Runner<'hw, RelayHandler: odp_client::server::RelayHandler> { inner: &'hw ServiceInner<'hw, RelayHandler>, } -impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> - odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw, RelayHandler> +impl<'hw, RelayHandler: odp_client::server::RelayHandler> odp_service_common::runnable_service::ServiceRunner<'hw> + for Runner<'hw, RelayHandler> { /// Run the service event loop. async fn run(self) -> embedded_services::Never { @@ -56,11 +56,11 @@ impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> } } -pub struct Service<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { +pub struct Service<'hw, RelayHandler: odp_client::server::RelayHandler> { _inner: &'hw ServiceInner<'hw, RelayHandler>, } -impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> odp_service_common::runnable_service::Service<'hw> +impl<'hw, RelayHandler: odp_client::server::RelayHandler> odp_service_common::runnable_service::Service<'hw> for Service<'hw, RelayHandler> { type Resources = Resources<'hw, RelayHandler>; @@ -77,18 +77,18 @@ impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> odp_servic } } -pub struct InitParams<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { +pub struct InitParams<'hw, RelayHandler: odp_client::server::RelayHandler> { pub espi: espi::Espi<'hw>, pub relay_handler: RelayHandler, } -struct ServiceInner<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { +struct ServiceInner<'hw, RelayHandler: odp_client::server::RelayHandler> { espi: Mutex>, host_tx_queue: Channel, HOST_TX_QUEUE_SIZE>, relay_handler: RelayHandler, } -impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> ServiceInner<'hw, RelayHandler> { +impl<'hw, RelayHandler: odp_client::server::RelayHandler> ServiceInner<'hw, RelayHandler> { async fn new(mut init_params: InitParams<'hw, RelayHandler>) -> Self { init_params.espi.wait_for_plat_reset().await; @@ -229,7 +229,7 @@ impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> ServiceInn espi: &mut espi::Espi<'hw>, port_event: &espi::PortEvent, ) -> Result<(), Error> { - use embedded_services::relay::mctp::RelayHeader; + use odp_client::server::RelayHeader; info!("Host Request received"); espi.complete_port(port_event.port); @@ -263,7 +263,7 @@ impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> ServiceInn espi: &mut espi::Espi<'hw>, result: HostResultMessage, ) -> Result<(), Error> { - use embedded_services::relay::mctp::RelayResponse; + use odp_client::server::RelayResponse; let mut assembly_buf = [0u8; ASSEMBLY_BUF_SIZE]; let mut mctp_ctx = mctp_rs::MctpPacketContext::new(mctp_rs::smbus_espi::SmbusEspiMedium, assembly_buf.as_mut_slice()); diff --git a/examples/pico-de-gallo/Cargo.lock b/examples/pico-de-gallo/Cargo.lock index 6f6ff368..03193db9 100644 --- a/examples/pico-de-gallo/Cargo.lock +++ b/examples/pico-de-gallo/Cargo.lock @@ -76,12 +76,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "bare-metal" version = "0.2.5" @@ -114,22 +108,6 @@ dependencies = [ "log", ] -[[package]] -name = "bit-register" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/odp-utilities?tag=v0.1.0#583015c08ad9855f310bdb25d5cf9abff77b5e08" -dependencies = [ - "num-traits", -] - -[[package]] -name = "bit-register" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/odp-utilities#583015c08ad9855f310bdb25d5cf9abff77b5e08" -dependencies = [ - "num-traits", -] - [[package]] name = "bitfield" version = "0.13.2" @@ -437,14 +415,11 @@ dependencies = [ name = "embedded-services" version = "0.1.0" dependencies = [ - "bitfield 0.17.0", "cortex-m", "critical-section", "embassy-futures", "embassy-sync", "log", - "mctp-rs", - "paste", "portable-atomic", "serde", ] @@ -488,19 +463,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "espi-device" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/haf-ec-service#290aa80a4c281857f3bed94581200b330119286c" -dependencies = [ - "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities?tag=v0.1.0)", - "bitflags", - "num-traits", - "num_enum", - "static_assertions", - "subenum", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -576,12 +538,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "io-kit-sys" version = "0.4.1" @@ -707,17 +663,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "mctp-rs" -version = "0.1.0" -dependencies = [ - "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities)", - "espi-device", - "num_enum", - "smbus-pec", - "thiserror", -] - [[package]] name = "memchr" version = "2.8.0" @@ -760,15 +705,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "num_enum" version = "0.7.6" @@ -829,12 +765,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pico-de-gallo" version = "0.1.0" @@ -1205,12 +1135,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "static_cell" version = "2.1.1" @@ -1220,18 +1144,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "subenum" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee3fb942ed39f3971438fcc7e05e20717e599e14c5c7cb50edd0df2a44b274" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "syn" version = "2.0.117" diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index 655a00c7..c6ab7371 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -656,14 +656,11 @@ dependencies = [ name = "embedded-services" version = "0.1.0" dependencies = [ - "bitfield 0.17.0", "cortex-m", "critical-section", "defmt 0.3.100", "embassy-futures", "embassy-sync", - "mctp-rs", - "paste", "portable-atomic", "serde", ] @@ -916,6 +913,7 @@ name = "mctp-rs" version = "0.1.0" dependencies = [ "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities)", + "crc", "defmt 0.3.100", "embedded-batteries", "espi-device", @@ -1028,6 +1026,18 @@ dependencies = [ "syn", ] +[[package]] +name = "odp-client" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "defmt 0.3.100", + "embedded-io 0.7.1", + "heapless 0.9.3", + "mctp-rs", + "paste", +] + [[package]] name = "odp-service-common" version = "0.1.0" @@ -1202,6 +1212,7 @@ dependencies = [ "embedded-services", "embedded-usb-pd", "mimxrt600-fcb 0.1.0", + "odp-client", "odp-service-common", "panic-probe", "platform-service", @@ -1429,8 +1440,8 @@ version = "0.1.0" dependencies = [ "defmt 0.3.100", "embedded-mcu-hal", - "embedded-services", "num_enum", + "odp-client", "time-alarm-service-interface", ] diff --git a/examples/rt685s-evk/Cargo.toml b/examples/rt685s-evk/Cargo.toml index b411ed71..3ac35d6e 100644 --- a/examples/rt685s-evk/Cargo.toml +++ b/examples/rt685s-evk/Cargo.toml @@ -50,6 +50,7 @@ mimxrt600-fcb = "0.1.0" embedded-cfu-protocol = { git = "https://github.com/OpenDevicePartnership/embedded-cfu"} embedded-services = { path = "../../embedded-service", features = ["defmt"] } +odp-client = { path = "../../odp-client", features = ["defmt"] } odp-service-common = { path = "../../odp-service-common" } power-button-service = { path = "../../power-button-service", features = [ "defmt", diff --git a/examples/rt685s-evk/src/bin/time_alarm.rs b/examples/rt685s-evk/src/bin/time_alarm.rs index 8673d909..a77564d3 100644 --- a/examples/rt685s-evk/src/bin/time_alarm.rs +++ b/examples/rt685s-evk/src/bin/time_alarm.rs @@ -45,8 +45,8 @@ async fn main(spawner: embassy_executor::Spawner) { ) .expect("Failed to spawn time alarm service"); - use embedded_services::relay::mctp::impl_odp_mctp_relay_handler; - impl_odp_mctp_relay_handler!( + use odp_client::impl_odp_relay_handler; + impl_odp_relay_handler!( EspiRelayHandler; TimeAlarm, 0x0B, crate::TimeAlarmServiceRelayHandlerType; ); diff --git a/examples/std/Cargo.lock b/examples/std/Cargo.lock index b202d82b..9d5f01d4 100644 --- a/examples/std/Cargo.lock +++ b/examples/std/Cargo.lock @@ -284,6 +284,21 @@ dependencies = [ "volatile-register", ] +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "critical-section" version = "1.2.0" @@ -336,6 +351,7 @@ dependencies = [ "embassy-sync", "embedded-services", "log", + "odp-client", "rtt-target", ] @@ -343,8 +359,8 @@ dependencies = [ name = "debug-service-messages" version = "0.1.0" dependencies = [ - "embedded-services", "num_enum", + "odp-client", ] [[package]] @@ -634,14 +650,11 @@ dependencies = [ name = "embedded-services" version = "0.1.0" dependencies = [ - "bitfield 0.17.0", "cortex-m", "critical-section", "embassy-futures", "embassy-sync", "log", - "mctp-rs", - "paste", "portable-atomic", "serde", ] @@ -914,6 +927,7 @@ name = "mctp-rs" version = "0.1.0" dependencies = [ "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities)", + "crc", "espi-device", "num_enum", "smbus-pec", @@ -992,6 +1006,17 @@ dependencies = [ "syn", ] +[[package]] +name = "odp-client" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "embedded-io 0.7.1", + "heapless 0.9.3", + "mctp-rs", + "paste", +] + [[package]] name = "odp-service-common" version = "0.1.0" diff --git a/odp-client/Cargo.toml b/odp-client/Cargo.toml index 76b82cb9..d78e8710 100644 --- a/odp-client/Cargo.toml +++ b/odp-client/Cargo.toml @@ -10,7 +10,9 @@ default = [] defmt = ["mctp-rs/defmt", "dep:defmt"] [dependencies] +bitfield = { workspace = true } defmt = { workspace = true, optional = true } embedded-io = { workspace = true } heapless = { workspace = true } -mctp-rs = { workspace = true, features = ["odp", "serial"] } +mctp-rs = { workspace = true, features = ["odp", "serial", "espi"] } +paste = { workspace = true } diff --git a/odp-client/src/client.rs b/odp-client/src/client.rs index 977ffaf4..3a50e49e 100644 --- a/odp-client/src/client.rs +++ b/odp-client/src/client.rs @@ -30,12 +30,7 @@ pub trait Relay { /// The parse closure runs inside `invoke` so that the borrow of the /// client's internal buffer is confined to the closure scope; any /// owned value returned from the closure escapes cleanly. - fn invoke( - &mut self, - header: OdpHeader, - body: &[u8], - parse: F, - ) -> Result + fn invoke(&mut self, header: OdpHeader, body: &[u8], parse: F) -> Result where F: FnOnce(OdpResponse<'_>) -> Result; } @@ -53,17 +48,15 @@ pub struct OdpClient { impl OdpClient { /// Create a new client, consuming `transport` by value. pub fn new(transport: T) -> Self { - Self { transport, buf: [0u8; 256] } + Self { + transport, + buf: [0u8; 256], + } } } impl Relay for OdpClient { - fn invoke( - &mut self, - header: OdpHeader, - body: &[u8], - parse: F, - ) -> Result + fn invoke(&mut self, header: OdpHeader, body: &[u8], parse: F) -> Result where F: FnOnce(OdpResponse<'_>) -> Result, { @@ -93,6 +86,9 @@ impl Relay for OdpClient { }); } - parse(OdpResponse { header: resp_header, body: &self.buf[4..n] }) + parse(OdpResponse { + header: resp_header, + body: &self.buf[4..n], + }) } } diff --git a/odp-client/src/error.rs b/odp-client/src/error.rs index edeed259..26426e31 100644 --- a/odp-client/src/error.rs +++ b/odp-client/src/error.rs @@ -28,7 +28,12 @@ impl defmt::Format for OdpError { OdpError::BufferTooSmall => defmt::write!(f, "OdpError::BufferTooSmall"), OdpError::UnexpectedResponseKind => defmt::write!(f, "OdpError::UnexpectedResponseKind"), OdpError::UnexpectedMessageId { expected, got } => { - defmt::write!(f, "OdpError::UnexpectedMessageId {{ expected: {}, got: {} }}", expected, got) + defmt::write!( + f, + "OdpError::UnexpectedMessageId {{ expected: {}, got: {} }}", + expected, + got + ) } OdpError::Decode => defmt::write!(f, "OdpError::Decode"), } diff --git a/odp-client/src/lib.rs b/odp-client/src/lib.rs index e8fbc7a4..fc66277c 100644 --- a/odp-client/src/lib.rs +++ b/odp-client/src/lib.rs @@ -4,16 +4,28 @@ //! Consumers depend on this crate, NOT on `mctp-rs` directly. The wire format //! lives in `mctp_rs::odp` and is re-exported here for convenience. -pub use mctp_rs::odp::{ODP_MESSAGE_TYPE, OdpHeader, OdpMessage, OdpService}; +pub use client::{OdpClient, OdpResponse, Relay}; pub use error::OdpError; -pub use transport::OdpTransport; -pub use mctp_serial::MctpSerialTransport; pub use loopback::LoopbackTransport; -pub use client::{OdpClient, OdpResponse, Relay}; +pub use mctp_rs::odp::{ODP_MESSAGE_TYPE, OdpHeader, OdpMessage, OdpService}; +pub use mctp_serial::MctpSerialTransport; +pub use serializable::{MessageSerializationError, SerializableMessage, SerializableResult}; +pub use server::{MctpError, RelayHandler, RelayHeader, RelayResponse, RelayServiceHandler, RelayServiceHandlerTypes}; +pub use transport::OdpTransport; mod client; mod error; mod loopback; mod mctp_serial; -mod server; +pub mod serializable; +pub mod server; mod transport; + +/// Hidden re-exports used by the [`impl_odp_relay_handler`] macro. +/// Not part of the public API — do not depend on these directly. +#[doc(hidden)] +pub mod _macro_internal { + pub use bitfield; + pub use mctp_rs; + pub use paste; +} diff --git a/odp-client/src/mctp_serial.rs b/odp-client/src/mctp_serial.rs index 868460d5..179f895b 100644 --- a/odp-client/src/mctp_serial.rs +++ b/odp-client/src/mctp_serial.rs @@ -1,9 +1,8 @@ -use crate::{OdpError, OdpTransport, ODP_MESSAGE_TYPE}; +use crate::{ODP_MESSAGE_TYPE, OdpError, OdpTransport}; use embedded_io::{Read, ReadExactError, Write}; use mctp_rs::{ - EndpointId, MctpMedium, MctpMessageHeaderTrait, MctpMessageTag, MctpMessageTrait, - MctpPacketContext, MctpPacketError, MctpPacketResult, MctpReplyContext, MctpSequenceNumber, - MctpSerialMedium, + EndpointId, MctpMedium, MctpMessageHeaderTrait, MctpMessageTag, MctpMessageTrait, MctpPacketContext, + MctpPacketError, MctpPacketResult, MctpReplyContext, MctpSequenceNumber, MctpSerialMedium, }; /// MCTP serial framing END flag (DSP0253 0x7E). @@ -149,8 +148,7 @@ impl OdpTransport for MctpSerialTransport { } // 2. MCTP-strip via a fresh PacketContext borrowing assembly_buf. - let mut rx_ctx = - MctpPacketContext::::new(MctpSerialMedium, &mut self.assembly_buf); + let mut rx_ctx = MctpPacketContext::::new(MctpSerialMedium, &mut self.assembly_buf); let message = rx_ctx .deserialize_packet(&self.rx_frame_buf[..filled]) .map_err(|_| OdpError::Decode)? diff --git a/odp-client/src/serializable.rs b/odp-client/src/serializable.rs new file mode 100644 index 00000000..72e35a98 --- /dev/null +++ b/odp-client/src/serializable.rs @@ -0,0 +1,97 @@ +//! Helper code for serialization/deserialization of arbitrary messages to/from the embedded controller via a relay service, e.g. the eSPI service. + +/// Error type for serializing/deserializing messages +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MessageSerializationError { + /// The message payload does not represent a valid message + InvalidPayload(&'static str), + + /// The message discriminant does not represent a known message type + UnknownMessageDiscriminant(u16), + + /// The provided buffer is too small to serialize the message + BufferTooSmall, + + /// Unspecified error + Other(&'static str), +} + +/// Trait for serializing and deserializing messages +pub trait SerializableMessage: Sized { + /// Serializes the message into the provided buffer. + /// On success, returns the number of bytes written + fn serialize(self, buffer: &mut [u8]) -> Result; + + /// Returns the discriminant needed to deserialize this type of message. + fn discriminant(&self) -> u16; + + /// Deserializes the message from the provided buffer. + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result; +} + +// Prevent other types from implementing SerializableResult - they should instead use SerializableMessage on a Response type and an Error type +#[doc(hidden)] +mod private { + pub trait Sealed {} + + impl Sealed for Result {} +} + +/// Responses sent over MCTP are called "Results" and are of type Result where T and E both implement SerializableMessage +pub trait SerializableResult: private::Sealed + Sized { + /// The type of the result when the operation being responded to succeeded + type SuccessType: SerializableMessage; + + /// The type of the result when the operation being responded to failed + type ErrorType: SerializableMessage; + + /// Returns true if the result represents a successful operation, false otherwise + fn is_ok(&self) -> bool; + + /// Returns a unique discriminant that can be used to deserialize the specific type of result. + /// Discriminants can be reused for success and error messages. + fn discriminant(&self) -> u16; + + /// Writes the result into the provided buffer. + /// On success, returns the number of bytes written + fn serialize(self, buffer: &mut [u8]) -> Result; + + /// Attempts to deserialize the result from the provided buffer. + fn deserialize(is_error: bool, discriminant: u16, buffer: &[u8]) -> Result; +} + +impl SerializableResult for Result +where + T: SerializableMessage, + E: SerializableMessage, +{ + type SuccessType = T; + type ErrorType = E; + + fn is_ok(&self) -> bool { + Result::::is_ok(self) + } + + fn discriminant(&self) -> u16 { + match self { + Ok(success_value) => success_value.discriminant(), + Err(error_value) => error_value.discriminant(), + } + } + + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Ok(success_value) => success_value.serialize(buffer), + Err(error_value) => error_value.serialize(buffer), + } + } + + fn deserialize(is_error: bool, discriminant: u16, buffer: &[u8]) -> Result { + if is_error { + Ok(Err(E::deserialize(discriminant, buffer)?)) + } else { + Ok(Ok(T::deserialize(discriminant, buffer)?)) + } + } +} diff --git a/odp-client/src/server.rs b/odp-client/src/server.rs index 02e7ed1d..9aff56be 100644 --- a/odp-client/src/server.rs +++ b/odp-client/src/server.rs @@ -1 +1,406 @@ -// Stub module — populated by subsequent PLAN-006 tasks. +//! Server-side relay traits and the [`impl_odp_relay_handler`] macro. +//! +//! Contains helper traits for services that relay comms messages over MCTP, +//! plus the macro that aggregates a collection of `RelayServiceHandler` +//! implementations into a single `RelayHandler` suitable for use by a relay +//! service (e.g. the eSPI service). + +/// Error type for MCTP relay operations +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MctpError { + /// The endpoint ID does not correspond to a known service + UnknownEndpointId, +} + +/// Trait for types that are used by a relay service to relay messages from your service over the wire. +/// If you are implementing this trait, you should also implement RelayServiceHandler. +/// +pub trait RelayServiceHandlerTypes { + /// The request type that this service handler processes + type RequestType: crate::serializable::SerializableMessage; + + /// The result type that this service handler processes + type ResultType: crate::serializable::SerializableResult; +} + +/// Trait for a service that can be relayed over an external bus (e.g. battery service, thermal service, time-alarm service) +/// +pub trait RelayServiceHandler: RelayServiceHandlerTypes { + /// Process the provided request and yield a result. + fn process_request<'a>( + &'a self, + request: Self::RequestType, + ) -> impl core::future::Future + 'a; +} + +// Traits below this point are intended for consumption by relay services (e.g. the eSPI service), not individual services that want their messages relayed. +// In general, you should not implement these yourself; rather, you should leverage the `impl_odp_relay_handler` macro to do that for you. + +/// Contains additional methods that must be implemented on the relay header type. +/// Do not implement this yourself - rather, rely on the `impl_odp_relay_handler` macro to implement this. +#[doc(hidden)] +pub trait RelayHeader { + /// Return the ID of the service associated with the request + fn get_service_id(&self) -> ServiceIdType; +} + +/// Contains additional methods that must be implemented on the relay response type. +/// Do not implement this yourself - rather, rely on the `impl_odp_relay_handler` macro to implement this. +#[doc(hidden)] +pub trait RelayResponse { + /// Construct an MCTP header suitable for representing the result based on the provided service handler ID and result + fn create_header(&self, service_id: &ServiceIdType) -> HeaderType; +} + +/// Trait for aggregating collections of services that can be relayed over an external bus. +/// Do not implement this yourself - rather, rely on the `impl_odp_relay_handler` macro to implement this. +/// +pub trait RelayHandler { + /// The type that uniquely identifies individual services. Generally expected to be a C-style enum. + type ServiceIdType: Into + TryFrom + Copy; + + /// The header type used by request and result enums + type HeaderType: mctp_rs::MctpMessageHeaderTrait + RelayHeader; + + /// An enum over all possible request types + type RequestEnumType: for<'buf> mctp_rs::MctpMessageTrait<'buf, Header = Self::HeaderType>; + + /// An enum over all possible result types + type ResultEnumType: for<'buf> mctp_rs::MctpMessageTrait<'buf, Header = Self::HeaderType> + + RelayResponse; + + /// Process the provided request and yield a result. + fn process_request<'a>( + &'a self, + message: Self::RequestEnumType, + ) -> impl core::future::Future + 'a; +} + +/// This macro generates a relay type over a collection of message types, which can be used by a relay service to +/// receive messages over the wire and translate them into calls to a particular service on the EC. +/// +/// This is the recommended way to implement a relay handler - you should not implement the RelayHandler trait yourself. +/// +/// This macro will emit a type with the name you specify that is generic over a lifetime for the hardware (probably 'static in production code), +/// implements the `RelayHandler` trait, and has a single constructor method `new` that takes as arguments references to the service handler +/// types that you specify that have the 'hardware lifetime'. +/// +/// The macro takes the following inputs once: +/// relay_type_name: The name of the relay type to generate. This is arbitrary. The macro will emit a type with this name. +/// +/// Followed by a list of any number of service entries, which are specified by the following inputs: +/// service_name: A name to assign to generated identifiers associated with the service, e.g. "Battery". +/// This can be arbitrary. +/// service_id: A unique u8 that addresses that service on the EC. +/// service_handler_type: A type that implements the RelayServiceHandler trait, which will be used to process messages +/// for this service. +/// +/// Example usage: +/// +/// ```ignore +/// +/// impl_odp_relay_handler!( +/// MyRelayHandlerType; +/// Battery, 0x9, battery_service_relay::RelayHandler>; +/// TimeAlarm, 0xB, time_alarm_service_relay::RelayHandler>; +/// ); +/// +/// let relay_handler = MyRelayHandlerType::new(battery_service_instance, time_alarm_service_instance); +/// +/// // Then, pass relay_handler to your relay service (e.g. eSPI service), which should be generic over an `impl RelayHandler`. +/// +/// ``` +/// +#[macro_export] +macro_rules! impl_odp_relay_handler { + ( + $relay_type_name:ident; + $( + $service_name:ident, + $service_id:expr, + $service_handler_type:ty; + )+ + ) => { + $crate::_macro_internal::paste::paste! { + mod [< _odp_impl_ $relay_type_name:snake >] { + use $crate::_macro_internal::bitfield::bitfield; + use core::convert::Infallible; + use $crate::_macro_internal::mctp_rs::smbus_espi::SmbusEspiMedium; + use $crate::_macro_internal::mctp_rs::{MctpMedium, MctpMessageHeaderTrait, MctpMessageTrait, MctpPacketError, MctpPacketResult}; + use $crate::serializable::{SerializableMessage, SerializableResult}; + use $crate::server::RelayServiceHandler; + + #[derive(Debug, PartialEq, Eq, Clone, Copy)] + #[repr(u8)] + pub enum OdpService { + $( + $service_name = $service_id, + )+ + } + + impl From for u8 { + fn from(val: OdpService) -> u8 { + val as u8 + } + } + + impl TryFrom for OdpService { + type Error = u8; + fn try_from(value: u8) -> Result { + match value { + $( + $service_id => Ok(OdpService::$service_name), + )+ + other => Err(other), + } + } + } + + pub enum HostRequest { + $( + $service_name(<$service_handler_type as $crate::server::RelayServiceHandlerTypes>::RequestType), + )+ + } + + impl MctpMessageTrait<'_> for HostRequest { + type Header = OdpHeader; + const MESSAGE_TYPE: u8 = 0x7D; // ODP message type + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + match self { + $( + HostRequest::$service_name(request) => SerializableMessage::serialize(request, buffer) + .map_err(|_| MctpPacketError::SerializeError(concat!("Failed to serialize ", stringify!($service_name), " request"))), + )+ + } + } + + fn deserialize(header: &Self::Header, buffer: &'_ [u8]) -> MctpPacketResult { + Ok(match header.service { + $( + OdpService::$service_name => Self::$service_name( + <$service_handler_type as $crate::server::RelayServiceHandlerTypes>::RequestType::deserialize(header.message_id, buffer) + .map_err(|_| MctpPacketError::CommandParseError(concat!("Could not parse ", stringify!($service_name), " request")))?, + ), + )+ + }) + } + } + + bitfield! { + /// Wire format for ODP MCTP headers. Not user-facing - use OdpHeader instead. + #[derive(Copy, Clone, PartialEq, Eq)] + struct OdpHeaderWireFormat(u32); + impl Debug; + impl new; + /// If true, represents a request; otherwise, represents a result + is_request, set_is_request: 25; + + /// The service ID that this message is related to + /// Note: Error checking is done when you access the field, not when you construct the OdpHeader. Take care when constructing a header. + u8, service_id, set_service_id: 23, 16; + + /// On results, indicates if the result message is an error. Unused on requests. + is_error, set_is_error: 15; + + /// The message type/discriminant + u16, message_id, set_message_id: 14, 0; + } + + #[derive(Copy, Clone, PartialEq, Eq)] + pub enum OdpMessageType { + Request, + Result { is_error: bool }, + } + + #[derive(Copy, Clone, PartialEq, Eq)] + pub struct OdpHeader { + pub message_type: OdpMessageType, + pub service: OdpService, + pub message_id: u16, + } + + impl From for OdpHeaderWireFormat { + fn from(src: OdpHeader) -> Self { + Self::new( + matches!(src.message_type, OdpMessageType::Request), + src.service.into(), + match src.message_type { + OdpMessageType::Request => false, // unused on requests + OdpMessageType::Result { is_error } => is_error, + }, + src.message_id, + ) + } + } + + impl TryFrom for OdpHeader { + type Error = MctpPacketError; + + fn try_from(src: OdpHeaderWireFormat) -> Result { + let service = OdpService::try_from(src.service_id()) + .map_err(|_| MctpPacketError::HeaderParseError("invalid odp service in odp header"))?; + + let message_type = if src.is_request() { + OdpMessageType::Request + } else { + OdpMessageType::Result { + is_error: src.is_error(), + } + }; + + Ok(OdpHeader { + message_type, + service, + message_id: src.message_id(), + }) + } + } + + impl MctpMessageHeaderTrait for OdpHeader { + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + let wire_format = OdpHeaderWireFormat::from(self); + let bytes = wire_format.0.to_be_bytes(); + buffer + .get_mut(0..bytes.len()) + .ok_or(MctpPacketError::SerializeError("buffer too small for odp header"))? + .copy_from_slice(&bytes); + + Ok(bytes.len()) + } + + fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M> { + let bytes = buffer + .get(0..core::mem::size_of::()) + .ok_or(MctpPacketError::HeaderParseError("buffer too small for odp header"))?; + let raw = u32::from_be_bytes( + bytes + .try_into() + .map_err(|_| MctpPacketError::HeaderParseError("buffer too small for odp header"))?, + ); + + let parsed_wire_format = OdpHeaderWireFormat(raw); + let header = OdpHeader::try_from(parsed_wire_format) + .map_err(|_| MctpPacketError::HeaderParseError("invalid odp header received"))?; + + Ok(( + header, + buffer + .get(core::mem::size_of::()..) + .ok_or(MctpPacketError::HeaderParseError("buffer too small for odp header"))?, + )) + } + } + + impl $crate::server::RelayHeader for OdpHeader { + fn get_service_id(&self) -> OdpService { + self.service + } + } + + #[derive(Clone)] + pub enum HostResult { + $( + $service_name(<$service_handler_type as $crate::server::RelayServiceHandlerTypes>::ResultType), + )+ + } + + impl $crate::server::RelayResponse for HostResult { + fn create_header(&self, service_id: &OdpService) -> OdpHeader { + match (self) { + $( + (HostResult::$service_name(result)) => OdpHeader { + message_type: OdpMessageType::Result { is_error: !result.is_ok() }, + service: *service_id, + message_id: result.discriminant(), + }, + )+ + } + } + } + + impl MctpMessageTrait<'_> for HostResult { + const MESSAGE_TYPE: u8 = 0x7D; // ODP message type + type Header = OdpHeader; + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + match self { + $( + HostResult::$service_name(result) => result + .serialize(buffer) + .map_err(|_| MctpPacketError::SerializeError(concat!("Failed to serialize ", stringify!($service_name), " result"))), + )+ + } + } + + fn deserialize(header: &Self::Header, buffer: &'_ [u8]) -> MctpPacketResult { + match header.service { + $( + OdpService::$service_name => { + match header.message_type { + OdpMessageType::Request => { + Err(MctpPacketError::CommandParseError(concat!("Received ", stringify!($service_name), " request when expecting result"))) + } + OdpMessageType::Result { is_error } => { + Ok(HostResult::$service_name(<$service_handler_type as $crate::server::RelayServiceHandlerTypes>::ResultType::deserialize(is_error, header.message_id, buffer) + .map_err(|_| MctpPacketError::CommandParseError(concat!("Could not parse ", stringify!($service_name), " result")))?)) + } + } + }, + )+ + } + } + } + + + pub struct $relay_type_name { + $( + [<$service_name:snake _handler>]: $service_handler_type, + )+ + } + + impl $relay_type_name { + pub fn new( + $( + [<$service_name:snake _handler>]: $service_handler_type, + )+ + ) -> Self { + Self { + $( + [<$service_name:snake _handler>], + )+ + } + } + } + + impl $crate::server::RelayHandler for $relay_type_name { + type ServiceIdType = OdpService; + type HeaderType = OdpHeader; + type RequestEnumType = HostRequest; + type ResultEnumType = HostResult; + + fn process_request<'a>( + &'a self, + message: HostRequest, + ) -> impl core::future::Future + 'a { + async move { + match message { + $( + HostRequest::$service_name(request) => { + let result = self.[<$service_name:snake _handler>].process_request(request).await; + HostResult::$service_name(result) + } + )+ + } + } + } + } + } // end mod __odp_impl + + // Allows this generated relay type to be publicly re-exported + pub use [< _odp_impl_ $relay_type_name:snake >]::$relay_type_name; + + } // end paste! + }; // end macro arm +} // end macro diff --git a/odp-client/tests/client_invoke.rs b/odp-client/tests/client_invoke.rs index d261b152..01ff9b9f 100644 --- a/odp-client/tests/client_invoke.rs +++ b/odp-client/tests/client_invoke.rs @@ -51,7 +51,10 @@ impl StubTransport { fn with_recv(bytes: &[u8]) -> Self { let mut buf = [0u8; 64]; buf[..bytes.len()].copy_from_slice(bytes); - Self { recv_bytes: buf, recv_len: bytes.len() } + Self { + recv_bytes: buf, + recv_len: bytes.len(), + } } } @@ -85,11 +88,12 @@ fn invoke_with_mismatched_response_message_id_returns_unexpected() { is_error: false, message_id: 0x11, }; - let err = client - .invoke(req_header, &[], |_| Ok::<(), OdpError>(())) - .unwrap_err(); + let err = client.invoke(req_header, &[], |_| Ok::<(), OdpError>(())).unwrap_err(); assert_eq!( err, - OdpError::UnexpectedMessageId { expected: 0x11, got: 0x99 } + OdpError::UnexpectedMessageId { + expected: 0x11, + got: 0x99 + } ); } diff --git a/odp-client/tests/error.rs b/odp-client/tests/error.rs index 3b033fbc..509b1ba9 100644 --- a/odp-client/tests/error.rs +++ b/odp-client/tests/error.rs @@ -15,7 +15,10 @@ fn odp_error_is_copy_eq_and_debug() { #[test] fn odp_error_unexpected_message_id_carries_expected_and_got() { - let e = OdpError::UnexpectedMessageId { expected: 0x1234, got: 0x5678 }; + let e = OdpError::UnexpectedMessageId { + expected: 0x1234, + got: 0x5678, + }; match e { OdpError::UnexpectedMessageId { expected, got } => { assert_eq!(expected, 0x1234); diff --git a/odp-client/tests/mctp_serial.rs b/odp-client/tests/mctp_serial.rs index 965a95a9..af29411f 100644 --- a/odp-client/tests/mctp_serial.rs +++ b/odp-client/tests/mctp_serial.rs @@ -8,7 +8,10 @@ struct LoopbackUart { impl LoopbackUart { fn new() -> Self { - Self { buf: Vec::new(), cursor: 0 } + Self { + buf: Vec::new(), + cursor: 0, + } } } diff --git a/thermal-service-relay/Cargo.toml b/thermal-service-relay/Cargo.toml index 43f7c1db..c1cd1559 100644 --- a/thermal-service-relay/Cargo.toml +++ b/thermal-service-relay/Cargo.toml @@ -7,7 +7,7 @@ repository.workspace = true [dependencies] defmt = { workspace = true, optional = true } -embedded-services.workspace = true +odp-client.workspace = true thermal-service-interface.workspace = true num_enum.workspace = true uuid.workspace = true @@ -16,4 +16,4 @@ uuid.workspace = true workspace = true [features] -defmt = ["dep:defmt"] +defmt = ["dep:defmt", "odp-client/defmt"] diff --git a/thermal-service-relay/src/lib.rs b/thermal-service-relay/src/lib.rs index fceae864..5cde311c 100644 --- a/thermal-service-relay/src/lib.rs +++ b/thermal-service-relay/src/lib.rs @@ -191,12 +191,12 @@ impl ThermalServiceRelayHandler { } } -impl embedded_services::relay::mctp::RelayServiceHandlerTypes for ThermalServiceRelayHandler { +impl odp_client::server::RelayServiceHandlerTypes for ThermalServiceRelayHandler { type RequestType = ThermalRequest; type ResultType = ThermalResult; } -impl embedded_services::relay::mctp::RelayServiceHandler for ThermalServiceRelayHandler { +impl odp_client::server::RelayServiceHandler for ThermalServiceRelayHandler { async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { match request { ThermalRequest::ThermalGetTmpRequest { instance_id } => self.sensor_get_tmp(instance_id).await, diff --git a/thermal-service-relay/src/serialization.rs b/thermal-service-relay/src/serialization.rs index 6b1a7a63..890ea952 100644 --- a/thermal-service-relay/src/serialization.rs +++ b/thermal-service-relay/src/serialization.rs @@ -1,5 +1,5 @@ use crate::DeciKelvin; -use embedded_services::relay::{MessageSerializationError, SerializableMessage}; +use odp_client::{MessageSerializationError, SerializableMessage}; // Standard MPTF requests expected by the thermal subsystem #[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] diff --git a/time-alarm-service-relay/Cargo.toml b/time-alarm-service-relay/Cargo.toml index e098b190..3c716abf 100644 --- a/time-alarm-service-relay/Cargo.toml +++ b/time-alarm-service-relay/Cargo.toml @@ -11,9 +11,9 @@ ignored = ["log"] [dependencies] defmt = { workspace = true, optional = true } log = { workspace = true, optional = true } -embedded-services.workspace = true embedded-mcu-hal.workspace = true num_enum.workspace = true +odp-client.workspace = true time-alarm-service-interface.workspace = true [features] @@ -21,8 +21,9 @@ defmt = [ "dep:defmt", "embedded-mcu-hal/defmt", "time-alarm-service-interface/defmt", + "odp-client/defmt", ] -log = ["dep:log", "embedded-services/log"] +log = ["dep:log"] [lints] workspace = true diff --git a/time-alarm-service-relay/src/lib.rs b/time-alarm-service-relay/src/lib.rs index ed444a39..f8c3edb7 100644 --- a/time-alarm-service-relay/src/lib.rs +++ b/time-alarm-service-relay/src/lib.rs @@ -17,12 +17,12 @@ impl TimeAlarmServiceRelayHandler { } } -impl embedded_services::relay::mctp::RelayServiceHandlerTypes for TimeAlarmServiceRelayHandler { +impl odp_client::server::RelayServiceHandlerTypes for TimeAlarmServiceRelayHandler { type RequestType = AcpiTimeAlarmRequest; type ResultType = AcpiTimeAlarmResult; } -impl embedded_services::relay::mctp::RelayServiceHandler for TimeAlarmServiceRelayHandler { +impl odp_client::server::RelayServiceHandler for TimeAlarmServiceRelayHandler { async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { match request { AcpiTimeAlarmRequest::GetCapabilities => { diff --git a/time-alarm-service-relay/src/serialization.rs b/time-alarm-service-relay/src/serialization.rs index bc77f46e..d43257e3 100644 --- a/time-alarm-service-relay/src/serialization.rs +++ b/time-alarm-service-relay/src/serialization.rs @@ -1,5 +1,5 @@ use core::array::TryFromSliceError; -use embedded_services::relay::{MessageSerializationError, SerializableMessage}; +use odp_client::{MessageSerializationError, SerializableMessage}; use time_alarm_service_interface::{ AcpiDaylightSavingsTimeStatus, AcpiTimerId, AcpiTimestamp, AlarmExpiredWakePolicy, AlarmTimerSeconds, TimeAlarmDeviceCapabilities, TimerStatus, diff --git a/uart-service/Cargo.toml b/uart-service/Cargo.toml index beb37355..ede516f7 100644 --- a/uart-service/Cargo.toml +++ b/uart-service/Cargo.toml @@ -19,6 +19,7 @@ defmt = { workspace = true, optional = true } log = { workspace = true, optional = true } embassy-sync.workspace = true mctp-rs = { workspace = true } +odp-client.workspace = true embedded-io-async.workspace = true [features] @@ -28,6 +29,7 @@ defmt = [ "embedded-services/defmt", "embassy-sync/defmt", "mctp-rs/defmt", + "odp-client/defmt", ] log = ["dep:log", "embedded-services/log"] diff --git a/uart-service/src/lib.rs b/uart-service/src/lib.rs index e3ecffe6..93a2a222 100644 --- a/uart-service/src/lib.rs +++ b/uart-service/src/lib.rs @@ -15,10 +15,10 @@ use embassy_sync::channel::Channel; use embedded_io_async::Read as UartRead; use embedded_io_async::Write as UartWrite; use embedded_services::GlobalRawMutex; -use embedded_services::relay::mctp::{RelayHandler, RelayHeader, RelayResponse}; use embedded_services::trace; use mctp_rs::MctpMedium; use mctp_rs::smbus_espi::{SmbusEspiMedium, SmbusEspiReplyContext}; +use odp_client::server::{RelayHandler, RelayHeader, RelayResponse}; // Should be as large as the largest possible MCTP packet and its metadata. const BUF_SIZE: usize = 256; diff --git a/uart-service/src/task.rs b/uart-service/src/task.rs index 06a2b8c0..291bb6f4 100644 --- a/uart-service/src/task.rs +++ b/uart-service/src/task.rs @@ -2,8 +2,8 @@ use crate::{Error, Service}; use embedded_io_async::Read as UartRead; use embedded_io_async::Write as UartWrite; use embedded_services::error; -use embedded_services::relay::mctp::RelayHandler; use mctp_rs::MctpMedium; +use odp_client::server::RelayHandler; pub async fn uart_service( uart_service: &Service, From b00c3a7a3f1710fe725a0bbe29c1b6bb936817f1 Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Thu, 28 May 2026 09:25:24 -0700 Subject: [PATCH 4/4] refactor(odp-client): polish public API and scrub MCTP from caller-facing surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups landed together to give the crate a clean v1 public surface before downstream services start adopting it. 1. `Relay::invoke` returns the decoded response by value-with-borrow instead of taking a `FnOnce(OdpResponse) -> Result` parse closure: fn invoke<'a>(&'a mut self, ...) -> Result, OdpError>; The response body still borrows from the client's internal buffer; the borrow checker enforces the same scope discipline the closure did, but call sites no longer nest decode logic in a closure, the trait drops its `R, F` generics, and `Relay` becomes object-safe (covered by a new `relay_is_object_safe_via_dyn` test). 2. Public renames to drop "Mctp"/"mctp" from caller-facing names: - `MctpSerialTransport` -> `SerialTransport` (`src/mctp_serial.rs` -> `src/serial.rs`, `tests/mctp_serial.rs` -> `tests/serial.rs`) - `MctpError` -> deleted (was unreferenced; whole variant set was MCTP-shaped leftovers) - `ODP_MESSAGE_TYPE`: removed from `pub use` (now an internal `mctp_rs::odp::ODP_MESSAGE_TYPE` reference in `src/serial.rs`) - `OdpMessage`: removed from `pub use` (no consumers, internal or external) The `_macro_internal` hidden re-exports of `mctp_rs`/`bitfield`/ `paste` are unchanged — they remain necessary for the relay macro. 3. Crate-level docs and Cargo.toml description rewritten so the public `odp-client` surface no longer directs readers to `mctp_rs::odp` to understand the wire format. Per-file doc audit trimmed stale or verbose comments throughout. Deferred: `RelayHandler::HeaderType / RequestEnumType / ResultEnumType` still expose `mctp_rs::MctpMessageHeaderTrait` / `MctpMessageTrait` bounds. De-leaking these requires designing a parallel transport- neutral trait family — out of scope for this PR. --- odp-client/Cargo.toml | 2 +- odp-client/src/client.rs | 54 +++------ odp-client/src/error.rs | 7 +- odp-client/src/lib.rs | 16 +-- odp-client/src/loopback.rs | 7 +- odp-client/src/{mctp_serial.rs => serial.rs} | 40 +++---- odp-client/src/serializable.rs | 43 +++---- odp-client/src/server.rs | 109 ++++++++---------- odp-client/src/transport.rs | 20 ++-- odp-client/tests/client_invoke.rs | 41 ++++--- .../tests/{mctp_serial.rs => serial.rs} | 30 +++-- 11 files changed, 166 insertions(+), 203 deletions(-) rename odp-client/src/{mctp_serial.rs => serial.rs} (76%) rename odp-client/tests/{mctp_serial.rs => serial.rs} (57%) diff --git a/odp-client/Cargo.toml b/odp-client/Cargo.toml index d78e8710..73d46497 100644 --- a/odp-client/Cargo.toml +++ b/odp-client/Cargo.toml @@ -3,7 +3,7 @@ name = "odp-client" version.workspace = true edition.workspace = true license.workspace = true -description = "Sync ODP MCTP client + async server macro. Hides mctp-rs from consumers." +description = "Transport-blind sync ODP client and server-side service-handler macro." [features] default = [] diff --git a/odp-client/src/client.rs b/odp-client/src/client.rs index 3a50e49e..9ea9536e 100644 --- a/odp-client/src/client.rs +++ b/odp-client/src/client.rs @@ -1,45 +1,32 @@ use crate::{OdpError, OdpHeader, OdpTransport}; -/// Decoded ODP response handed to the parse closure inside [`Relay::invoke`]. +/// Decoded ODP response returned by [`Relay::invoke`]. /// -/// The body slice borrows from the client's internal assembly buffer; only -/// owned values can escape the closure. +/// The body slice borrows from the relay's internal assembly buffer; the +/// caller must finish reading the response before calling `invoke` again or +/// dropping the relay. +#[derive(Debug)] pub struct OdpResponse<'a> { pub header: OdpHeader, pub body: &'a [u8], } -/// Transport-blind abstraction over "issue a request to the EC and parse the -/// response". Per-service code depends on `R: Relay`, never on a concrete -/// `OdpClient`. +/// Transport-blind abstraction over "issue a request and receive a +/// decoded response". Per-service code depends on `R: Relay` (or +/// `&mut dyn Relay`) rather than a concrete [`OdpClient`]. /// -/// The closure pattern is required because the response body slice borrows -/// from the client's internal buffer; parsing inside the closure guarantees -/// only owned types escape. -/// -/// # Object safety -/// -/// `invoke` is generic over `R` (return type) and `F` (closure type), so -/// `Relay` is **not** object-safe. Use it as a bound (`impl Relay` / -/// `R: Relay`), not as `dyn Relay`. +/// The returned [`OdpResponse`] borrows from the relay's internal +/// buffer, so the borrow checker prevents calling `invoke` again or +/// dropping the relay until the response is consumed — no copy is required. pub trait Relay { - /// Encode `header` + `body` as an ODP wire frame, hand them to the - /// underlying transport, receive the response, validate the - /// `message_id` round-trip, and pass an [`OdpResponse`] to `parse`. - /// - /// The parse closure runs inside `invoke` so that the borrow of the - /// client's internal buffer is confined to the closure scope; any - /// owned value returned from the closure escapes cleanly. - fn invoke(&mut self, header: OdpHeader, body: &[u8], parse: F) -> Result - where - F: FnOnce(OdpResponse<'_>) -> Result; + /// Encode `header` + `body`, send via the underlying transport, + /// receive the response, validate the `message_id` round-trip, and + /// return the decoded [`OdpResponse`]. + fn invoke<'a>(&'a mut self, header: OdpHeader, body: &[u8]) -> Result, OdpError>; } -/// Sync ODP-over-transport client. -/// -/// Owns the transport and a 256-byte assembly buffer used for both request -/// encoding and response decoding (sequentially — the phases never overlap -/// because send completes before recv begins). +/// Sync ODP client. Owns the transport and a 256-byte buffer used +/// sequentially for request encoding and response decoding. pub struct OdpClient { transport: T, buf: [u8; 256], @@ -56,10 +43,7 @@ impl OdpClient { } impl Relay for OdpClient { - fn invoke(&mut self, header: OdpHeader, body: &[u8], parse: F) -> Result - where - F: FnOnce(OdpResponse<'_>) -> Result, - { + fn invoke<'a>(&'a mut self, header: OdpHeader, body: &[u8]) -> Result, OdpError> { let need = 4 + body.len(); if need > self.buf.len() { return Err(OdpError::BufferTooSmall); @@ -86,7 +70,7 @@ impl Relay for OdpClient { }); } - parse(OdpResponse { + Ok(OdpResponse { header: resp_header, body: &self.buf[4..n], }) diff --git a/odp-client/src/error.rs b/odp-client/src/error.rs index 26426e31..4bb1e3a9 100644 --- a/odp-client/src/error.rs +++ b/odp-client/src/error.rs @@ -1,11 +1,10 @@ /// Errors surfaced by the `odp-client` API. /// -/// The transport layer maps lower-level MCTP / framing errors into these -/// variants so that callers see one consistent error surface and don't have -/// to depend on `mctp-rs` directly. +/// Transport implementations map their own lower-level errors into these +/// variants so callers see one consistent surface. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum OdpError { - /// Underlying transport (serial / MCTP framing) reported a failure. + /// Underlying transport reported a failure. Transport, /// Timed out waiting for a response from the peer. Timeout, diff --git a/odp-client/src/lib.rs b/odp-client/src/lib.rs index fc66277c..490b0c7f 100644 --- a/odp-client/src/lib.rs +++ b/odp-client/src/lib.rs @@ -1,22 +1,24 @@ #![no_std] -//! Sync ODP client + transport abstraction. +//! Transport-blind sync ODP client and server-side service-handler macro. //! -//! Consumers depend on this crate, NOT on `mctp-rs` directly. The wire format -//! lives in `mctp_rs::odp` and is re-exported here for convenience. +//! Callers issue ODP requests through an [`OdpClient`] (or any other +//! [`Relay`]) without depending on a concrete transport. The transport is +//! selected at construction time and hidden behind the [`OdpTransport`] +//! trait, so swapping the underlying medium does not affect call sites. pub use client::{OdpClient, OdpResponse, Relay}; pub use error::OdpError; pub use loopback::LoopbackTransport; -pub use mctp_rs::odp::{ODP_MESSAGE_TYPE, OdpHeader, OdpMessage, OdpService}; -pub use mctp_serial::MctpSerialTransport; +pub use mctp_rs::odp::{OdpHeader, OdpService}; +pub use serial::SerialTransport; pub use serializable::{MessageSerializationError, SerializableMessage, SerializableResult}; -pub use server::{MctpError, RelayHandler, RelayHeader, RelayResponse, RelayServiceHandler, RelayServiceHandlerTypes}; +pub use server::{RelayHandler, RelayHeader, RelayResponse, RelayServiceHandler, RelayServiceHandlerTypes}; pub use transport::OdpTransport; mod client; mod error; mod loopback; -mod mctp_serial; +mod serial; pub mod serializable; pub mod server; mod transport; diff --git a/odp-client/src/loopback.rs b/odp-client/src/loopback.rs index 9714d116..5f1d20e6 100644 --- a/odp-client/src/loopback.rs +++ b/odp-client/src/loopback.rs @@ -2,11 +2,8 @@ use crate::{OdpError, OdpTransport}; use heapless::Vec; /// In-memory test double for [`OdpTransport`]. Echoes what was sent on -/// the next [`OdpTransport::recv_message`] call. -/// -/// Useful for unit testing per-service client logic without standing -/// up a real UART. Capacity is fixed at 256 bytes — large enough for -/// any single ODP message we expect to round-trip in tests. +/// the next [`OdpTransport::recv_message`] call. Capacity is fixed at +/// 256 bytes. /// /// # Lifecycle /// diff --git a/odp-client/src/mctp_serial.rs b/odp-client/src/serial.rs similarity index 76% rename from odp-client/src/mctp_serial.rs rename to odp-client/src/serial.rs index 179f895b..80b80344 100644 --- a/odp-client/src/mctp_serial.rs +++ b/odp-client/src/serial.rs @@ -1,5 +1,6 @@ -use crate::{ODP_MESSAGE_TYPE, OdpError, OdpTransport}; +use crate::{OdpError, OdpTransport}; use embedded_io::{Read, ReadExactError, Write}; +use mctp_rs::odp::ODP_MESSAGE_TYPE; use mctp_rs::{ EndpointId, MctpMedium, MctpMessageHeaderTrait, MctpMessageTag, MctpMessageTrait, MctpPacketContext, MctpPacketError, MctpPacketResult, MctpReplyContext, MctpSequenceNumber, MctpSerialMedium, @@ -8,8 +9,6 @@ use mctp_rs::{ /// MCTP serial framing END flag (DSP0253 0x7E). const SERIAL_END_FLAG: u8 = 0x7E; -/// Inner buffer sizes — match the legacy `ec_relay.rs` which has been -/// proven to fit single-packet ODP messages. const ASSEMBLY_BUF_LEN: usize = 128; const RX_FRAME_BUF_LEN: usize = 128; @@ -64,23 +63,18 @@ fn reply_context(src: EndpointId, dst: EndpointId) -> MctpReplyContext { +/// `src_eid` / `dst_eid` populate the framing header of outbound frames. +/// Inbound frames are decoded but the EIDs are not validated against +/// expectation — the serial medium has no addressing of its own. +pub struct SerialTransport { uart: U, src_eid: EndpointId, dst_eid: EndpointId, @@ -88,11 +82,9 @@ pub struct MctpSerialTransport { assembly_buf: [u8; ASSEMBLY_BUF_LEN], } -impl MctpSerialTransport { - /// Create a new transport wrapping `uart`, using `src_eid` / `dst_eid` - /// in the MCTP header of every outbound frame. All MCTP framing - /// (DSP0253 serial encapsulation) is handled internally; callers only - /// see raw ODP wire bytes. +impl SerialTransport { + /// Construct a transport over `uart`. `src_eid` / `dst_eid` are the + /// endpoint IDs stamped into every outbound frame. pub fn new(uart: U, src_eid: EndpointId, dst_eid: EndpointId) -> Self { Self { uart, @@ -104,7 +96,7 @@ impl MctpSerialTransport { } } -impl OdpTransport for MctpSerialTransport { +impl OdpTransport for SerialTransport { fn send_message(&mut self, payload: &[u8]) -> Result<(), OdpError> { if payload.len() < 4 { return Err(OdpError::BufferTooSmall); @@ -147,14 +139,14 @@ impl OdpTransport for MctpSerialTransport { } } - // 2. MCTP-strip via a fresh PacketContext borrowing assembly_buf. + // 2. Strip framing via a fresh PacketContext borrowing assembly_buf. let mut rx_ctx = MctpPacketContext::::new(MctpSerialMedium, &mut self.assembly_buf); let message = rx_ctx .deserialize_packet(&self.rx_frame_buf[..filled]) .map_err(|_| OdpError::Decode)? .ok_or(OdpError::Decode)?; - // 3. Copy out the MCTP body (= ODP header+body wire bytes). + // 3. Copy out the message body (= ODP header+body wire bytes). let body = message.message_buffer.body(); if buf.len() < body.len() { return Err(OdpError::BufferTooSmall); diff --git a/odp-client/src/serializable.rs b/odp-client/src/serializable.rs index 72e35a98..dab03dca 100644 --- a/odp-client/src/serializable.rs +++ b/odp-client/src/serializable.rs @@ -1,36 +1,37 @@ -//! Helper code for serialization/deserialization of arbitrary messages to/from the embedded controller via a relay service, e.g. the eSPI service. +//! Serialization/deserialization helpers for ODP relay request/response message types. -/// Error type for serializing/deserializing messages +/// Error type for serializing/deserializing messages. #[derive(Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum MessageSerializationError { - /// The message payload does not represent a valid message + /// The message payload does not represent a valid message. InvalidPayload(&'static str), - /// The message discriminant does not represent a known message type + /// The message discriminant does not represent a known message type. UnknownMessageDiscriminant(u16), - /// The provided buffer is too small to serialize the message + /// The provided buffer is too small to serialize the message. BufferTooSmall, - /// Unspecified error + /// Unspecified error. Other(&'static str), } -/// Trait for serializing and deserializing messages +/// Trait for serializing and deserializing messages. pub trait SerializableMessage: Sized { - /// Serializes the message into the provided buffer. - /// On success, returns the number of bytes written + /// Serialize the message into `buffer`, returning the number of bytes written. fn serialize(self, buffer: &mut [u8]) -> Result; - /// Returns the discriminant needed to deserialize this type of message. + /// Discriminant used to identify the concrete message type on deserialize. fn discriminant(&self) -> u16; - /// Deserializes the message from the provided buffer. + /// Deserialize a message of the type identified by `discriminant` from `buffer`. fn deserialize(discriminant: u16, buffer: &[u8]) -> Result; } -// Prevent other types from implementing SerializableResult - they should instead use SerializableMessage on a Response type and an Error type +// Sealed: only `Result` may implement `SerializableResult`. Custom response +// types should implement `SerializableMessage` on a Response and an Error type and +// use `Result` for the result. #[doc(hidden)] mod private { pub trait Sealed {} @@ -38,26 +39,26 @@ mod private { impl Sealed for Result {} } -/// Responses sent over MCTP are called "Results" and are of type Result where T and E both implement SerializableMessage +/// Sealed trait implemented for `Result` where both `T` and `E` +/// implement [`SerializableMessage`]. Used for response ("result") types. pub trait SerializableResult: private::Sealed + Sized { - /// The type of the result when the operation being responded to succeeded + /// The success arm's message type. type SuccessType: SerializableMessage; - /// The type of the result when the operation being responded to failed + /// The error arm's message type. type ErrorType: SerializableMessage; - /// Returns true if the result represents a successful operation, false otherwise + /// `true` if this result represents a successful operation. fn is_ok(&self) -> bool; - /// Returns a unique discriminant that can be used to deserialize the specific type of result. - /// Discriminants can be reused for success and error messages. + /// Discriminant of the inner success or error message. Success and + /// error variants may reuse the same discriminant. fn discriminant(&self) -> u16; - /// Writes the result into the provided buffer. - /// On success, returns the number of bytes written + /// Serialize the inner message into `buffer`, returning the number of bytes written. fn serialize(self, buffer: &mut [u8]) -> Result; - /// Attempts to deserialize the result from the provided buffer. + /// Deserialize a result. `is_error` selects between success and error decoding. fn deserialize(is_error: bool, discriminant: u16, buffer: &[u8]) -> Result; } diff --git a/odp-client/src/server.rs b/odp-client/src/server.rs index 9aff56be..858826f2 100644 --- a/odp-client/src/server.rs +++ b/odp-client/src/server.rs @@ -1,31 +1,21 @@ //! Server-side relay traits and the [`impl_odp_relay_handler`] macro. //! -//! Contains helper traits for services that relay comms messages over MCTP, -//! plus the macro that aggregates a collection of `RelayServiceHandler` -//! implementations into a single `RelayHandler` suitable for use by a relay -//! service (e.g. the eSPI service). - -/// Error type for MCTP relay operations -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum MctpError { - /// The endpoint ID does not correspond to a known service - UnknownEndpointId, -} +//! Provides traits that individual service handlers implement, plus the +//! macro that aggregates them into a single [`RelayHandler`] suitable for +//! plugging into a relay service (e.g. the eSPI service). -/// Trait for types that are used by a relay service to relay messages from your service over the wire. -/// If you are implementing this trait, you should also implement RelayServiceHandler. -/// +/// Companion-types trait for [`RelayServiceHandler`]: declares the +/// request and result types for a single service. pub trait RelayServiceHandlerTypes { - /// The request type that this service handler processes + /// The request type this service handler processes. type RequestType: crate::serializable::SerializableMessage; - /// The result type that this service handler processes + /// The result type this service handler produces. type ResultType: crate::serializable::SerializableResult; } -/// Trait for a service that can be relayed over an external bus (e.g. battery service, thermal service, time-alarm service) -/// +/// Trait for a service that can be relayed over an external bus (e.g. +/// battery service, thermal service, time-alarm service). pub trait RelayServiceHandler: RelayServiceHandlerTypes { /// Process the provided request and yield a result. fn process_request<'a>( @@ -34,39 +24,40 @@ pub trait RelayServiceHandler: RelayServiceHandlerTypes { ) -> impl core::future::Future + 'a; } -// Traits below this point are intended for consumption by relay services (e.g. the eSPI service), not individual services that want their messages relayed. -// In general, you should not implement these yourself; rather, you should leverage the `impl_odp_relay_handler` macro to do that for you. +// Traits below this point are intended for consumption by relay services (e.g. the eSPI service), +// not individual services that want their messages relayed. Implement these via the +// `impl_odp_relay_handler` macro rather than by hand. -/// Contains additional methods that must be implemented on the relay header type. -/// Do not implement this yourself - rather, rely on the `impl_odp_relay_handler` macro to implement this. +/// Additional methods required on the relay header type. Implemented by +/// the [`impl_odp_relay_handler`] macro; do not implement directly. #[doc(hidden)] pub trait RelayHeader { - /// Return the ID of the service associated with the request + /// Return the ID of the service associated with the request. fn get_service_id(&self) -> ServiceIdType; } -/// Contains additional methods that must be implemented on the relay response type. -/// Do not implement this yourself - rather, rely on the `impl_odp_relay_handler` macro to implement this. +/// Additional methods required on the relay response type. Implemented +/// by the [`impl_odp_relay_handler`] macro; do not implement directly. #[doc(hidden)] pub trait RelayResponse { - /// Construct an MCTP header suitable for representing the result based on the provided service handler ID and result + /// Construct a header for this result given its service ID. fn create_header(&self, service_id: &ServiceIdType) -> HeaderType; } -/// Trait for aggregating collections of services that can be relayed over an external bus. -/// Do not implement this yourself - rather, rely on the `impl_odp_relay_handler` macro to implement this. -/// +/// Aggregates a collection of services into a single relay surface. +/// Implemented by the [`impl_odp_relay_handler`] macro; do not implement +/// directly. pub trait RelayHandler { - /// The type that uniquely identifies individual services. Generally expected to be a C-style enum. + /// The type that uniquely identifies individual services. Generally a C-style enum. type ServiceIdType: Into + TryFrom + Copy; - /// The header type used by request and result enums + /// The header type used by request and result enums. type HeaderType: mctp_rs::MctpMessageHeaderTrait + RelayHeader; - /// An enum over all possible request types + /// An enum over all possible request types. type RequestEnumType: for<'buf> mctp_rs::MctpMessageTrait<'buf, Header = Self::HeaderType>; - /// An enum over all possible result types + /// An enum over all possible result types. type ResultEnumType: for<'buf> mctp_rs::MctpMessageTrait<'buf, Header = Self::HeaderType> + RelayResponse; @@ -77,41 +68,33 @@ pub trait RelayHandler { ) -> impl core::future::Future + 'a; } -/// This macro generates a relay type over a collection of message types, which can be used by a relay service to -/// receive messages over the wire and translate them into calls to a particular service on the EC. -/// -/// This is the recommended way to implement a relay handler - you should not implement the RelayHandler trait yourself. +/// Generates a relay type that aggregates multiple service handlers into a +/// single [`RelayHandler`] suitable for use by a relay service (e.g. the +/// eSPI service). This is the recommended way to obtain a `RelayHandler` +/// — do not implement that trait by hand. /// -/// This macro will emit a type with the name you specify that is generic over a lifetime for the hardware (probably 'static in production code), -/// implements the `RelayHandler` trait, and has a single constructor method `new` that takes as arguments references to the service handler -/// types that you specify that have the 'hardware lifetime'. +/// Inputs: +/// - `relay_type_name`: identifier for the generated struct +/// - For each service: +/// - `service_name`: identifier used to name fields and variants +/// - `service_id`: unique `u8` addressing the service on the EC +/// - `service_handler_type`: a type implementing [`RelayServiceHandler`] /// -/// The macro takes the following inputs once: -/// relay_type_name: The name of the relay type to generate. This is arbitrary. The macro will emit a type with this name. +/// The generated type exposes a `new` constructor taking one +/// `service_handler_type` argument per registered service. /// -/// Followed by a list of any number of service entries, which are specified by the following inputs: -/// service_name: A name to assign to generated identifiers associated with the service, e.g. "Battery". -/// This can be arbitrary. -/// service_id: A unique u8 that addresses that service on the EC. -/// service_handler_type: A type that implements the RelayServiceHandler trait, which will be used to process messages -/// for this service. -/// -/// Example usage: +/// # Example /// /// ```ignore +/// impl_odp_relay_handler!( +/// MyRelayHandlerType; +/// Battery, 0x9, battery_service_relay::RelayHandler>; +/// TimeAlarm, 0xB, time_alarm_service_relay::RelayHandler>; +/// ); /// -/// impl_odp_relay_handler!( -/// MyRelayHandlerType; -/// Battery, 0x9, battery_service_relay::RelayHandler>; -/// TimeAlarm, 0xB, time_alarm_service_relay::RelayHandler>; -/// ); -/// -/// let relay_handler = MyRelayHandlerType::new(battery_service_instance, time_alarm_service_instance); -/// -/// // Then, pass relay_handler to your relay service (e.g. eSPI service), which should be generic over an `impl RelayHandler`. -/// +/// let relay_handler = MyRelayHandlerType::new(battery_handler, time_alarm_handler); +/// // Pass relay_handler to a relay service that is generic over `impl RelayHandler`. /// ``` -/// #[macro_export] macro_rules! impl_odp_relay_handler { ( @@ -189,7 +172,7 @@ macro_rules! impl_odp_relay_handler { } bitfield! { - /// Wire format for ODP MCTP headers. Not user-facing - use OdpHeader instead. + /// Wire format for ODP headers. Not user-facing — use OdpHeader instead. #[derive(Copy, Clone, PartialEq, Eq)] struct OdpHeaderWireFormat(u32); impl Debug; diff --git a/odp-client/src/transport.rs b/odp-client/src/transport.rs index d2079d0d..6fa4c22b 100644 --- a/odp-client/src/transport.rs +++ b/odp-client/src/transport.rs @@ -1,21 +1,17 @@ use crate::OdpError; -/// Carries a single fully-framed ODP message over the wire. +/// Carries fully-framed ODP messages over an arbitrary medium. /// -/// Implementations encapsulate any MCTP framing internally. The public -/// surface is bytes only — callers do not need to know whether the -/// underlying transport is MCTP-over-serial, MCTP-over-eSPI, or anything -/// else. This is the seam that lets us swap transports without rewriting -/// callers. +/// Implementations own the underlying medium and any framing. Callers +/// hand in raw ODP wire bytes (header + body) and receive raw ODP wire +/// bytes back — they do not need to know what is underneath. This is the +/// seam that lets transports be swapped without rewriting callers. pub trait OdpTransport { /// Send one fully-framed ODP message (header + body, wire bytes). - /// - /// Returns once the message has been handed to the transport. fn send_message(&mut self, payload: &[u8]) -> Result<(), OdpError>; - /// Receive one fully-framed ODP message into `buf`. - /// - /// Returns the number of bytes written. Returns - /// `OdpError::BufferTooSmall` if `buf` cannot hold the entire message. + /// Receive one fully-framed ODP message into `buf`. Returns the number + /// of bytes written, or [`OdpError::BufferTooSmall`] if `buf` cannot + /// hold the entire message. fn recv_message(&mut self, buf: &mut [u8]) -> Result; } diff --git a/odp-client/tests/client_invoke.rs b/odp-client/tests/client_invoke.rs index 01ff9b9f..0b72c437 100644 --- a/odp-client/tests/client_invoke.rs +++ b/odp-client/tests/client_invoke.rs @@ -1,7 +1,7 @@ -use odp_client::{LoopbackTransport, OdpClient, OdpError, OdpHeader, OdpResponse, OdpService, Relay}; +use odp_client::{LoopbackTransport, OdpClient, OdpError, OdpHeader, OdpService, Relay}; #[test] -fn invoke_round_trips_request_and_parses_response() { +fn invoke_round_trips_request_and_returns_response() { // LoopbackTransport echoes the request bytes back as the response. let transport = LoopbackTransport::new(); let mut client = OdpClient::new(transport); @@ -13,14 +13,10 @@ fn invoke_round_trips_request_and_parses_response() { message_id: 0x11, }; - let parsed: u8 = client - .invoke(req_header, &[0xAA, 0xBB], |resp: OdpResponse<'_>| { - assert_eq!(resp.header.service, OdpService::Battery); - assert_eq!(resp.header.message_id, 0x11); - Ok(resp.body[0]) - }) - .unwrap(); - assert_eq!(parsed, 0xAA); + let resp = client.invoke(req_header, &[0xAA, 0xBB]).unwrap(); + assert_eq!(resp.header.service, OdpService::Battery); + assert_eq!(resp.header.message_id, 0x11); + assert_eq!(resp.body, &[0xAA, 0xBB]); } #[test] @@ -34,9 +30,7 @@ fn invoke_with_oversized_body_returns_buffer_too_small() { message_id: 1, }; let huge = [0u8; 253]; - let err = client - .invoke(req_header, &huge, |_| Ok::<(), OdpError>(())) - .unwrap_err(); + let err = client.invoke(req_header, &huge).unwrap_err(); assert_eq!(err, OdpError::BufferTooSmall); } @@ -88,7 +82,7 @@ fn invoke_with_mismatched_response_message_id_returns_unexpected() { is_error: false, message_id: 0x11, }; - let err = client.invoke(req_header, &[], |_| Ok::<(), OdpError>(())).unwrap_err(); + let err = client.invoke(req_header, &[]).unwrap_err(); assert_eq!( err, OdpError::UnexpectedMessageId { @@ -97,3 +91,22 @@ fn invoke_with_mismatched_response_message_id_returns_unexpected() { } ); } + +#[test] +fn relay_is_object_safe_via_dyn() { + // Proves the trait is object-safe: dynamic dispatch through &mut dyn Relay. + let mut client = OdpClient::new(LoopbackTransport::new()); + let relay: &mut dyn Relay = &mut client; + + let req_header = OdpHeader { + is_request: true, + service: OdpService::Thermal, + is_error: false, + message_id: 0x42, + }; + + let resp = relay.invoke(req_header, &[0xDE, 0xAD]).unwrap(); + assert_eq!(resp.header.service, OdpService::Thermal); + assert_eq!(resp.header.message_id, 0x42); + assert_eq!(resp.body, &[0xDE, 0xAD]); +} diff --git a/odp-client/tests/mctp_serial.rs b/odp-client/tests/serial.rs similarity index 57% rename from odp-client/tests/mctp_serial.rs rename to odp-client/tests/serial.rs index af29411f..af6719ba 100644 --- a/odp-client/tests/mctp_serial.rs +++ b/odp-client/tests/serial.rs @@ -1,5 +1,5 @@ use mctp_rs::{EC_EID, SP_EID}; -use odp_client::{MctpSerialTransport, OdpTransport}; +use odp_client::{OdpTransport, SerialTransport}; struct LoopbackUart { buf: Vec, @@ -43,18 +43,17 @@ impl embedded_io::Write for LoopbackUart { } #[test] -fn mctp_serial_transport_round_trips_one_message() { - // The transport encapsulates MCTP framing. We hand in ODP wire - // bytes (4-byte header + body), the transport MCTP-frames them - // before writing to the UART. On read, the transport reads the - // serial frame, MCTP-strips, and returns the original ODP bytes. +fn serial_transport_round_trips_one_message() { + // Hand in ODP wire bytes (4-byte header + body); the transport + // frames them before writing to the UART and strips the framing + // on read, returning the original ODP bytes. let uart = LoopbackUart::new(); - let mut t = MctpSerialTransport::new(uart, SP_EID, EC_EID); + let mut t = SerialTransport::new(uart, SP_EID, EC_EID); // Arbitrary 6 bytes: first 4 are the OdpHeader byte pattern, last 2 - // are the body. The transport does NOT semantically validate the - // header — it just blits bytes — so we can use a service-id (0x55) - // that isn't a real OdpService variant. + // are the body. The transport does not semantically validate the + // header — it just blits bytes — so a service-id (0x55) that is not a + // real OdpService variant works here. let payload = [0x01u8, 0x55, 0x00, 0x00, 0xDE, 0xAD]; t.send_message(&payload).unwrap(); @@ -64,14 +63,11 @@ fn mctp_serial_transport_round_trips_one_message() { } #[test] -fn mctp_serial_transport_send_with_too_short_payload_errors() { - // Header is 4 bytes; payload < 4 has no valid header bytes to - // wrap. The transport should reject it. +fn serial_transport_send_with_too_short_payload_errors() { + // Header is 4 bytes; a payload shorter than that has no valid + // header bytes to wrap and must be rejected. let uart = LoopbackUart::new(); - let mut t = MctpSerialTransport::new(uart, SP_EID, EC_EID); + let mut t = SerialTransport::new(uart, SP_EID, EC_EID); let err = t.send_message(&[0x01, 0x02]).unwrap_err(); - // Any OdpError variant — we just want to prove it doesn't panic - // and doesn't silently produce malformed framing. We use BufferTooSmall - // semantically (the input doesn't carry a full header). assert_eq!(err, odp_client::OdpError::BufferTooSmall); }