Skip to content

Make uart-service::Service generic over MctpMedium; delete fake-PEC strips#852

Open
dymk wants to merge 5 commits into
OpenDevicePartnership:v0.2.0from
dymk:dymk/phase-34-uart-service-generic
Open

Make uart-service::Service generic over MctpMedium; delete fake-PEC strips#852
dymk wants to merge 5 commits into
OpenDevicePartnership:v0.2.0from
dymk:dymk/phase-34-uart-service-generic

Conversation

@dymk
Copy link
Copy Markdown
Contributor

@dymk dymk commented May 19, 2026

Make uart-service::Service generic over MctpMedium; delete fake-PEC strips

Summary

Makes uart-service::Service genuinely generic over mctp_rs::MctpMedium, so the same service implementation works for both the existing SmbusEspi medium and the DSP0253-serial medium. Adds one new method to the MctpMedium trait — frame_complete(buf) -> Result<Option<usize>, ...> — that lets generic byte-stream consumers detect frame boundaries without knowing the medium's framing details. Deletes three fake-PEC-strip workarounds that were SmbusEspi-shaped accidents (the real PEC byte is already handled by SmbusEspiMedium's serialize/deserialize via the smbus_pec crate).

Also adds a small additive MctpMessageBuffer::body() + message_type() accessor pair so callers can read raw body bytes without going through MctpMessage::parse_as. Useful for a downstream SP-side service that parses a small fixed-layout payload and doesn't want to define a full MctpMessageTrait impl just to access the body slice.

API changes

MctpMedium trait gains a required frame_complete(buf) -> Result<Option<usize>, ...> method. Both shipped media (SmbusEspiMedium, MctpSerialMedium) ship impls in this PR. Out-of-tree implementors (none known) need to add a frame_complete impl.

uart-service::Service::new(relay) is now Service::<R, M>::new(relay, medium, reply_context). The Error enum is now Error<M: MctpMedium> to surface medium-specific MCTP errors. Existing SmbusEspi callers migrate via a one-line swap:

// Before:
let service: Service<MyRelay> = Service::new(relay).unwrap();

// After:
let service: DefaultService<MyRelay> = DefaultService::default_smbusespi(relay).unwrap();

MctpReplyContext<M> now derives Clone, Copy (additive — required for storing it as a field on Service).

What's added

  • MctpMedium::frame_complete(buf) -> Result<Option<usize>, Self::Error> — sync, byte-stream-agnostic frame-completeness predicate. Supports both length-prefix framing (SmbusEspi: 4-byte header → byte_count → body + PEC) and sentinel framing (DSP0253: scan for 0x7E end-flag).
  • Service<R, M: MctpMedium + Copy> — genuinely generic over medium. Service::new(relay, medium, reply_context) takes medium-specific addressing as a constructor arg.
  • DefaultService<R> = Service<R, SmbusEspiMedium> type alias + DefaultService::default_smbusespi(relay) constructor that bakes in the existing SmbusEspiReplyContext { destination_slave_address: 1, source_slave_address: 0 }.
  • MctpMessageBuffer::body() -> &[u8] + MctpMessageBuffer::message_type() -> u8 accessors (additive; no existing API removed).

What's removed

  • uart-service/src/lib.rs// Last byte is PEC, ignore for now strip. Obviated by frame_complete-driven framing (the medium reports the exact frame length, including the PEC byte; MctpPacketContext::deserialize_packet consumes the PEC correctly).
  • espi-service/src/espi_service.rs// TODO: workaround because mctp_rs expects a PEC byte, so we hardcode a 0 block.
  • espi-service/src/espi_service.rs// Last byte is PEC, ignore for now strip in serialize_packet_from_subsystem.
  • Now-unused constants SMBUS_HEADER_SIZE and SMBUS_LEN_IDX in uart-service/src/lib.rs.

Consumer migrations (NOT in this PR)

The three in-tree consumers (dev-qemu, dev-imxrt, dev-npcx) live in a separate repo and update their Service::new(relay) call sites to DefaultService::default_smbusespi(relay) in a follow-up PR there.

Test results

cargo test --workspace consumer pass count is unchanged (no new tests added by this PR; the frame_complete impls are covered indirectly by existing media round-trip tests).

