| // 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::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; |
| |
| /// Outcome of [`FirmwareDevice::run_terminus`]. |
| pub enum RunTerminusResult { |
| /// The loop exited normally (currently unreachable: `run_terminus` only |
| /// returns via an error today, but this variant exists so a future, |
| /// well-defined completion condition does not require an API change). |
| 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, |
| } |
| } |
| |
| /// 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`]), 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 `responder_transport` 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 loop. When |
| /// idle, the poll blocks for the caller-supplied `timeout_millis`. |
| /// |
| /// The responder listener is registered once, before the loop starts, and |
| /// reused for every poll (rather than being registered and dropped on |
| /// each iteration). This matters because the underlying MCTP stack |
| /// requires an active listener registration to accept an inbound request |
| /// of a given message type: registering a fresh listener on every poll |
| /// would leave a window during initiator (FD-to-UA) traffic in which no |
| /// listener is bound, silently dropping any Update Agent command that |
| /// arrives in that window. |
| /// |
| /// This method loops indefinitely and returns only on error. |
| /// 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. |
| /// |
| /// [`should_start_initiator_mode`]: pldm_interface::firmware_device::fd_context::FirmwareDeviceContext |
| pub fn run_terminus( |
| &mut self, |
| remote_eid: u8, |
| buf: &mut [u8], |
| timeout_millis: u32, |
| requester_timeout_millis: u32, |
| ) -> RunTerminusResult { |
| match self.run_terminus_inner(remote_eid, buf, timeout_millis, requester_timeout_millis) { |
| Ok(()) => RunTerminusResult::Completed, |
| Err(e) => RunTerminusResult::StoppedByError(e), |
| } |
| } |
| |
| fn run_terminus_inner( |
| &mut self, |
| remote_eid: u8, |
| buf: &mut [u8], |
| timeout_millis: u32, |
| requester_timeout_millis: u32, |
| ) -> Result<(), PldmServiceError> { |
| let mut responder_listener = self |
| .responder_transport |
| .responder_listener(timeout_millis)?; |
| // Scratch buffer for FD-initiated (outbound) requests, reused across |
| // iterations rather than re-zeroed on every loop pass. |
| let mut fw_buf = [0u8; FD_MAX_MSG]; |
| |
| 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 Some(pldm_len) = self |
| .cmd_interface |
| .generate_initiator_request(&mut fw_buf) |
| .map_err(PldmServiceError::MsgHandler)? |
| { |
| let resp_len = self.requester_transport.send_request( |
| remote_eid, |
| pldm_len, |
| &mut 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))?; |
| self.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 |
| }; |
| responder_listener.set_timeout(poll_timeout); |
| match self.responder_transport.respond_once( |
| &mut responder_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); |
| } |
| self.cmd_interface |
| .handle_responder_msg(framed_buf) |
| .map_err(PldmServiceError::MsgHandler) |
| }, |
| ) { |
| Ok(()) => {} |
| // A short poll timeout while an initiator request is active |
| // just means no UA command arrived in that window; keep |
| // looping so the transfer can continue. |
| Err(PldmServiceError::Mctp(e)) if initiator_active && e.is_timeout() => {} |
| Err(e) => return Err(e), |
| } |
| } |
| } |
| } |