Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
484 changes: 231 additions & 253 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions services/mctp/api/src/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,18 @@ pub struct StackListener<'s, C: MctpClient> {
timeout: u32,
}

impl<C: MctpClient> StackListener<'_, C> {
/// Update the timeout used by subsequent [`recv`](MctpListener::recv) calls.
///
/// Useful for a listener kept alive across many `recv` calls (e.g. to
/// avoid dropping and re-registering the underlying listener handle
/// between calls) whose desired wait time varies per call. `timeout_millis`
/// of `0` blocks indefinitely.
pub fn set_timeout(&mut self, timeout_millis: u32) {
self.timeout = timeout_millis;
}
}

impl<'s, C: MctpClient> MctpListener for StackListener<'s, C> {
type RespChannel<'a>
= StackRespChannel<'s, C>
Expand Down
57 changes: 57 additions & 0 deletions services/pldm/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Licensed under the Apache-2.0 license
# SPDX-License-Identifier: Apache-2.0

load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")

package(default_visibility = ["//visibility:public"])

rust_library(
name = "pldm_service",
srcs = glob(["src/**/*.rs"]),
crate_name = "openprot_pldm_service",
edition = "2024",
deps = [
"//services/mctp/api:mctp_api",
"@rust_crates//:mctp",
"@rust_crates//:mctp-lib",
"@rust_crates//:pldm-common",
"@rust_crates//:pldm-interface",
],
)

rust_test(
name = "pldm_service_test",
crate = ":pldm_service",
)

rust_test(
name = "base_host_test",
srcs = ["tests/base_host.rs"],
crate_root = "tests/base_host.rs",
edition = "2024",
deps = [
":pldm_service",
"//services/mctp/api:mctp_api",
"//services/mctp/server:mctp_server_lib",
"@rust_crates//:mctp",
"@rust_crates//:mctp-lib",
"@rust_crates//:pldm-common",
"@rust_crates//:pldm-interface",
],
)

rust_test(
name = "firmware_update_host_test",
srcs = ["tests/firmware_update_host.rs"],
crate_root = "tests/firmware_update_host.rs",
edition = "2024",
deps = [
":pldm_service",
"//services/mctp/api:mctp_api",
"//services/mctp/server:mctp_server_lib",
"@rust_crates//:mctp",
"@rust_crates//:mctp-lib",
"@rust_crates//:pldm-common",
"@rust_crates//:pldm-interface",
],
)
127 changes: 127 additions & 0 deletions services/pldm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# openprot-pldm-service

Platform-independent PLDM Firmware Device (FD) service, talking PLDM-over-MCTP
directly to a remote Update Agent (UA).

## Overview

This crate bridges [`openprot-mctp-api`](../mctp/api) and
[`pldm-interface`](https://github.com/OpenPRoT/pldm-lib/tree/main/pldm-interface)
so that firmware can run the PLDM firmware-update state machine and exchange
PLDM messages over MCTP without depending on any particular MCTP
implementation or OS.

`FirmwareDevice` owns two [`MctpPldmTransport`] instances and drives both
directions of traffic itself — there is no separate responder/requester
process or platform-specific IPC bridge:

```text
┌──────────────────────────┐
│ Application / Firmware │ constructs FirmwareDevice, calls run_terminus()
└───────────┬──────────────┘
┌──────────────────────────────────────────┐
│ openprot-pldm-service │◄── this crate

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: improper formatting

│ FirmwareDevice<'a, O: FdOps, Cr, Cq> │
│ - cmd_interface: CmdInterface<'a, O> │ PLDM FW-update state machine
│ - responder_transport: inbound UA→FD │
│ - requester_transport: outbound FD→UA │
└───────────┬──────────────┬────────────────┘
│ │ MctpPldmTransport<C: MctpClient>
▼ ▼
┌──────────────────────────────────────────┐
│ openprot-mctp-api │ Stack<C: MctpClient>
└───────────┬───────────────────────────────┘

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: improper formatting

│ IPC / transport
┌──────────────────────────┐
│ MCTP Server │
└──────────────────────────┘
```

## Key types

| Type | Description |
|------|-------------|
| `FirmwareDevice<'a, O: FdOps, Cr: MctpClient, Cq: MctpClient>` | Owns the `CmdInterface` FW-update state machine plus the responder/requester `MctpPldmTransport`s; `run_terminus()` drives both to completion |
| `MctpPldmTransport<C: MctpClient>` | Wraps a `Stack<C>` and manages the MCTP PLDM framing byte (`0x01`) for sends/receives |
| `PldmServiceError` | Union of MCTP transport errors (`Mctp`), PLDM handler errors (`MsgHandler`), buffer/arithmetic overflow, and IPC errors |
| `PLDM_MSG_TYPE` (`pldm_common::util::mctp_transport::MCTP_PLDM_MSG_TYPE`) | MCTP message-type constant for PLDM (`0x01`) |

## Usage

```rust,ignore
use openprot_pldm_service::firmware_device::FirmwareDevice;
use openprot_pldm_service::MctpPldmTransport;
use pldm_interface::config::PLDM_PROTOCOL_CAPABILITIES;

// `fd_ops` implements `FdOps` (platform-specific flash / component logic).
// `responder_client` / `requester_client` are `MctpClient` implementations
// (they may share the same underlying MCTP endpoint).
let responder_transport = MctpPldmTransport::new(responder_client);
let requester_transport = MctpPldmTransport::new(requester_client);

let mut fd = FirmwareDevice::init(
&fd_ops,
&PLDM_PROTOCOL_CAPABILITIES,
responder_transport,
requester_transport,
);

const UA_EID: u8 = 8;
let mut buf = [0u8; 1024];

// `run_terminus` loops forever, interleaving inbound UA commands with any
// FD-initiated requests (e.g. RequestFirmwareData) once an update begins.
// It returns only on error; a `timeout_millis`/`requester_timeout_millis`
// of `0` blocks indefinitely while idle.
if let Err(e) = fd.run_terminus(UA_EID, &mut buf, 0, 0) {
// handle or log error
}
```

## Buffer layout

All transport methods (`MctpPldmTransport::send_request`,
`recv_and_respond`, `respond_once`) and `FirmwareDevice::run_terminus` use the
same flat-buffer convention:

```text
buf[0] : MCTP message-type byte (0x01) — managed by MctpPldmTransport
buf[1..] : PLDM request / response bytes
```

Size the buffer to accommodate the largest PLDM message your application
expects (typically ≤ 4096 bytes; smaller for embedded targets). See
`FD_IPC_MAX_MSG` in [`src/firmware_device.rs`](src/firmware_device.rs) for the
scratch-buffer size used internally for FD-initiated requests.

## Build

```
bazel build //services/pldm:pldm_service
```

Run the host-side integration tests:

```
bazel build -c dbg --strip=never //services/pldm:base_host_test
bazel build -c dbg --strip=never //services/pldm:firmware_update_host_test
```

After changing `pldm-common` / `pldm-interface` versions in
`third_party/crates_io/Cargo.toml`, re-pin the lock file. This repo is
bzlmod-only (no `WORKSPACE` file), so use `bazel build` with
`CARGO_BAZEL_REPIN=1`, not `bazel sync` (which errors with "WORKSPACE has to
be enabled"):

```
CARGO_BAZEL_REPIN=1 bazel build //services/pldm:pldm_service
```

## Dependencies

- [`openprot-mctp-api`](../mctp/api) — MCTP stack facade and traits
- [`pldm-interface`](https://github.com/OpenPRoT/pldm-lib/tree/main/pldm-interface) — PLDM command dispatcher (`CmdInterface`, `FdOps`, `FirmwareDeviceContext`)
- [`pldm-common`](https://github.com/OpenPRoT/pldm-lib/tree/main/pldm-common) — PLDM protocol types and MCTP transport helpers
44 changes: 44 additions & 0 deletions services/pldm/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0

//! Error types for the PLDM service.

use core::fmt;

use openprot_mctp_api::MctpError;
use pldm_interface::error::MsgHandlerError;

/// Errors returned by PLDM service operations.
#[derive(Debug)]
pub enum PldmServiceError {
/// An MCTP transport or stack error.
Mctp(MctpError),
/// A PLDM message handler error (codec failure, unsupported command, etc.).
MsgHandler(MsgHandlerError),
/// A buffer size or arithmetic overflow.
Overflow,
}

impl fmt::Display for PldmServiceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PldmServiceError::Mctp(e) => write!(f, "MCTP transport error: {e:?}"),
PldmServiceError::MsgHandler(e) => write!(f, "PLDM message handler error: {e:?}"),
PldmServiceError::Overflow => write!(f, "buffer size or arithmetic overflow"),
}
}
}

impl core::error::Error for PldmServiceError {}

impl From<MctpError> for PldmServiceError {
fn from(e: MctpError) -> Self {
PldmServiceError::Mctp(e)
}
}

impl From<MsgHandlerError> for PldmServiceError {
fn from(e: MsgHandlerError) -> Self {
PldmServiceError::MsgHandler(e)
}
}
Loading