@dymk dymk force-pushed the dymk/phase-34-uart-service-generic branch from 7ee94ae to c5a4fb7 Compare May 19, 2026 18:22
@dymk dymk requested a review from Copilot May 19, 2026 18:27
@dymk dymk marked this pull request as ready for review May 19, 2026 18:30
@dymk dymk requested review from a team as code owners May 19, 2026 18:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes uart-service usable with multiple MCTP media by parameterizing the service over mctp_rs::MctpMedium, and adds a new MctpMedium::frame_complete() hook so byte-stream transports can detect medium frame boundaries generically. It also removes SMBus PEC-related workarounds in uart-service and espi-service now that PEC is handled correctly by SmbusEspiMedium’s (de)serialization.

Changes:

  • Add MctpMedium::frame_complete(buf) -> Result<Option<usize>, _> and implement it for SmbusEspiMedium, MctpSerialMedium, and TestMedium.
  • Refactor uart-service::Service to Service<R, M> with a DefaultService<R> = Service<R, SmbusEspiMedium> convenience alias/constructor.
  • Add MctpMessageBuffer::body() and message_type() accessors and remove fake PEC stripping in uart-service and espi-service.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
uart-service/src/task.rs Updates UART task signature for generic Service<R, M> and adjusts error logging.
uart-service/src/lib.rs Makes UART service generic over MctpMedium, adds framing-based RX loop, introduces DefaultService.
mctp-rs/src/test_util.rs Implements frame_complete() for the test medium.
mctp-rs/src/medium/smbus_espi.rs Implements frame_complete() for SMBus/eSPI framing using the header byte_count.
mctp-rs/src/medium/serial.rs Implements frame_complete() for DSP0253 serial by scanning for the end flag.
mctp-rs/src/medium/mod.rs Extends the MctpMedium trait with the new frame_complete() requirement and docs.
mctp-rs/src/mctp_packet_context.rs Makes MctpReplyContext Clone + Copy to support storage in uart-service::Service.
mctp-rs/src/lib.rs Updates docs for new trait method and adds MctpMessageBuffer accessors.
espi-service/src/espi_service.rs Removes fake PEC injection/stripping so packets are handled as-is by the medium.

Comment thread uart-service/src/lib.rs
Comment thread mctp-rs/src/medium/serial.rs
Comment thread uart-service/src/task.rs Outdated
Comment thread uart-service/src/lib.rs
Copy link
Copy Markdown
Contributor

