blob: 99908335055b6bacb80f4e5026b67ebe245f15d6 [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 talks to the Update Agent (UA)
//! directly over MCTP via two [`MctpPldmTransport`] instances:
//!
//! * **`responder_transport`** – listens for inbound PLDM FW-update commands
//! from the UA and replies in place.
//! * **`requester_transport`** – forwards FD-initiated PLDM requests (e.g.
//! `RequestFirmwareData`) to the UA at `remote_eid` and receives the
//! response.
//!
//! This lets a single process own the whole FD state machine and its MCTP
//! I/O, without needing to bridge to separate responder/requester processes
//! over platform-specific IPC.
//!
//! ## Buffer layout
//!
//! Both transports 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 iteration of [`FirmwareDevice::run_terminus`] performs two interleaved
//! phases:
//!
//! 1. **Initiator (outbound)** – while the FD is in update mode, generate the
//! next FD-initiated request via [`CmdInterface::generate_initiator_request`],
//! send it to the UA through `requester_transport`, and feed the response
//! back via [`CmdInterface::process_initiator_response`].
//! 2. **Responder (inbound)** – poll `responder_transport` for an inbound UA
//! command and reply via [`CmdInterface::handle_responder_msg`]. Polling
//! every iteration keeps the responder path live during a transfer so the
//! Update Agent can send `CancelUpdate` at any time.
use openprot_mctp_api::stack::StackListener;
use openprot_mctp_api::MctpClient;
use pldm_interface::cmd_interface::CmdInterface;
use pldm_interface::control_context::ProtocolCapability;
use pldm_interface::firmware_device::fd_context::FirmwareDeviceContext;
use pldm_interface::firmware_device::fd_ops::FdOps;
use crate::error::{PldmMemError, PldmServiceError};
use crate::transport::MctpPldmTransport;
/// Maximum PLDM-over-MCTP message size (MCTP-type byte + PLDM payload).
pub const FD_MAX_MSG: usize = 1024;
/// Poll timeout (milliseconds) used for the inbound responder listener while
/// an initiator (FD-to-UA) request is active.
///
/// A short, non-zero timeout lets [`FirmwareDevice::run_terminus`] check for
/// an inbound Update Agent command (e.g. `CancelUpdate`) between successive
/// outbound requests without blocking the transfer; a lack of a message
/// within this window is expected and is not treated as an error.
const RESPONDER_POLL_TIMEOUT_MILLIS: u32 = 1;
/// Update-lifecycle notification out of the PLDM FD state machine, one per
/// state-machine edge.
///
/// Every variant payload is `Copy` and lifetime-free by design. Anything
/// buffer-shaped (image chunks, package data, version strings) lands in
/// flash or an `FdOps`-owned buffer; an event carries at most the small
/// fixed-size values that name or qualify the edge.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FdEvent {
/// The Update Agent's `RequestUpdate` was accepted: the FD moved out of
/// `Idle` (into `LearnComponents`) and the success response has already
/// been sent. Emitted exactly once per accepted `RequestUpdate`;
/// rejected ones (`ALREADY_IN_UPDATE_MODE`, bad transfer size) never
/// reach here because they leave the FD state unchanged.
UpdateRequested,
}
/// Receiver for [`FdEvent`] notifications out of the PLDM FD state machine.
///
/// [`FirmwareDevice::run_terminus`] owns the PLDM state machine but has no
/// knowledge of the platform's update orchestration; this trait is the seam
/// between the two. It is deliberately PLDM-flavored (no orchestrator
/// types) so that depending on this crate never pulls in the orchestrator
/// stack; the mapping to an orchestrator event lives in an adapter crate,
/// following the same rule as the orchestrator's HAL adapters.
///
/// [`FdEvent`] is `#[non_exhaustive]`: implementors match the variants they
/// care about and ignore the rest, so new FD lifecycle events do not break
/// existing sinks.
pub trait FdEventSink {
/// Receive one FD lifecycle event.
fn notify(&mut self, event: FdEvent);
}
/// Drop update notifications, for callers with no orchestration to notify.
impl FdEventSink for () {
fn notify(&mut self, _event: FdEvent) {}
}
/// Outcome of [`FirmwareDevice::run_terminus`]/[`FirmwareDevice::run_until`].
pub enum RunTerminusResult {
/// The loop exited without an error. Reachable via
/// [`run_until`](FirmwareDevice::run_until) once its stop condition
/// returns `false` — which says nothing about protocol state, only that
/// the caller chose to stop. Unreachable via
/// [`run_terminus`](FirmwareDevice::run_terminus), whose stop condition
/// never fires; a well-defined *protocol* completion condition remains
/// open work there.
Completed,
/// The loop was stopped by an unrecoverable error.
StoppedByError(PldmServiceError),
}
/// PLDM Firmware Device service.
///
/// Owns the PLDM firmware-update state machine ([`CmdInterface`]) and drives
/// it via [`run_terminus`](FirmwareDevice::run_terminus), talking directly to
/// the Update Agent over MCTP through `responder_transport` (inbound UA
/// commands) and `requester_transport` (outbound FD-initiated requests).
pub struct FirmwareDevice<'a, O: FdOps, Cr: MctpClient, Cq: MctpClient> {
cmd_interface: CmdInterface<'a, O>,
responder_transport: MctpPldmTransport<Cr>,
requester_transport: MctpPldmTransport<Cq>,
}
impl<'a, O: FdOps, Cr: MctpClient, Cq: MctpClient> FirmwareDevice<'a, O, Cr, Cq> {
/// Create a new [`FirmwareDevice`] with the given protocol capabilities
/// and MCTP transports.
///
/// `protocol_capabilities` should advertise at least
/// [`PldmSupportedType::FwUpdate`] so that the [`CmdInterface`] accepts
/// and routes firmware-update commands correctly. `responder_transport`
/// answers inbound UA commands; `requester_transport` forwards
/// FD-initiated requests to the UA.
///
/// [`PldmSupportedType::FwUpdate`]: pldm_common::protocol::base::PldmSupportedType::FwUpdate
pub fn init(
fdops: &'a O,
protocol_capabilities: &'a [ProtocolCapability<'a>],
responder_transport: MctpPldmTransport<Cr>,
requester_transport: MctpPldmTransport<Cq>,
) -> Self {
FirmwareDevice {
cmd_interface: CmdInterface::new(
protocol_capabilities,
FirmwareDeviceContext::new(fdops),
),
responder_transport,
requester_transport,
}
}
/// The step body, taking each field it needs directly rather than
/// `&mut self`.
///
/// This has to be a free-standing function, not a method: `run_until`
/// holds `listener`, which borrows `responder_transport` for the whole
/// session, alongside this step's need for `&mut cmd_interface`. A
/// method taking `&mut self` claims all of `self` at once — including
/// `responder_transport` — which the borrow checker sees as conflicting
/// with `listener`'s outstanding borrow even though the two never
/// actually touch the same data at the same time. Passing the fields
/// this needs individually, instead of through `self`, lets the borrow
/// checker see they are disjoint.
#[allow(clippy::too_many_arguments)]
fn run_once_inner(
cmd_interface: &mut CmdInterface<'_, O>,
requester_transport: &MctpPldmTransport<Cq>,
responder_transport: &MctpPldmTransport<Cr>,
listener: &mut StackListener<'_, Cr>,
fw_buf: &mut [u8; FD_MAX_MSG],
buf: &mut [u8],
remote_eid: u8,
timeout_millis: u32,
requester_timeout_millis: u32,
sink: &mut impl FdEventSink,
) -> Result<(), PldmServiceError> {
// Phase 1: while in initiator mode, issue at most ONE outbound
// request this step. We deliberately fall through to the responder
// poll below (no early return) so an Update Agent command such as
// CancelUpdate is serviced between every RequestFirmwareData.
let initiator_active = cmd_interface.fd_ctx.should_start_initiator_mode();
if initiator_active
&& let Some(pldm_len) = cmd_interface
.generate_initiator_request(fw_buf)
.map_err(PldmServiceError::MsgHandler)?
{
let resp_len = requester_transport.send_request(
remote_eid,
pldm_len,
fw_buf,
requester_timeout_millis,
)?;
let resp_total_len = resp_len
.checked_add(1)
.ok_or(PldmServiceError::PldmMem(PldmMemError::OverflowMaxSize))?;
let resp = fw_buf
.get_mut(..resp_total_len)
.ok_or(PldmServiceError::PldmMem(PldmMemError::BufferTooSmall))?;
cmd_interface
.process_initiator_response(resp)
.map_err(PldmServiceError::MsgHandler)?;
}
// Phase 2: poll for an inbound command so the responder path stays
// live during a transfer and the Update Agent can cancel at any
// time. `handle_responder_msg` receives the *whole* buffer because
// responses may be larger than the request they answer (e.g. GetTid:
// 4-byte request, 5-byte response). Commands from any EID other than
// `remote_eid` are dropped without a response.
let poll_timeout = if initiator_active {
RESPONDER_POLL_TIMEOUT_MILLIS
} else {
timeout_millis
};
listener.set_timeout(poll_timeout);
// Sampled around the responder poll: `RequestUpdate` is the only
// command that takes the FD out of `Idle`, so the false→true edge
// of `is_update_mode()` identifies exactly one accepted
// `RequestUpdate` (the initiator phase above never leaves `Idle`).
let was_update_mode = cmd_interface.fd_ctx.is_update_mode();
match responder_transport.respond_once(
listener,
buf,
|framed_buf, _req_total_len, source_eid| {
// Only act on commands from the UA this instance serves;
// silently drop anything else (e.g. a rogue endpoint).
if source_eid != remote_eid {
return Ok(0);
}
cmd_interface
.handle_responder_msg(framed_buf)
.map_err(PldmServiceError::MsgHandler)
},
) {
Ok(()) => {
if !was_update_mode && cmd_interface.fd_ctx.is_update_mode() {
sink.notify(FdEvent::UpdateRequested);
}
Ok(())
}
// A short poll timeout while an initiator request is active
// just means no UA command arrived in that window; keep going
// so the transfer can continue.
Err(PldmServiceError::Mctp(e)) if initiator_active && e.is_timeout() => Ok(()),
Err(e) => Err(e),
}
}
/// Run the firmware-device service loop, stopping as soon as
/// `should_continue` returns `false` — even if the session is not
/// otherwise over.
///
/// Each step performs the same two interleaved phases documented on
/// [`run_terminus`](Self::run_terminus):
///
/// 1. **Initiator** — while the FD is in update mode
/// ([`should_start_initiator_mode`]), generate at most one outbound
/// request (e.g. `RequestFirmwareData`) via
/// [`CmdInterface::generate_initiator_request`], send it to `remote_eid`
/// through `requester_transport`, and feed the response back into the
/// state machine via [`CmdInterface::process_initiator_response`].
/// 2. **Responder** — poll for an inbound Update Agent command and reply
/// via [`CmdInterface::handle_responder_msg`]. While an initiator
/// request is active, this poll uses a short timeout so the transfer
/// keeps making progress; a lack of a message within that window is
/// expected and does not end the session. When idle, the poll blocks
/// for the caller-supplied `timeout_millis`.
///
/// `should_continue` is checked once per step, before the step runs. A
/// caller that wants to interleave other work on this thread does it
/// from inside `should_continue` (returning `true` to keep going) rather
/// than being handed control back directly between steps: the responder
/// listener has to stay registered for the entire session — the
/// underlying MCTP stack requires an active registration to accept an
/// inbound request of a given message type, so a caller-visible gap
/// between steps (dropping the listener, doing other work, re-arming it)
/// would silently drop any Update Agent command arriving in that window.
/// Threading the listener through a public step-by-step API without that
/// gap would need `StackListener` to stop borrowing `responder_transport`
/// (it currently also implements `Drop`, which rules out storing it as a
/// `FirmwareDevice` field too) — a change to `services/mctp/api`, not
/// this crate.
///
/// A `timeout_millis` of `0` blocks indefinitely while idle.
///
/// `requester_timeout_millis` bounds how long each `send_request` call
/// (Phase 1) is allowed to wait for the UA's response to an FD-initiated
/// request. It is intentionally a separate value from `timeout_millis`:
/// reusing `timeout_millis` here would let a `0` (block indefinitely)
/// idle-timeout also apply to the wait for the UA's response, which could
/// block this call — and therefore Phase 2's responder poll — forever if
/// the UA never replies. A `requester_timeout_millis` of `0` still blocks
/// indefinitely if that behavior is desired; callers that want the
/// responder path to stay live even during a stalled FD-initiated
/// request should pass a bounded value instead.
///
/// `sink` receives [`FdEvent::UpdateRequested`] once per
/// accepted `RequestUpdate` (the FD's only `Idle` → non-`Idle`
/// transition), after the success response has been sent. Callers with
/// nothing to notify pass `&mut ()`.
///
/// Returns [`StoppedByError`](RunTerminusResult::StoppedByError) if a
/// step fails, or [`Completed`](RunTerminusResult::Completed) once
/// `should_continue` returns `false` — the latter says nothing about
/// protocol state; it only means the caller chose to stop here. A
/// well-defined *protocol* completion condition is still open work.
///
/// [`should_start_initiator_mode`]: pldm_interface::firmware_device::fd_context::FirmwareDeviceContext
pub fn run_until(
&mut self,
remote_eid: u8,
buf: &mut [u8],
timeout_millis: u32,
requester_timeout_millis: u32,
sink: &mut impl FdEventSink,
mut should_continue: impl FnMut() -> bool,
) -> RunTerminusResult {
let mut listener = match self.responder_transport.responder_listener(timeout_millis) {
Ok(listener) => listener,
Err(e) => return RunTerminusResult::StoppedByError(e),
};
// Scratch buffer for FD-initiated (outbound) requests, reused across
// steps rather than re-zeroed on every one.
let mut fw_buf = [0u8; FD_MAX_MSG];
while should_continue() {
// Field-by-field, not `&mut self` — see `run_once_inner`'s doc.
if let Err(e) = Self::run_once_inner(
&mut self.cmd_interface,
&self.requester_transport,
&self.responder_transport,
&mut listener,
&mut fw_buf,
buf,
remote_eid,
timeout_millis,
requester_timeout_millis,
sink,
) {
return RunTerminusResult::StoppedByError(e);
}
}
RunTerminusResult::Completed
}
/// Run the firmware-device service loop to completion, blocking the
/// calling thread.
///
/// A thin wrapper over [`run_until`](Self::run_until) with a
/// stop condition that never fires — see `run_until` for the full
/// behavior. This method loops indefinitely and returns only on error.
/// A `timeout_millis` of `0` blocks indefinitely while idle.
pub fn run_terminus(
&mut self,
remote_eid: u8,
buf: &mut [u8],
timeout_millis: u32,
requester_timeout_millis: u32,
sink: &mut impl FdEventSink,
) -> RunTerminusResult {
self.run_until(
remote_eid,
buf,
timeout_millis,
requester_timeout_millis,
sink,
|| true,
)
}
}