libpldm: Add firmware update FD responder

This provides a FD responder implementation, with an ops struct to allow
for platform customization. Applications provide incoming messages to
pldm_fd_handle_msg(), and periodically call pldm_fd_progress() for
asynchronous events.

Change-Id: I034262e8b2c45b5bea61369d5f966dd7e530ba9e
Signed-off-by: Matt Johnston <matt@codeconstruct.com.au>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e04e0e..adcbb06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -65,6 +65,8 @@
 
 9. Add Firmware Device side firmware_update encode/decode functions
 
+10. Add firmware update FD responder
+
 ### Fixed
 
 1. dsp: platform: Fix location of closing paren in overflow detection
diff --git a/include/libpldm/firmware_fd.h b/include/libpldm/firmware_fd.h
new file mode 100644
index 0000000..5c45390
--- /dev/null
+++ b/include/libpldm/firmware_fd.h
@@ -0,0 +1,341 @@
+/* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later */
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <libpldm/pldm.h>
+#include <libpldm/base.h>
+#include <libpldm/utils.h>
+#include <libpldm/firmware_update.h>
+
+/** @struct pldm_firmware_component_standalone
+ *
+ *  A PLDM Firmware Update Component representation, for use
+ *  with pldm_fd_ops callbacks.
+*/
+struct pldm_firmware_component_standalone {
+	uint16_t comp_classification;
+	uint16_t comp_identifier;
+	uint8_t comp_classification_index;
+
+	struct pldm_firmware_version active_ver;
+	struct pldm_firmware_version pending_ver;
+
+	bitfield16_t comp_activation_methods;
+	bitfield32_t capabilities_during_update;
+};
+
+/** @struct pldm_firmware_update_component
+ *
+ * An entry for Pass Component Table or Update Component
+*/
+struct pldm_firmware_update_component {
+	uint16_t comp_classification;
+	uint16_t comp_identifier;
+	uint8_t comp_classification_index;
+	uint32_t comp_comparison_stamp;
+	struct pldm_firmware_string version;
+
+	/* Not set for PassComponentTable */
+	uint32_t comp_image_size;
+	/* Not set for PassComponentTable */
+	bitfield32_t update_option_flags;
+};
+
+/** @struct pldm_fd_ops
+ *
+ *  Device-specific callbacks provided by an application,
+ *  to define the device update behaviour.
+ *
+ *  These will be called by the FD responder when pldm_fd_handle_msg()
+ *  or pldm_fd_progress() are called by the application.
+ *
+ *  Note that return values vary between functions. Some return a PLDM
+ *  completion code or status code which will be sent to the UA, others
+ *  return a negative errno on failure.
+*/
+struct pldm_fd_ops {
+	/** @brief Provide PLDM descriptors
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[out] ret_descriptors_count - count of descriptors returned
+	 *  @param[out] ret_descriptors - array of descriptors.
+	 *
+	 *  @return 0 on success, a negative errno value on failure.
+	 *  (specific errno value is ignored)
+	 */
+	int (*device_identifiers)(
+		void *ctx, uint8_t *ret_descriptors_count,
+		const struct pldm_descriptor **ret_descriptors);
+
+	/** @brief Provide PLDM component table from the application
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[out] ret_entry_count - length of returned ret_entries
+	 *  @param[out] ret_entries - an array of component pointers
+	 *
+	 *  @return 0 on success, a negative errno value on failure.
+	 *  (specific errno value is ignored)
+	 *
+	 *  It will be called several times in an update flow.
+	 */
+	int (*components)(
+		void *ctx, uint16_t *ret_entry_count,
+		const struct pldm_firmware_component_standalone ***ret_entries);
+
+	/** @brief Return imageset version from the application
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[out] ret_active - ActiveComponentVersion string
+	 *  @param[out] ret_active - PendingComponentVersion string
+	 *
+	 *  @return 0 on success, a negative errno value on failure.
+	 *  (specific errno value is ignored)
+	 *
+	 *  This is used by the FD responder for GetFirmwareParameters.
+	 */
+	int (*imageset_versions)(void *ctx,
+				 struct pldm_firmware_string *ret_active,
+				 struct pldm_firmware_string *ret_pending);
+
+	/** @brief Called on PassComponentTable or UpdateComponent
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[in] update - will be set for UpdateComponent, and indicates that
+	 *  			an update flow is starting, with the same comp used
+	 * 			for subsequent firmware_data, verify, apply callbacks.
+	 *  @param[in] comp - the component being used. The FD implementation
+	 *		      will only pass comp that has already been
+	 *  		      validated against the pldm_fd_ops.components callback.
+	 *
+	 *  @return PLDM_CRC_COMP_CAN_BE_UPDATED if the component can be updated.
+	 */
+	enum pldm_component_response_codes (*update_component)(
+		void *ctx, bool update,
+		const struct pldm_firmware_update_component *comp);
+
+	/** @brief Provide the transfer size to use
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[in] ua_max_transfer_size - size requested by the UA.
+	 *
+	 *  @return The transfer size to use. This will be clamped to
+	 *  32 <= size <= ua_max_transfer_size.
+	 *  The final data chunk may be shorter.
+	 */
+	uint32_t (*transfer_size)(void *ctx, uint32_t ua_max_transfer_size);
+
+	/* @brief Provides firmware update data from the UA
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[in] offset - offset of the data
+	 *  @param[in] data - firmware data buffer
+	 *  @param[in] len - length of data
+	 *  @param[in] comp - the relevant component
+	 *
+	 *  @return TransferComplete code - either
+	 *			enum pldm_firmware_update_common_error_codes or
+	 *			enum pldm_firmware_update_transfer_result_values.
+	 *
+	 * PLDM_FWUP_TRANSFER_SUCCESS will accept the data chunk, other codes will
+	 * abort the transfer, returning that code as TransferComplete
+	 */
+	uint8_t (*firmware_data)(
+		void *ctx, uint32_t offset, const uint8_t *data, uint32_t len,
+		const struct pldm_firmware_update_component *comp);
+
+	/* @brief Requests the application verify the update
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[in] comp - the relevant component
+	 *  @param[out] ret_pending - set when verify will run asynchronously
+	 *  @param[out] ret_progress_percent - can optionally be set
+	 *                                     during asynchronous verify,
+	 *                                     or leave defaulted (101).
+	 *
+	 *  @return VerifyComplete code - either
+	 *			enum pldm_firmware_update_common_error_codes or
+	 *			enum pldm_firmware_update_verify_result_values.
+	 *
+	 * verify() will only be called once all firmware_data (up to the UA-specified
+	 * comp_image_size) has been provided. Implementations should check that length
+	 * as part of verification, if not already checked.
+	 *
+	 * If the verify is going to complete asynchronously, implementations set
+	 * *ret_pending=true and return PLDM_FWUP_VERIFY_SUCCESS. The FD will then
+	 * call verify() again when pldm_fd_progress() is called.
+	 */
+	uint8_t (*verify)(void *ctx,
+			  const struct pldm_firmware_update_component *comp,
+			  bool *ret_pending, uint8_t *ret_progress_percent);
+
+	/* @brief Requests the application apply the update
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[in] comp - the relevant component
+	 *  @param[out] ret_pending - set when apply will run asynchronously
+	 *  @param[out] ret_progress_percent - can optionally be set
+	 *                                     during asynchronous apply,
+	 *                                     or leave defaulted (101).
+	 *
+	 *  @return ApplyComplete code - either
+	 *			enum pldm_firmware_update_common_error_codes or
+	 *			enum pldm_firmware_update_apply_result_values.
+	 *
+	 * If the apply is going to complete asynchronously, implementations set
+	 * *ret_pending=true and return PLDM_FWUP_APPLY_SUCCESS. The FD will then
+	 * call apply() again when pldm_fd_progress() is called.
+	 */
+	uint8_t (*apply)(void *ctx,
+			 const struct pldm_firmware_update_component *comp,
+			 bool *ret_pending, uint8_t *ret_progress_percent);
+
+	/* @brief Activates new firmware
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[in] self_contained - Self Contained Activation is requested
+	 *  @param[out] ret_estimated_time - a time in seconds to perform
+	 *				     self activation, or may be left as 0.
+	 *
+	 *  @return PLDM completion code
+	 *
+	 * The device implementation is responsible for checking that
+	 * expected components have been updated, returning
+	 * PLDM_FWUP_INCOMPLETE_UPDATE if not.
+	 */
+	uint8_t (*activate)(void *ctx, bool self_contained,
+			    uint16_t *ret_estimated_time);
+
+	/* @brief Cancel Update Component
+	 *
+	 *  @param[in] ctx - callback context
+	 *  @param[in] comp - the relevant component
+	 *
+	 * Called when a component update is cancelled prior to being applied.
+	 * This function is called for both Cancel Update Component
+	 * and Cancel Update (when a component is currently in progress). */
+	void (*cancel_update_component)(
+		void *ctx, const struct pldm_firmware_update_component *comp);
+
+	/* @brief Returns a monotonic timestamp
+	 *
+	 *  @param[in] ctx - callback context
+	 *
+	 *  @return timestamp in milliseconds, from an arbitrary origin.
+	            Must not go backwards.
+	 */
+	uint64_t (*now)(void *ctx);
+};
+
+/* Static storage can be allocated with
+ * PLDM_SIZEOF_PLDM_FD macro */
+#define PLDM_ALIGNOF_PLDM_FD 8
+struct pldm_fd;
+
+/** @brief Allocate and initialise a FD responder
+ *
+ * @param[in] ops - Application provided callbacks which define the device
+ *                  update behaviour
+ * @param[in] ops_ctx - opaque context pointer that will be passed as ctx
+ *                      to ops callbacks
+ *
+ * @return a malloced struct pldm_fd, owned by the caller. It should be released
+ *         with free(). Returns NULL on failure.
+ *
+ * This will call pldm_fd_setup() on the allocated pldm_fd.
+ */
+struct pldm_fd *pldm_fd_new(const struct pldm_fd_ops *ops, void *ops_ctx);
+
+/** @brief Initialise a FD responder struct
+ *
+ * @param[in] fd - A pointer to a struct pldm_fd. Applications can allocate this
+ *                 in static storage of size PLDM_SIZEOF_PLDM_FD if required.
+ * @param[in] pldm_fd_size - applications should pass PLDM_SIZEOF_PLDM_FD, to check
+ *                 for consistency with the fd pointer.
+ * @param[in] ops - Application provided callbacks which define the device
+ *                  update behaviour
+ * @param[in] ops_ctx - opaque context pointer that will be passed as ctx
+ *                      to ops callbacks
+ *
+ * @return 0 on success, a negative errno value on failure.
+ */
+int pldm_fd_setup(struct pldm_fd *fd, size_t pldm_fd_size,
+		  const struct pldm_fd_ops *ops, void *ops_ctx);
+
+/** @brief Handle a PLDM Firmware Update message
+ *
+ * @param[in] fd
+ * @param[in] remote_address - the source address of the message.
+ * @param[in] in_msg - PLDM incoming message payload
+ * @param[in] in_len - length of in_msg buffer
+ * @param[out] out_msg - PLDM outgoing message payload buffer
+ * @param[inout] out_len - length of available out_msg buffer, will be updated
+ *                         with the length written to out_msg.
+ *
+ * @return 0 on success, a negative errno value on failure.
+ *
+ * Will return a message to send if out_len > 0
+ * and returning 0.
+ */
+int pldm_fd_handle_msg(struct pldm_fd *fd, pldm_tid_t remote_address,
+		       const void *in_msg, size_t in_len, void *out_msg,
+		       size_t *out_len);
+
+/** @brief Handle periodic progress events
+ *
+ * @param[in] fd
+ * @param[out] out_msg - PLDM outgoing message payload buffer
+ * @param[inout] out_len - length of available out_msg buffer, will be updated
+ *                         with the length written to out_msg.
+ * @param[out] remote_address - destination address for the message to send.
+ * 				This is the address used to initiate the update,
+ * 				from a previous pldm_fd_handle_msg call.
+ *
+ * @return 0 on success, a negative errno value on failure.
+ *
+ * Will return a message to send to remote_address if out_len > 0
+ * and returning 0.
+ *
+ * This could be called periodically by the application to send retries
+ * during an update flow. A 1 second interval is recommended.
+ */
+int pldm_fd_progress(struct pldm_fd *fd, void *out_msg, size_t *out_len,
+		     pldm_tid_t *remote_address);
+
+/** @brief Set update mode idle timeout
+ *
+ * @param[in] fd
+ * @param[in] time - Amount of time before the FD shall exit from update mode
+ *                   if no command is received, in milliseconds. FD_T1.
+ *                   Should be 60000-120000 (60-120 sec), initial default is
+ *                   120000.
+ *
+ * @return 0 on success, a negative errno value on failure.
+ */
+int pldm_fd_set_update_idle_timeout(struct pldm_fd *fd, uint32_t time);
+
+/** @brief Set request retry time
+ *
+ * @param[in] fd
+ * @param[in] time - Time for retries of Request Firmware Data,
+ *                   Verify, Apply commands, in miliseconds. FD_T2.
+ * 					 Should be 1000-5000, initial default is 1000.
+ *
+ * Will return a message to send to remote_address if out_len > 0
+ * and returning 0.
+ *
+ * This could be called periodically by the application to send retries
+ * during an update flow. A 1 second interval is recommended.
+ *
+ * @return 0 on success, a negative errno value on failure.
+ */
+int pldm_fd_set_request_retry_time(struct pldm_fd *fd, uint32_t time);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/include/libpldm/meson.build b/include/libpldm/meson.build
index 9dff827..a95cb56 100644
--- a/include/libpldm/meson.build
+++ b/include/libpldm/meson.build
@@ -5,6 +5,7 @@
     'bios_table.h',
     'compiler.h',
     'entity.h',