@kurtjd kurtjd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, and you said there will be a follow up PR in odp-embedded-controller, but has this been validated with the ec-test-tui/cli using the serial source (e.g.: https://github.com/OpenDevicePartnership/odp-platform-common/blob/main/ec/test-lib/src/serial.rs)?

If it doesn't work and require changes, you will need a simultaneous PR put up over there to make sure they agree. I would like to have PRs up here, in odp-emebdded-controller, and in odp-platform-common to make sure verything is updated simultaneously.

Comment thread uart-service/src/task.rs Outdated
@dymk dymk force-pushed the dymk/phase-34-uart-service-generic branch 2 times, most recently from 3023f6c to 876878c Compare May 19, 2026 19:58
Comment thread uart-service/src/lib.rs Outdated
Comment thread uart-service/src/lib.rs Outdated
Comment thread uart-service/src/lib.rs Outdated
@dymk dymk force-pushed the dymk/phase-34-uart-service-generic branch 3 times, most recently from ec56d26 to f8a6988 Compare May 19, 2026 21:34
@kurtjd
Copy link
Copy Markdown
Contributor

kurtjd commented May 19, 2026

@kurtjd — validated. Three findings, all on your side:

1. PR #852 makes zero API changes that ec-test-lib / ec-test-cli / ec-test-tui depend on.

Source audit of odp-platform-common's ec/ workspace:

  • They use only battery_service_messages, time_alarm_service_messages, thermal_service_messages, and embedded_services::relay::{MessageSerializationError, SerializableMessage}.
  • No usage of uart_service::*, mctp_rs::MctpMedium, MctpMessageBuffer, frame_complete, or DefaultService (the surfaces this PR changes).
$ grep -rn "uart_service\|MctpMedium\|MctpMessageBuffer\|frame_complete\|DefaultService" odp-platform-common/ec --include="*.rs"
(no output)

2. The PR for odp-embedded-controller will not affect ec-test-tui either.

The followup PR is scoped to platform/dev-qemu, which is the QEMU dev platform — not a target ec-test-tui exercises. ec-test-tui drives real EC hardware (dev-imxrt, dev-mcxa, dev-npcx) over USB-serial; those platforms continue to speak SmbusEspi-shaped MCTP unchanged.

3. Heads-up: odp-platform-common is already broken against current v0.2.0 HEAD, independent of PR #852.

Upstream PRs #780 (battery) and #776 (time-alarm) split *-service-messages crates into *-interface + *-relay. odp-platform-common/ec/Cargo.toml still references the old crate names; it builds today only because Cargo.lock pins to SHA 5a0ed0d.

$ cd odp-platform-common/ec && cargo update
error: no matching package named `battery-service-messages` found
location searched: Git repository https://github.com/OpenDevicePartnership/embedded-services?branch=v0.2.0
required by package `ec-test-cli`

So a simultaneous-PRs coordination is unnecessary for #852 / #odp-embedded-controller-followup specifically, but odp-platform-common does need its own update for the unrelated upstream rename — happy to file an issue or open a PR there if you want.

This doesn't address my concern. The ec-tets-tui/cli currently manually push bytes over serial in a way that should agree with what the uart-service is expecting. I just wanted to ensure that the serial source on the ec test app was validated against this change. If it's broken, then a PR should be opened there simultaneously to update it. It's not a matter of the Rust interface breaking that I'm referring to, jjust the format on the wire.

Also, the followup PR shouldn't just be scoped to dev-qemu. All the dev-platforms instantiate the uart-service, and the ec-test-tui is in fact expected to talk to all of them over the serial flag. In a nutshell, I want to ensure the test app can speak to all the existing dev platforms (including dev-qemu) over --source serial.

@dymk
Copy link
Copy Markdown
Contributor Author

dymk commented May 19, 2026

@kurtjd re: breaking the TUI app - I'll put up a PR to make the changes there as well to fix the smbus/mctp workaround. Does it support being ran outside of a WinVOS image and talking to a virtual serial port? If so it'd be nice to have it as part of the E2E tests that run in CI.

@kurtjd
Copy link
Copy Markdown
Contributor

kurtjd commented May 19, 2026

@kurtjd re: breaking the TUI app - I'll put up a PR to make the changes there as well to fix the smbus/mctp workaround. Does it support being ran outside of a WinVOS image and talking to a virtual serial port? If so it'd be nice to have it as part of the E2E tests that run in CI.

Yes, the ec-test-app can be run from any host machine with the --source serial flag a provided port (e.g. COM21 or /dev/tty/USB0 or /dev/pty/2).

I recently integrated it into CI on the odp-embedded-controller side here: OpenDevicePartnership/odp-embedded-controller#14

dymk added 3 commits May 20, 2026 09:54
Add a new method `frame_complete(&self, buf: &[u8]) -> Result<Option<usize>, Self::Error>`
to the `MctpMedium` trait that returns `Ok(Some(len))` when buf contains
a complete medium-framed packet starting at buf[0], `Ok(None)` when buf
has a partial packet, `Err(...)` when buf is malformed.

This lets generic consumers (uart-service::Service<R, M>) read packets
from byte streams without knowing the framing details:

- `SmbusEspiMedium::frame_complete` reads the 4-byte SMBUS header,
  parses the byte_count field, and returns `Ok(Some(4 + byte_count + 1))`
  when the full body+PEC has been received. Length-prefix framing.
- `MctpSerialMedium::frame_complete` scans buf for the 0x7E end-flag
  (skipping a leading flag if present) and returns the position +1.
  DSP0253 sentinel framing.

BREAKING CHANGE: this is a new required trait method (no default
impl). Both shipped media (SmbusEspi + Serial) are updated in this
same commit so the change is non-breaking at the workspace level.
Out-of-tree implementations of `MctpMedium` (none known) will need
to add a `frame_complete` impl.

Coordinated with the uart-service genericization commit later in this
PR.

Also fixes a pre-existing rustdoc intra-doc-link in
`MctpMedium::Encoding`'s doc comment (introduced by the in-tree
mctp-rs port in upstream PR OpenDevicePartnership#844) that points at the now-private
`crate::buffer_encoding` module. Re-points the link to the public
`crate::BufferEncoding` and `crate::PassthroughEncoding` re-exports.
Surfaced as a CI failure on the nightly/doc job on this PR's first
push; trivial to fix here vs as a separate upstream PR.
…delete fake-PEC strips

Make `embedded-services::uart-service::Service<R: RelayHandler>` →
`Service<R: RelayHandler, M: MctpMedium + Copy>`. Move the SmbusEspi-specific
reply-context construction out of `process_response` into a new
`Service::new(relay, medium, reply_context)` constructor signature, storing
`medium` and `reply_context` as fields. Replace the inlined SMBUS-header
parse in `wait_for_request` with a generic incremental-read loop that calls
`self.medium.frame_complete(...)` (added in the previous commit).

Backwards-compat:
- `pub type DefaultService<R> = Service<R, SmbusEspiMedium>;`
- `impl<R: RelayHandler> DefaultService<R> { pub fn default_smbusespi(relay) }`
  constructor helper that hardcodes the existing SmbusEspi reply context
  (`SmbusEspiReplyContext { destination_slave_address: 1, source_slave_address: 0 }`).

Existing in-tree consumers update their import to `DefaultService` AND
change `Service::new(relay).unwrap()` →
`DefaultService::default_smbusespi(relay).unwrap()` (1-2 lines each).
Those consumer updates land in a separate downstream PR.

Also DELETE the 2 fake-PEC strip workarounds (no longer needed):
- `uart-service/src/lib.rs` `// Last byte is PEC, ignore for now`
- `espi-service/src/espi_service.rs` `// TODO: workaround
  because mctp_rs expects a PEC byte, so we hardcode a 0`
- `espi-service/src/espi_service.rs` `// Last byte is PEC, ignore for now`

Under a serial medium (DSP0253) PEC is irrelevant — DSP0253 uses
FCS-16, not PEC. Under SmbusEspiMedium, the workarounds were
SmbusEspi-shaped accidents that are obviated by `frame_complete`-driven
framing.

BREAKING CHANGE: `Service::new` signature changed from `new(relay)`
to `new(relay, medium, reply_context)`. The
`DefaultService::default_smbusespi(relay)` constructor is the migration
path for SmbusEspi callers. The `Error` enum is now `Error<M>` generic
over the medium. The `MctpReplyContext<M>` struct now derives
`Clone, Copy` (additive — required for storing in `Service`).
The previous commit kept `task::uart_service` hardcoded to
`&DefaultService<R>` and `Error<SmbusEspiMedium>` because no in-tree
consumer needed the generic form. A downstream consumer that
instantiates `Service<R, MctpSerialMedium>` cannot be passed where
`&DefaultService<R>` is required, and the loop body's helper methods
(`wait_for_request`, `wait_for_response`, `process_response`) are
crate-private so the consumer cannot inline its own wrapper.

Make the function generic over `M: MctpMedium + Copy` to match
`Service`'s existing bound. Pure type-signature change; loop body is
byte-identical. Existing SmbusEspiMedium callers (e.g.
`DefaultService::default_smbusespi(relay)`) continue to work via
inference because `M` is inferred from the `&Service<R, M>` argument.
dymk added 2 commits May 20, 2026 09:54
…sk loop

After the genericization, naïve `error!("... {:?}", e)` no longer
compiles because `Error<M>: defmt::Format` requires `M::Error:
defmt::Format`, and that bound cannot be expressed as a
feature-gated where-clause on stable Rust (rust-lang/rust#115590).

Restore per-variant diagnostic signal without the format-trait
bound by matching on the `Error<M>` discriminant and emitting a
fixed static string per variant. Six variants — Comms, Uart, Mctp,
Serialize, Buffer — each get an unambiguous log line, with
`Serialize(s)` interpolating its `&'static str` payload.

Also splits the loop into separate `log_request_error` and
`log_response_error` helpers so the request vs response site is
clear in the log output.
Expose the message body slice and message_type byte on MctpMessageBuffer
so callers can read raw bytes without going through MctpMessage::parse_as.

Use case: a consumer wants to parse a small fixed-layout payload (e.g.,
4 little-endian u32 fields for ACPI BST) without defining a full
MctpMessageTrait impl just to access self.message_buffer.rest.

Pure additive: no public API removed; existing parse_as / can_parse_as
paths unchanged.
@dymk dymk force-pushed the dymk/phase-34-uart-service-generic branch from f8a6988 to bfb1fb3 Compare May 20, 2026 16:54
Comment thread uart-service/src/task.rs
/// feature-gated where-clause on stable Rust (rust-lang/rust#115590).
/// Matching on the variant + emitting a fixed string preserves the
/// per-variant signal without that bound.
fn log_request_error<M: MctpMedium>(e: &Error<M>) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good and seems like everything is in place to ensure nothing breaks.

Minor nit: I think it's better to have a single:

fn log_error<M: MctpMedium>(msg: &str, e: &Error<M>)

Then just call it like:
log_error("uart-service request:", &e)
log_error("uart-service response:", &e)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants