From b850fd174f767c3621a4ce09c9073db88efa0079 Mon Sep 17 00:00:00 2001 From: SimpleX-T Date: Tue, 19 May 2026 08:45:13 +0100 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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()