| // 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, |
| ) |
| } |
| } |