fpsensor: introduce fp_info_v3

Revert the previous format of the FP_INFO V2 host command.
Introduce a new V3 version to support the updated format.

This change is needed for backward compatibility, to support firmwares
that use an old version of the v2 format.

BUG=b:498133007
TEST="ectool.py fpinfo" for both version && ectool --name=cros_fp fpinfo

Change-Id: I9014cf51c5da5800e5b064590a3f7799ad833ce1
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/ec/+/7716618
Tested-by: Dawid Niedźwiecki <dawidn@google.com>
Reviewed-by: Patryk Duda <patrykd@google.com>
Commit-Queue: Firas Sammoura <fsammoura@google.com>
diff --git a/common/fpsensor/fpsensor.cc b/common/fpsensor/fpsensor.cc
index 27d33b0..0f8ce80 100644
--- a/common/fpsensor/fpsensor.cc
+++ b/common/fpsensor/fpsensor.cc
@@ -442,11 +442,11 @@
 
 static enum ec_status fp_command_info(struct host_cmd_handler_args *args)
 {
-	struct ec_response_fp_info_v2 *r =
-		static_cast<ec_response_fp_info_v2 *>(args->response);
-	const size_t response_size =
-		sizeof(struct ec_response_fp_info_v2) +
-		FP_MAX_CAPTURE_TYPES * sizeof(struct fp_image_frame_params);
+	struct ec_response_fp_info_v3 *r =
+		static_cast<ec_response_fp_info_v3 *>(args->response);
+	size_t response_size =
+		sizeof(struct ec_response_fp_info_v3) +
+		FP_MAX_CAPTURE_TYPES * sizeof(struct fp_image_frame_params_v2);
 
 	if (response_size > args->response_max) {
 		return EC_RES_OVERFLOW;
@@ -470,11 +470,44 @@
 	r->template_info.template_dirty = global_context.templ_dirty;
 	r->template_info.template_version = FP_TEMPLATE_FORMAT_VERSION;
 
+	if (args->version == 2) {
+		struct ec_response_fp_info_v2 *r_v2 =
+			static_cast<ec_response_fp_info_v2 *>(args->response);
+		/* Convert to v2 format. The formats differ only in the frame
+		 * array, which is located at the end of the structures
+		 *
+		 * SAFETY: 'r->image_frame_params' and r_v2->image_frame_params
+		 * overlap inexactly, but copying data is safe because we copy
+		 * data forward (from the first field of the structure to the
+		 * last).
+		 */
+		for (int i = 0; i < FP_MAX_CAPTURE_TYPES; i++) {
+			r_v2->image_frame_params[i].frame_size =
+				r->image_frame_params[i].frame_size;
+			r_v2->image_frame_params[i].pixel_format =
+				r->image_frame_params[i].pixel_format;
+			r_v2->image_frame_params[i].width =
+				r->image_frame_params[i].width;
+			r_v2->image_frame_params[i].height =
+				r->image_frame_params[i].height;
+			r_v2->image_frame_params[i].bpp =
+				r->image_frame_params[i].bpp;
+			r_v2->image_frame_params[i].fp_capture_type =
+				r->image_frame_params[i].fp_capture_type;
+			r_v2->image_frame_params[i].reserved =
+				r->image_frame_params[i].reserved;
+		}
+		response_size = sizeof(struct ec_response_fp_info_v2) +
+				FP_MAX_CAPTURE_TYPES *
+					sizeof(struct fp_image_frame_params);
+	}
+
 	args->response_size = response_size;
 
 	return EC_RES_SUCCESS;
 }
-DECLARE_HOST_COMMAND(EC_CMD_FP_INFO, fp_command_info, EC_VER_MASK(2));
+DECLARE_HOST_COMMAND(EC_CMD_FP_INFO, fp_command_info,
+		     EC_VER_MASK(2) | EC_VER_MASK(3));
 
 BUILD_ASSERT(FP_CONTEXT_NONCE_BYTES == 12);
 
diff --git a/common/fpsensor/fpsensor_debug.cc b/common/fpsensor/fpsensor_debug.cc
index 19029cf..305a561 100644
--- a/common/fpsensor/fpsensor_debug.cc
+++ b/common/fpsensor/fpsensor_debug.cc
@@ -66,7 +66,7 @@
  */
 test_export_static enum ec_error_list
 upload_pgm_image(uint8_t *frame,
-		 const struct fp_image_frame_params &image_frame_params)
+		 const struct fp_image_frame_params_v2 &image_frame_params)
 {
 	uint8_t *ptr = frame;
 	uint8_t bytes_per_pixel = DIV_ROUND_UP(image_frame_params.bpp, 8);
@@ -129,15 +129,15 @@
 }
 
 test_export_static int
