validity: Add capture program infrastructure (Iteration 5)

Implement the capture command building infrastructure ported from
python-validity's sensor.py and timeslot.py. This provides all the
algorithms needed to construct sensor capture commands for calibration,
enrollment, and identification modes.

New components:
- TLV chunk parsing (split/merge) for capture programs
- Timeslot DSP instruction decoder (16 opcodes, 1-3 bytes each)
- Timeslot table patching (Call repeat multiplication, factory cal)
- Line Update Type 1 algorithm for 0xb5-class sensors
- build_cmd_02(): main capture command builder
- Factory bits parsing (subtag 3 cal values, subtag 7 cal data)
- Frame averaging with multi-line deinterlacing
- Calibration data processing (scale/accumulate/clip)
- Clean slate format with SHA256 verification
- Bitpack compression for factory calibration values
- Finger ID mapping (FpFinger <-> VCSFW subtype 1-10)
- LED control commands (glow_start_scan, glow_end_scan)
- CaptureProg lookup table for firmware 6.x type-1 devices
- OPEN_CAPTURE_SETUP state in the open SSM

27 unit tests covering all components.
Full test suite: 36 OK, 0 Fail, 2 Skipped.
This commit is contained in:
Leonardo Francisco 2026-04-06 00:09:42 -04:00 committed by lewohart
parent 3af922d69b
commit 977e09da2d
7 changed files with 3199 additions and 1 deletions

View file

