-
-
Notifications
You must be signed in to change notification settings - Fork 47
Add driver_53xc.py — image capture for 27c6:533c (GF5288_GM168SEC) - #75
Add driver_53xc.py — image capture for 27c6:533c (GF5288_GM168SEC) #75nikicat wants to merge 5 commits into
Conversation
Every Device method assumes the sensor sends an ACK frame before the reply: if isinstance(self.protocol, protocol.USBProtocol): check_ack( check_message_protocol( check_message_pack(self.protocol.read()), COMMAND_ACK), COMMAND_FIRMWARE_VERSION) That assumption does not hold for every device. 27c6:533c (GF5288_GM168SEC, Dell XPS 13 9310) ACKs reset and read_sensor_register but answers firmware_version and enable_chip with the reply alone. The unconditional read then consumes that reply, fails to parse it as an ACK, and the command errors out on a perfectly healthy sensor: firmware_version() -> ValueError: Invalid message protocol enable_chip(True) -> USBTimeoutError: [Errno 110] Operation timed out Add Device._expect_ack(), which reads one frame and puts it back via Device.read() when it turns out not to be an ACK, so the caller reads it as the reply. Sensors that do ACK are unaffected -- the ACK is consumed and verified exactly as before. This is strictly more permissive, so nothing that worked before changes. It also collapses 31 five-line blocks into one-liners, for a net -88 lines. Verified on 27c6:533c, all six commands now succeeding where two used to fail: nop OK enable_chip OK (was: USBTimeoutError) firmware_version OK GF5288_GM168SEC_APP_13016 (was: ValueError) reset OK (True, 1024) read_sensor_register OK 0c a1 00 22 read_otp OK 68 e6 86 4a 54 ec 15 04 ... Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
...d protocol 533c (tested on a Dell XPS 13 9310; also reported for the XPS 13 9300 and XPS 15 9500) has no open driver and its chip model cannot be read passively -- the USB descriptors say only "Goodix/FingerPrint". identifier.py asks the sensor directly, trying both message framings. For 533c the result is: Firmware version: GF5288_GM168SEC_APP_13016 Chip ID: 0x220ca1 Sensor speaks the wrapped protocol So it is GF5288 silicon -- same chip as the 53x5 family, and 0x220ca1 >> 8 is the 0x220C that driver_53x5.py already accepts as sensor type 9 -- but a different secure element (GM168SEC vs HTSEC) reached over the wrapped framing rather than the direct one driver_53x5.py is built on. Neither existing driver matches as-is; a 53xc driver is 53x5's device logic on goodix.py's transport. One framing delta found already: goodix.Device.firmware_version() expects an ACK frame before the data reply, and 533c replies with no ACK, so identifier.read_firmware_version() skips ACKs instead of requiring one. Read-only by construction: ping, firmware version, sensor reset and register reads. No firmware erase and no PSK write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
recon.py reports what a sensor will tell you without writing anything: firmware version, PSK slot state, chip ID, OTP, and a firmware readback attempt. The readback matters because mcu_erase_app is irreversible without an image to flash back and upstream ships none (firmware/ is empty, *.bin is gitignored) -- so it is worth knowing whether a device will hand its firmware over before considering any provisioning. On 27c6:533c it will not: Firmware: GF5288_GM168SEC_APP_13016 PSK 0xbb020003 / 7 / 1 / 2: not present Chip ID: 0c a1 00 22 (0x220ca1 -- what driver_53x5 expects) OTP (32 bytes): 68 e6 86 4a 54 ec 15 04 ... Firmware dumped: 0 bytes (read_firmware times out in APP mode) identifier.py carried its own ACK-tolerant firmware_version reader as a workaround; the previous commit fixes that in goodix.py, so it now calls device.firmware_version() directly. Note the PSK slots report nothing at the flag values 51x7 and 55x4 use. A USB capture of the vendor driver shows it sending a 17-byte preset_psk_read request with extra fields where goodix.py sends 8, which is the likely reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
preset_psk_read takes optional length and offset. Without them goodix.py sends the 8-byte short form and 533c answers "not present" even though a PSK is there -- which is what recon.py was doing, and why it reported empty slots. A USB capture of the vendor driver shows it sending the 16-byte form, and supplying length/offset makes goodix.py behave identically: preset_psk_read(0xbb020001, 32, 0) -> 32-byte hash preset_psk_read(0xbb010002, 102, 0) -> 102-byte wrapped PSK blob On 533c the hash is: 66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925 which is sha256 of 32 zero bytes -- the sensor already holds the all-zero PSK that driver_51x7 and driver_55x4 provision. recon.py now says so explicitly, because knowing it beforehand means not needing preset_psk_write at all, and on this device provisioning is irreversible (read_firmware returns nothing in APP mode, so there is no image to flash back). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
533c is GF5288 silicon (chip 0x220ca1, same as driver_53x5 expects) with a GM168SEC secure element, speaking the wrapped framing rather than the direct one. So this is 53x5's device logic on 51x7's transport. Verified end to end on a Dell XPS 13 9310: Firmware: GF5288_GM168SEC_APP_13016 PSK: all-zero, as expected Reset OK, number 1024 Chip ID: 0c a1 00 22 TLS established FDT template: 9a 9a a6 a6 a2 a2 98 98 97 97 a4 a4 a1 a1 ... Capturing reference frame (no finger)... gain 0xc2: 14334 B encrypted -> 14260 B plain Waiting for finger -- touch the sensor... gain 0x86: 14334 B encrypted -> 14260 B plain Wrote fingerprint.pgm Four things differ from the existing wrapped drivers: 1. No provisioning. 533c already holds the all-zero PSK, and read_firmware returns nothing in APP mode, so there is no image to flash back if a write went wrong. The driver checks the PSK hash and refuses to run if it does not match, rather than calling preset_psk_write. 2. TLS application data is decrypted in process from the session keys, derived from the randoms observed while relaying the handshake, rather than read back from openssl's stdout. openssl is still used as the TLS server. This keeps the image bytes on a single code path that can be tested offline against a capture, instead of depending on how the subprocess frames what it echoes. 3. The FDT threshold template is derived, not hardcoded. The baseline reply is 12-bit samples; halving each and emitting it twice reproduces exactly what the vendor driver sends. 4. Finger detection polls. The sensor answers fdt_down only when a finger lands, well past the 5 s default USB read timeout. Images need a reference frame subtracted -- raw captures are dominated by fixed-pattern noise. Fitting scale and offset by least squares absorbs the gain difference too, so one reference frame serves every exposure (the vendor sweeps gain 0xc2 / 0xad / 0x86). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
daemonhorn
commented
Aug 22, 2026
Nice work -- this is a much cleaner solution than what I independently arrived at for the same device (I'd opened #76 with a driver built around shelling out to openssl s_server and reading its stdout, plus local per-driver ACK-order workarounds instead of #72's library-level fix; closing it in favor of this stack). A couple of things from my side that might still be useful, from static analysis of the closed .so plus a live capture of the vendor driver's own traffic against a second, different physical 533c unit:
Independent cross-validation on different hardware: all-zero PSK already provisioned and 108x88 sensor size both confirmed on my unit too (not an XPS 13 9310) -- for the PSK that's from a live vendor-driver USB capture that never calls COMMAND_PRESET_PSK_WRITE_R at all, and for the dimensions it's from reshaping decoded pixel data at each plausible width and observing which one produces a coherent 2D shape rather than pure banding. Good sign this isn't specific to one laptop model.
PSK-write ACK convention, in case it's ever needed: this chip's COMMAND_PRESET_PSK_WRITE_R response uses the opposite success convention from every other model goodix.py was written against -- message[0] == 0x01 means success here, not the 0x00 that Device.preset_psk_write() checks for. I only found this because an early write attempt that goodix.py logged as "failed" had actually succeeded (confirmed via a live vendor capture showing the device already held the PSK it was supposedly failing to write). Your driver's design of refusing to run rather than provisioning sidesteps needing this, but it'd bite anyone who reuses preset_psk_write()'s return value directly against this PID.
DEVICE_CONFIG template locations, as a fallback for machines without a working vendor driver to sniff: the closed .so (libfprint-tod-goodix-53xc-0.0.4.so) has six named config templates in .rodata, keyed by what look like project codenames -- MilanF, MilanG, MilanH, MilanL, MilanFn, ChicagoHS -- at fixed offsets. MilanFn (offset 0x103380) is what got accepted (post-checksum-fix, per the shared fix_config_checksum algorithm) on my 0x533c unit, though without live vendor traffic to compare against I can't tell whether it happened to already carry usable per-unit calibration or whether the resulting image would need driver_53x5.py-style OTP splicing to look as good as what you're getting from a live-captured config. Might be a useful bootstrap for 530c/538c owners who don't have easy access to a live vendor-driver capture setup. Full details (including which offset maps to which candidate) at https://github.com/daemonhorn/goodix-533c-re/blob/main/findings/device-config.md if useful -- happy to dig further into any of it if it'd help.
daemonhorn
commented
Aug 22, 2026
Ran your full stack (#72/#73/#75) against my own 533c unit, not just diffed it -- correction and cross-validation from that:
Correction to my last comment: I speculated the six static DEVICE_CONFIG templates might be a fallback "for owners without a live vendor capture to sniff," on the assumption DEVICE_CONFIG was per-unit-calibrated. That's wrong -- your DEVICE_CONFIG, captured on your XPS 13 9310, was accepted verbatim by upload_config_mcu() on my (different) unit, and produced identical results downstream. So it's not per-machine after all, which also means your PR description's "other 533c machines may need their own" caveat is probably overcautious -- worth softening once someone else confirms.
Further cross-validation, same unit:
nop()/firmware_version()/reset()all ran clean through_expect_ack()-- no hang, no exception. Whatever ordering quirk I hit while building my own driver, your fix in Tolerate sensors that do not ACK every command #72 handles it fine here too (or my unit just doesn't exhibit it; either way, no bug to report).- Chip ID
0x220ca1, and both capture byte counts (14334 B encrypted -> 14260 B plain) match your log exactly. - Finger detection genuinely works:
mcu_switch_to_fdt_downfired on a real touch, and reference-vs-live frame correlation was 0.88 with the mean shifting from 3600 to 3996 (0-4095 range) -- real capacitive signal, not a stale/cached frame. - What I couldn't reproduce: the resulting
fingerprint.pgmdidn't show clearly visible ridge flow for me the way your writeup describes, even after contrast stretching / per-row detrending. Given the 0.88 correlation (not ~1.0, so there's real differential signal; not the 0.5-ish you'd expect from mostly-noise), my best guess is the single global lineara*reference+bfit inflat_field()doesn't fully compensate between gain0xc2(reference) and0x86(live) if that gain change isn't perfectly linear on this unit -- but I haven't isolated it further, and it might just be my finger contact technique. Not confident enough to call this a bug -- flagging as a "didn't fully reproduce visually, protocol-level data all checks out" data point rather than a report against your driver.
(Also, independent of your PR: tool.write_pgm() emits {height} {width} where the PGM spec wants width first, so every .pgm this repo writes reads transposed in a standards-compliant viewer, and flat_field()'s output contains negative values that most PGM readers reject outright. Pre-existing repo issue, not something your PR introduced -- just flagging since it tripped me up rendering the output.)
Net: the protocol/config/geometry side of this is now confirmed across two different physical units, which is about as solid as this gets without a third tester. Nice work.
daemonhorn
commented
Aug 22, 2026
Follow-up to my earlier comment (correlation r=0.88, no visible ridges): found it. Ran a gain sweep with a finger held down through all captures (0x40, 0x50, 0x60, 0x70, 0x86, 0xa0, 0xc2), logging min/max/mean/clipped-pixel-count per gain:
gain 0x40: mean=4094.7 clipped=9501/9504 (fully saturated)
gain 0x50: mean=4092.7 clipped=9393/9504
gain 0x60: mean=4069.7 clipped=9116/9504
gain 0x70: mean=4040.9 clipped=9114/9504
gain 0x86: mean=3909.0 clipped=4462/9504 <- your hardcoded live gain
gain 0xa0: mean=2872.2 clipped=0/9504
gain 0xc2: mean=1446.6 clipped=0/9504 <- your hardcoded *reference* gain
On my unit, 0x86 (the live-capture gain in run_driver()) saturates nearly half the frame with a finger present -- ridge contrast was being clipped off the top of the 12-bit range. 0xc2, which your driver uses for the reference frame, turns out to be the only fully headroom-safe gain for the live frame on my hardware. Re-running flat_field() with the live capture at 0xc2 instead of 0x86 (reference still at 0xc2 too, so no gain mismatch to compensate for) produces a clearly ridge-structured image -- curved, branching bands, not the flat banding I was getting before. Attached for reference (contrast-stretched + median-filtered).
So: the protocol/config were never the problem (as established), and this wasn't a flat_field linearity issue either -- it was gain calibration, and the values you hardcoded for your XPS 13 9310 saturate on mine. Given this varies by unit, might be worth either making the live-capture gain configurable/auto-detected (e.g. a quick pre-sweep checking for clipping before committing to a capture gain), or at minimum noting in the driver that 0x86 is XPS-13-9310-specific and other units may need to sweep for a headroom-safe value.
Planning to build a native (non-TOD) libfprint driver from this next, using goodix-fp-linux-dev/libfprint's goodixtls/goodix5xx.c base class (confirmed it builds cleanly against current libfprint) -- will report back if anything else generalizable turns up.
Adds a driver for 27c6:533c, verified end to end on a Dell XPS 13 9310.
533c is the same GF5288 silicon as the 53x5 family — chip
0x220ca1, which is exactly whatdriver_53x5.pyaccepts as sensor type 9 — but pairs it with aGM168SECsecure element and speaks the wrapped framing rather than the direct one. So this is 53x5's device logic on 51x7's transport.Run
The resulting PGM shows clear ridge flow. Sensor is ×ばつ88 at 12 bpp, the same geometry as 53x5.
Four differences from the existing wrapped drivers
1. It never provisions, by design. 533c already ships holding the all-zero PSK, and
read_firmwarereturns nothing in APP mode — so there is no image to flash back if a write went wrong, andmcu_erase_appwould be a one-way trip. The driver checks the PSK hash and refuses to run if it does not match, rather than callingpreset_psk_write. That felt safer than the erase-and-reflash loop the other drivers use, given no recovery path exists on this part.2. TLS application data is decrypted in process from the session keys, derived from the client/server randoms observed while relaying the handshake. openssl is still the TLS server. This keeps the image bytes on one code path that can be tested offline against a pcap, rather than depending on how the subprocess frames what it echoes.
3. The FDT threshold template is derived, not hardcoded. The baseline reply is 12-bit samples; halving each and emitting it twice reproduces the vendor driver's bytes exactly. So it self-calibrates rather than carrying magic numbers, unlike the hardcoded templates in
driver_51x7/driver_55x4.4. Finger detection polls, because the sensor answers
fdt_downonly when a finger lands — well past the 5 s default USB read timeout.Image processing
Raw frames are dominated by fixed-pattern noise and do not resemble a fingerprint on their own; a reference frame with no finger has to be subtracted. Fitting scale and offset by least squares absorbs the gain difference as well, so one reference frame serves every exposure — useful because the vendor sweeps gain
0xc2→0xad→0x86.Scope
Image capture only — no enrolment, no matching,
sigfmnot wired up.DEVICE_CONFIGis captured verbatim from the vendor driver on a 9310; other 533c machines may need their own.Tested only on my one device. Happy to adjust anything that does not fit the conventions here.