From b850fd174f767c3621a4ce09c9073db88efa0079 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Tue, 19 May 2026 08:45:13 +0100 Subject: [PATCH 01/16] Add support for sensor type 0xd51 (138a:00ab, 06cb:00b7) The 0xd51 chip does not emit the b[0]=2 "finger detected" interrupt during capture, so the wait-finger loop in Sensor.capture() hangs forever. Accept b[0]=3 as a substitute (gated on the real device type so existing chips are unaffected) and pass the interrupt through to the wait-capture-complete loop. Also wires the two known USB IDs through SupportedDevices, blobs, firmware_tables, the udev rule, and aliases sensor type 0xd51 to the 0x199 profile so SensorTypeInfo / SensorCaptureProg lookups succeed (no native profile exists yet). Closes #181 Closes #225 Closes #238 --- debian/python3-validity.udev | 2 ++ validitysensor/blobs.py | 4 ++++ validitysensor/firmware_tables.py | 7 ++++++- validitysensor/sensor.py | 25 +++++++++++++++++++++++-- validitysensor/usb.py | 2 ++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/debian/python3-validity.udev b/debian/python3-validity.udev index d9ca628..bb4a74a 100644 --- a/debian/python3-validity.udev +++ b/debian/python3-validity.udev @@ -4,6 +4,8 @@ ENV{DEVTYPE}!="usb_device", GOTO="python_validity_end" ATTRS{idVendor}=="138a", ATTRS{idProduct}=="0090", GOTO="python_validity_match" ATTRS{idVendor}=="138a", ATTRS{idProduct}=="0097", GOTO="python_validity_match" ATTRS{idVendor}=="06cb", ATTRS{idProduct}=="009a", GOTO="python_validity_match" +ATTRS{idVendor}=="138a", ATTRS{idProduct}=="00ab", GOTO="python_validity_match" +ATTRS{idVendor}=="06cb", ATTRS{idProduct}=="00b7", GOTO="python_validity_match" GOTO="python_validity_end" diff --git a/validitysensor/blobs.py b/validitysensor/blobs.py index a9f16ab..fb2a1a0 100644 --- a/validitysensor/blobs.py +++ b/validitysensor/blobs.py @@ -8,9 +8,13 @@ def __load_blob(blob: str) -> bytes: from . import blobs_97 as blobs elif usb.usb_dev().idProduct == 0x009d: from . import blobs_9d as blobs + elif usb.usb_dev().idProduct == 0x00ab: + from . import blobs_97 as blobs # HP EliteBook 840 G5; verified elif usb.usb_dev().idVendor == 0x06cb: if usb.usb_dev().idProduct == 0x009a: from . import blobs_9a as blobs + elif usb.usb_dev().idProduct == 0x00b7: + from . import blobs_9a as blobs # HP G6 series; same sensor type as 0x00ab globals()[blob] = getattr(blobs, blob) return globals()[blob] diff --git a/validitysensor/firmware_tables.py b/validitysensor/firmware_tables.py index bd0b867..dbbc6cd 100644 --- a/validitysensor/firmware_tables.py +++ b/validitysensor/firmware_tables.py @@ -29,5 +29,10 @@ SupportedDevices.DEV_90: '6_07f_Lenovo.xpfwext', SupportedDevices.DEV_97: '6_07f_lenovo_mis_qm.xpfwext', SupportedDevices.DEV_9a: '6_07f_lenovo_mis_qm.xpfwext', - SupportedDevices.DEV_9d: '6_07f_lenovo_mis_qm.xpfwext' + SupportedDevices.DEV_9d: '6_07f_lenovo_mis_qm.xpfwext', + # 0xd51-sensor variants ship with firmware pre-loaded; xpfwext upload is + # only needed for factory-reset / unprovisioned chips. The HP softpaq + # filename matches what extracted from HP's Windows driver (sp135736.exe). + SupportedDevices.DEV_AB: '6_07f_hp_cmit_mis_qm.xpfwext', # HP EliteBook 840 G5 + SupportedDevices.DEV_B7: '6_07f_hp_cmit_mis_qm.xpfwext', # HP G6 series (same chip family) } diff --git a/validitysensor/sensor.py b/validitysensor/sensor.py index dd019bb..dff7b1a 100644 --- a/validitysensor/sensor.py +++ b/validitysensor/sensor.py @@ -24,7 +24,8 @@ calib_data_path = PYTHON_VALIDITY_DATA_DIR + 'calib-data.bin' line_update_type1_devices = [ - 0xB5, 0x885, 0xB3, 0x143B, 0x1055, 0xE1, 0x8B1, 0xEA, 0xE4, 0xED, 0x1825, 0x1FF5, 0x199 + 0xB5, 0x885, 0xB3, 0x143B, 0x1055, 0xE1, 0x8B1, 0xEA, 0xE4, 0xED, 0x1825, 0x1FF5, 0x199, + 0xD51, # HP EliteBook 840 G5 (138a:00ab) / HP G6 series (06cb:00b7) ] @@ -224,6 +225,17 @@ class Sensor: def open(self): self.device_info = identify_sensor() + self.real_device_type = self.device_info.type + + # Sensor type 0xd51 (HP EliteBook 840 G5 138a:00ab, HP G6 series 06cb:00b7) + # has no native SensorTypeInfo / SensorCaptureProg entry. Empirically the + # 0x199 profile produces images that the on-chip matcher accepts after + # enrollment/verify; the 0xdb profile does not. Spoofing keeps the rest + # of this method (calibration switch, capture program lookup) on a code + # path that works. + if self.device_info.type == 0xd51: + logging.info('Sensor type 0xd51 — aliasing to 0x199 profile') + self.device_info.type = 0x199 logging.info('Opening sensor: %s' % self.device_info.name) self.type_info = SensorTypeInfo.get_by_type(self.device_info.type) @@ -702,14 +714,23 @@ def capture(self, mode: CaptureMode) -> typing.Tuple[int, int, int, int]: raise Exception('wait_start: Unexpected interrupt type %s' % hexlify(b).decode()) # wait for finger + # Sensor type 0xd51 (138a:00ab, 06cb:00b7) does not emit the + # b[0]=2 "finger detected" interrupt — it jumps directly from the + # start ack to b[0]=3 capture events. Accept b[0]=3 as a substitute + # and pass the interrupt through to the wait-capture-complete loop. + saved_b = None while True: b = usb.wait_int() if b[0] == 2: break + if b[0] == 3 and getattr(self, 'real_device_type', None) == 0xd51: + saved_b = b + break # wait capture complete while True: - b = usb.wait_int() + b = saved_b if saved_b is not None else usb.wait_int() + saved_b = None if b[0] != 3: raise Exception('Unexpected interrupt type %s' % hexlify(b).decode()) diff --git a/validitysensor/usb.py b/validitysensor/usb.py index 464b092..22bca4f 100644 --- a/validitysensor/usb.py +++ b/validitysensor/usb.py @@ -18,6 +18,8 @@ class SupportedDevices(Enum): DEV_97 = (0x138a, 0x0097) DEV_9d = (0x138a, 0x009d) DEV_9a = (0x06cb, 0x009a) + DEV_AB = (0x138a, 0x00ab) # HP EliteBook 840 G5 — sensor type 0xd51 + DEV_B7 = (0x06cb, 0x00b7) # HP G6 series — sensor type 0xd51 @classmethod def from_usbid(cls, vendorid, productid): From 20df336480f5170230e426a43fe75054e6da17be Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Wed, 20 May 2026 19:40:28 +0100 Subject: [PATCH 02/16] Add FIRMWARE_URIS entries for DEV_AB and DEV_B7 Without these entries `validity-sensors-firmware` crashes with a KeyError for users on 138a:00ab and 06cb:00b7 who follow the README's standard install flow. The 0xd51-family chips ship with firmware pre-loaded so the downloader is only needed for factory-reset chips, but the script should not crash. Both PIDs point at HP softpaq sp135736.exe (the same blob extracted to 6_07f_hp_cmit_mis_qm.xpfwext in FIRMWARE_NAMES). sha512 verified against the canonical ftp.hp.com URL. Reported by a 06cb:00b7 user on PR #256. --- validitysensor/firmware_tables.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/validitysensor/firmware_tables.py b/validitysensor/firmware_tables.py index dbbc6cd..5e3317c 100644 --- a/validitysensor/firmware_tables.py +++ b/validitysensor/firmware_tables.py @@ -22,6 +22,16 @@ 'driver': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe', 'referral': 'https://download.lenovo.com/pccbbs/mobiles/nz3gf07w.exe', 'sha512': 'a4a4e6058b1ea8ab721953d2cfd775a1e7bc589863d160e5ebbb90344858f147d695103677a8df0b2de0c95345df108bda97196245b067f45630038fb7c807cd' + }, + SupportedDevices.DEV_AB: { + 'driver': 'https://ftp.hp.com/pub/softpaq/sp135501-136000/sp135736.exe', + 'referral': 'https://support.hp.com/us-en/drivers', + 'sha512': 'f9a91e2796a5070f1f40099e2318aa9716e2e6a31b9ba6a93986c450eedbfb0b323dff55c5e4536466946da3e01985f367b1db27bbd7b65f4c333ce0cd47b78c' + }, + SupportedDevices.DEV_B7: { + 'driver': 'https://ftp.hp.com/pub/softpaq/sp135501-136000/sp135736.exe', + 'referral': 'https://support.hp.com/us-en/drivers', + 'sha512': 'f9a91e2796a5070f1f40099e2318aa9716e2e6a31b9ba6a93986c450eedbfb0b323dff55c5e4536466946da3e01985f367b1db27bbd7b65f4c333ce0cd47b78c' } } From 4ac8bbd3aa014ac2c24e2794dd3b1cb0a25e6c9a Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Thu, 21 May 2026 13:04:21 +0100 Subject: [PATCH 03/16] Replace existing finger record on same-subtype re-enroll The chip's database rejects creating a second finger record with the same subtype for the same user, which previously caused fprintd-enroll to fail at the final stage with enroll-failed after all per-stage captures had passed. Detect and delete any pre-existing record with the same subtype right before db.new_finger. Placing the delete here (inside do_create_finger, after all captures have completed) rather than at EnrollStart matters: pre-deleting before the enrollment session starts left the chip in a state where every subsequent capture returned retry-scan indefinitely until the daemon was restarted. By the time do_create_finger runs the captures are done and the chip is ready to accept the save, so deletion at this point doesn't disrupt session state. --- validitysensor/sensor.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/validitysensor/sensor.py b/validitysensor/sensor.py index dff7b1a..984e5ff 100644 --- a/validitysensor/sensor.py +++ b/validitysensor/sensor.py @@ -832,11 +832,20 @@ def enroll(self, identity: SidIdentity, subtype: int, def do_create_finger(final_template: bytes, tid: bytes): tinfo = self.make_finger_data(subtype, final_template, tid) - usr = db.lookup_user(identity) - if usr is None: + existing = db.lookup_user(identity) + if existing is None: usr = db.new_user(identity) else: - usr = usr.dbid + # Replace any existing enrollment for this finger slot. The chip + # rejects creating a second record with the same subtype for the + # same user. Deleting here (after all captures are done) keeps + # the chip's enroll session uninterrupted — earlier attempts to + # pre-delete before EnrollStart left the chip in a state where + # subsequent captures kept returning retry-scan indefinitely. + for f in existing.fingers: + if f['subtype'] == subtype: + db.del_record(f['dbid']) + usr = existing.dbid recid = db.new_finger(usr, tinfo) usb.wait_int() From 90d5d67ea195d029bb2808176116d284daa40206 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Thu, 21 May 2026 13:06:09 +0100 Subject: [PATCH 04/16] validity-sensors-firmware: fall back to cabextract for non-Inno archives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lenovo softpaqs (the previously supported devices) ship as Inno Setup installers, which is what innoextract handles. HP softpaqs (sp135736.exe used for the new DEV_AB and DEV_B7 entries) are CAB-wrapped self- extracting exes — innoextract rejects them with "Not a supported Inno Setup installer!" and the postinst surfaces a noisy Python traceback. Try innoextract first; on CalledProcessError or FileNotFoundError, fall back to cabextract. The standalone tool-availability check accepts either extractor so installations on either family work out of the box. Note: dropped the `-F ` filter from cabextract — its pattern matches the full path inside the cab (e.g. src/driver/INF/x64/6_07f_... xpfwext) not just the basename, so -F would silently skip the target. Extract everything; the existing `find` afterward locates the file regardless of the subdirectory it landed in. --- bin/validity-sensors-firmware | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/bin/validity-sensors-firmware b/bin/validity-sensors-firmware index c9d357b..124fd75 100755 --- a/bin/validity-sensors-firmware +++ b/bin/validity-sensors-firmware @@ -59,10 +59,23 @@ def download_and_extract_fw(dev_type, fwdir, fwuri=None): raise Exception('Hash mismatch for driver download! Expected {}, got {}'.format( expected_hash, actual_hash)) - subprocess.check_call([ - 'innoextract', '--output-dir', fwdir, '--include', fwname, '--collisions', 'overwrite', - fwarchive - ]) + # Lenovo softpaqs are Inno Setup installers; HP softpaqs are CAB-wrapped + # self-extracting exes. Try innoextract first, fall back to cabextract. + try: + subprocess.check_call([ + 'innoextract', '--output-dir', fwdir, '--include', fwname, + '--collisions', 'overwrite', fwarchive + ], stderr=subprocess.DEVNULL) + except (subprocess.CalledProcessError, FileNotFoundError): + try: + # No -F filter: HP softpaqs nest the target under e.g. src/driver/INF/x64/, + # and cabextract -F matches the full path. Extract everything; the find + # call below locates the target file regardless of subdirectory. + subprocess.check_call(['cabextract', '-q', '-d', fwdir, fwarchive]) + except (subprocess.CalledProcessError, FileNotFoundError) as e: + raise Exception( + 'Failed to extract {} from {}: neither innoextract nor cabextract ' + 'could handle the archive ({}).'.format(fwname, fwarchive, e)) fwpath = subprocess.check_output(['find', fwdir, '-name', fwname]).decode('utf-8').strip() print('Found firmware at {}'.format(fwpath)) @@ -91,10 +104,16 @@ if __name__ == "__main__": if not dev_type: raise Exception('No supported validity device found') - try: - subprocess.check_call(['innoextract', '--version'], stdout=subprocess.DEVNULL) - except Exception as e: - print('Impossible to run innoextract: {}'.format(e)) + have_extractor = False + for tool in ('innoextract', 'cabextract'): + try: + subprocess.check_call([tool, '--version'], stdout=subprocess.DEVNULL) + have_extractor = True + break + except (subprocess.CalledProcessError, FileNotFoundError): + continue + if not have_extractor: + print('Need at least one of innoextract or cabextract installed.') sys.exit(1) with tempfile.TemporaryDirectory() as fwdir: From d1936a1c094af37e4a5f41b4937a48181afb516a Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Thu, 21 May 2026 13:06:57 +0100 Subject: [PATCH 05/16] Debian packaging: PAM auto-enable, cabextract dep, version 0.16~hp3 debian/python3-validity.postinst: run pam-auth-update --package --enable fprintd so sudo / screen-unlock / GNOME Settings fingerprint flows work immediately after install, without the user having to know about pam-auth-update. debian/control: recommend libpam-fprintd (the PAM module our postinst enables) and cabextract (the fallback extractor used by the new validity-sensors-firmware path for HP softpaqs). Bumped changelog through 0.16~hp1/hp2/hp3, with hp3 documenting the same-finger re-enroll fix shipped this release. --- debian/changelog | 38 ++++++++++++++++++++++++++++++++ debian/control | 1 + debian/python3-validity.postinst | 3 +++ 3 files changed, 42 insertions(+) diff --git a/debian/changelog b/debian/changelog index 8d7c453..534b4c2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,41 @@ +python-validity (0.16~hp3) noble; urgency=medium + + * sensor.enroll: replace existing finger record when re-enrolling the same + subtype for the same user. Without this, the chip's db rejects creating + a duplicate, surfacing as enroll-failed at the final stage after all + captures pass. The delete is performed inside do_create_finger (right + before db.new_finger) so the chip's enroll session isn't disrupted — + pre-deleting before EnrollStart caused subsequent captures to return + retry-scan indefinitely until the daemon was restarted. + + -- SimpleX-T Thu, 21 May 2026 23:00:00 +0100 + +python-validity (0.16~hp2) noble; urgency=medium + + * validity-sensors-firmware: fall back to cabextract when innoextract + cannot handle the archive. HP softpaqs (e.g. sp135736.exe used for + 138a:00ab and 06cb:00b7) are CAB-wrapped self-extracting exes, not + Inno Setup installers, so innoextract rejected them and the postinst + surfaced a noisy Python traceback. With the fallback, HP softpaq + extraction now works, and either extractor satisfies the runtime + requirement. + * Recommends: cabextract (so the fallback path is available by default). + + -- SimpleX-T Thu, 21 May 2026 11:00:00 +0100 + +python-validity (0.16~hp1) noble; urgency=medium + + * Add support for sensor type 0xd51 (138a:00ab, 06cb:00b7) — HP EliteBook + 840 G5 and related models. Fixes a chip-specific interrupt protocol + that caused fprintd-verify to hang forever on these devices. + * Add FIRMWARE_URIS entries for DEV_AB and DEV_B7 so that + validity-sensors-firmware no longer crashes with KeyError on these PIDs. + * Enable libpam-fprintd via pam-auth-update in postinst so that sudo / + screen-unlock prompt for fingerprint immediately after install. + * Recommend libpam-fprintd. + + -- SimpleX-T Wed, 20 May 2026 22:25:34 +0100 + python-validity (0.15~ppa2) noble; urgency=medium * Change all write paths to /var/run/python-validity diff --git a/debian/control b/debian/control index 2b09da3..99c2ac9 100644 --- a/debian/control +++ b/debian/control @@ -19,6 +19,7 @@ Depends: ${python3:Depends}, dbus, open-fprintd (>= 0.6~), innoextract (>= 1.6~) +Recommends: libpam-fprintd, cabextract Description: Validity Fingerprint Sensor DBus Driver This package adds support to some Validity sensors. . diff --git a/debian/python3-validity.postinst b/debian/python3-validity.postinst index ca62874..7dfc806 100644 --- a/debian/python3-validity.postinst +++ b/debian/python3-validity.postinst @@ -8,5 +8,8 @@ if [ "$1" = "configure" ]; then systemctl daemon-reload || true udevadm control --reload-rules || true udevadm trigger || true + if [ -x /usr/sbin/pam-auth-update ]; then + pam-auth-update --package --enable fprintd || true + fi fi From bc5dd32c734ccd7e71c84dd1c1a0685600837277 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Thu, 21 May 2026 13:18:37 +0100 Subject: [PATCH 06/16] Emit verify-retry-scan at most once per VerifyStart cycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pam_fprintd re-prints "Place your finger on the reader" for every verify-retry-scan signal it receives. On the 0xd51 chip the capture loop is chatty — a single 10-second verify window can fire the signal 20+ times, flooding the terminal during sudo authentication. Suppress all but the first verify-retry-scan per VerifyStart so the user sees one initial prompt and one early "place again" hint, then silence until match or timeout. Enrollment behavior is unchanged because per-stage retry hints are useful there (each stage is a discrete user action where lift-and-retry feedback matters). --- dbus_service/dbus-service | 13 ++++++++++++- debian/changelog | 12 ++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/dbus_service/dbus-service b/dbus_service/dbus-service index 1c63e8d..b30c24e 100755 --- a/dbus_service/dbus-service +++ b/dbus_service/dbus-service @@ -114,8 +114,19 @@ class Device(dbus.service.Object): self.VerifyFingerSelected('any') + # pam_fprintd re-prints "Place your finger on the reader" for every + # verify-retry-scan signal, which on this chip's chatty capture loop + # can fire 30+ times per verify cycle and floods the terminal during + # sudo. Emit it at most once per VerifyStart so the user gets one + # initial prompt + one early retry hint, then silence until match + # or timeout. Enroll keeps its per-stage signals — those are useful + # because each stage is a discrete user action. + retry_emitted = [False] + def update_cb(e): - self.VerifyStatus('verify-retry-scan', False) + if not retry_emitted[0]: + self.VerifyStatus('verify-retry-scan', False) + retry_emitted[0] = True def run(): try: diff --git a/debian/changelog b/debian/changelog index 534b4c2..ec18078 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +python-validity (0.16~hp4) noble; urgency=medium + + * VerifyStart: emit verify-retry-scan at most once per verify cycle so + pam_fprintd doesn't repeat "Place your finger on the reader" for every + one of the chip's chatty capture-quality retries. On the 0xd51 chip the + capture loop fires the signal 20+ times during a 10s timeout window, + which flooded the terminal during sudo. Enrollment is unchanged — + per-stage retry-scans there are useful because each stage is a discrete + user action. + + -- SimpleX-T Thu, 21 May 2026 23:30:00 +0100 + python-validity (0.16~hp3) noble; urgency=medium * sensor.enroll: replace existing finger record when re-enrolling the same From 122a147e7d196f008263760a22268a1f395b2464 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Thu, 21 May 2026 19:30:31 +0100 Subject: [PATCH 07/16] Exclude local build artifacts from the source tarball debian/source/options: tar-ignore .pybuild, build, *.egg-info, __pycache__, *.pyc. 3.0 (native) source packages bundle the working tree verbatim, so any leftover pybuild cache from prior local debuild runs gets shipped to Launchpad. pybuild's cached state embeds the developer's absolute paths (e.g. /home//.../python-validity/.pybuild/...), which the build chroot can't write to. The first per-series build succeeded because pybuild created the cache fresh; subsequent series builds inherited the dirty cache and failed with Permission denied trying to write into the baked-in path. This is a packaging-only change; no source code differs. --- debian/changelog | 12 ++++++++++++ debian/source/options | 5 +++++ 2 files changed, 17 insertions(+) create mode 100644 debian/source/options diff --git a/debian/changelog b/debian/changelog index ec18078..c34f7c7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +python-validity (0.16~hp5) noble; urgency=medium + + * debian/source/options: tar-ignore .pybuild, build, *.egg-info, + __pycache__, *.pyc. Without this, dpkg-source for 3.0 (native) + packages bundles leftover local build artifacts (cached pybuild + state with absolute paths from the developer's machine) into the + source tarball, which caused per-series Launchpad builds after + the first one to fail with "Permission denied" trying to write + into a baked-in /home//... path. + + -- SimpleX-T Thu, 21 May 2026 13:30:00 +0100 + python-validity (0.16~hp4) noble; urgency=medium * VerifyStart: emit verify-retry-scan at most once per verify cycle so diff --git a/debian/source/options b/debian/source/options new file mode 100644 index 0000000..8dc4dda --- /dev/null +++ b/debian/source/options @@ -0,0 +1,5 @@ +tar-ignore = ".pybuild" +tar-ignore = "build" +tar-ignore = "*.egg-info" +tar-ignore = "__pycache__" +tar-ignore = "*.pyc" From baec76e43d3f1cd1cf753e72927f46adbf26b70a Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Thu, 21 May 2026 23:53:55 +0100 Subject: [PATCH 08/16] Add diagnostic logging for chip geometry and per-verify retry count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sensor.py: log resolved capture geometry once at chip open. Useful when investigating whether the 0x199 spoof produces appropriate dimensions for whatever chip the daemon is talking to. The log line includes real_type vs spoofed_type, lines_2d from the capture program, computed lines_per_frame, bytes_per_line, line_width, and lines_per_calibration_data — everything needed to recognize a profile mismatch without re-instrumenting. dbus-service: in VerifyStart, log every internal chip retry-scan to the journal independent of the D-Bus signal throttle. Lets users and support tickets quantify capture quality without manual instrumentation — `journalctl -u python3-validity.service | grep 'Chip capture retry-scan'` gives the raw count immediately. Both changes are INFO-level log lines, no functional impact. About 1 log per chip open + 0-3 lines per verify in normal use. --- dbus_service/dbus-service | 8 ++++++++ debian/changelog | 16 ++++++++++++++++ validitysensor/sensor.py | 15 +++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/dbus_service/dbus-service b/dbus_service/dbus-service index b30c24e..98909e6 100755 --- a/dbus_service/dbus-service +++ b/dbus_service/dbus-service @@ -122,8 +122,16 @@ class Device(dbus.service.Object): # or timeout. Enroll keeps its per-stage signals — those are useful # because each stage is a discrete user action. retry_emitted = [False] + retry_count = [0] def update_cb(e): + # Always log every chip retry to journal — useful for the + # task #17 capture-quality benchmark. The D-Bus emit below + # is still throttled to once per cycle (avoids spamming + # pam_fprintd which re-prints the prompt on each signal). + retry_count[0] += 1 + logging.info('Chip capture retry-scan #%d (user=%s)', + retry_count[0], user) if not retry_emitted[0]: self.VerifyStatus('verify-retry-scan', False) retry_emitted[0] = True diff --git a/debian/changelog b/debian/changelog index c34f7c7..d8087e8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,19 @@ +python-validity (0.16~hp6) noble; urgency=medium + + * Diagnostic logging (no behavior change): + - sensor.py: log resolved capture geometry once at chip open + (real_type, spoofed_type, lines_per_frame, bytes_per_line, + line_width, calibration data lines). Makes future "is the + spoofed-to-0x199 profile right for this chip?" questions + answerable from journal alone. + - dbus-service: log every internal chip retry-scan during + VerifyStart, independent of the D-Bus signal throttle. + Lets users / support tickets quantify capture quality. + * Both changes are INFO-level log lines only. ~1 log per chip + open + ~0-3 lines per verify in normal use. + + -- SimpleX-T Thu, 21 May 2026 23:55:00 +0100 + python-validity (0.16~hp5) noble; urgency=medium * debian/source/options: tar-ignore .pybuild, build, *.egg-info, diff --git a/validitysensor/sensor.py b/validitysensor/sensor.py index 984e5ff..7ed5e79 100644 --- a/validitysensor/sensor.py +++ b/validitysensor/sensor.py @@ -266,6 +266,21 @@ def open(self): self.lines_per_frame = lines_2d * self.type_info.repeat_multiplier self.bytes_per_line = self.type_info.bytes_per_line + # Diagnostic (task #17): log resolved capture geometry so we can + # tell whether the 0x199-spoofed profile matches what the 0xd51 + # chip actually expects. lines_2d is extracted from the capture + # program's 0x2f chunk; if the chip is producing a different + # frame size, this is where the mismatch first shows up. + logging.info( + 'Capture geometry: real_type=0x%x spoofed_type=0x%x ' + 'lines_2d=%d repeat_multiplier=%d lines_per_frame=%d ' + 'bytes_per_line=0x%x line_width=%d ' + 'lines_per_calibration_data=%d', + self.real_device_type, self.device_info.type, lines_2d, + self.type_info.repeat_multiplier, self.lines_per_frame, + self.bytes_per_line, self.type_info.line_width, + self.type_info.lines_per_calibration_data) + factory_bits = get_factory_bits(0x0e00) self.factory_calibration_values = factory_bits[3][4:] From cd0def0702b21c3d741960e813d59ae42faf11dc Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Sun, 24 May 2026 21:30:51 +0100 Subject: [PATCH 09/16] usb.py: defensive USB reset at open_dev() entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multiple users reported the 0xd51-family chips (138a:00ab, 06cb:00b7) getting stuck after cold boot, unclean exit, or suspend/resume — the chip accepts the bulk-OUT but never responds on bulk-IN, so the first cleartext cmd (cmd 3e get_flash_info) times out and the daemon restart-loops every 15 seconds. The workaround users were running manually is a USB-level reset: sudo systemctl stop python3-validity open-fprintd sudo udevadm trigger --attr-match=idVendor=138a --attr-match=idProduct=00ab sudo systemctl start python3-validity open-fprintd The reset call here is the in-driver equivalent. The chip's USB address can shift after reset, so we re-find by vid/pid. Reported by Killersparrow1 (issue #238, Fedora 44, sensor vanishes on reboot) and a separate Arch / ZBook G5 user (USBTimeoutError on cmd 3e). The patch matches what the project memory has flagged for the past two sessions as "kept local and not in PR" — turns out it was the actually-load-bearing piece. Locally confirmed: clean daemon restart, sudo matches in 1 retry, no traceback, no "USB reset failed" warning. --- debian/changelog | 21 +++++++++++++++++++++ validitysensor/usb.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/debian/changelog b/debian/changelog index d8087e8..c3becb3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,24 @@ +python-validity (0.16~hp7) noble; urgency=medium + + * usb.py: defensive USB reset at the start of open_dev(). The 0xd51- + family chips (138a:00ab, 06cb:00b7) can be left in a "stuck" + protocol state across cold boot, unclean exit of the daemon, or + sudden suspend/resume — in that state the chip accepts the + bulk-OUT but never replies on bulk-IN, so the first cleartext + command (get_flash_info, cmd 3e) times out and the daemon enters + a 15-second restart loop. The reset is the in-driver equivalent + of the manual `udevadm trigger --attr-match=idVendor=... + --attr-match=idProduct=...` workaround multiple users were + running to recover after every reboot. Reported by Killersparrow1 + (uunicorn/python-validity#238, Fedora 44, sensor vanishes on + reboot) and a separate Arch / ZBook G5 user (USBTimeoutError on + cmd 3e). Also observed intermittently on the maintainer's + machine. + * After reset, re-find the device by vid/pid because the USB + address can shift across the reset. + + -- SimpleX-T Sun, 25 May 2026 00:30:00 +0100 + python-validity (0.16~hp6) noble; urgency=medium * Diagnostic logging (no behavior change): diff --git a/validitysensor/usb.py b/validitysensor/usb.py index 22bca4f..0cd293e 100644 --- a/validitysensor/usb.py +++ b/validitysensor/usb.py @@ -1,5 +1,6 @@ import errno import logging +import time import typing from binascii import hexlify, unhexlify from enum import Enum @@ -63,6 +64,34 @@ def open_dev(self, dev: ucore.Device): if dev is None: raise Exception('No matching devices found') + # Defensive USB reset on init. + # + # The 0xd51-family chips (HP 138a:00ab / 06cb:00b7) can be left in + # a "stuck" protocol state across a previous unclean exit of this + # daemon, a cold boot, or a sudden suspend/resume. In that state + # the chip accepts the bulk-OUT but never replies on bulk-IN, so + # the very first cleartext command (typically `cmd 3e` + # get_flash_info) times out — the daemon then restart-loops at + # 15s intervals and the sensor is "vanished" until a manual USB + # reset. This block is the in-driver equivalent of the manual + # `udevadm trigger --attr-match=idVendor=... --attr-match=idProduct=...` + # workaround users have been running to recover. + # + # Reported by Killersparrow1 (#238, Fedora 44, vanishes on reboot) + # and Maarten (Arch, ZBook G5, USBTimeoutError on first 3e). Also + # observed locally on the maintainer's machine (sensor prompts but + # doesn't detect after a while). + try: + vid, pid = dev.idVendor, dev.idProduct + dev.reset() + time.sleep(0.5) + # USB address may shift after reset; re-find by vid/pid. + dev = ucore.find(idVendor=vid, idProduct=pid) + if dev is None: + raise Exception('Device disappeared after USB reset') + except USBError as e: + logging.warning('open_dev: USB reset failed (often non-fatal): %s', e) + self.dev = dev self.dev.default_timeout = 15000 dev.set_configuration() From 0c00f0a6d5ae9070c9da46b40af1296bf0b4f97c Mon Sep 17 00:00:00 2001 From: "Gary T. Giesen" Date: Mon, 6 Jul 2026 10:33:40 -0400 Subject: [PATCH 10/16] Add sensor type 0x969 (HP ZBook Studio x360 G5, 138a:00ab) The 138a:00ab USB ID is shared across HP machines but maps to different sensor silicon. The EliteBook 840 G5 reports sensor type 0xd51; the ZBook Studio x360 G5 reports 0x969. Both lack native SensorTypeInfo / SensorCaptureProg entries, and empirically both work when aliased to the 0x199 capture profile -- the on-chip matcher accepts the resulting images. Mirror the existing 0xd51 handling for 0x969: - add 0x969 to line_update_type1_devices - alias 0x969 -> 0x199 in open() - accept the b[0]==3 finger interrupt when real_device_type is 0x969 Verified on an HP ZBook Studio x360 G5: init + calibration + enroll (5 stages, enroll-completed) + verify (verify-match). --- validitysensor/sensor.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/validitysensor/sensor.py b/validitysensor/sensor.py index 7ed5e79..f5ff2ff 100644 --- a/validitysensor/sensor.py +++ b/validitysensor/sensor.py @@ -26,6 +26,7 @@ line_update_type1_devices = [ 0xB5, 0x885, 0xB3, 0x143B, 0x1055, 0xE1, 0x8B1, 0xEA, 0xE4, 0xED, 0x1825, 0x1FF5, 0x199, 0xD51, # HP EliteBook 840 G5 (138a:00ab) / HP G6 series (06cb:00b7) + 0x969, # HP ZBook Studio x360 G5 (138a:00ab -- same PID, different silicon) ] @@ -227,14 +228,15 @@ def open(self): self.device_info = identify_sensor() self.real_device_type = self.device_info.type - # Sensor type 0xd51 (HP EliteBook 840 G5 138a:00ab, HP G6 series 06cb:00b7) - # has no native SensorTypeInfo / SensorCaptureProg entry. Empirically the - # 0x199 profile produces images that the on-chip matcher accepts after - # enrollment/verify; the 0xdb profile does not. Spoofing keeps the rest - # of this method (calibration switch, capture program lookup) on a code - # path that works. - if self.device_info.type == 0xd51: - logging.info('Sensor type 0xd51 — aliasing to 0x199 profile') + # Sensor types 0xd51 (HP EliteBook 840 G5 138a:00ab, HP G6 series + # 06cb:00b7) and 0x969 (HP ZBook Studio x360 G5 138a:00ab -- same PID, + # different silicon) have no native SensorTypeInfo / SensorCaptureProg + # entry. Empirically the 0x199 profile produces images that the on-chip + # matcher accepts after enrollment/verify; the 0xdb profile does not. + # Spoofing keeps the rest of this method (calibration switch, capture + # program lookup) on a code path that works. + if self.device_info.type in (0xd51, 0x969): + logging.info('Sensor type 0x%x — aliasing to 0x199 profile' % self.device_info.type) self.device_info.type = 0x199 logging.info('Opening sensor: %s' % self.device_info.name) @@ -738,7 +740,7 @@ def capture(self, mode: CaptureMode) -> typing.Tuple[int, int, int, int]: b = usb.wait_int() if b[0] == 2: break - if b[0] == 3 and getattr(self, 'real_device_type', None) == 0xd51: + if b[0] == 3 and getattr(self, 'real_device_type', None) in (0xd51, 0x969): saved_b = b break From 3b231940082ffcad45bb5e24562d47a347c23807 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Tue, 7 Jul 2026 22:16:28 +0100 Subject: [PATCH 11/16] sensor.py: recover 0x969 chips reporting 0x199 post-resume After suspend/resume, 0x969 chips re-enumerate reporting sensor type 0x199 directly rather than 0x969. Without intervention Sensor.open() skips the alias-to-0x199 block and capture()'s b[0]==3 interrupt fix (which keys off real_device_type), so verify hangs indefinitely post-resume. Detect via the device *name*, which is stable across boot and resume: FM-3439-xxx and FM- 154-xxx are the two known 0x969 model families (HP ZBook 17 G6, ZBook Studio G5, ProBook G6). Genuine 0x199 chips (FM-3367-xxx, FM-3380-xxx, FM-155-xxx) don't match either pattern. Diagnosed and tested by @Karloss1234 on Kubuntu 26.04 with an HP ZBook 17 G6 (06cb:00b7). Reported at Point 4 of the review on uunicorn/python-validity#256. 0xd51 users: if verify hangs after resume, please report; we may need the same treatment for 'FM-154-xxx' (no space). --- validitysensor/sensor.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/validitysensor/sensor.py b/validitysensor/sensor.py index f5ff2ff..23d760a 100644 --- a/validitysensor/sensor.py +++ b/validitysensor/sensor.py @@ -238,6 +238,24 @@ def open(self): if self.device_info.type in (0xd51, 0x969): logging.info('Sensor type 0x%x — aliasing to 0x199 profile' % self.device_info.type) self.device_info.type = 0x199 + elif self.device_info.type == 0x199 and ( + 'FM-3439' in self.device_info.name or 'FM- 154' in self.device_info.name): + # After suspend/resume, 0x969 chips re-enumerate reporting sensor + # type 0x199 directly rather than 0x969. Without intervention the + # alias block above is skipped, real_device_type stays at 0x199, + # and capture()'s `b[0]==3` interrupt fix (which keys off + # real_device_type) is bypassed — so verify hangs indefinitely + # post-resume. The device *name* is stable across boot and resume, + # so we key off it: FM-3439-xxx and FM- 154-xxx are the two known + # 0x969 model families (HP ZBook 17 G6, ZBook Studio G5, ProBook + # G6). Genuine 0x199 chips (FM-3367-xxx, FM-3380-xxx, FM-155-xxx) + # don't match either pattern. + # + # 0xd51 users: if verify hangs after resume, please report — we + # may need the same treatment for 'FM-154-xxx' (no space). + logging.info('Sensor %s reporting 0x199 on resume — treating as 0x969' + % self.device_info.name.strip()) + self.real_device_type = 0x969 logging.info('Opening sensor: %s' % self.device_info.name) self.type_info = SensorTypeInfo.get_by_type(self.device_info.type) From 4ba618bda8a36c1e72464b5217c0b3eb3e257692 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Tue, 7 Jul 2026 22:17:47 +0100 Subject: [PATCH 12/16] Surface 0404 at reset_blob with an actionable error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both @bcoutts (fresh factory 0xd51 on ProBook 445R G6) and @ntoyiakhona06-creator (Windows-Hello-paired 0xd51 on EliteBook 840 G5) hit the same failure: usb.cmd(reset_blob) returns status 0404 — from opposite chip states (factory-fresh vs. previously paired). reset_blob is byte-identical across blobs_97 / blobs_9a / blobs_9d (shared "Prometheus" blob), and was originally extracted from Windows drivers for 0x199-class chips. The failure is not blob content or selection — it's that 0xd51 / 0x969 silicon does not accept this reset primitive at all. Until someone extracts a working reset_blob for the newer chip family, init_flash on a factory-fresh chip and factory_reset for unpair-from-Windows are both unsupported on this hardware. Catch 0404 at both call sites and raise a specific, actionable error (pointing at the tracking PR and telling users what to attach when reporting) instead of the bare "Failed: 0404" that assert_status produces. Also add a "known issue" section to the README. References: uunicorn/python-validity#256 (bcoutts, ntoyiakhona06-creator comments). --- validitysensor/init_flash.py | 20 +++++++++++++++++++- validitysensor/sensor.py | 21 ++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/validitysensor/init_flash.py b/validitysensor/init_flash.py index 61c918b..a4937d4 100644 --- a/validitysensor/init_flash.py +++ b/validitysensor/init_flash.py @@ -127,7 +127,25 @@ def init_flash(): else: logging.info('Flash was not initialized yet. Formatting...') - assert_status(usb.cmd(reset_blob)) + rsp = usb.cmd(reset_blob) + status, = unpack(' Date: Tue, 7 Jul 2026 22:18:01 +0100 Subject: [PATCH 13/16] db.py: fix del_record; raise a clear error when the DB partition is full MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related flash-management issues found by @Karloss1234 while tracing 04b5 errors during enroll on an HP ZBook 17 G6 (Point 6 of the review on uunicorn/python-validity#256). 1. del_record was sending 0x48 without the db_write_enable prefix that new_record and every write in flash.py use. On 0x969 chips this returns error 04b6 (silent) and the record stays on flash — deletes never actually free space. Mirror new_record's shape: db_write_enable + try/finally: call_cleanups(). 2. Even with (1) fixed, the chip does not automatically compact the database partition. Windows only triggers compaction as part of its own enroll flow after a TPM reset, so a long-running Linux- only setup accumulates uncompacted dead records until enroll fails at new_finger with the chip's opaque 04b5. new_record was already fetching db_info() with a TODO to actually check the numbers — actually check them. If the payload plus a small header estimate doesn't fit in Info.free, raise a clear error that names the recovery path (erase_flash(4) + reboot), instead of letting the on-wire 04b5 bubble up. --- validitysensor/db.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/validitysensor/db.py b/validitysensor/db.py index accda03..fa20c80 100644 --- a/validitysensor/db.py +++ b/validitysensor/db.py @@ -196,7 +196,16 @@ def get_record_children(self, dbid: int): return rec def del_record(self, dbid: int): - assert_status(tls.cmd(pack(' Date: Tue, 7 Jul 2026 22:18:14 +0100 Subject: [PATCH 14/16] README: document 0xd51/0x969 caveats surfaced by PR #256 reviewers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three additions from the discussion on uunicorn/python-validity#256: - Template competition (Windows Hello vs. Linux fprintd sharing the on-chip database) with the "enroll different fingers per OS" and "erase partition 4" workarounds. From @Karloss1234's Point 2. - Kubuntu / KDE lock-screen PAM setup — three /etc/pam.d files and a pam_permit.so → pam_unix.so sddm-greeter fix. Not needed on GNOME. From @Karloss1234's Point 5. - Known issue: 0404 on reset_blob for 0xd51/0x969 silicon (factory init and factory_reset are unsupported on this chip family). Tells users what to attach when reporting. Code changes for the same items are in prior commits; this is docs. --- README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/README.md b/README.md index e327899..1cd7439 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,30 @@ $ sudo systemctl enable open-fprintd-resume open-fprintd-suspend For even more error procedures, check [this Arch comment thread](https://aur.archlinux.org/packages/python-validity/#comment-755904) or [this python-validity bug comment thread](https://github.com/uunicorn/python-validity/issues/3). +#### `factory_reset` / `init_flash` fails with `0404` + +On 0xd51 and 0x969 silicon (HP EliteBook 840 G5, HP G6 family, +HP ZBook Studio x360 G5, and likely other 138a:00ab / 06cb:00b7 variants) +the `reset_blob` we ship — extracted from Windows drivers for older +0x199-class Prometheus chips — is rejected by the chip with status `0404`. +This affects two scenarios: + +- **Factory-fresh chip** (e.g. after a UEFI BIOS reset). `init_flash` + cannot format the flash and the daemon crash-loops. +- **Windows-Hello-paired chip.** After hitting the "Signature verification + failed" error, users typically try `playground/factory-reset.py`; on + these chips it fails at the very first command with `0404`. + +There is currently **no known Linux-side workaround** — we do not have a +reset_blob known to work on 0xd51 / 0x969. If you hit this, please add +your hardware details (`dmidecode -t 1`, `lsusb -v`, and the failing +journal output) to +[uunicorn/python-validity#256](https://github.com/uunicorn/python-validity/pull/256) +so affected models can be tracked. Windows-paired users can, as a +workaround, boot Windows and reinstall the Synaptics driver (Device +Manager → uninstall with "delete driver software" → reboot → let Windows +reinstall) to re-pair the chip on the Windows side. + ## Enabling fingerprint for system authentication if it doesn't come automatically, you might need to make changes to files in `/etc/pam.d` to enable fingerprint login (depending on your distro). @@ -137,6 +161,49 @@ user_to_sid: ``` Note the indentation; each entry has to be preceded by at least one space. +### Template competition (0xd51 / 0x969 chips) + +The chip's on-chip matcher scores captured images against **every** enrolled +template — including any Windows Hello templates written by a previous +Windows session — and returns the highest-scoring match. On some HP models +(reported for the ZBook G6 family, but likely broader) Windows Hello writes +very high-quality templates that consistently outscore Linux `fprintd` +templates for the same finger, so `fprintd-verify` silently loses even when +enrollment succeeded. + +Two workarounds, in order of preference: + +1. **Enroll different fingers per OS.** Right-index in Linux, right-middle + in Windows (or whichever split you prefer). No competition, both OSes + keep fingerprint auth. +2. **Erase the on-chip database from Linux.** Wipes all templates on both + OSes; Windows Hello fingerprint login stops working until you re-enroll + in Windows. PIN / TPM state is unaffected. See + `playground/erase-flash.py` (partition `4`). + +Investigated and documented by @Karloss1234 on PR +[uunicorn/python-validity#256](https://github.com/uunicorn/python-validity/pull/256). + +### KDE / Kubuntu lock-screen PAM + +On Kubuntu the greeter/lock-screen PAM stacks aren't touched by +`pam-auth-update`. To wire the fingerprint reader into the KDE lock screen +you need three files under `/etc/pam.d` mirroring the same `sufficient` +line: + +``` +# /etc/pam.d/kde, /etc/pam.d/kde-fingerprint, /etc/pam.d/kde-smartcard +#%PAM-1.0 +auth sufficient pam_fprintd.so max_tries=3 timeout=10 +auth required pam_unix.so +``` + +Also check `/etc/pam.d/sddm-greeter` for a `pam_permit.so` fallback and +replace it with `pam_unix.so` — otherwise the lock screen can unlock +without authentication after fingerprint timeout. + +Contributed by @Karloss1234; not required on GNOME / Ubuntu proper. + ## Playground This package contains a set of scripts you can use to do a low-level debugging of the sensor protocol. From 7925b97ac4aeb0db1a5b80679703afb813251520 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Tue, 7 Jul 2026 23:26:25 +0100 Subject: [PATCH 15/16] dbus-service: watchdog on VerifyStart for wedged-chip state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0xd51 / 0x969 chips can enter a "wedged capture-quality gate" state after long uptime + heavy dev cycles: every captured frame is rejected by the on-chip quality gate and no capture-complete interrupt is ever emitted, so sensor.identify() loops in wait_int() indefinitely. Nothing the daemon can do from userspace clears this — even sensor.cancel() only unblocks wait_int; the wedge itself survives systemctl restart. Recovery is a real cold power cycle (shutdown, unplug charger, hold power ~15s, boot). Historically the daemon-side identify() thread would keep running past pam_fprintd's 10s timeout, and diagnosing the state took ~30 minutes of guessing because the journal just showed retry-scan repetitions with no summary. Add a retry-count watchdog to VerifyStart's update_cb: after 25 consecutive retries with no capture-complete (typically 12-15s), log a specific warning naming the wedge condition and the required recovery path, and call sensor.cancel() to unblock the identify() thread. The outer except in run() then emits verify-no-match cleanly. 25 is empirically past the tail of a healthy verify (~2 retries typical, p95 = 2 in the task #17 benchmark) with comfortable margin. Enrollment path is left alone — its retry-scans are per-stage user actions, not the same signature. Diagnosed on the maintainer's own 840 G5 (0xd51) after 4 days of uptime + a diagnostic init.open() that didn't clean up. --- dbus_service/dbus-service | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/dbus_service/dbus-service b/dbus_service/dbus-service index 98909e6..41aed4d 100755 --- a/dbus_service/dbus-service +++ b/dbus_service/dbus-service @@ -124,6 +124,19 @@ class Device(dbus.service.Object): retry_emitted = [False] retry_count = [0] + # Watchdog: 0xd51 / 0x969 chips can enter a "wedged capture-quality + # gate" state after long uptime + heavy dev cycles, where every + # captured frame is rejected and no capture-complete interrupt is + # ever emitted. sensor.identify() then loops in wait_int() until + # something external cancels it (pam_fprintd's 10s timeout). This + # threshold lets us abort daemon-side after ~25 rejected frames + # (typically 12-15s), emit a specific journal message pointing at + # the real recovery path (cold power cycle, not a systemctl + # restart), and unblock the identify() thread cleanly. See the + # discussion on PR uunicorn/python-validity#256 for the wedge + # signature and recovery notes. + CHIP_WEDGE_RETRY_THRESHOLD = 25 + def update_cb(e): # Always log every chip retry to journal — useful for the # task #17 capture-quality benchmark. The D-Bus emit below @@ -132,6 +145,18 @@ class Device(dbus.service.Object): retry_count[0] += 1 logging.info('Chip capture retry-scan #%d (user=%s)', retry_count[0], user) + if retry_count[0] == CHIP_WEDGE_RETRY_THRESHOLD: + logging.warning( + 'Chip appears wedged after %d consecutive retries with ' + 'no capture-complete interrupt. Aborting verify. This ' + 'usually means the on-chip capture-quality gate is ' + 'rejecting every frame, which the daemon cannot reset ' + 'from userspace. Recovery: cold power cycle (shutdown, ' + 'unplug charger, hold power ~15s, boot). A `systemctl ' + 'restart python3-validity` is unlikely to help; the ' + 'wedge is below the daemon.', + retry_count[0]) + sensor.cancel() if not retry_emitted[0]: self.VerifyStatus('verify-retry-scan', False) retry_emitted[0] = True From a825eaf96dd1f480a304e0570e686abae0400932 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Tue, 7 Jul 2026 23:28:26 +0100 Subject: [PATCH 16/16] debian/changelog: 0.16~hp8 for noble Rolls up: - 0x969 support (ggiesen SimpleX-T#2, Karloss1234) - post-resume 0x969 recovery (Karloss1234) - 0404 friendlier error at reset_blob (bcoutts, ntoyiakhona06) - del_record db_write_enable fix + flash-full pre-check (Karloss1234) - VerifyStart watchdog for wedged-chip state - README caveats for 0xd51/0x969 Full context in uunicorn/python-validity#256. --- debian/changelog | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/debian/changelog b/debian/changelog index c3becb3..70af3ae 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,64 @@ +python-validity (0.16~hp8) noble; urgency=medium + + * sensor.py: add support for sensor type 0x969 alongside 0xd51. HP + ZBook Studio x360 G5 uses the same 138a:00ab PID but a different + silicon variant reporting 0x969; HP G6 family on 06cb:00b7 also + covers this variant. Mirrors the existing 0xd51 handling across + line_update_type1_devices, the alias-to-0x199 block in + Sensor.open(), and the b[0]==3 interrupt fix in Sensor.capture(). + Contributed by @ggiesen (SimpleX-T/python-validity#2), verified + on HP ZBook Studio x360 G5. Also independently developed by + @Karloss1234 on HP ZBook 17 G6. + * sensor.py: recover 0x969 chips reporting 0x199 post-resume. On + suspend/resume, 0x969 chips re-enumerate reporting sensor type + 0x199 directly, causing Sensor.open() to skip the alias + + interrupt fix. Detect via the device name (FM-3439-xxx or + FM- 154-xxx — the two known 0x969 model families) and restore + real_device_type = 0x969. Diagnosed by @Karloss1234 on Kubuntu + 26.04. + * init_flash.py, sensor.py: catch status 0404 from usb.cmd(reset_ + blob) at init_flash() and factory_reset(), raise an actionable + error naming the chip-family limitation (0xd51 / 0x969 silicon + does not accept the reset_blob shipped by this driver, which + was extracted from Windows drivers for older 0x199-class chips). + No fix — until a working reset_blob for the newer chips is + obtained, init on factory-fresh chips and factory_reset for + Windows-Hello-paired chips remain unsupported on this family. + But the daemon no longer surfaces a bare "Failed: 0404" that + users can't act on. Reported by @bcoutts (fresh chip, HP ProBook + 445R G6) and @ntoyiakhona06-creator (Windows-paired 840 G5). + * db.py: fix del_record missing db_write_enable. On 0x969 chips + the delete cmd (0x48) returns 04b6 silently without the + write-enable prefix, so records marked deleted never actually + freed flash space. Mirror new_record's shape: db_write_enable + + try/finally: call_cleanups(). Diagnosed by @Karloss1234 while + tracing 04b5 flash-full errors on enroll. + * db.py: proactive flash-full check in new_record. The chip does + not automatically compact the database partition after deletes, + so long-running Linux-only installs accumulate uncompacted + dead records until enroll fails at new_finger with an opaque + 04b5. Use the db_info() call that was already there (with a + TODO to check the numbers) to actually check them; raise a + clear error naming the recovery path (erase_flash(4) + reboot) + before the on-wire failure. + * dbus-service: watchdog on VerifyStart. 0xd51 / 0x969 chips can + enter a "wedged capture-quality gate" state after long uptime + + heavy dev cycles where every captured frame is rejected and no + capture-complete interrupt is ever emitted — sensor.identify() + then loops in wait_int() indefinitely. Add a retry-count + threshold (25 consecutive retries, ~12-15s) that calls + sensor.cancel() and logs a specific "chip appears wedged; cold + power cycle required" warning to the journal. Recovery is a + real cold power cycle (shutdown, unplug charger, hold power + ~15s, boot) — the wedge sits below the daemon and no systemctl + restart can clear it. + * README: document Windows Hello template competition workarounds + (enroll different fingers per OS, or erase partition 4), the + Kubuntu / KDE lock-screen PAM stack recipe, and the known 0404 + reset_blob issue. + + -- SimpleX-T Tue, 07 Jul 2026 23:27:29 +0100 + python-validity (0.16~hp7) noble; urgency=medium * usb.py: defensive USB reset at the start of open_dev(). The 0xd51-