Skip to content

Add driver_53xc.py — image capture for 27c6:533c (GF5288_GM168SEC) - #75

Open
nikicat wants to merge 5 commits into
goodix-fp-linux-dev:masterfrom
nikicat:driver-53xc
Open

Add driver_53xc.py — image capture for 27c6:533c (GF5288_GM168SEC)#75
nikicat wants to merge 5 commits into
goodix-fp-linux-dev:masterfrom
nikicat:driver-53xc

Conversation

@nikicat

@nikicat nikicat commented Aug 6, 2026

Copy link
Copy Markdown

Stacked on #72 and #73. This branch contains both; please merge those first.

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 what driver_53x5.py accepts as sensor type 9 — but pairs it with a GM168SEC secure element and speaks the wrapped framing rather than the direct one. So this is 53x5's device logic on 51x7's transport.

Run

$ python3 run_533c.py
Firmware: GF5288_GM168SEC_APP_13016
PSK: all-zero, as expected
Reset OK, number 1024
Chip ID: 0c a1 00 22
OTP: 68 e6 86 4a 54 ec 15 04 68 ca 35 bb 02 08 6c 37 ...
TLS established
FDT template: 9a 9a a6 a6 a2 a2 98 98 97 97 a4 a4 a1 a1 98 98 92 92 9c 9c 99 99 91 91
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

The resulting PGM shows clear ridge flow. Sensor is 108×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_firmware returns nothing in APP mode — so there is no image to flash back if a write went wrong, and mcu_erase_app would be a one-way trip. The driver checks the PSK hash and refuses to run if it does not match, rather than calling preset_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_down only 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 0xc20xad0x86.

Scope

Image capture only — no enrolment, no matching, sigfm not wired up. DEVICE_CONFIG is 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.

nikicat and others added 5 commits August 6, 2026 19:41
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

Copy link
Copy Markdown

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

Copy link
Copy Markdown

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_down fired 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.pgm didn'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 linear a*reference+b fit in flat_field() doesn't fully compensate between gain 0xc2 (reference) and 0x86 (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

Copy link
Copy Markdown

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants