blob: 2c211420ac286510ec1af8fd75594c5a7bcc47ed [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! PLDM Firmware Device (FD) service.
//!
//! [`FirmwareDevice`] owns the PLDM firmware-update state machine and the
//! platform-specific flash operations. It is intended to run as an isolated
//! process that sits between two IPC channels:
//!
//! * **`fd_cmd`** – receives raw MCTP-framed PLDM FW-update command bytes
//! forwarded by the `pldm_responder` process and sends the response back.
//! * **`fw_req`** – sends raw MCTP-framed PLDM request bytes (e.g.
//! `RequestFirmwareData`) to the `pldm_requester` process and receives the
//! UA response.
//!
//! The two IPC ends are abstracted via the [`FdUaRspChannel`] and [`FdUaCmdChannel`]
//! traits so that the service crate remains independent of the Hubris / Pigweed
//! IPC codegen.
//!
//! ## Buffer layout
//!
//! Both channels carry the same flat buffer convention used throughout this
//! crate:
//!
//! ```text
//! buf[0] : MCTP message-type (0x01)
//! buf[1..] : PLDM message (header + data)
//! ```
//!
//! ## Main loop
//!
//! Each call to [`FirmwareDevice::run_once`] performs one full UA-command
//! cycle:
//!
//! 1. **Phase 1 – inbound**: receive one FW-update command from `fd_cmd`,
//! dispatch it through [`CmdInterface::handle_responder_msg`], reply.
//! 2. **Phase 2 – outbound**: repeatedly call
//! [`CmdInterface::generate_initiator_request`]; while a request is
//! pending forward it over `fw_req`, receive the UA response, and feed it
//! back via [`CmdInterface::process_initiator_response`]. Stop when no
//! more requests are pending.
use pldm_interface::cmd_interface::CmdInterface;
use pldm_interface::firmware_device::fd_ops::FdOps;
use pldm_interface::firmware_device::fd_context::FirmwareDeviceContext;
use pldm_interface::control_context::ProtocolCapability;
use crate::error::PldmServiceError;
/// Maximum PLDM-over-IPC message size (MCTP-type byte + PLDM payload).
pub const FD_IPC_MAX_MSG: usize = 1024;
/// Server-side channel for receiving PLDM firmware-device commands and sending
/// responses back to the caller.
///
/// Implemented by platform-specific IPC glue (e.g. `IpcFdUaRspChannel` in
/// `openprot-pldm-firmware-device-ipc`).
pub trait FdUaRspChannel {
/// Receive one incoming PLDM message into `buf`.
///
/// Returns the number of bytes written. `timeout_millis` of `0` blocks
/// indefinitely; other values are a best-effort hint to the implementation.
fn recv(&self, buf: &mut [u8], timeout_millis: u32) -> Result<usize, PldmServiceError>;
/// Non-blocking poll for an incoming PLDM message.
///
/// Returns `Ok(Some(len))` with the number of bytes written into `buf` if a
/// message was waiting, or `Ok(None)` if the channel is currently empty.
fn try_recv(&self, buf: &mut [u8]) -> Result<Option<usize>, PldmServiceError>;
/// Send a PLDM response back through the channel.
fn respond(&self, buf: &[u8]) -> Result<(), PldmServiceError>;
/// Block until the channel has an inbound message ready, yielding the CPU
/// to other tasks in the meantime.
///
/// This does **not** consume the message; a subsequent [`try_recv`] returns
/// it. It lets a polling loop park when idle instead of busy-spinning.
/// `timeout_millis` of `0` blocks indefinitely; other values are a
/// best-effort hint to the implementation.
///
/// [`try_recv`]: FdUaRspChannel::try_recv
fn wait_readable(&self, timeout_millis: u32) -> Result<(), PldmServiceError>;
}
/// Server-side channel used by [`PldmRequester`] to receive raw PLDM requests
/// forwarded by [`FirmwareDevice`] and send the MCTP response back.
///
/// This is the counterpart of [`FdUaCmdChannel`]: `FirmwareDevice` calls
/// `FdUaCmdChannel::transact`; the `pldm_requester` process implements
/// `UaFdRspChannel` on the other end of that same IPC connection.
///
/// Implemented by platform-specific IPC glue (e.g. `IpcUaFdRspChannel` in
/// `openprot-pldm-firmware-device-ipc`).
///
/// [`PldmRequester`]: crate::requester::PldmRequester
pub trait UaFdRspChannel {
/// Receive one raw PLDM request from [`FirmwareDevice`].
///
/// `buf[0]` will be the MCTP message-type byte (`0x01`); `buf[1..]`
/// contains the PLDM payload. Returns the total number of bytes written
/// (including the framing byte).
fn recv(&self, buf: &mut [u8]) -> Result<usize, PldmServiceError>;
/// Send the PLDM response back to [`FirmwareDevice`].
///
/// `buf[0]` must be the MCTP message-type byte; `buf[1..]` must contain
/// the PLDM response payload.
fn respond(&self, buf: &[u8]) -> Result<(), PldmServiceError>;
}
/// Client-side channel for sending PLDM firmware-update requests to the Update
/// Agent and receiving its responses.
///
/// Implemented by platform-specific IPC glue (e.g. `IpcFdUaCmdChannel` in
/// `openprot-pldm-firmware-device-ipc`).
pub trait FdUaCmdChannel {
/// Perform a synchronous request/response round-trip.
///
/// Sends `req` and blocks until the response arrives, writing it into
/// `resp`. Returns the number of response bytes written.
fn transact(&self, req: &[u8], resp: &mut [u8]) -> Result<usize, PldmServiceError>;
}
/// Client-side channel for sending PLDM firmware-command requests to the Firmware
/// Device and receiving its responses.
///
/// This is the counterpart of [`FdUaRspChannel`]: `PldmResponder` calls
/// `UaFdCmdChannel::transact`; `FirmwareDevice` implements `FdUaRspChannel` on
/// the other end of that same IPC connection.
///
/// Implemented by platform-specific IPC glue (e.g. `IpcUaFdCmdChannel` in
/// `openprot-pldm-firmware-device-ipc`).
pub trait UaFdCmdChannel {
/// Perform a synchronous request/response round-trip.
///
/// Sends `req` and blocks until the response arrives, writing it into
/// `resp`. Returns the number of response bytes written.
fn transact(&self, req: &[u8], resp: &mut [u8]) -> Result<usize, PldmServiceError>;
}
/// PLDM Firmware Device service.
///
/// Owns the PLDM firmware-update state machine ([`CmdInterface`]) and drives it
/// via [`run_terminus`](FirmwareDevice::run_terminus), bridging the inbound
/// (`fd_rsp`) and outbound (`fw_req`) IPC channels.
pub struct FirmwareDevice<'a> {
cmd_interface: CmdInterface<'a>,
}
impl<'a> FirmwareDevice<'a> {
/// Create a new [`FirmwareDevice`] with the given protocol capabilities.
///
/// `protocol_capabilities` should advertise at least
/// [`PldmSupportedType::FwUpdate`] so that the [`CmdInterface`] accepts
/// and routes firmware-update commands correctly.
///
/// [`PldmSupportedType::FwUpdate`]: pldm_common::protocol::base::PldmSupportedType::FwUpdate
pub fn init(fdops: &'a dyn FdOps, protocol_capabilities: &'a [ProtocolCapability<'a>]) -> Self {
FirmwareDevice {
cmd_interface: CmdInterface::new(protocol_capabilities, FirmwareDeviceContext::new(fdops)),
}
}
/// Run the firmware-device service loop.
///
/// Each iteration performs two interleaved phases:
///
/// 1. **Initiator** — while the FD is in update mode
/// ([`should_start_initiator_mode`]), issue at most one outbound request
/// (e.g. `RequestFirmwareData`) via `fw_req` and feed the response back
/// into the state machine.
/// 2. **Responder** — always poll `fd_rsp` (non-blocking) for an inbound
/// Update Agent command and reply. Polling every iteration keeps the
/// responder path live during a transfer so the Update Agent can send
/// `CancelUpdate` at any time.
///
/// When neither phase has work pending, the loop parks on
/// [`FdUaRspChannel::wait_readable`] to yield the CPU instead of spinning.
///
/// This method loops indefinitely and returns only on error. A
/// `timeout_millis` of `0` blocks indefinitely while idle.
///
/// [`should_start_initiator_mode`]: pldm_interface::firmware_device::fd_context::FirmwareDeviceContext
pub fn run_terminus(
&mut self,
fd_rsp: &impl FdUaRspChannel,
fw_req: &impl FdUaCmdChannel,
buf: &mut [u8],
timeout_millis: u32,
) -> Result<(), PldmServiceError> {
loop {
// Phase 1: while in initiator mode, issue at most ONE outbound
// request per iteration. We deliberately fall through to the
// responder poll below (no `continue`) so an Update Agent command
// such as CancelUpdate is serviced between every RequestFirmwareData.
let initiator_active = self.cmd_interface.fd_ctx.should_start_initiator_mode();
if initiator_active {
let mut fw_buf = [0u8; FD_IPC_MAX_MSG];
let mut fw_resp = [0u8; FD_IPC_MAX_MSG];
self.cmd_interface.handle_initiator_msg(&mut fw_buf)
.map_err(PldmServiceError::MsgHandler)?;
// Build a request using pldm-lib
fw_req.transact(&fw_buf, &mut fw_resp)?;
self.cmd_interface.handle_initiator_response(&mut fw_resp)
.map_err(PldmServiceError::MsgHandler)?;
}
// Phase 2: always poll (non-blocking) for an inbound command so the
// responder path stays live during a transfer and the Update Agent
// can cancel at any time. The handler receives the *whole* buffer
// because responses may be larger than the request they answer
// (e.g. GetTid: 4-byte request, 5-byte response).
if let Some(_msg_len) = fd_rsp.try_recv(buf)? {
let resp_len = self
.cmd_interface
.handle_responder_msg(buf)
.map_err(PldmServiceError::MsgHandler)?;
let resp = buf.get(..resp_len).ok_or(PldmServiceError::InvalidResponseLength)?;
fd_rsp.respond(resp)?;
} else if !initiator_active {
// Fully idle: no initiator work pending and no inbound message.
// Block (yield the CPU) until the channel is readable instead
// of busy-spinning.
fd_rsp.wait_readable(timeout_millis)?;
}
}
}
}