-get_image_frame_params(struct fp_image_frame_params &image_frame_params,
+get_image_frame_params(struct fp_image_frame_params_v2 &image_frame_params,
 		       const enum fp_capture_type capture_type)
 {
 #if defined(HAVE_FP_PRIVATE_DRIVER) || defined(BOARD_HOST)
 	size_t fp_sensor_get_info_v2_size =
-		sizeof(struct ec_response_fp_info_v2) +
-		sizeof(struct fp_image_frame_params) * FP_MAX_CAPTURE_TYPES;
+		sizeof(struct ec_response_fp_info_v3) +
+		sizeof(struct fp_image_frame_params_v2) * FP_MAX_CAPTURE_TYPES;
 	std::vector<uint8_t> buffer(fp_sensor_get_info_v2_size);
-	auto *info = reinterpret_cast<ec_response_fp_info_v2 *>(buffer.data());
+	auto *info = reinterpret_cast<ec_response_fp_info_v3 *>(buffer.data());
 
 	if (fp_sensor_get_info(info, buffer.size()) < 0) {
 		return EC_ERROR_UNKNOWN;
@@ -177,7 +177,7 @@
 
 	const enum ec_error_list rc = fp_console_action(mode);
 	if (rc == EC_SUCCESS) {
-		struct fp_image_frame_params image_frame_params{};
+		struct fp_image_frame_params_v2 image_frame_params{};
 		int ret = get_image_frame_params(
 			image_frame_params,
 			global_context.current_capture_type);
@@ -243,7 +243,7 @@
 	if (system_is_locked())
 		return EC_ERROR_ACCESS_DENIED;
 
-	struct fp_image_frame_params image_frame_params{};
+	struct fp_image_frame_params_v2 image_frame_params{};
 	int ret = get_image_frame_params(image_frame_params,
 					 global_context.current_capture_type);
 	if (ret != EC_RES_SUCCESS) {
@@ -300,10 +300,10 @@
 {
 #if defined(HAVE_FP_PRIVATE_DRIVER) || defined(BOARD_HOST)
 	size_t fp_sensor_get_info_v2_size =
-		sizeof(struct ec_response_fp_info_v2) +
-		sizeof(struct fp_image_frame_params) * FP_MAX_CAPTURE_TYPES;
+		sizeof(struct ec_response_fp_info_v3) +
+		sizeof(struct fp_image_frame_params_v2) * FP_MAX_CAPTURE_TYPES;
 	std::vector<uint8_t> buffer(fp_sensor_get_info_v2_size);
-	auto *info = reinterpret_cast<ec_response_fp_info_v2 *>(buffer.data());
+	auto *info = reinterpret_cast<ec_response_fp_info_v3 *>(buffer.data());
 
 	if (fp_sensor_get_info(info, buffer.size()) < 0) {
 		ccprintf("Failed to get fp_info_v2\n");
diff --git a/common/fpsensor/fpsensor_frame_size.cc b/common/fpsensor/fpsensor_frame_size.cc
index e6cb59d..c695872 100644
--- a/common/fpsensor/fpsensor_frame_size.cc
+++ b/common/fpsensor/fpsensor_frame_size.cc
@@ -14,11 +14,11 @@
 void FpFrameSizeCache::populate_cache(uint32_t max_frame_size_bytes)
 {
 	const size_t buffer_size =
-		sizeof(struct ec_response_fp_info_v2) +
-		sizeof(struct fp_image_frame_params) * frame_sizes_.size();
+		sizeof(struct ec_response_fp_info_v3) +
+		sizeof(struct fp_image_frame_params_v2) * frame_sizes_.size();
 
 	std::vector<uint8_t> buffer(buffer_size);
-	auto *info = reinterpret_cast<struct ec_response_fp_info_v2 *>(
+	auto *info = reinterpret_cast<struct ec_response_fp_info_v3 *>(
 		buffer.data());
 
 	if (fp_sensor_get_info(info, buffer.size()) < 0) {
diff --git a/common/mock/fpsensor_mock.c b/common/mock/fpsensor_mock.c
index 8f80514..bbac025 100644
--- a/common/mock/fpsensor_mock.c
+++ b/common/mock/fpsensor_mock.c
@@ -32,7 +32,7 @@
 	return mock_ctrl_fp_sensor.fp_sensor_deinit_return;
 }
 
-int fp_sensor_get_info(struct ec_response_fp_info_v2 *resp, size_t resp_size)
+int fp_sensor_get_info(struct ec_response_fp_info_v3 *resp, size_t resp_size)
 {
 	memset(resp, 0, sizeof(*resp));
 
diff --git a/driver/fingerprint/egis/egis_private.c b/driver/fingerprint/egis/egis_private.c
index 341dc07..0688727 100644
--- a/driver/fingerprint/egis/egis_private.c
+++ b/driver/fingerprint/egis/egis_private.c
@@ -44,7 +44,7 @@
 	.pixel_format = V4L2_PIX_FMT_GREY, .width = FP_SENSOR_RES_X_EGIS, \
 	.height = FP_SENSOR_RES_Y_EGIS, .bpp = FP_SENSOR_TEST_BPP_EGIS
 
-static const struct fp_image_frame_params egis_image_frame_params[] = {
+static const struct fp_image_frame_params_v2 egis_image_frame_params[] = {
 	[EGIS_CAPTURE_NORMAL_FORMAT] =
 	{
 		EGIS_DEFAULT_IMAGE_PARAMS,
@@ -163,9 +163,9 @@
 	return egis_sensor_deinit();
 }
 
-int fp_sensor_get_info(struct ec_response_fp_info_v2 *resp, size_t resp_size)
+int fp_sensor_get_info(struct ec_response_fp_info_v3 *resp, size_t resp_size)
 {
-	if (sizeof(struct ec_response_fp_info_v2) +
+	if (sizeof(struct ec_response_fp_info_v3) +
 		    sizeof(egis_image_frame_params) >
 	    resp_size) {
 		return EC_RES_OVERFLOW;
diff --git a/driver/fingerprint/elan/elan_private.c b/driver/fingerprint/elan/elan_private.c
index e260645..05a0db6 100644
--- a/driver/fingerprint/elan/elan_private.c
+++ b/driver/fingerprint/elan/elan_private.c
@@ -41,7 +41,7 @@
 	.pixel_format = V4L2_PIX_FMT_GREY, .width = FP_SENSOR_RES_X_ELAN, \
 	.height = FP_SENSOR_RES_Y_ELAN, .bpp = FP_SENSOR_RES_BPP_ELAN
 
-static const struct fp_image_frame_params elan_image_frame_params[] = {
+static const struct fp_image_frame_params_v2 elan_image_frame_params[] = {
 	[ELAN_CAPTURE_VENDOR_FORMAT] =
 	{
 		ELAN_DEFAULT_IMAGE_PARAMS,
@@ -177,18 +177,18 @@
 }
 
 /**
- * Fill the 'ec_response_fp_info_v2' buffer with the sensor information
+ * Fill the 'ec_response_fp_info_v3' buffer with the sensor information
  *
  * @param[out] resp retrieve the version, sensor and template information
  *
  * @return EC_SUCCESS on success.
  * @return EC_RES_ERROR on error.
  */
-int fp_sensor_get_info(struct ec_response_fp_info_v2 *resp, size_t resp_size)
+int fp_sensor_get_info(struct ec_response_fp_info_v3 *resp, size_t resp_size)
 {
 	CPRINTF("========%s=======\n", __func__);
 
-	if (sizeof(struct ec_response_fp_info_v2) +
+	if (sizeof(struct ec_response_fp_info_v3) +
 		    sizeof(elan_image_frame_params) >
 	    resp_size) {
 		return EC_RES_OVERFLOW;
diff --git a/driver/fingerprint/fpc/bep/fpc_private.c b/driver/fingerprint/fpc/bep/fpc_private.c
index f3605ed..1926e85 100644
--- a/driver/fingerprint/fpc/bep/fpc_private.c
+++ b/driver/fingerprint/fpc/bep/fpc_private.c
@@ -62,7 +62,7 @@
 	.pixel_format = V4L2_PIX_FMT_GREY, .width = FP_SENSOR_RES_X_FPC, \
 	.height = FP_SENSOR_RES_Y_FPC, .bpp = FP_SENSOR_RES_BPP_FPC
 
-static const struct fp_image_frame_params fpc1025_image_frame_params[] = {
+static const struct fp_image_frame_params_v2 fpc1025_image_frame_params[] = {
 	[FPC_CAPTURE_VENDOR_FORMAT] =
 	{
 		FPC1025_DEFAULT_RAW_IMAGE_PARAMS,
@@ -292,9 +292,9 @@
 	return rc;
 }
 
-int fp_sensor_get_info(struct ec_response_fp_info_v2 *resp, size_t resp_size)
+int fp_sensor_get_info(struct ec_response_fp_info_v3 *resp, size_t resp_size)
 {
-	if (sizeof(struct ec_response_fp_info_v2) +
+	if (sizeof(struct ec_response_fp_info_v3) +
 		    sizeof(fpc1025_image_frame_params) >
 	    resp_size) {
 		return EC_RES_OVERFLOW;
diff --git a/driver/fingerprint/fpc/libfp/fpc_private.c b/driver/fingerprint/fpc/libfp/fpc_private.c
index d0d6e3b..130784b 100644
--- a/driver/fingerprint/fpc/libfp/fpc_private.c
+++ b/driver/fingerprint/fpc/libfp/fpc_private.c
@@ -72,7 +72,7 @@
 	.pixel_format = V4L2_PIX_FMT_GREY, .width = FP_SENSOR_RES_X_FPC, \
 	.height = FP_SENSOR_RES_Y_FPC, .bpp = FP_SENSOR_RES_BPP_FPC
 
-static const struct fp_image_frame_params fpc1145_image_frame_params[] = {
+static const struct fp_image_frame_params_v2 fpc1145_image_frame_params[] = {
 	[FPC_CAPTURE_VENDOR_FORMAT] =
 	{
 		FPC1145_DEFAULT_RAW_IMAGE_PARAMS,
@@ -345,9 +345,9 @@
 	return EC_SUCCESS;
 }
 
-int fp_sensor_get_info(struct ec_response_fp_info_v2 *resp, size_t resp_size)
+int fp_sensor_get_info(struct ec_response_fp_info_v3 *resp, size_t resp_size)
 {
-	if (sizeof(struct ec_response_fp_info_v2) +
+	if (sizeof(struct ec_response_fp_info_v3) +
 		    sizeof(fpc1145_image_frame_params) >
 	    resp_size) {
 		return EC_RES_OVERFLOW;
diff --git a/include/ec_cmd_api.h b/include/ec_cmd_api.h
index 00b30f2..b374661 100644
--- a/include/ec_cmd_api.h
+++ b/include/ec_cmd_api.h
@@ -144,6 +144,13 @@
 	return CROS_EC_COMMAND(h, EC_CMD_FP_INFO, 2, NULL, 0, r, resp_size);
 }
 
+static inline int ec_cmd_fp_info_v3(CROS_EC_COMMAND_INFO *h,
+				    struct ec_response_fp_info_v3 *r,
+				    size_t resp_size)
+{
+	return CROS_EC_COMMAND(h, EC_CMD_FP_INFO, 3, NULL, 0, r, resp_size);
+}
+
 /*
  * Section 2: EC interface functions that can be generated with the help
  * of template macros.
diff --git a/include/ec_commands.h b/include/ec_commands.h
index 0273820..c8cab3f 100644
--- a/include/ec_commands.h
+++ b/include/ec_commands.h
@@ -8612,7 +8612,6 @@
 struct fp_image_frame_params {
 	/* Image frame characteristics */
 	uint32_t frame_size;
-	uint32_t image_data_offset_bytes; /**< Byte offset of image buffer */
 	uint32_t pixel_format; /* using V4L2_PIX_FMT_ */
 	uint16_t width;
 	uint16_t height;
@@ -8621,7 +8620,7 @@
 	uint8_t fp_capture_type;
 	uint8_t reserved; /**< padding for alignment */
 } __ec_align4;
-BUILD_ASSERT(sizeof(struct fp_image_frame_params) == 20);
+BUILD_ASSERT(sizeof(struct fp_image_frame_params) == 16);
 
 struct ec_response_fp_info_v2 {
 	/* Sensor identification */
@@ -8634,6 +8633,31 @@
 } __ec_align4;
 BUILD_ASSERT(sizeof(struct ec_response_fp_info_v2) == 36);
 
+struct fp_image_frame_params_v2 {
+	/* Image frame characteristics */
+	uint32_t frame_size;
+	uint32_t image_data_offset_bytes; /**< Byte offset of image buffer */
+	uint32_t pixel_format; /* using V4L2_PIX_FMT_ */
+	uint16_t width;
+	uint16_t height;
+	uint16_t bpp;
+	/** Type of image capture from enum fp_capture_type. */
+	uint8_t fp_capture_type;
+	uint8_t reserved; /**< padding for alignment */
+} __ec_align4;
+BUILD_ASSERT(sizeof(struct fp_image_frame_params_v2) == 20);
+
+struct ec_response_fp_info_v3 {
+	/* Sensor identification */
+	struct fp_sensor_info sensor_info;
+	/* Template/finger current information */
+	struct fp_template_info template_info;
+	/* fingerprint image frame parameters */
+	struct fp_image_frame_params_v2
+		image_frame_params[FLEXIBLE_ARRAY_MEMBER_SIZE];
+} __ec_align4;
+BUILD_ASSERT(sizeof(struct ec_response_fp_info_v3) == 36);
+
 /* Get the last captured finger frame or a template content */
 #define EC_CMD_FP_FRAME 0x0404
 
diff --git a/include/fpsensor/fpsensor.h b/include/fpsensor/fpsensor.h
index 70e2190..7428f75 100644
--- a/include/fpsensor/fpsensor.h
+++ b/include/fpsensor/fpsensor.h
@@ -84,7 +84,7 @@
 int fp_sensor_deinit(void);
 
 /**
- * Fill the @p ec_response_fp_info_v2 buffer with the sensor information
+ * Fill the @p ec_response_fp_info_v3 buffer with the sensor information
  * as required by the EC_CMD_FP_INFO host command.
  *
  * Fills both the static information and information read from the sensor at
@@ -96,7 +96,7 @@
  * @return EC_SUCCESS on success
  * @return EC_RES_ERROR on error
  */
-int fp_sensor_get_info(struct ec_response_fp_info_v2 *resp, size_t resp_size);
+int fp_sensor_get_info(struct ec_response_fp_info_v3 *resp, size_t resp_size);
 
 /**
  * Put the sensor in its lowest power state.
diff --git a/util/ectool_usb/ec_commands.py b/util/ectool_usb/ec_commands.py
index 9886ccf..a55213c 100644
--- a/util/ectool_usb/ec_commands.py
+++ b/util/ectool_usb/ec_commands.py
@@ -351,6 +351,63 @@
         )
 
 
+class FpInfoCmd3(ECCommand):
+    """Gets FP information (version 3)."""
+
+    def __init__(self):
+        # 4 bytes of vendor id
+        # 4 bytes of product id
+        # 4 bytes of model id
+        # 4 bytes of version
+        # 2 bytes of num of capture types
+        # 2 bytes of errors
+        # 4 bytes of template size
+        # 2 bytes of template max
+        # 2 bytes of template valid
+        # 4 bytes of template dirty
+        # 4 bytes of template version
+        # unknown number of image_frame
+        response_msg = [
+            ("vendor_id", "I"),
+            ("product_id", "I"),
+            ("model_id", "I"),
+            ("version", "I"),
+            ("num_capture_types", "H"),
+            ("errors", "H"),
+            ("template_size", "I"),
+            ("template_max", "H"),
+            ("template_valid", "H"),
+            ("template_dirty", "I"),
+            ("template_version", "I"),
+            ("image_frame_params", ""),
+        ]
+        # fp_image_frame_params
+        # 4 bytes of frame_size;
+        # 4 bytes of image offset;
+        # 4 bytes of pixel_format;
+        # 2 bytes of width;
+        # 2 bytes of height;
+        # 2 bytes of bpp;
+        # 1 byte of fp_capture_type;
+        # 1 byte of reserved;
+        image_frame = [
+            ("frame_size", "I"),
+            ("image_data_offset_bytes", "I"),
+            ("pixel_format", "I"),
+            ("width", "H"),
+            ("height", "H"),
+            ("bpp", "H"),
+            ("fp_capture_type", "B"),
+            ("reserved", "B"),
+        ]
+        super().__init__(
+            ECCommandsIds.FP_INFO,
+            3,
+            response_msg=response_msg,
+            variable_payload_msg=image_frame,
+        )
+
+
 class FpFrameCmd0(ECCommand):
     """Gets FP frame (version 0)."""
 
@@ -561,7 +618,7 @@
     ECCommandsIds.REBOOT_EC: {0: RebootECCmd0},
     ECCommandsIds.ENTER_BOOTLOADER: {0: EnterBootloaderCmd0},
     ECCommandsIds.FP_MODE: {0: FpModeCmd0},
-    ECCommandsIds.FP_INFO: {1: FpInfoCmd1, 2: FpInfoCmd2},
+    ECCommandsIds.FP_INFO: {1: FpInfoCmd1, 2: FpInfoCmd2, 3: FpInfoCmd3},
     ECCommandsIds.FP_FRAME: {0: FpFrameCmd0, 1: FpFrameCmd1},
     ECCommandsIds.FP_VENDOR: {0: FpVendorCmd0},
     ECCommandsIds.RWSIG_ACTION: {0: RwSigActionCmd0},
diff --git a/util/ectool_usb/ectool.py b/util/ectool_usb/ectool.py
index c61ba7b..9d4dbc4 100644
--- a/util/ectool_usb/ectool.py
+++ b/util/ectool_usb/ectool.py
@@ -176,7 +176,7 @@
         print("Template valid: " + str(fp_info.response.template_valid))
         print("Template dirty: " + hex(fp_info.response.template_dirty))
         print("Template version: " + hex(fp_info.response.template_version))
-    elif fp_info.cmd_version == 2:
+    elif fp_info.cmd_version in (2, 3):
         print("Vendor ID: " + hex(fp_info.response.vendor_id))
         print("Product ID: " + hex(fp_info.response.product_id))
         print("Model ID: " + hex(fp_info.response.model_id))
@@ -196,6 +196,11 @@
         ):
             print("Image frame params nr: " + str(i))
             print("\tFrame size: " + str(image_frame_params.frame_size))
+            if fp_info.cmd_version == 3:
+                print(
+                    "\tImage offset: "
+                    + str(image_frame_params.image_data_offset_bytes)
+                )
             print("\tPixel format: " + hex(image_frame_params.pixel_format))
             print("\tWidth: " + str(image_frame_params.width))
             print("\tHeight: " + str(image_frame_params.height))
diff --git a/zephyr/shim/src/fpsensor.c b/zephyr/shim/src/fpsensor.c
index 05ec62a..a79237c 100644
--- a/zephyr/shim/src/fpsensor.c
+++ b/zephyr/shim/src/fpsensor.c
@@ -110,14 +110,14 @@
 	return 0;
 }
 
-int fp_sensor_get_info(struct ec_response_fp_info_v2 *resp, size_t resp_size)
+int fp_sensor_get_info(struct ec_response_fp_info_v3 *resp, size_t resp_size)
 {
 	if (resp == NULL) {
 		return -EINVAL;
 	}
 
 	const size_t expected_min_size =
-		sizeof(struct ec_response_fp_info_v2) +
+		sizeof(struct ec_response_fp_info_v3) +
 		NUM_IMAGE_CAPTURE_TYPES *
 			sizeof(struct fingerprint_image_frame_params);
 
diff --git a/zephyr/test/fingerprint/task/CMakeLists.txt b/zephyr/test/fingerprint/task/CMakeLists.txt
index 83e3c1f..2e3d722 100644
--- a/zephyr/test/fingerprint/task/CMakeLists.txt
+++ b/zephyr/test/fingerprint/task/CMakeLists.txt
@@ -31,3 +31,5 @@
 	src/fpsensor_ascp.cc)
 target_sources_ifdef(CONFIG_LINK_TEST_SUITE_FINGERPRINT_FRAME_SIZE app PRIVATE
 	src/fpsensor_frame_size.cc)
+target_sources_ifdef(CONFIG_LINK_TEST_SUITE_FINGERPRINT_FP_INFO app PRIVATE
+	src/fpsensor_fp_info.cc)
diff --git a/zephyr/test/fingerprint/task/Kconfig b/zephyr/test/fingerprint/task/Kconfig
index b8cb7de..be44168 100644
--- a/zephyr/test/fingerprint/task/Kconfig
+++ b/zephyr/test/fingerprint/task/Kconfig
@@ -74,4 +74,10 @@
 	  Link and execute fingerprint subsystem tests that focus
 	  on finger frame size.
 
+config LINK_TEST_SUITE_FINGERPRINT_FP_INFO
+	bool "Link fingerprint FP_INFO tests"
+	help
+	  Link and execute fingerprint subsystem tests that focus
+	  on FP_INFO host command.
+
 source "Kconfig.zephyr"
diff --git a/zephyr/test/fingerprint/task/src/fpsensor_debug.cc b/zephyr/test/fingerprint/task/src/fpsensor_debug.cc
index 872389c..33a5ca4 100644
--- a/zephyr/test/fingerprint/task/src/fpsensor_debug.cc
+++ b/zephyr/test/fingerprint/task/src/fpsensor_debug.cc
@@ -25,11 +25,11 @@
 static const struct device *const fp_sensor_dev =
 	DEVICE_DT_GET(DT_CHOSEN(cros_fp_fingerprint_sensor));
 
-static_assert(sizeof(struct fp_image_frame_params) ==
+static_assert(sizeof(struct fp_image_frame_params_v2) ==
 		      sizeof(struct fingerprint_image_frame_params),
 	      "Frame param structures must be the same size");
 
-int get_image_frame_params(struct fp_image_frame_params &image_frame_params,
+int get_image_frame_params(struct fp_image_frame_params_v2 &image_frame_params,
 			   enum fp_capture_type capture_type);
 
 static int is_locked;
@@ -188,7 +188,7 @@
 
 enum ec_error_list
 upload_pgm_image(uint8_t *frame,
-		 const struct fp_image_frame_params &image_frame_params);
+		 const struct fp_image_frame_params_v2 &image_frame_params);
 
 ZTEST(fpsensor_debug, test_upload_pgm_image_wrong_bpp)
 {
@@ -223,7 +223,7 @@
 		  FP_CAPTURE_SIMPLE_IMAGE, FP_CAPTURE_PATTERN0,
 		  FP_CAPTURE_PATTERN1, FP_CAPTURE_QUALITY_TEST,
 		  FP_CAPTURE_RESET_TEST, FP_CAPTURE_TYPE_MAX });
-	constexpr struct fp_image_frame_params zero_params{};
+	constexpr struct fp_image_frame_params_v2 zero_params{};
 
 	for (enum fp_capture_type current_capture_type : kCaptureTypesArray) {
 		const struct fingerprint_image_frame_params *expected_params =
@@ -236,7 +236,7 @@
 			}
 		}
 
-		struct fp_image_frame_params image_frame_params{};
+		struct fp_image_frame_params_v2 image_frame_params{};
 		int rv = get_image_frame_params(image_frame_params,
 						current_capture_type);
 
@@ -247,7 +247,7 @@
 				rv, current_capture_type);
 			zassert_mem_equal(
 				&image_frame_params, expected_params,
-				sizeof(struct fp_image_frame_params),
+				sizeof(struct fp_image_frame_params_v2),
 				"Struct comparison failed for type %d",
 				current_capture_type);
 		} else {
@@ -257,7 +257,7 @@
 				EC_ERROR_INVAL, rv);
 			zassert_mem_equal(
 				&image_frame_params, &zero_params,
-				sizeof(struct fp_image_frame_params),
+				sizeof(struct fp_image_frame_params_v2),
 				"Struct contents should not change on failure");
 		}
 	}
diff --git a/zephyr/test/fingerprint/task/src/fpsensor_enroll.cc b/zephyr/test/fingerprint/task/src/fpsensor_enroll.cc
index fe01800..e612cc6 100644
--- a/zephyr/test/fingerprint/task/src/fpsensor_enroll.cc
+++ b/zephyr/test/fingerprint/task/src/fpsensor_enroll.cc
@@ -35,11 +35,11 @@
 static uint8_t image_buffer[IMAGE_SIZE];
 
 static const size_t test_info_buffer_size =
-	sizeof(struct ec_response_fp_info_v2) +
-	sizeof(struct fp_image_frame_params) * FP_MAX_CAPTURE_TYPES;
+	sizeof(struct ec_response_fp_info_v3) +
+	sizeof(struct fp_image_frame_params_v2) * FP_MAX_CAPTURE_TYPES;
 static uint8_t buffer[test_info_buffer_size];
-static struct ec_response_fp_info_v2 *test_info_buffer =
-	(struct ec_response_fp_info_v2 *)buffer;
+static struct ec_response_fp_info_v3 *test_info_buffer =
+	(struct ec_response_fp_info_v3 *)buffer;
 
 static int enroll_percent;
 static int enroll_step_return_val;
@@ -454,7 +454,7 @@
 	zassert_false(response.mode & FP_MODE_ENROLL_SESSION);
 
 	/* Confirm that there is 1 valid template. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_valid, 1);
 	/* Don't forget that template_dirty is a bitmask. */
diff --git a/zephyr/test/fingerprint/task/src/fpsensor_fp_info.cc b/zephyr/test/fingerprint/task/src/fpsensor_fp_info.cc
new file mode 100644
index 0000000..5c56748
--- /dev/null
+++ b/zephyr/test/fingerprint/task/src/fpsensor_fp_info.cc
@@ -0,0 +1,96 @@
+/* Copyright 2026 The ChromiumOS Authors
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "common.h"
+#include "console.h"
+#include "system.h"
+
+#include <zephyr/drivers/emul.h>
+#include <zephyr/fff.h>
+#include <zephyr/ztest.h>
+
+#include <ec_commands.h>
+#include <fpsensor/fpsensor.h>
+#include <fpsensor/fpsensor_state_driver.h>
+#include <fpsensor/fpsensor_utils.h>
+#include <fpsensor_driver.h>
+#include <host_command.h>
+#include <mkbp_event.h>
+
+DEFINE_FFF_GLOBALS;
+
+FAKE_VALUE_FUNC(int, mkbp_send_event, uint8_t);
+
+static const size_t fp_info_v3_buffer_size =
+	sizeof(struct ec_response_fp_info_v3) +
+	sizeof(struct fp_image_frame_params_v2) * FP_MAX_CAPTURE_TYPES;
+static uint8_t fp_info_v3_buffer[fp_info_v3_buffer_size];
+static struct ec_response_fp_info_v3 *test_fp_info_v3_buffer =
+	(struct ec_response_fp_info_v3 *)fp_info_v3_buffer;
+
+/* Use the same response buffer size to store FP info data in v3 format. */
+static const size_t fp_info_v2_buffer_size = fp_info_v3_buffer_size;
+static uint8_t fp_info_v2_buffer[fp_info_v2_buffer_size];
+static struct ec_response_fp_info_v2 *test_fp_info_v2_buffer =
+	(struct ec_response_fp_info_v2 *)fp_info_v2_buffer;
+
+ZTEST(fp_info, test_v2_v3)
+{
+	zassert_ok(ec_cmd_fp_info_v2(NULL, test_fp_info_v2_buffer,
+				     fp_info_v2_buffer_size));
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_fp_info_v3_buffer,
+				     fp_info_v3_buffer_size));
+
+	zassert_equal(test_fp_info_v2_buffer->sensor_info.vendor_id,
+		      test_fp_info_v3_buffer->sensor_info.vendor_id);
+	zassert_equal(test_fp_info_v2_buffer->sensor_info.product_id,
+		      test_fp_info_v3_buffer->sensor_info.product_id);
+	zassert_equal(test_fp_info_v2_buffer->sensor_info.model_id,
+		      test_fp_info_v3_buffer->sensor_info.model_id);
+	zassert_equal(test_fp_info_v2_buffer->sensor_info.version,
+		      test_fp_info_v3_buffer->sensor_info.version);
+	zassert_equal(test_fp_info_v2_buffer->sensor_info.num_capture_types,
+		      test_fp_info_v3_buffer->sensor_info.num_capture_types);
+	zassert_equal(test_fp_info_v2_buffer->sensor_info.errors,
+		      test_fp_info_v3_buffer->sensor_info.errors);
+
+	zassert_equal(test_fp_info_v2_buffer->template_info.template_size,
+		      test_fp_info_v3_buffer->template_info.template_size);
+	zassert_equal(test_fp_info_v2_buffer->template_info.template_max,
+		      test_fp_info_v3_buffer->template_info.template_max);
+	zassert_equal(test_fp_info_v2_buffer->template_info.template_valid,
+		      test_fp_info_v3_buffer->template_info.template_valid);
+	zassert_equal(test_fp_info_v2_buffer->template_info.template_dirty,
+		      test_fp_info_v3_buffer->template_info.template_dirty);
+	zassert_equal(test_fp_info_v2_buffer->template_info.template_version,
+		      test_fp_info_v3_buffer->template_info.template_version);
+	for (int i = 0; i < FP_MAX_CAPTURE_TYPES; i++) {
+		zassert_equal(
+			test_fp_info_v2_buffer->image_frame_params[i].frame_size,
+			test_fp_info_v3_buffer->image_frame_params[i]
+				.frame_size);
+		zassert_equal(test_fp_info_v2_buffer->image_frame_params[i]
+				      .pixel_format,
+			      test_fp_info_v3_buffer->image_frame_params[i]
+				      .pixel_format);
+		zassert_equal(
+			test_fp_info_v2_buffer->image_frame_params[i].width,
+			test_fp_info_v3_buffer->image_frame_params[i].width);
+		zassert_equal(
+			test_fp_info_v2_buffer->image_frame_params[i].height,
+			test_fp_info_v3_buffer->image_frame_params[i].height);
+		zassert_equal(
+			test_fp_info_v2_buffer->image_frame_params[i].bpp,
+			test_fp_info_v3_buffer->image_frame_params[i].bpp);
+		zassert_equal(test_fp_info_v2_buffer->image_frame_params[i]
+				      .fp_capture_type,
+			      test_fp_info_v3_buffer->image_frame_params[i]
+				      .fp_capture_type);
+		zassert_equal(
+			test_fp_info_v2_buffer->image_frame_params[i].reserved,
+			test_fp_info_v3_buffer->image_frame_params[i].reserved);
+	}
+}
+ZTEST_SUITE(fp_info, NULL, NULL, NULL, NULL, NULL);
diff --git a/zephyr/test/fingerprint/task/src/fpsensor_init.cc b/zephyr/test/fingerprint/task/src/fpsensor_init.cc
index 47c8fa0..0615300 100644
--- a/zephyr/test/fingerprint/task/src/fpsensor_init.cc
+++ b/zephyr/test/fingerprint/task/src/fpsensor_init.cc
@@ -29,11 +29,11 @@
 #define fp_sim DEVICE_DT_GET(DT_CHOSEN(cros_fp_fingerprint_sensor))
 
 static const size_t test_info_buffer_size =
-	sizeof(struct ec_response_fp_info_v2) +
-	sizeof(struct fp_image_frame_params) * FP_MAX_CAPTURE_TYPES;
+	sizeof(struct ec_response_fp_info_v3) +
+	sizeof(struct fp_image_frame_params_v2) * FP_MAX_CAPTURE_TYPES;
 static uint8_t buffer[test_info_buffer_size];
-static struct ec_response_fp_info_v2 *test_info_buffer =
-	(struct ec_response_fp_info_v2 *)buffer;
+static struct ec_response_fp_info_v3 *test_info_buffer =
+	(struct ec_response_fp_info_v3 *)buffer;
 
 ZTEST_USER(fpsensor_init, test_tpm_seed_init)
 {
@@ -126,7 +126,7 @@
 	const int dead_pixels = 3;
 
 	/* Confirm that number of dead pixels is unknown. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(
 		FP_ERROR_DEAD_PIXELS(test_info_buffer->sensor_info.errors),
@@ -148,7 +148,7 @@
 	zassert_true(state.maintenance_ran);
 
 	/* Confirm that number of dead pixels is correct. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(
 		FP_ERROR_DEAD_PIXELS(test_info_buffer->sensor_info.errors),
@@ -187,7 +187,7 @@
 	zassert_true(state.maintenance_ran);
 
 	/* Confirm that number of dead pixels is correct. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(
 		FP_ERROR_DEAD_PIXELS(test_info_buffer->sensor_info.errors),
@@ -226,7 +226,7 @@
 	zassert_true(state.maintenance_ran);
 
 	/* Confirm that number of dead pixels is correct. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(
 		FP_ERROR_DEAD_PIXELS(test_info_buffer->sensor_info.errors),
@@ -265,7 +265,7 @@
 	zassert_true(state.maintenance_ran);
 
 	/* Confirm that number of dead pixels is correct. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(
 		FP_ERROR_DEAD_PIXELS(test_info_buffer->sensor_info.errors),
diff --git a/zephyr/test/fingerprint/task/src/fpsensor_match.cc b/zephyr/test/fingerprint/task/src/fpsensor_match.cc
index 46440be2..e5888a1 100644
--- a/zephyr/test/fingerprint/task/src/fpsensor_match.cc
+++ b/zephyr/test/fingerprint/task/src/fpsensor_match.cc
@@ -38,11 +38,11 @@
 static const uint8_t fake_rollback_entropy[] = "some_rollback_entropy";
 
 static const size_t test_info_buffer_size =
-	sizeof(struct ec_response_fp_info_v2) +
-	sizeof(struct fp_image_frame_params) * FP_MAX_CAPTURE_TYPES;
+	sizeof(struct ec_response_fp_info_v3) +
+	sizeof(struct fp_image_frame_params_v2) * FP_MAX_CAPTURE_TYPES;
 static uint8_t buffer[test_info_buffer_size];
-static struct ec_response_fp_info_v2 *test_info_buffer =
-	(struct ec_response_fp_info_v2 *)buffer;
+static struct ec_response_fp_info_v3 *test_info_buffer =
+	(struct ec_response_fp_info_v3 *)buffer;
 
 /* The fake TPM seed is "very_secret_32_bytes_of_tpm_seed" */
 #define FAKE_TPM_SEED                                                       \
@@ -584,7 +584,7 @@
 	zassert_equal(mock_alg_match_fake.call_count, 1);
 
 	/* Confirm that dirty templates bitmap is correct. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_dirty, 0x1);
 }
@@ -631,7 +631,7 @@
 	zassert_equal(mock_alg_match_fake.call_count, 1);
 
 	/* Confirm that dirty templates bitmap is correct. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_dirty, 0x0);
 }
@@ -677,7 +677,7 @@
 	zassert_equal(mock_alg_match_fake.call_count, 1);
 
 	/* Confirm that dirty templates bitmap is correct. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_dirty, 0x0);
 }
@@ -723,7 +723,7 @@
 
 	/* Confirm that dirty templates bitmap is correct (no templates dirty).
 	 */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_dirty, 0x0);
 }
diff --git a/zephyr/test/fingerprint/task/src/fpsensor_shim.cc b/zephyr/test/fingerprint/task/src/fpsensor_shim.cc
index c4bbb3c..af149eb 100644
--- a/zephyr/test/fingerprint/task/src/fpsensor_shim.cc
+++ b/zephyr/test/fingerprint/task/src/fpsensor_shim.cc
@@ -49,11 +49,11 @@
 		DT_NODELABEL(fpsensor_sim)) };
 
 static const size_t test_info_buffer_size =
-	sizeof(struct ec_response_fp_info_v2) +
-	sizeof(struct fp_image_frame_params) * FP_MAX_CAPTURE_TYPES;
+	sizeof(struct ec_response_fp_info_v3) +
+	sizeof(struct fp_image_frame_params_v2) * FP_MAX_CAPTURE_TYPES;
 static uint8_t buffer[test_info_buffer_size];
-static struct ec_response_fp_info_v2 *test_info_buffer =
-	(struct ec_response_fp_info_v2 *)buffer;
+static struct ec_response_fp_info_v3 *test_info_buffer =
+	(struct ec_response_fp_info_v3 *)buffer;
 
 ZTEST_USER(fpsensor_shim, test_shim_sensor_type_elan)
 {
diff --git a/zephyr/test/fingerprint/task/src/fpsensor_template.cc b/zephyr/test/fingerprint/task/src/fpsensor_template.cc
index a1fc1ab..e42947a 100644
--- a/zephyr/test/fingerprint/task/src/fpsensor_template.cc
+++ b/zephyr/test/fingerprint/task/src/fpsensor_template.cc
@@ -155,11 +155,11 @@
 static uint8_t encrypted_template[FP_ALGORITHM_ENCRYPTED_TEMPLATE_SIZE];
 
 static const size_t test_info_buffer_size =
-	sizeof(struct ec_response_fp_info_v2) +
-	sizeof(struct fp_image_frame_params) * FP_MAX_CAPTURE_TYPES;
+	sizeof(struct ec_response_fp_info_v3) +
+	sizeof(struct fp_image_frame_params_v2) * FP_MAX_CAPTURE_TYPES;
 static uint8_t buffer[test_info_buffer_size];
-static struct ec_response_fp_info_v2 *test_info_buffer =
-	(struct ec_response_fp_info_v2 *)buffer;
+static struct ec_response_fp_info_v3 *test_info_buffer =
+	(struct ec_response_fp_info_v3 *)buffer;
 
 /*
  * Size of params buffer for FP_TEMPLATE command. Its size must be big enough
@@ -226,7 +226,7 @@
 	}
 
 	/* Confirm that there is 1 valid template. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_valid, 1);
 }
@@ -295,7 +295,7 @@
 	zassert_equal(EC_RES_UNAVAILABLE, status);
 
 	/* Confirm that there is no valid template. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_valid, 0);
 }
@@ -1080,7 +1080,7 @@
 	}
 
 	/* Confirm that there is 1 valid template. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_valid, 1);
 }
@@ -1133,7 +1133,7 @@
 	}
 
 	/* Confirm that there is no valid template. */
-	zassert_ok(ec_cmd_fp_info_v2(NULL, test_info_buffer,
+	zassert_ok(ec_cmd_fp_info_v3(NULL, test_info_buffer,
 				     test_info_buffer_size));
 	zassert_equal(test_info_buffer->template_info.template_valid, 0);
 }
diff --git a/zephyr/test/fingerprint/task/testcase.yaml b/zephyr/test/fingerprint/task/testcase.yaml
index 9a82af8..990c14a 100644
--- a/zephyr/test/fingerprint/task/testcase.yaml
+++ b/zephyr/test/fingerprint/task/testcase.yaml
@@ -80,3 +80,9 @@
     - CONFIG_LINK_TEST_SUITE_FINGERPRINT_FRAME_SIZE=y
     extra_conf_files:
     - prj.conf
+
+  fingerprint.task.fp_info:
+    extra_configs:
+    - CONFIG_LINK_TEST_SUITE_FINGERPRINT_FP_INFO=y
+    extra_conf_files:
+    - prj.conf