@ -183,6 +183,7 @@ typedef enum {
OPEN_SENSOR_IDENTIFY_RECV,
OPEN_SENSOR_FACTORY_BITS,
OPEN_SENSOR_FACTORY_BITS_RECV,
OPEN_CAPTURE_SETUP,
OPEN_DONE,
OPEN_NUM_STATES,
} ValidityOpenSsmState;
@ -618,6 +619,48 @@ open_run_state (FpiSsm *ssm,
}
break;
case OPEN_CAPTURE_SETUP:
{
FpiDeviceValidity *self = FPI_DEVICE_VALIDITY (dev);
/* Initialize capture state from sensor identification and factory bits.
* Requires: sensor.type_info and sensor.device_info from IDENTIFY,
* sensor.factory_bits from FACTORY_BITS. */
if (!self->sensor.type_info || !self->sensor.device_info)
{
fp_info ("No sensor type info — skipping capture setup");
fpi_ssm_next_state (ssm);
return;
}
validity_capture_state_init (&self->capture);
if (!validity_capture_state_setup (&self->capture,
self->sensor.type_info,
self->sensor.device_info->type,
self->version_info.version_major,
self->version_info.version_minor,
self->sensor.factory_bits,
self->sensor.factory_bits_len))
{
fp_warn ("Capture state setup failed — "
"enrollment/verification will not be available");
/* Non-fatal: device can still be used for identification
* if calibration data exists on flash */
}
else
{
fp_info ("Capture state: %u bytes/line, %u lines/frame, "
"type1=%d",
self->capture.bytes_per_line,
self->capture.lines_per_frame,
self->capture.is_type1_device);
}
fpi_ssm_next_state (ssm);
}
break;
case OPEN_DONE:
/* All init commands sent. Mark open complete. */
fpi_ssm_mark_completed (ssm);
@ -657,6 +700,7 @@ dev_open (FpDevice *device)
self->interrupt_cancellable = g_cancellable_new ();
validity_tls_init (&self->tls);
validity_sensor_state_init (&self->sensor);
validity_capture_state_init (&self->capture);
if (!g_usb_device_claim_interface (fpi_device_get_usb_device (device), 0, 0, &error))
{
@ -685,6 +729,7 @@ dev_close (FpDevice *device)
g_clear_pointer (&self->cmd_response_data, g_free);
self->cmd_response_len = 0;
validity_capture_state_clear (&self->capture);
validity_sensor_state_clear (&self->sensor);
validity_tls_free (&self->tls);

View file

@ -22,6 +22,7 @@
#include "fpi-device.h"
#include "fpi-ssm.h"
#include "validity_capture.h"
#include "validity_sensor.h"
#include "validity_tls.h"
@ -102,6 +103,9 @@ struct _FpiDeviceValidity
/* Sensor identification and HAL state (post-TLS) */
ValiditySensorState sensor;
/* Capture program infrastructure and calibration state */
ValidityCaptureState capture;
/* Firmware extension status */
gboolean fwext_loaded;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,395 @@
/*
* Capture program infrastructure for Validity/Synaptics VCSFW sensors
*
* Implements the capture command builder (cmd 0x02), timeslot table
* patching, factory calibration parsing, frame averaging, and
* calibration data processing.
*
* Reference: python-validity sensor.py, timeslot.py, generated_tables.py
*
* Copyright (C) 2024 libfprint contributors
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <glib.h>
#include "validity_sensor.h"
/* ================================================================
* Capture mode passed to build_cmd_02 to select capture behavior
* Values match python-validity CaptureMode enum.
* ================================================================ */
typedef enum {
VALIDITY_CAPTURE_CALIBRATE = 1,
VALIDITY_CAPTURE_IDENTIFY = 2,
VALIDITY_CAPTURE_ENROLL = 3,
} ValidityCaptureMode;
/* ================================================================
* Capture program chunk TLV entry from the CaptureProg table.
* Format: type(2LE) | size(2LE) | data(size bytes)
* ================================================================ */
typedef struct
{
guint16 type;
guint16 size;
guint8 *data; /* owned; g_free() when done */
} ValidityCaptureChunk;
/* ================================================================
* Capture state kept in the device struct, initialized during open
* ================================================================ */
typedef struct
{
/* Capture program binary (from CaptureProg table lookup) */
const guint8 *capture_prog;
gsize capture_prog_len;
/* Geometry derived from SensorTypeInfo + CaptureProg */
guint16 lines_per_frame;
guint16 bytes_per_line;
/* Calibration parameters (derived from sensor geometry) */
guint16 key_calibration_line;
guint8 calibration_frames;
guint8 calibration_iterations;
/* Factory calibration values (from factory_bits subtag 3) */
guint8 *factory_calibration_values;
gsize factory_calibration_values_len;
/* Optional factory calibration data (from factory_bits subtag 7) */
guint8 *factory_calib_data;
gsize factory_calib_data_len;
/* Accumulated calibration data (built during calibration loop) */
guint8 *calib_data;
gsize calib_data_len;
/* Whether this is a line_update_type_1 device */
gboolean is_type1_device;
} ValidityCaptureState;
/* ================================================================
* Chunk parsing split/merge TLV-encoded capture program
* ================================================================ */
/*
* Split a capture program binary into an array of chunks.
* Returns an array of ValidityCaptureChunk (caller must free with
* validity_capture_chunks_free). Sets n_chunks to the count.
* Returns NULL on parse error.
*/
ValidityCaptureChunk *validity_capture_split_chunks (const guint8 *data,
gsize data_len,
gsize *n_chunks);
/*
* Merge an array of chunks back into a binary blob.
* Caller must g_free() the returned buffer.
* Returns NULL on error and sets out_len to 0.
*/
guint8 *validity_capture_merge_chunks (const ValidityCaptureChunk *chunks,
gsize n_chunks,
gsize *out_len);
/*
* Free an array of chunks (including each chunk's data).
*/
void validity_capture_chunks_free (ValidityCaptureChunk *chunks,
gsize n_chunks);
/* ================================================================
* Timeslot instruction decoder
*
* The timeslot table is a bytecode program for the sensor DSP.
* Each instruction is 1-3 bytes. We need to decode them to find
* specific instructions for patching.
*
* Returns: opcode in *opcode, instruction length in *insn_len,
* operands in operands[] (up to 3), count in *n_operands.
* Returns FALSE if the instruction cannot be decoded.
* ================================================================ */
gboolean validity_capture_decode_insn (const guint8 *data,
gsize data_len,
guint8 *opcode,
guint8 *insn_len,
guint32 operands[3],
guint8 *n_operands);
/* Timeslot instruction opcodes */
#define TST_OP_NOOP 0
#define TST_OP_END_OF_TABLE 1
#define TST_OP_RETURN 2
#define TST_OP_CLEAR_SO 3
#define TST_OP_END_OF_DATA 4
#define TST_OP_MACRO 5
#define TST_OP_ENABLE_RX 6
#define TST_OP_IDLE_RX 7
#define TST_OP_ENABLE_SO 8
#define TST_OP_DISABLE_SO 9
#define TST_OP_INTERRUPT 10
#define TST_OP_CALL 11
#define TST_OP_FEATURES 12
#define TST_OP_REG_WRITE 13
#define TST_OP_SAMPLE 14
#define TST_OP_SAMPLE_REPEAT 15
/*
* Find the Nth instruction with the given opcode.
* Returns the byte offset (pc) of the instruction, or -1 if not found.
*/
gssize validity_capture_find_nth_insn (const guint8 *data,
gsize data_len,
guint8 target_opcode,
guint n);
/*
* Find the Nth Register Write instruction to a specific register address.
* Returns the byte offset (pc), or -1 if not found.
*/
gssize validity_capture_find_nth_regwrite (const guint8 *data,
gsize data_len,
guint32 reg_addr,
guint n);
/* ================================================================
* Timeslot table patching
* ================================================================ */
/*
* First pass: patch the timeslot table multiplier.
* For each "Call" instruction (opcode 0x10-0x17), if repeat > 1,
* multiply it by mult and optionally increment the address.
* Modifies data in-place. Returns TRUE on success.
*/
gboolean validity_capture_patch_timeslot_table (guint8 *data,
gsize data_len,
gboolean inc_address,
guint8 mult);
/*
* Second pass: find the register write to 0x8000203C in the Call
* target subroutine and patch its value with the factory calibration
* value at key_calibration_line.
* Modifies data in-place. Returns TRUE on success; FALSE means
* no matching instruction was found (non-fatal).
*/
gboolean validity_capture_patch_timeslot_again (guint8 *data,
gsize data_len,
const guint8 *factory_calibration_values,
gsize factory_cal_len,
guint16 key_calibration_line);
/* ================================================================
* build_cmd_02 main capture command builder
* ================================================================ */
/*
* Build a capture command (cmd 0x02).
* Format: 0x02 | bytes_per_line(2LE) | req_lines(2LE) | merged_chunks
*
* The capture program is loaded from capture_state->capture_prog,
* split into chunks, patched (timeslot, line_update), and reassembled.
*
* Returns a newly-allocated buffer (caller must g_free) or NULL on error.
* Sets out_len to the buffer size.
*/
guint8 *validity_capture_build_cmd_02 (const ValidityCaptureState *capture,
const ValiditySensorTypeInfo *type_info,
ValidityCaptureMode mode,
gsize *out_len);
/* ================================================================
* Factory bits parsing
* ================================================================ */
/*
* Parse the raw factory bits response (from cmd 0x6f).
* Extracts subtag 3 (factory_calibration_values) and optionally
* subtag 7 (factory_calib_data).
*
* Response format (after 2-byte status, already stripped):
* wtf(4LE) | entries(4LE) | entry[]:
* ptr(4LE) | length(2LE) | tag(2LE) | subtag(2LE) | flags(2LE) | data(length)
*
* Returns TRUE if at least subtag 3 was found.
*/
gboolean validity_capture_parse_factory_bits (const guint8 *data,
gsize data_len,
guint8 **cal_values,
gsize *cal_values_len,
guint8 **cal_data,
gsize *cal_data_len);
/* ================================================================
* Frame averaging
* ================================================================ */
/*
* Average raw multi-frame capture data from EP 0x82.
* Deinterlaces frames and averages interleaved lines.
*
* Returns a newly-allocated buffer (one frame), or NULL on error.
* The caller must g_free() the buffer.
*/
guint8 *validity_capture_average_frames (const guint8 *raw_data,
gsize raw_len,
guint16 lines_per_frame,
guint16 bytes_per_line,
guint16 lines_per_calibration_data,
guint8 calibration_frames,
gsize *out_len);
/* ================================================================
* Calibration data processing
* ================================================================ */
/*
* Process averaged calibration frame into calibration data.
* Applies scaling and accumulates with existing calib_data.
* Updates calib_data/calib_data_len in place.
*
* If calib_data is NULL, initializes from the frame.
* If non-NULL, combines (adds signed values, clips).
*/
void validity_capture_process_calibration (guint8 **calib_data,
gsize *calib_data_len,
const guint8 *averaged_frame,
gsize frame_len,
guint16 bytes_per_line);
/*
* Build the clean slate format for flash persistence.
* Format: magic(2LE=0x5002) | inner_len(2LE) | sha256(32) | zeroes(32) |
* data_len(2LE) | data | trailing_zero(2LE=0)
*
* Returns a newly-allocated buffer or NULL on error.
*/
guint8 *validity_capture_build_clean_slate (const guint8 *averaged_frame,
gsize frame_len,
gsize *out_len);
/*
* Verify clean slate format (check magic and SHA256 hash).
* Returns TRUE if the format is valid.
*/
gboolean validity_capture_verify_clean_slate (const guint8 *data,
gsize data_len);
/* ================================================================
* Bitpack compress factory calibration values for Line Update
* ================================================================ */
/*
* Pack calibration values using minimum-bit encoding.
* Returns packed data, minimum value (v1), bit count (v0),
* and packed length.
*
* Python-validity bitpack(): find min, max delta, encode each value
* as (value - min) in minimum bits, pack as little-endian.
*
* Returns a newly-allocated buffer or NULL on error.
*/
guint8 *validity_capture_bitpack (const guint8 *values,
gsize values_len,
guint8 *out_v0,
guint8 *out_v1,
gsize *out_len);
/* ================================================================
* Finger ID mapping
* ================================================================ */
/*
* Map FpFinger enum value to VCSFW finger subtype (1-10).
* Returns 0 if the finger is not recognized.
*/
guint16 validity_finger_to_subtype (guint finger);
/*
* Map VCSFW finger subtype (1-10) to FpFinger enum value.
* Returns -1 if the subtype is not recognized.
*/
gint validity_subtype_to_finger (guint16 subtype);
/* ================================================================
* LED control commands for user feedback during capture
* ================================================================ */
/*
* Build the LED "glow start scan" command (sent via TLS app).
* Returns a static buffer and sets out_len.
*/
const guint8 *validity_capture_glow_start_cmd (gsize *out_len);
/*
* Build the LED "glow end scan" command (sent via TLS app).
* Returns a static buffer and sets out_len.
*/
const guint8 *validity_capture_glow_end_cmd (gsize *out_len);
/* ================================================================
* CaptureProg table lookup
* ================================================================ */
/*
* Look up the capture program for a given ROM version and sensor type.
* Returns a pointer to the static blob data and sets out_len.
* Returns NULL if no matching entry is found.
*/
const guint8 *validity_capture_prog_lookup (guint8 rom_major,
guint8 rom_minor,
guint16 dev_type,
gsize *out_len);
/* ================================================================
* Capture state lifecycle
* ================================================================ */
void validity_capture_state_init (ValidityCaptureState *state);
void validity_capture_state_clear (ValidityCaptureState *state);
/*
* Initialize capture state from sensor info and factory bits.
* Loads the CaptureProg, computes geometry, sets calibration params.
* Returns TRUE on success.
*/
gboolean validity_capture_state_setup (ValidityCaptureState *state,
const ValiditySensorTypeInfo *type_info,
guint16 dev_type,
guint8 rom_major,
guint8 rom_minor,
const guint8 *factory_bits,
gsize factory_bits_len);
/* Chunk type IDs used in capture programs */
#define CAPT_CHUNK_REPLY_CONFIG 0x0017
#define CAPT_CHUNK_FINGER_DETECT 0x0026
#define CAPT_CHUNK_IMAGE_RECON 0x002e
#define CAPT_CHUNK_2D_PARAMS 0x002f
#define CAPT_CHUNK_LINE_UPDATE 0x0030
#define CAPT_CHUNK_TIMESLOT_2D 0x0034
#define CAPT_CHUNK_TS_OFFSET 0x0029
#define CAPT_CHUNK_TS_FD_OFFSET 0x0035
#define CAPT_CHUNK_LINE_UPDATE_XFORM 0x0043
#define CAPT_CHUNK_INTERLEAVE 0x0044
#define CAPT_CHUNK_WTF 0x004e
/* Capture program cookie for the ACM Config chunk */
#define CAPT_CHUNK_ACM_CONFIG 0x002a
#define CAPT_CHUNK_CEM_CONFIG 0x002c

View file

@ -158,7 +158,8 @@ driver_sources = {
'drivers/validity/vcsfw_protocol.c',
'drivers/validity/validity_tls.c',
'drivers/validity/validity_fwext.c',
'drivers/validity/validity_sensor.c' ],
'drivers/validity/validity_sensor.c',
'drivers/validity/validity_capture.c' ],
}
helper_sources = {

View file

@ -367,6 +367,22 @@ if 'validity' in supported_drivers
suite: ['unit-tests'],
env: envs,
)
# Validity capture infrastructure unit tests (needs OpenSSL for clean slate)
if openssl_dep.found()
validity_capture_test = executable('test-validity-capture',
sources: 'test-validity-capture.c',
dependencies: [ libfprint_private_dep, openssl_dep ],
c_args: common_cflags,
link_with: libfprint_drivers,
install: false,
)
test('validity-capture',
validity_capture_test,
suite: ['unit-tests'],
env: envs,
)
endif
endif
# Run udev rule generator with fatal warnings

File diff suppressed because it is too large Load diff