+    'firmware_fd.h',
     'firmware_update.h',
     'fru.h',
     'instance-id.h',
@@ -37,3 +38,19 @@
 
 
 install_headers(libpldm_headers, subdir: 'libpldm', preserve_path: true)
+
+# TODO: these should depend on the input headers so they rebuild
+# on changes, unclear how to do that.
+include_src = include_directories('../../src', is_system: true)
+sizeof_pldm_fd = compiler.sizeof(
+    'struct pldm_fd',
+    prefix: '#include "firmware_device/fd-internal.h"',
+    include_directories: [include_src, libpldm_include_dir],
+)
+sizes_h = configure_file(
+    configuration: {'sizeof_pldm_fd': sizeof_pldm_fd},
+    input: 'sizes.h.in',
+    output: 'sizes.h',
+    install: true,
+    install_dir: 'libpldm',
+)
diff --git a/include/libpldm/sizes.h.in b/include/libpldm/sizes.h.in
new file mode 100644
index 0000000..85c0330
--- /dev/null
+++ b/include/libpldm/sizes.h.in
@@ -0,0 +1,4 @@
+#pragma once
+
+/* sizeof(struct pldm_fd) */
+#define PLDM_SIZEOF_PLDM_FD @sizeof_pldm_fd@
diff --git a/src/firmware_device/fd-internal.h b/src/firmware_device/fd-internal.h
new file mode 100644
index 0000000..3873c80
--- /dev/null
+++ b/src/firmware_device/fd-internal.h
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later */
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <libpldm/pldm.h>
+#include <libpldm/firmware_update.h>
+#include <libpldm/firmware_fd.h>
+#include <libpldm/utils.h>
+
+typedef uint64_t pldm_fd_time_t;
+
+struct pldm_fd_req {
+	enum pldm_fd_req_state {
+		// pldm_fd_req instance is unused
+		PLDM_FD_REQ_UNUSED = 0,
+		// Ready to send a request
+		PLDM_FD_REQ_READY,
+		// Waiting for a response
+		PLDM_FD_REQ_SENT,
+		// Completed and failed, will not send more requests.
+		// Waiting for a cancel from the UA.
+		PLDM_FD_REQ_FAILED,
+	} state;
+
+	/* Set once when ready to move to next state, will return
+     * this result for TransferComplete/VerifyComplete/ApplyComplete request. */
+	bool complete;
+	/* Only valid when complete is set */
+	uint8_t result;
+
+	/* Only valid in SENT state */
+	uint8_t instance_id;
+	uint8_t command;
+	pldm_fd_time_t sent_time;
+};
+
+struct pldm_fd_download {
+	uint32_t offset;
+};
+
+struct pldm_fd_verify {
+	uint8_t progress_percent;
+};
+
+struct pldm_fd_apply {
+	uint8_t progress_percent;
+};
+
+struct pldm_fd {
+	enum pldm_firmware_device_states state;
+	enum pldm_firmware_device_states prev_state;
+
+	/* Reason for last transition to idle state,
+     * only valid when state == PLDM_FD_STATE_IDLE */
+	enum pldm_get_status_reason_code_values reason;
+
+	/* State-specific content */
+	union {
+		struct pldm_fd_download download;
+		struct pldm_fd_verify verify;
+		struct pldm_fd_apply apply;
+	} specific;
+	/* Details of the component currently being updated.
+     * Set by UpdateComponent, available during download/verify/apply.
+     * Also used as temporary storage for PassComponentTable */
+	struct pldm_firmware_update_component update_comp;
+	bitfield32_t update_flags;
+
+	/* Used for download/verify/apply requests */
+	struct pldm_fd_req req;
+
+	/* Address of the UA */
+	pldm_tid_t ua_address;
+	bool ua_address_set;
+
+	/* Maximum size allowed by the UA or platform implementation */
+	uint32_t max_transfer;
+
+	/* Timestamp for FD T1 timeout, milliseconds */
+	pldm_fd_time_t update_timestamp_fd_t1;
+
+	pldm_fd_time_t fd_t1_timeout;
+	pldm_fd_time_t fd_t2_retry_time;
+
+	const struct pldm_fd_ops *ops;
+	void *ops_ctx;
+};
diff --git a/src/firmware_device/fd.c b/src/firmware_device/fd.c
new file mode 100644
index 0000000..116657c
--- /dev/null
+++ b/src/firmware_device/fd.c
@@ -0,0 +1,1422 @@
+/* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later */
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <libpldm/pldm.h>
+#include <libpldm/firmware_update.h>
+#include <libpldm/firmware_fd.h>
+#include <libpldm/utils.h>
+#include <compiler.h>
+#include <msgbuf.h>
+
+#include "fd-internal.h"
+
+/* FD_T1 Update mode idle timeout, 120 seconds (range [60s, 120s])*/
+static const pldm_fd_time_t DEFAULT_FD_T1_TIMEOUT = 120000;
+
+/* FD_T2 "Retry request for firmware data", 1 second (range [1s, 5s]) */
+static const pldm_fd_time_t DEFAULT_FD_T2_RETRY_TIME = 1000;
+
+static const uint8_t INSTANCE_ID_COUNT = 32;
+static const uint8_t PROGRESS_PERCENT_NOT_SUPPORTED = 101;
+
+/* Ensure that public definition is kept updated */
+static_assert(alignof(struct pldm_fd) == PLDM_ALIGNOF_PLDM_FD,
+	      "PLDM_ALIGNOF_PLDM_FD wrong");
+
+/* Maybe called with success or failure completion codes, though
+ * success only makes sense for responses without a body.
+ * Returns 0 or negative errno. */
+LIBPLDM_CC_NONNULL
+static int pldm_fd_reply_cc(uint8_t ccode,
+			    const struct pldm_header_info *req_hdr,
+			    struct pldm_msg *resp, size_t *resp_payload_len)
+{
+	int status;
+
+	/* 1 byte completion code */
+	if (*resp_payload_len < 1) {
+		return -EOVERFLOW;
+	}
+	*resp_payload_len = 1;
+
+	status = encode_cc_only_resp(req_hdr->instance, PLDM_FWUP,
+				     req_hdr->command, ccode, resp);
+	if (status != PLDM_SUCCESS) {
+		return -EINVAL;
+	}
+	return 0;
+}
+
+/* Must be called with a negative errno.
+ * Returns 0 or negative errno. */
+LIBPLDM_CC_NONNULL
+static int pldm_fd_reply_errno(int err, const struct pldm_header_info *req_hdr,
+			       struct pldm_msg *resp, size_t *resp_payload_len)
+{
+	uint8_t ccode = PLDM_ERROR;
+
+	assert(err < 0);
+	switch (err) {
+	case -EINVAL:
+		// internal error, shouldn't occur.
+		ccode = PLDM_ERROR;
+		break;
+	case -EPROTO:
+		// Bad data from peer
+		ccode = PLDM_ERROR_INVALID_DATA;
+		break;
+	case -EOVERFLOW:
+	case -EBADMSG:
+		// Bad data from peer
+		ccode = PLDM_ERROR_INVALID_LENGTH;
+		break;
+	default:
+		// general error
+		ccode = PLDM_ERROR;
+	}
+
+	return pldm_fd_reply_cc(ccode, req_hdr, resp, resp_payload_len);
+}
+
+LIBPLDM_CC_NONNULL
+static void pldm_fd_set_state(struct pldm_fd *fd,
+			      enum pldm_firmware_device_states state)
+{
+	/* pldm_fd_set_idle should be used instead */
+	assert(state != PLDM_FD_STATE_IDLE);
+
+	if (fd->state == state) {
+		return;
+	}
+
+	fd->prev_state = fd->state;
+	fd->state = state;
+}
+
+LIBPLDM_CC_NONNULL
+static void pldm_fd_set_idle(struct pldm_fd *fd,
+			     enum pldm_get_status_reason_code_values reason)
+{
+	fd->prev_state = fd->state;
+	fd->state = PLDM_FD_STATE_IDLE;
+	fd->reason = reason;
+	fd->ua_address_set = false;
+}
+
+LIBPLDM_CC_NONNULL
+static void pldm_fd_idle_timeout(struct pldm_fd *fd)
+{
+	enum pldm_get_status_reason_code_values reason = PLDM_FD_INITIALIZATION;
+
+	switch (fd->state) {
+	case PLDM_FD_STATE_IDLE:
+		return;
+	case PLDM_FD_STATE_LEARN_COMPONENTS:
+		reason = PLDM_FD_TIMEOUT_LEARN_COMPONENT;
+		break;
+	case PLDM_FD_STATE_READY_XFER:
+		reason = PLDM_FD_TIMEOUT_READY_XFER;
+		break;
+	case PLDM_FD_STATE_DOWNLOAD:
+		reason = PLDM_FD_TIMEOUT_DOWNLOAD;
+		break;
+	case PLDM_FD_STATE_VERIFY:
+		reason = PLDM_FD_TIMEOUT_VERIFY;
+		break;
+	case PLDM_FD_STATE_APPLY:
+		reason = PLDM_FD_TIMEOUT_APPLY;
+		break;
+	case PLDM_FD_STATE_ACTIVATE:
+		/* Not a timeout */
+		reason = PLDM_FD_ACTIVATE_FW;
+		break;
+	}
+
+	pldm_fd_set_idle(fd, reason);
+}
+
+LIBPLDM_CC_NONNULL
+static void pldm_fd_get_aux_state(const struct pldm_fd *fd, uint8_t *aux_state,
+				  uint8_t *aux_state_status)
+{
+	*aux_state_status = 0;
+
+	switch (fd->req.state) {
+	case PLDM_FD_REQ_UNUSED:
+		*aux_state = PLDM_FD_IDLE_LEARN_COMPONENTS_READ_XFER;
+		break;
+	case PLDM_FD_REQ_SENT:
+		*aux_state = PLDM_FD_OPERATION_IN_PROGRESS;
+		break;
+	case PLDM_FD_REQ_READY:
+		if (fd->req.complete) {
+			*aux_state = PLDM_FD_OPERATION_SUCCESSFUL;
+		} else {
+			*aux_state = PLDM_FD_OPERATION_IN_PROGRESS;
+		}
+		break;
+	case PLDM_FD_REQ_FAILED:
+		*aux_state = PLDM_FD_OPERATION_FAILED;
+		*aux_state_status = fd->req.result;
+		break;
+	}
+}
+
+LIBPLDM_CC_NONNULL
+static uint64_t pldm_fd_now(struct pldm_fd *fd)
+{
+	return fd->ops->now(fd->ops_ctx);
+}
+
+LIBPLDM_CC_NONNULL
+static bool pldm_fd_req_should_send(struct pldm_fd *fd)
+{
+	pldm_fd_time_t now = pldm_fd_now(fd);
+
+	switch (fd->req.state) {
+	case PLDM_FD_REQ_UNUSED:
+		assert(false);
+		return false;
+	case PLDM_FD_REQ_READY:
+		return true;
+	case PLDM_FD_REQ_FAILED:
+		return false;
+	case PLDM_FD_REQ_SENT:
+		if (now < fd->req.sent_time) {
+			/* Time went backwards */
+			return false;
+		}
+
+		/* Send if retry time has elapsed */
+		return (now - fd->req.sent_time) >= fd->fd_t2_retry_time;
+	}
+	return false;
+}
+
+/* Allocate the next instance ID. Only one request is outstanding so cycling
+ * through the range is OK */
+LIBPLDM_CC_NONNULL
+static uint8_t pldm_fd_req_next_instance(struct pldm_fd_req *req)
+{
+	req->instance_id = (req->instance_id + 1) % INSTANCE_ID_COUNT;
+	return req->instance_id;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_qdi(struct pldm_fd *fd, const struct pldm_header_info *hdr,
+		       const struct pldm_msg *req LIBPLDM_CC_UNUSED,
+		       size_t req_payload_len, struct pldm_msg *resp,
+		       size_t *resp_payload_len)
+{
+	uint8_t descriptor_count;
+	const struct pldm_descriptor *descriptors;
+	int rc;
+
+	/* QDI has no request data */
+	if (req_payload_len != PLDM_QUERY_DEVICE_IDENTIFIERS_REQ_BYTES) {
+		return pldm_fd_reply_cc(PLDM_ERROR_INVALID_LENGTH, hdr, resp,
+					resp_payload_len);
+	}
+
+	/* Retrieve platform-specific data */
+	rc = fd->ops->device_identifiers(fd->ops_ctx, &descriptor_count,
+					 &descriptors);
+	if (rc) {
+		return pldm_fd_reply_cc(PLDM_ERROR, hdr, resp,
+					resp_payload_len);
+	}
+
+	rc = encode_query_device_identifiers_resp(hdr->instance,
+						  descriptor_count, descriptors,
+						  resp, resp_payload_len);
+
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_fw_param(struct pldm_fd *fd,
+			    const struct pldm_header_info *hdr,
+			    const struct pldm_msg *req LIBPLDM_CC_UNUSED,
+			    size_t req_payload_len, struct pldm_msg *resp,
+			    size_t *resp_payload_len)
+{
+	uint16_t entry_count;
+	const struct pldm_firmware_component_standalone **entries;
+	struct pldm_msgbuf _buf;
+	struct pldm_msgbuf *buf = &_buf;
+	int rc;
+
+	/* No request data */
+	if (req_payload_len != PLDM_GET_FIRMWARE_PARAMETERS_REQ_BYTES) {
+		return pldm_fd_reply_cc(PLDM_ERROR_INVALID_LENGTH, hdr, resp,
+					resp_payload_len);
+	}
+
+	/* Retrieve platform-specific data */
+	rc = fd->ops->components(fd->ops_ctx, &entry_count, &entries);
+	if (rc) {
+		return pldm_fd_reply_cc(PLDM_ERROR, hdr, resp,
+					resp_payload_len);
+	}
+
+	rc = pldm_msgbuf_init_errno(buf, 0, resp->payload, *resp_payload_len);
+	if (rc) {
+		return rc;
+	}
+
+	/* Add the fixed parameters */
+	{
+		struct pldm_get_firmware_parameters_resp_full fwp = {
+			.completion_code = PLDM_SUCCESS,
+			// TODO defaulted to 0, could have a callback.
+			.capabilities_during_update = { 0 },
+			.comp_count = entry_count,
+		};
+		/* fill active and pending strings */
+		rc = fd->ops->imageset_versions(
+			fd->ops_ctx, &fwp.active_comp_image_set_ver_str,
+			&fwp.pending_comp_image_set_ver_str);
+		if (rc) {
+			return pldm_fd_reply_cc(PLDM_ERROR, hdr, resp,
+						resp_payload_len);
+		}
+
+		size_t len = buf->remaining;
+		rc = encode_get_firmware_parameters_resp(hdr->instance, &fwp,
+							 resp, &len);
+		if (rc) {
+			return pldm_fd_reply_errno(rc, hdr, resp,
+						   resp_payload_len);
+		}
+		rc = pldm_msgbuf_skip(buf, len);
+		if (rc) {
+			return rc;
+		}
+	}
+
+	/* Add the component table entries */
+	for (uint16_t i = 0; i < entry_count; i++) {
+		const struct pldm_firmware_component_standalone *e = entries[i];
+		void *out = NULL;
+		size_t len;
+
+		struct pldm_component_parameter_entry_full comp = {
+			.comp_classification = e->comp_classification,
+			.comp_identifier = e->comp_identifier,
+			.comp_classification_index =
+				e->comp_classification_index,
+
+			.active_ver = e->active_ver,
+			.pending_ver = e->pending_ver,
+
+			.comp_activation_methods = e->comp_activation_methods,
+			.capabilities_during_update =
+				e->capabilities_during_update,
+		};
+
+		if (pldm_msgbuf_peek_remaining(buf, &out, &len)) {
+			return rc;
+		}
+		rc = encode_get_firmware_parameters_resp_comp_entry(&comp, out,
+								    &len);
+		if (rc) {
+			return pldm_fd_reply_errno(rc, hdr, resp,
+						   resp_payload_len);
+		}
+		rc = pldm_msgbuf_skip(buf, len);
+		if (rc) {
+			return rc;
+		}
+	}
+
+	return pldm_msgbuf_destroy_used(buf, *resp_payload_len,
+					resp_payload_len);
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_request_update(struct pldm_fd *fd,
+				  const struct pldm_header_info *hdr,
+				  const struct pldm_msg *req,
+				  size_t req_payload_len, struct pldm_msg *resp,
+				  size_t *resp_payload_len, uint8_t address)
+{
+	struct pldm_request_update_req_full upd;
+	const struct pldm_request_update_resp resp_data = {
+		.fd_meta_data_len = 0,
+		.fd_will_send_pkg_data = 0,
+	};
+	int rc;
+
+	if (fd->state != PLDM_FD_STATE_IDLE) {
+		return pldm_fd_reply_cc(PLDM_FWUP_ALREADY_IN_UPDATE_MODE, hdr,
+					resp, resp_payload_len);
+	}
+
+	rc = decode_request_update_req(req, req_payload_len, &upd);
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	/* No metadata nor pkg data */
+	rc = encode_request_update_resp(hdr->instance, &resp_data, resp,
+					resp_payload_len);
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	// TODO pass num_of_comp and image_set_ver to application?
+
+	fd->max_transfer =
+		fd->ops->transfer_size(fd->ops_ctx, upd.max_transfer_size);
+	if (fd->max_transfer > upd.max_transfer_size) {
+		/* Limit to UA's size */
+		fd->max_transfer = upd.max_transfer_size;
+	}
+	if (fd->max_transfer < PLDM_FWUP_BASELINE_TRANSFER_SIZE) {
+		/* Don't let it be zero, that will loop forever  */
+		fd->max_transfer = PLDM_FWUP_BASELINE_TRANSFER_SIZE;
+	}
+	fd->ua_address = address;
+	fd->ua_address_set = true;
+
+	pldm_fd_set_state(fd, PLDM_FD_STATE_LEARN_COMPONENTS);
+
+	return 0;
+}
+
+/* wrapper around ops->cancel, will only run ops->cancel when a component update is
+ * active */
+LIBPLDM_CC_NONNULL
+static void pldm_fd_maybe_cancel_component(struct pldm_fd *fd)
+{
+	bool cancel = false;
+
+	switch (fd->state) {
+	case PLDM_FD_STATE_DOWNLOAD:
+	case PLDM_FD_STATE_VERIFY:
+		cancel = true;
+		break;
+	case PLDM_FD_STATE_APPLY:
+		/* In apply state, once the application ops->apply() has completed
+		 * successfully the component is no longer in update state.
+		 * In that case the cancel should not be forwarded to the application.
+		 * This can occur if a cancel is received while waiting for the
+		 * response to a success ApplyComplete. */
+		cancel = !(fd->req.complete &&
+			   fd->req.result == PLDM_FWUP_APPLY_SUCCESS);
+		break;
+	default:
+		break;
+	}
+
+	if (cancel) {
+		/* Call the platform handler for the current component in progress */
+		fd->ops->cancel_update_component(fd->ops_ctx, &fd->update_comp);
+	}
+}
+
+/* Wrapper around ops->update_component() that first checks that the component
+ * is in the list returned from ops->components() */
+LIBPLDM_CC_NONNULL
+static enum pldm_component_response_codes pldm_fd_check_update_component(
+	struct pldm_fd *fd, bool update,
+	const struct pldm_firmware_update_component *comp)
+{
+	bool found;
+	uint16_t entry_count;
+	int rc;
+
+	const struct pldm_firmware_component_standalone **entries;
+	rc = fd->ops->components(fd->ops_ctx, &entry_count, &entries);
+	if (rc) {
+		return PLDM_CRC_COMP_NOT_SUPPORTED;
+	}
+
+	found = false;
+	for (uint16_t i = 0; i < entry_count; i++) {
+		if (entries[i]->comp_classification ==
+			    comp->comp_classification &&
+		    entries[i]->comp_identifier == comp->comp_identifier &&
+		    entries[i]->comp_classification_index ==
+			    comp->comp_classification_index) {
+			found = true;
+			break;
+		}
+	}
+	if (found) {
+		return fd->ops->update_component(fd->ops_ctx, update, comp);
+	}
+	return PLDM_CRC_COMP_NOT_SUPPORTED;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_pass_comp(struct pldm_fd *fd,
+			     const struct pldm_header_info *hdr,
+			     const struct pldm_msg *req, size_t req_payload_len,
+			     struct pldm_msg *resp, size_t *resp_payload_len)
+{
+	struct pldm_pass_component_table_req_full pcomp;
+	uint8_t comp_response_code;
+	int rc;
+
+	if (fd->state != PLDM_FD_STATE_LEARN_COMPONENTS) {
+		return pldm_fd_reply_cc(PLDM_FWUP_INVALID_STATE_FOR_COMMAND,
+					hdr, resp, resp_payload_len);
+	}
+
+	/* fd->update_comp is used as temporary storage during PassComponent validation */
+	/* Some portions are unused for PassComponentTable */
+	fd->update_comp.comp_image_size = 0;
+	fd->update_comp.update_option_flags.value = 0;
+
+	rc = decode_pass_component_table_req(req, req_payload_len, &pcomp);
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	fd->update_comp.comp_classification = pcomp.comp_classification;
+	fd->update_comp.comp_identifier = pcomp.comp_identifier;
+	fd->update_comp.comp_classification_index =
+		pcomp.comp_classification_index;
+	fd->update_comp.comp_comparison_stamp = pcomp.comp_comparison_stamp;
+	memcpy(&fd->update_comp.version, &pcomp.version, sizeof(pcomp.version));
+
+	comp_response_code =
+		pldm_fd_check_update_component(fd, false, &fd->update_comp);
+
+	const struct pldm_pass_component_table_resp resp_data = {
+		/* Component Response Code is 0 for ComponentResponse, 1 otherwise */
+		.comp_resp = (comp_response_code != 0),
+		.comp_resp_code = comp_response_code,
+	};
+
+	rc = encode_pass_component_table_resp(hdr->instance, &resp_data, resp,
+					      resp_payload_len);
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	if (pcomp.transfer_flag & PLDM_END) {
+		pldm_fd_set_state(fd, PLDM_FD_STATE_READY_XFER);
+	}
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_update_comp(struct pldm_fd *fd,
+			       const struct pldm_header_info *hdr,
+			       const struct pldm_msg *req,
+			       size_t req_payload_len, struct pldm_msg *resp,
+			       size_t *resp_payload_len)
+{
+	struct pldm_update_component_req_full up;
+	uint8_t comp_response_code;
+	int rc;
+
+	if (fd->state != PLDM_FD_STATE_READY_XFER) {
+		return pldm_fd_reply_cc(PLDM_FWUP_INVALID_STATE_FOR_COMMAND,
+					hdr, resp, resp_payload_len);
+	}
+
+	rc = decode_update_component_req(req, req_payload_len, &up);
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	/* Store update_comp to pass to further callbacks. This persists
+	 * until the component update completes or is cancelled */
+	fd->update_comp.comp_classification = up.comp_classification;
+	fd->update_comp.comp_identifier = up.comp_identifier;
+	fd->update_comp.comp_classification_index =
+		up.comp_classification_index;
+	fd->update_comp.comp_comparison_stamp = up.comp_comparison_stamp;
+	fd->update_comp.comp_image_size = up.comp_image_size;
+	fd->update_comp.update_option_flags = up.update_option_flags;
+	memcpy(&fd->update_comp.version, &up.version, sizeof(up.version));
+
+	comp_response_code =
+		pldm_fd_check_update_component(fd, true, &fd->update_comp);
+
+	// Mask to only the "Force Update" flag, others are not handled.
+	bitfield32_t update_flags = {
+		.bits.bit0 = fd->update_comp.update_option_flags.bits.bit0
+	};
+
+	const struct pldm_update_component_resp resp_data = {
+		/* Component Response Code is 0 for ComponentResponse, 1 otherwise */
+		.comp_compatibility_resp = (comp_response_code != 0),
+		.comp_compatibility_resp_code = comp_response_code,
+		.update_option_flags_enabled = update_flags,
+		.time_before_req_fw_data = 0,
+	};
+
+	rc = encode_update_component_resp(hdr->instance, &resp_data, resp,
+					  resp_payload_len);
+	if (rc) {
+		/* Encoding response failed */
+		if (comp_response_code == PLDM_CRC_COMP_CAN_BE_UPDATED) {
+			/* Inform the application of cancellation. Call it directly
+			 * rather than going through pldm_fd_maybe_cancel_component() */
+			fd->ops->cancel_update_component(fd->ops_ctx,
+							 &fd->update_comp);
+		}
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	/* Set up download state */
+	if (comp_response_code == PLDM_CRC_COMP_CAN_BE_UPDATED) {
+		memset(&fd->specific, 0x0, sizeof(fd->specific));
+		fd->update_flags = update_flags;
+		fd->req.state = PLDM_FD_REQ_READY;
+		fd->req.complete = false;
+		pldm_fd_set_state(fd, PLDM_FD_STATE_DOWNLOAD);
+	}
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_get_status(struct pldm_fd *fd,
+			      const struct pldm_header_info *hdr,
+			      const struct pldm_msg *req LIBPLDM_CC_UNUSED,
+			      size_t req_payload_len, struct pldm_msg *resp,
+			      size_t *resp_payload_len)
+{
+	int rc;
+
+	/* No request data */
+	if (req_payload_len != PLDM_GET_STATUS_REQ_BYTES) {
+		return pldm_fd_reply_cc(PLDM_ERROR_INVALID_LENGTH, hdr, resp,
+					resp_payload_len);
+	}
+
+	/* Defaults */
+	struct pldm_get_status_resp st = {
+		.current_state = fd->state,
+		.previous_state = fd->prev_state,
+		.progress_percent = PROGRESS_PERCENT_NOT_SUPPORTED,
+	};
+
+	pldm_fd_get_aux_state(fd, &st.aux_state, &st.aux_state_status);
+
+	switch (fd->state) {
+	case PLDM_FD_STATE_IDLE:
+		st.reason_code = fd->reason;
+		break;
+	case PLDM_FD_STATE_DOWNLOAD:
+		if (fd->update_comp.comp_image_size > 0) {
+			uint32_t one_percent =
+				fd->update_comp.comp_image_size / 100;
+			if (fd->update_comp.comp_image_size % 100 != 0) {
+				one_percent += 1;
+			}
+			st.progress_percent =
+				(fd->specific.download.offset / one_percent);
+		}
+		st.update_option_flags_enabled = fd->update_flags;
+		break;
+	case PLDM_FD_STATE_VERIFY:
+		st.update_option_flags_enabled = fd->update_flags;
+		st.progress_percent = fd->specific.verify.progress_percent;
+		break;
+	case PLDM_FD_STATE_APPLY:
+		st.update_option_flags_enabled = fd->update_flags;
+		st.progress_percent = fd->specific.apply.progress_percent;
+		break;
+	default:
+		break;
+	}
+
+	rc = encode_get_status_resp(hdr->instance, &st, resp, resp_payload_len);
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_cancel_update_comp(
+	struct pldm_fd *fd, const struct pldm_header_info *hdr,
+	const struct pldm_msg *req LIBPLDM_CC_UNUSED, size_t req_payload_len,
+	struct pldm_msg *resp, size_t *resp_payload_len)
+{
+	int rc;
+
+	/* No request data */
+	if (req_payload_len != PLDM_CANCEL_UPDATE_COMPONENT_REQ_BYTES) {
+		return pldm_fd_reply_cc(PLDM_ERROR_INVALID_LENGTH, hdr, resp,
+					resp_payload_len);
+	}
+
+	switch (fd->state) {
+	case PLDM_FD_STATE_DOWNLOAD:
+	case PLDM_FD_STATE_VERIFY:
+	case PLDM_FD_STATE_APPLY:
+		break;
+	default:
+		return pldm_fd_reply_cc(PLDM_FWUP_NOT_IN_UPDATE_MODE, hdr, resp,
+					resp_payload_len);
+	}
+
+	/* No response payload */
+	rc = pldm_fd_reply_cc(PLDM_SUCCESS, hdr, resp, resp_payload_len);
+	if (rc) {
+		return rc;
+	}
+
+	pldm_fd_maybe_cancel_component(fd);
+	pldm_fd_set_state(fd, PLDM_FD_STATE_READY_XFER);
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_cancel_update(struct pldm_fd *fd,
+				 const struct pldm_header_info *hdr,
+				 const struct pldm_msg *req LIBPLDM_CC_UNUSED,
+				 size_t req_payload_len, struct pldm_msg *resp,
+				 size_t *resp_payload_len)
+{
+	int rc;
+
+	/* No request data */
+	if (req_payload_len != PLDM_CANCEL_UPDATE_REQ_BYTES) {
+		return pldm_fd_reply_cc(PLDM_ERROR_INVALID_LENGTH, hdr, resp,
+					resp_payload_len);
+	}
+
+	if (fd->state == PLDM_FD_STATE_IDLE) {
+		return pldm_fd_reply_cc(PLDM_FWUP_NOT_IN_UPDATE_MODE, hdr, resp,
+					resp_payload_len);
+	}
+
+	/* Assume non_functioning_component_indication = False, in future
+	 * could add a platform callback */
+	const struct pldm_cancel_update_resp resp_data = {
+		.non_functioning_component_indication = 0,
+		.non_functioning_component_bitmap = 0,
+	};
+	rc = encode_cancel_update_resp(hdr->instance, &resp_data, resp,
+				       resp_payload_len);
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	pldm_fd_maybe_cancel_component(fd);
+	pldm_fd_set_idle(fd, PLDM_FD_CANCEL_UPDATE);
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_activate_firmware(struct pldm_fd *fd,
+				     const struct pldm_header_info *hdr,
+				     const struct pldm_msg *req,
+				     size_t req_payload_len,
+				     struct pldm_msg *resp,
+				     size_t *resp_payload_len)
+{
+	uint16_t estimated_time;
+	uint8_t ccode;
+	int rc;
+	bool self_contained;
+
+	rc = decode_activate_firmware_req(req, req_payload_len,
+					  &self_contained);
+	if (rc) {
+		return pldm_fd_reply_errno(rc, hdr, resp, resp_payload_len);
+	}
+
+	if (fd->state != PLDM_FD_STATE_READY_XFER) {
+		return pldm_fd_reply_cc(PLDM_FWUP_INVALID_STATE_FOR_COMMAND,
+					hdr, resp, resp_payload_len);
+	}
+
+	estimated_time = 0;
+	ccode = fd->ops->activate(fd->ops_ctx, self_contained, &estimated_time);
+
+	if (ccode == PLDM_SUCCESS ||
+	    ccode == PLDM_FWUP_ACTIVATION_NOT_REQUIRED) {
+		/* Transition through states so that the prev_state is correct */
+		pldm_fd_set_state(fd, PLDM_FD_STATE_ACTIVATE);
+		pldm_fd_set_idle(fd, PLDM_FD_ACTIVATE_FW);
+	}
+
+	if (ccode == PLDM_SUCCESS) {
+		const struct pldm_activate_firmware_resp resp_data = {
+			.estimated_time_activation = estimated_time,
+		};
+		rc = encode_activate_firmware_resp(hdr->instance, &resp_data,
+						   resp, resp_payload_len);
+		if (rc) {
+			return pldm_fd_reply_errno(rc, hdr, resp,
+						   resp_payload_len);
+		}
+	} else {
+		return pldm_fd_reply_cc(ccode, hdr, resp, resp_payload_len);
+	}
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static uint32_t pldm_fd_fwdata_size(struct pldm_fd *fd)
+{
+	uint32_t size;
+
+	if (fd->state != PLDM_FD_STATE_DOWNLOAD) {
+		assert(false);
+		return 0;
+	}
+
+	if (fd->specific.download.offset > fd->update_comp.comp_image_size) {
+		assert(false);
+		return 0;
+	}
+	size = fd->update_comp.comp_image_size - fd->specific.download.offset;
+
+	if (size > fd->max_transfer) {
+		size = fd->max_transfer;
+	}
+	return size;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_handle_fwdata_resp(struct pldm_fd *fd,
+				      const struct pldm_msg *resp,
+				      size_t resp_payload_len)
+{
+	struct pldm_fd_download *dl;
+	uint32_t fwdata_size;
+	uint8_t res;
+
+	if (fd->state != PLDM_FD_STATE_DOWNLOAD) {
+		return -EPROTO;
+	}
+
+	if (fd->req.state != PLDM_FD_REQ_SENT) {
+		/* Not waiting for a response, ignore it */
+		return -EPROTO;
+	}
+
+	dl = &fd->specific.download;
+	if (fd->req.complete) {
+		/* Received data after completion */
+		return -EPROTO;
+	}
+
+	switch (resp->payload[0]) {
+	case PLDM_SUCCESS:
+		break;
+	case PLDM_FWUP_RETRY_REQUEST_FW_DATA:
+		/* Just return, let the retry timer send another request later */
+		return 0;
+	default:
+		/* Send a TransferComplete failure */
+		fd->req.state = PLDM_FD_REQ_READY;
+		fd->req.complete = true;
+		fd->req.result = PLDM_FWUP_FD_ABORTED_TRANSFER;
+		return 0;
+	}
+
+	/* Handle the received data */
+
+	fwdata_size = pldm_fd_fwdata_size(fd);
+	if (resp_payload_len != fwdata_size + 1) {
+		/* Data is incorrect size. Could indicate MCTP corruption, drop it
+		 * and let retry timer handle it */
+		return -EOVERFLOW;
+	}
+
+	/* Check pldm_fd_fwdata_size calculation, should not fail */
+	if (dl->offset + fwdata_size < dl->offset ||
+	    dl->offset + fwdata_size > fd->update_comp.comp_image_size) {
+		assert(false);
+		return -EINVAL;
+	}
+
+	/* Provide the data chunk to the device */
+	res = fd->ops->firmware_data(fd->ops_ctx, dl->offset, &resp->payload[1],
+				     fwdata_size, &fd->update_comp);
+
+	fd->req.state = PLDM_FD_REQ_READY;
+	if (res == PLDM_FWUP_TRANSFER_SUCCESS) {
+		/* Move to next offset */
+		dl->offset += fwdata_size;
+		if (dl->offset == fd->update_comp.comp_image_size) {
+			/* Mark as complete, next progress() call will send the TransferComplete request */
+			fd->req.complete = true;
+			fd->req.result = PLDM_FWUP_TRANSFER_SUCCESS;
+		}
+	} else {
+		/* Pass the callback error as the TransferResult */
+		fd->req.complete = true;
+		fd->req.result = res;
+	}
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_handle_transfer_complete_resp(
+	struct pldm_fd *fd, const struct pldm_msg *resp LIBPLDM_CC_UNUSED,
+	size_t resp_payload_len LIBPLDM_CC_UNUSED)
+{
+	if (fd->state != PLDM_FD_STATE_DOWNLOAD) {
+		return -EPROTO;
+	}
+
+	if (fd->req.state != PLDM_FD_REQ_SENT) {
+		/* Not waiting for a response, ignore it */
+		return -EPROTO;
+	}
+
+	if (!fd->req.complete) {
+		/* Were waiting for RequestFirmwareData instead, ignore it */
+		return -EPROTO;
+	}
+
+	/* Disregard the response completion code */
+
+	/* Next state depends whether the transfer succeeded */
+	if (fd->req.result == PLDM_FWUP_TRANSFER_SUCCESS) {
+		/* Switch to Verify */
+		memset(&fd->specific, 0x0, sizeof(fd->specific));
+		fd->specific.verify.progress_percent =
+			PROGRESS_PERCENT_NOT_SUPPORTED;
+		fd->req.state = PLDM_FD_REQ_READY;
+		fd->req.complete = false;
+		pldm_fd_set_state(fd, PLDM_FD_STATE_VERIFY);
+	} else {
+		/* Wait for UA to cancel */
+		fd->req.state = PLDM_FD_REQ_FAILED;
+	}
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_handle_verify_complete_resp(
+	struct pldm_fd *fd, const struct pldm_msg *resp LIBPLDM_CC_UNUSED,
+	size_t resp_payload_len LIBPLDM_CC_UNUSED)
+{
+	if (fd->state != PLDM_FD_STATE_VERIFY) {
+		return -EPROTO;
+	}
+
+	if (fd->req.state != PLDM_FD_REQ_SENT) {
+		/* Not waiting for a response, ignore it */
+		return -EPROTO;
+	}
+
+	assert(fd->req.complete);
+
+	/* Disregard the response completion code */
+
+	/* Next state depends whether the verify succeeded */
+	if (fd->req.result == PLDM_FWUP_VERIFY_SUCCESS) {
+		/* Switch to Apply */
+		memset(&fd->specific, 0x0, sizeof(fd->specific));
+		fd->specific.apply.progress_percent =
+			PROGRESS_PERCENT_NOT_SUPPORTED;
+		fd->req.state = PLDM_FD_REQ_READY;
+		fd->req.complete = false;
+		pldm_fd_set_state(fd, PLDM_FD_STATE_APPLY);
+	} else {
+		/* Wait for UA to cancel */
+		fd->req.state = PLDM_FD_REQ_FAILED;
+	}
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_handle_apply_complete_resp(
+	struct pldm_fd *fd, const struct pldm_msg *resp LIBPLDM_CC_UNUSED,
+	size_t resp_payload_len LIBPLDM_CC_UNUSED)
+{
+	if (fd->state != PLDM_FD_STATE_APPLY) {
+		return -EPROTO;
+	}
+
+	if (fd->req.state != PLDM_FD_REQ_SENT) {
+		/* Not waiting for a response, ignore it */
+		return -EPROTO;
+	}
+
+	assert(fd->req.complete);
+
+	/* Disregard the response completion code */
+
+	/* Next state depends whether the apply succeeded */
+	if (fd->req.result == PLDM_FWUP_APPLY_SUCCESS) {
+		/* Switch to ReadyXfer */
+		fd->req.state = PLDM_FD_REQ_UNUSED;
+		pldm_fd_set_state(fd, PLDM_FD_STATE_READY_XFER);
+	} else {
+		/* Wait for UA to cancel */
+		fd->req.state = PLDM_FD_REQ_FAILED;
+	}
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_handle_resp(struct pldm_fd *fd, pldm_tid_t address,
+			       const void *resp_msg, size_t resp_len)
+{
+	size_t resp_payload_len;
+	const struct pldm_msg *resp = resp_msg;
+
+	if (!(fd->ua_address_set && fd->ua_address == address)) {
+		// Either an early response, or a response from a wrong TID */
+		return -EBUSY;
+	}
+
+	/* Must have a ccode */
+	if (resp_len < sizeof(struct pldm_msg_hdr) + 1) {
+		return -EINVAL;
+	}
+	resp_payload_len = resp_len - sizeof(struct pldm_msg_hdr);
+
+	if (fd->req.state != PLDM_FD_REQ_SENT) {
+		// No response was expected
+		return -EPROTO;
+	}
+
+	if (fd->req.instance_id != resp->hdr.instance_id) {
+		// Response wasn't for the expected request
+		return -EPROTO;
+	}
+	if (fd->req.command != resp->hdr.command) {
+		// Response wasn't for the expected request
+		return -EPROTO;
+	}
+
+	fd->update_timestamp_fd_t1 = pldm_fd_now(fd);
+
+	switch (resp->hdr.command) {
+	case PLDM_REQUEST_FIRMWARE_DATA:
+		return pldm_fd_handle_fwdata_resp(fd, resp, resp_payload_len);
+		break;
+	case PLDM_TRANSFER_COMPLETE:
+		return pldm_fd_handle_transfer_complete_resp(fd, resp,
+							     resp_payload_len);
+		break;
+	case PLDM_VERIFY_COMPLETE:
+		return pldm_fd_handle_verify_complete_resp(fd, resp,
+							   resp_payload_len);
+		break;
+	case PLDM_APPLY_COMPLETE:
+		return pldm_fd_handle_apply_complete_resp(fd, resp,
+							  resp_payload_len);
+		break;
+	default:
+		/* Unsolicited response. Already compared to command above */
+		assert(false);
+		return -EINVAL;
+	}
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_progress_download(struct pldm_fd *fd, struct pldm_msg *req,
+				     size_t *req_payload_len)
+{
+	uint8_t instance_id;
+	struct pldm_fd_download *dl;
+	int rc;
+
+	if (!pldm_fd_req_should_send(fd)) {
+		/* Nothing to do */
+		*req_payload_len = 0;
+		return 0;
+	}
+
+	instance_id = pldm_fd_req_next_instance(&fd->req);
+	dl = &fd->specific.download;
+	if (fd->req.complete) {
+		/* Send TransferComplete */
+		rc = encode_transfer_complete_req(instance_id, fd->req.result,
+						  req, req_payload_len);
+	} else {
+		/* Send a new RequestFirmwareData */
+		const struct pldm_request_firmware_data_req req_params = {
+			.offset = dl->offset,
+			.length = pldm_fd_fwdata_size(fd),
+		};
+
+		rc = encode_request_firmware_data_req(instance_id, &req_params,
+						      req, req_payload_len);
+	}
+
+	if (rc) {
+		return rc;
+	}
+
+	/* Wait for response */
+	fd->req.state = PLDM_FD_REQ_SENT;
+	fd->req.instance_id = req->hdr.instance_id;
+	fd->req.command = req->hdr.command;
+	fd->req.sent_time = pldm_fd_now(fd);
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_progress_verify(struct pldm_fd *fd, struct pldm_msg *req,
+				   size_t *req_payload_len)
+{
+	uint8_t instance_id;
+	int rc;
+
+	if (!pldm_fd_req_should_send(fd)) {
+		/* Nothing to do */
+		*req_payload_len = 0;
+		return 0;
+	}
+
+	if (!fd->req.complete) {
+		bool pending = false;
+		uint8_t res;
+		res = fd->ops->verify(fd->ops_ctx, &fd->update_comp, &pending,
+				      &fd->specific.verify.progress_percent);
+		if (pending) {
+			if (res == PLDM_FWUP_VERIFY_SUCCESS) {
+				/* Return without a VerifyComplete request.
+				* Will call verify() again on next call */
+				*req_payload_len = 0;
+				return 0;
+			}
+			/* This is an API infraction by the implementer, return a distinctive failure */
+			res = PLDM_FWUP_VENDOR_VERIFY_RESULT_RANGE_MAX;
+		}
+		fd->req.result = res;
+		fd->req.complete = true;
+	}
+
+	instance_id = pldm_fd_req_next_instance(&fd->req);
+	rc = encode_verify_complete_req(instance_id, fd->req.result, req,
+					req_payload_len);
+	if (rc) {
+		return rc;
+	}
+
+	/* Wait for response */
+	fd->req.state = PLDM_FD_REQ_SENT;
+	fd->req.instance_id = req->hdr.instance_id;
+	fd->req.command = req->hdr.command;
+	fd->req.sent_time = pldm_fd_now(fd);
+
+	return 0;
+}
+
+LIBPLDM_CC_NONNULL
+static int pldm_fd_progress_apply(struct pldm_fd *fd, struct pldm_msg *req,
+				  size_t *req_payload_len)
+{
+	uint8_t instance_id;
+	int rc;
+
+	if (!pldm_fd_req_should_send(fd)) {
+		/* Nothing to do */
+		*req_payload_len = 0;
+		return 0;
+	}
+
+	if (!fd->req.complete) {
+		bool pending = false;
+		uint8_t res;
+		res = fd->ops->apply(fd->ops_ctx, &fd->update_comp, &pending,
+				     &fd->specific.apply.progress_percent);
+		if (pending) {
+			if (res == PLDM_FWUP_APPLY_SUCCESS) {
+				/* Return without a ApplyComplete request.
+				* Will call apply() again on next call */
+				*req_payload_len = 0;
+				return 0;
+			}
+			/* This is an API infraction by the implementer, return a distinctive failure */
+			res = PLDM_FWUP_VENDOR_APPLY_RESULT_RANGE_MAX;
+		}
+		fd->req.result = res;
+		fd->req.complete = true;
+		if (fd->req.result ==
+		    PLDM_FWUP_APPLY_SUCCESS_WITH_ACTIVATION_METHOD) {
+			/* modified activation method isn't currently handled */
+			fd->req.result = PLDM_FWUP_APPLY_SUCCESS;
+		}
+	}
+
+	instance_id = pldm_fd_req_next_instance(&fd->req);
+	const struct pldm_apply_complete_req req_data = {
+		.apply_result = fd->req.result,
+		.comp_activation_methods_modification = { 0 },
+	};
+	rc = encode_apply_complete_req(instance_id, &req_data, req,
+				       req_payload_len);
+	if (rc) {
+		return rc;
+	}
+
+	/* Wait for response */
+	fd->req.state = PLDM_FD_REQ_SENT;
+	fd->req.instance_id = req->hdr.instance_id;
+	fd->req.command = req->hdr.command;
+	fd->req.sent_time = pldm_fd_now(fd);
+
+	return 0;
+}
+
+LIBPLDM_ABI_TESTING
+struct pldm_fd *pldm_fd_new(const struct pldm_fd_ops *ops, void *ops_ctx)
+{
+	struct pldm_fd *fd = malloc(sizeof(*fd));
+	if (fd) {
+		if (pldm_fd_setup(fd, sizeof(*fd), ops, ops_ctx) == 0) {
+			return fd;
+		}
+		free(fd);
+		fd = NULL;
+	}
+	return fd;
+}
+
+LIBPLDM_ABI_TESTING
+int pldm_fd_setup(struct pldm_fd *fd, size_t pldm_fd_size,
+		  const struct pldm_fd_ops *ops, void *ops_ctx)
+{
+	if (fd == NULL || ops == NULL) {
+		return -EINVAL;
+	}
+
+	if (ops->device_identifiers == NULL || ops->components == NULL ||
+	    ops->imageset_versions == NULL || ops->update_component == NULL ||
+	    ops->transfer_size == NULL || ops->firmware_data == NULL ||
+	    ops->verify == NULL || ops->activate == NULL ||
+	    ops->cancel_update_component == NULL || ops->now == NULL) {
+		return -EINVAL;
+	}
+
+	if (pldm_fd_size < sizeof(struct pldm_fd)) {
+		/* Safety check that sufficient storage was provided for *fd,
+		 * in case PLDM_SIZEOF_PLDM_FD is incorrect */
+		return -EINVAL;
+	}
+	memset(fd, 0x0, sizeof(*fd));
+	fd->ops = ops;
+	fd->ops_ctx = ops_ctx;
+	fd->fd_t1_timeout = DEFAULT_FD_T1_TIMEOUT;
+	fd->fd_t2_retry_time = DEFAULT_FD_T2_RETRY_TIME;
+
+	return 0;
+}
+
+LIBPLDM_ABI_TESTING
+int pldm_fd_handle_msg(struct pldm_fd *fd, pldm_tid_t remote_address,
+		       const void *in_msg, size_t in_len, void *out_msg,
+		       size_t *out_len)
+{
+	size_t req_payload_len;
+	size_t resp_payload_len;
+	struct pldm_header_info hdr;
+	const struct pldm_msg *req = in_msg;
+	struct pldm_msg *resp = out_msg;
+	uint8_t rc;
+
+	if (fd == NULL || in_msg == NULL || out_msg == NULL ||
+	    out_len == NULL) {
+		return -EINVAL;
+	}
+
+	if (in_len < sizeof(struct pldm_msg_hdr)) {
+		return -EOVERFLOW;
+	}
+	req_payload_len = in_len - sizeof(struct pldm_msg_hdr);
+
+	rc = unpack_pldm_header(&req->hdr, &hdr);
+	if (rc != PLDM_SUCCESS) {
+		return -EINVAL;
+	}
+
+	if (hdr.pldm_type != PLDM_FWUP) {
+		/* Caller should not have passed non-pldmfw */
+		return -ENOMSG;
+	}
+
+	if (hdr.msg_type == PLDM_RESPONSE) {
+		*out_len = 0;
+		return pldm_fd_handle_resp(fd, remote_address, in_msg, in_len);
+	}
+
+	if (hdr.msg_type != PLDM_REQUEST) {
+		return -EPROTO;
+	}
+
+	/* Space for header plus completion code */
+	if (*out_len < sizeof(struct pldm_msg_hdr) + 1) {
+		return -EOVERFLOW;
+	}
+	resp_payload_len = *out_len - sizeof(struct pldm_msg_hdr);
+
+	/* Check address */
+	switch (hdr.command) {
+	/* Information or cancel commands are always allowed */
+	case PLDM_QUERY_DEVICE_IDENTIFIERS:
+	case PLDM_GET_FIRMWARE_PARAMETERS:
+	case PLDM_GET_STATUS:
+	case PLDM_CANCEL_UPDATE:
+	case PLDM_QUERY_DOWNSTREAM_DEVICES:
+	case PLDM_QUERY_DOWNSTREAM_IDENTIFIERS:
+	case PLDM_QUERY_DOWNSTREAM_FIRMWARE_PARAMETERS:
+	/* Request Update handler will set address */
+	case PLDM_REQUEST_UPDATE:
+		break;
+	default:
+		/* Requests must come from the same address that requested the update */
+		if (!fd->ua_address_set || remote_address != fd->ua_address) {
+			return pldm_fd_reply_cc(PLDM_ERROR_NOT_READY, &hdr,
+						resp, &resp_payload_len);
+		}
+	}
+
+	/* Update timeout */
+	switch (hdr.command) {
+	case PLDM_REQUEST_UPDATE:
+	case PLDM_PASS_COMPONENT_TABLE:
+	case PLDM_UPDATE_COMPONENT:
+	case PLDM_CANCEL_UPDATE:
+		fd->update_timestamp_fd_t1 = pldm_fd_now(fd);
+		break;
+	default:
+		break;
+	}
+
+	/* Dispatch command */
+	switch (hdr.command) {
+	case PLDM_QUERY_DEVICE_IDENTIFIERS:
+		rc = pldm_fd_qdi(fd, &hdr, req, req_payload_len, resp,
+				 &resp_payload_len);
+		break;
+	case PLDM_GET_FIRMWARE_PARAMETERS:
+		rc = pldm_fd_fw_param(fd, &hdr, req, req_payload_len, resp,
+				      &resp_payload_len);
+		break;
+	case PLDM_REQUEST_UPDATE:
+		rc = pldm_fd_request_update(fd, &hdr, req, req_payload_len,
+					    resp, &resp_payload_len,
+					    remote_address);
+		break;
+	case PLDM_PASS_COMPONENT_TABLE:
+		rc = pldm_fd_pass_comp(fd, &hdr, req, req_payload_len, resp,
+				       &resp_payload_len);
+		break;
+	case PLDM_UPDATE_COMPONENT:
+		rc = pldm_fd_update_comp(fd, &hdr, req, req_payload_len, resp,
+					 &resp_payload_len);
+		break;
+	case PLDM_GET_STATUS:
+		rc = pldm_fd_get_status(fd, &hdr, req, req_payload_len, resp,
+					&resp_payload_len);
+		break;
+	case PLDM_CANCEL_UPDATE_COMPONENT:
+		rc = pldm_fd_cancel_update_comp(fd, &hdr, req, req_payload_len,
+						resp, &resp_payload_len);
+		break;
+	case PLDM_CANCEL_UPDATE:
+		rc = pldm_fd_cancel_update(fd, &hdr, req, req_payload_len, resp,
+					   &resp_payload_len);
+		break;
+	case PLDM_ACTIVATE_FIRMWARE:
+		rc = pldm_fd_activate_firmware(fd, &hdr, req, req_payload_len,
+					       resp, &resp_payload_len);
+		break;
+	default:
+		rc = pldm_fd_reply_cc(PLDM_ERROR_UNSUPPORTED_PLDM_CMD, &hdr,
+				      resp, &resp_payload_len);
+	}
+
+	if (rc == 0) {
+		*out_len = resp_payload_len + sizeof(struct pldm_msg_hdr);
+	}
+
+	return rc;
+}
+
+LIBPLDM_ABI_TESTING
+int pldm_fd_progress(struct pldm_fd *fd, void *out_msg, size_t *out_len,
+		     pldm_tid_t *address)
+{
+	size_t req_payload_len;
+	struct pldm_msg *req = out_msg;
+	int rc = -EINVAL;
+
+	if (fd == NULL || out_msg == NULL || out_len == NULL) {
+		return -EINVAL;
+	}
+
+	/* Space for header */
+	if (*out_len < sizeof(struct pldm_msg_hdr)) {
+		return -EOVERFLOW;
+	}
+	req_payload_len = *out_len - sizeof(struct pldm_msg_hdr);
+	*out_len = 0;
+
+	switch (fd->state) {
+	case PLDM_FD_STATE_DOWNLOAD:
+		rc = pldm_fd_progress_download(fd, req, &req_payload_len);
+		break;
+	case PLDM_FD_STATE_VERIFY:
+		rc = pldm_fd_progress_verify(fd, req, &req_payload_len);
+		break;
+	case PLDM_FD_STATE_APPLY:
+		rc = pldm_fd_progress_apply(fd, req, &req_payload_len);
+		break;
+	case PLDM_FD_STATE_IDLE:
+		req_payload_len = 0;
+		break;
+	default:
+		req_payload_len = 0;
+		// Other Update Mode states have a timeout
+		if ((pldm_fd_now(fd) - fd->update_timestamp_fd_t1) >
+		    fd->fd_t1_timeout) {
+			pldm_fd_maybe_cancel_component(fd);
+			pldm_fd_idle_timeout(fd);
+		}
+		break;
+	}
+
+	if (rc == 0 && fd->ua_address_set && req_payload_len > 0) {
+		*out_len = req_payload_len + sizeof(struct pldm_msg_hdr);
+		*address = fd->ua_address;
+	}
+
+	return rc;
+}
+
+LIBPLDM_ABI_TESTING
+int pldm_fd_set_update_idle_timeout(struct pldm_fd *fd, uint32_t time)
+{
+	if (fd == NULL) {
+		return -EINVAL;
+	}
+
+	fd->fd_t1_timeout = time;
+	return 0;
+}
+
+LIBPLDM_ABI_TESTING
+int pldm_fd_set_request_retry_time(struct pldm_fd *fd, uint32_t time)
+{
+	if (fd == NULL) {
+		return -EINVAL;
+	}
+
+	fd->fd_t2_retry_time = time;
+	return 0;
+}
diff --git a/src/firmware_device/meson.build b/src/firmware_device/meson.build
new file mode 100644
index 0000000..3e21bda
--- /dev/null
+++ b/src/firmware_device/meson.build
@@ -0,0 +1 @@
+libpldm_sources += files('fd.c')
diff --git a/src/meson.build b/src/meson.build
index a8e54e7..3c8afc1 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -15,6 +15,8 @@
     subdir('oem/meta')
 endif
 
+subdir('firmware_device')
+
 libpldm_link_args = []
 foreach alias : libpldm_deprecated_aliases
     libpldm_link_args += '-Wl,--defsym=@0@=@1@'.format(alias[0], alias[1])