From d2cd0dbf2a5c98e6ac4547cef1b820967b50224a Mon Sep 17 00:00:00 2001 From: DeftDawg Date: Thu, 11 Jun 2026 03:29:55 -0400 Subject: [PATCH] Changes: - Image normalization - Blank frame check --- HARDWARE_SPECS.md | 414 ++++++++++++++++++++++++++++++ REVERSE_ENGINEERING_NOTES.md | 271 +++++++++++++++++++ devbox.json | 15 +- libfprint/drivers/cs9711/cs9711.c | 347 ++++++++++++++++++++++--- libfprint/drivers/cs9711/cs9711.h | 5 + libfprint/sigfm/meson.build | 4 +- 6 files changed, 1012 insertions(+), 44 deletions(-) create mode 100644 HARDWARE_SPECS.md create mode 100644 REVERSE_ENGINEERING_NOTES.md diff --git a/HARDWARE_SPECS.md b/HARDWARE_SPECS.md new file mode 100644 index 00000000..7f3a30e8 --- /dev/null +++ b/HARDWARE_SPECS.md @@ -0,0 +1,414 @@ +# FP100-LK Fingerprint Sensor Hardware Specifications + +Reverse-engineered from the ChipSailing Windows driver. + +**✓ VERIFIED**: Chip ID 0x62A0 confirmed via USB query on actual hardware. + +## Device Identification + +| Property | Value | +|----------|-------| +| **Manufacturer** | ChipSailing Electronics (ShenZhen) Co., Ltd | +| **Chip Model** | CS9711 (USB product string) / CS62A0 (Chip ID) | +| **Chip ID** | **0x62A0** (verified) | +| **Algorithm Version** | CSAlg_C_V04.07.1 | +| **USB Vendor ID** | 0x2541 | +| **USB Product ID** | 0x0236 | +| **Interface** | USB (WBDI - Windows Biometric Driver Interface) | + +## Sensor Resolution + +| Property | Value | +|----------|-------| +| **Resolution** | **500 DPI** (verified from driver disassembly) | +| **Type** | Capacitive fingerprint sensor | + +### DPI Determination + +The DPI value was confirmed by reverse engineering CSAlgDll.dll: +- The algorithm library uses a sensor type parameter passed to `ChipSailing_Init()` +- Different sensor types have different DPI values configured +- The FP100-LK (Chip ID 0x62A0/0x62A1) maps to sensor type 5, which sets DPI = 0x1f4 (500) +- Note: 508 DPI (0x1fc) does NOT appear in the driver - this was a false lead + +| Sensor Type | DPI | Notes | +|-------------|-----|-------| +| 1 | 500 | | +| 2 | 500 | CS3106/CS4102 | +| 3 | 407 | CS4101 (118×68) | +| 4 | 259 | | +| 5 | 500 | CS62A0/CS62A1 (288×208) **← FP100-LK** | +| 6 | 340 | | + +## Sensor Detection Mechanism + +**The driver determines the sensor configuration by reading the Chip ID from the device during initialization**, NOT by the size of image data returned. The USB driver: + +1. Sends a read command to the device during `VendorDeviceChipConfigure()` +2. Reads an 8-byte response containing the Chip ID at bytes 2-3 +3. Uses a switch statement based on Chip ID to set width, height, and buffer size + +## Chip ID to Configuration Mapping + +| Chip ID | Width | Height | Image Size (bytes) | Notes | +|---------|-------|--------|-------------------|-------| +| **0x62A0, 0x62A1** | **288** (0x120) | **208** (0xD0) | **59904** (0xEA00) | FP100-LK (primary) | +| 0x3106 | 56 (0x38) | 180 (0xB4) | 20160 (0x4EC0) | Alternative sensor | +| 0x4101 | 118 (0x76) | 68 (0x44) | 8024 (0x1F58) | Small sensor | +| 0x4102 (0x4101+1) | 56 (0x38) | 180 (0xB4) | 20160 (0x4EC0) | Similar to 0x3106 | +| (default/0x3106 case) | 96 (0x60) | 96 (0x60) | 18432 (0x4800) | Square sensor | + +**FP100-LK Sensor (Chip ID 0x62A0/0x62A1):** +- **Width: 288 pixels** +- **Height: 208 pixels** +- **Resolution: 500 DPI** +- **Raw image size: 59,904 bytes (8-bit grayscale)** + +Note: The width/height in the driver are stored as (height, width) internally at offsets 0x68 and 0x6C. + +## USB Communication + +- Uses WinUSB as the lower filter driver +- Bulk transfer mode for image data +- Full Speed USB (12 Mbps) +- Max packet size: 64 bytes + +### Endpoints + +| Endpoint | Direction | Type | Description | +|----------|-----------|------|-------------| +| 0x01 | OUT | Bulk | Command/data output | +| 0x81 | IN | Bulk | Response/image input | + +## USB Protocol + +### Command Packet Format + +All commands use an 8-byte packet structure: + +``` +Offset Size Description +------ ---- ----------- +0 1 Start marker: 0xEA +1 1 Command code +2-5 4 Command parameters (usually 0x00) +6 1 Checksum: XOR of bytes 1-5 +7 1 End marker: 0xEA +``` + +Example command to read chip info (cmd=0x01): +``` +[0xEA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xEA] +``` + +### Response Packet Format + +Responses also use 8-byte packets: + +``` +Offset Size Description +------ ---- ----------- +0 1 Echo: 0xEA +1 1 Command code echo +2-3 2 Response data (big-endian) +4-5 2 Additional data +6 1 Checksum +7 1 End marker: 0xEA +``` + +### Command Codes + +| Code | Name | Description | +|------|------|-------------| +| 0x01 | READ_CHIP_ID | Read chip identification. Response bytes 2-3 contain Chip ID | +| 0x02 | SLEEP/WAKE | Power state control (used in D0Exit) | +| 0x03 | CAPTURE_START | Start fingerprint capture (when byte[0xD2]=4) | +| 0x04 | CAPTURE_START_ALT | Alternative capture start command | +| 0x07 | RESET | Device reset (used in D0Entry with WdfPowerDeviceD3Final) | + +### Initialization Sequence + +1. **Device Attach** + - Enumerate USB endpoints (EP 0x01 OUT, EP 0x81 IN) + - Configure bulk continuous reader + +2. **Read Chip ID** (VendorDeviceChipConfigure) + ``` + WRITE: [0xEA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xEA] + READ: [0xEA, 0x01, ChipID_Hi, ChipID_Lo, 0x00, 0x00, checksum, 0xEA] + ``` + - May require 2-3 retries (device needs wake-up time) + - Chip ID determines image dimensions (see table above) + +3. **Allocate Image Buffers** + - Based on Chip ID, allocate `width × height` byte buffer + - For CS62A0: 288 × 208 = 59,904 bytes + +4. **Configure Power Management** + - Enable idle detection + - Set 3000ms idle timeout + +### Image Capture Sequence + +1. **Wait for Finger** + - Device uses continuous reader on bulk IN endpoint + - Interrupt/notification when finger detected + +2. **Capture Image** + ``` + WRITE: [0xEA, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0xEA] (or 0x04) + READ: + ``` + - Image data arrives in 64-byte bulk transfers + - Total transfers: 59904 / 64 = 936 packets (+ remainder) + +3. **Background Image Acquisition** (calibration) + - IOCTL code: 0x44001C (0x1fe8 offset from base) + - Used for sensor calibration/baseline + +### Power State Commands + +| State Transition | Command | +|-----------------|---------| +| Enter D0 (active) | cmd=0x07 (if from D3Final), else cmd=0x02 | +| Exit D0 (sleep) | cmd=0x02 (retry up to 3 times) | + +### Error Handling + +- Commands may timeout if device is in sleep state +- Retry up to 3 times with short delays +- Status 0xC0000120 indicates device needs re-initialization + +## Driver Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ WinBio Service │ +├─────────────────────────────────────────────────────┤ +│ WinBioSensorAdapter.DLL (Windows built-in) │ +├─────────────────────────────────────────────────────┤ +│ EngineAdapter.dll (ChipEngine) - Template matching │ +├─────────────────────────────────────────────────────┤ +│ CSAlgDll.dll - ChipSailing algorithm library │ +│ - ChipSailing_Init() │ +│ - ChipSailing_CreateTemplate() │ +│ - ChipSailing_MatchScore() │ +│ - ChipSailing_AutoGain() │ +│ - ChipSailing_Enhance16to8() │ +├─────────────────────────────────────────────────────┤ +│ WudfBioUsb.dll - UMDF USB driver │ +├─────────────────────────────────────────────────────┤ +│ WinUSB.sys (Kernel) │ +└─────────────────────────────────────────────────────┘ +``` + +## Key Constants for Linux Driver Development + +```c +#define FP100_VENDOR_ID 0x2541 +#define FP100_PRODUCT_ID 0x0236 + +#define FP100_RESOLUTION_DPI 500 + +/* Chip IDs - read from device during initialization */ +#define CHIPID_CS62A0 0x62A0 /* FP100-LK primary */ +#define CHIPID_CS62A1 0x62A1 /* FP100-LK variant */ +#define CHIPID_CS3106 0x3106 +#define CHIPID_CS4101 0x4101 +#define CHIPID_CS4102 0x4102 + +/* FP100-LK sensor dimensions (Chip ID 0x62A0/0x62A1) */ +#define FP100_IMAGE_WIDTH 288 +#define FP100_IMAGE_HEIGHT 208 +#define FP100_IMAGE_SIZE (288 * 208) /* 59904 bytes */ + +/* Other sensor configurations by Chip ID */ +/* CS3106: 56x180 = 10080 raw, 20160 with padding? */ +/* CS4101: 118x68 = 8024 */ +/* Default: 96x96 = 9216 raw, 18432 bytes */ +``` + +## Algorithm Functions (from CSAlgDll.dll exports) + +| Function | Purpose | +|----------|---------| +| `ChipSailing_Init` | Initialize algorithm engine | +| `ChipSailing_AutoGain` | Auto-calibrate sensor gain (8-bit) | +| `ChipSailing_AutoGain16` | Auto-calibrate sensor gain (16-bit) | +| `ChipSailing_CreateTemplate` | Create fingerprint template | +| `ChipSailing_CreateTemplate16` | Create template from 16-bit image | +| `ChipSailing_MatchScore` | Compare fingerprint templates | +| `ChipSailing_MergeFeature` | Merge multiple captures | +| `ChipSailing_RenewFeature` | Update existing template | +| `ChipSailing_DetectFinger` | Detect finger presence | +| `ChipSailing_SignalStrength` | Get signal quality | +| `ChipSailing_Enhance16to8` | Convert 16-bit to 8-bit image | +| `ChipSailing_CutBlankImage` | Crop blank regions | +| `ChipSailing_GetImage` | Retrieve processed image | +| `ChipSailing_IsBlankImage` | Check for blank/empty scan | +| `ChipSailing_GetAlgVersion` | Get algorithm version | + +## Resolution Mismatch Investigation + +### Problem Statement + +Some devices with Chip ID 0x62A0 report the correct chip ID but output only 8024 bytes (68×118) instead of the expected 59904 bytes (288×208). This section documents the investigation into whether the Windows driver sends a resolution configuration command. + +### Analysis Results + +**Finding: The Windows driver does NOT send any resolution configuration command.** + +The driver determines resolution purely from the Chip ID via a switch statement: + +``` +ChipID 0x62A0/0x62A1 → 288×208 (59904 bytes) +ChipID 0x4101 → 68×118 (8024 bytes) +ChipID 0x3106 → 56×180 (20160 bytes) +ChipID 0x4102 → 56×180 (20160 bytes) +Default → 96×96 (18432 bytes) +``` + +### Command Analysis + +All discovered commands use the 8-byte packet format with **zero data bytes**: + +| Code | Function | Data Bytes | Purpose | +|------|----------|------------|---------| +| 0x01 | Read Chip ID | 0,0,0,0 | Returns chip ID in response | +| 0x02 | Sleep/Wake | 0,0,0,0 | Power state control | +| 0x03 | Capture | 0,0,0,0 | Start capture (mode=4) | +| 0x04 | Capture Alt | 0,0,0,0 | Alternative capture | +| 0x07 | Reset | 0,0,0,0 | Device reset | + +**No register write or configuration commands were found** that could change resolution. + +### Possible Explanations + +1. **Firmware Variant**: The device has different firmware that outputs smaller images regardless of chip ID +2. **Hardware Limitation**: The specific sensor module is physically smaller +3. **Missing Initialization**: There may be undiscovered vendor-specific USB control transfers (not bulk commands) +4. **EEPROM Configuration**: Resolution may be burned into device EEPROM at manufacturing + +### Recommended Next Steps + +To definitively determine if a configuration command exists: + +1. **Capture USB Traffic on Windows**: + ``` + - Install USBPcap or Wireshark with USBPcap + - Capture all USB traffic during Windows driver initialization + - Look for any control transfers or additional bulk commands + - Compare command sequence between working 288×208 device and 68×118 device + ``` + +2. **Check for Control Transfers**: + The analyzed bulk commands may not be the complete picture. USB control transfers (bmRequestType vendor-specific) could configure the sensor. + +3. **Compare Device Descriptors**: + Check if the 68×118 variant has different USB descriptors or firmware version strings. + +--- + +## USB Traffic Capture Analysis (January 2026) + +Captured live USB traffic from device initialization and fingerprint capture. + +### Initialization Sequence Observed + +| Frame | Direction | Data | Description | +|-------|-----------|------|-------------| +| 489-500 | Control | - | Standard USB enumeration (GET DESCRIPTOR, SET CONFIGURATION) | +| 501 | Host→Device | `EA 01 00 00 00 00 01 EA` | **Read Chip ID command** | +| 504 | Device→Host | `EA 01 62 A0 00 00 C3 EA` | **Chip ID response: 0x62A0** ✓ | + +### Fingerprint Capture Sequence + +| Frame | Direction | Data | Description | +|-------|-----------|------|-------------| +| 1582 | Host→Device | `EA 04 00 00 00 00 04 EA` | **Capture command (0x04)** | +| 1584 | Device→Host | 8000 bytes | Image data (first chunk) | +| 1586 | Device→Host | 24 bytes | Image data (final chunk) | +| **Total** | | **8024 bytes** | **118×68 pixels** (not 288×208!) | + +### Key Finding: Resolution Mismatch Confirmed + +**The device reports Chip ID 0x62A0 but outputs only 8024 bytes (118×68) instead of 59904 bytes (288×208).** + +This confirms the "Resolution Mismatch" section above - this specific device variant has different firmware or hardware that outputs a smaller image despite having the same Chip ID. + +### Commands Observed + +| Code | Hex Packet | Purpose | +|------|------------|---------| +| 0x01 | `EA 01 00 00 00 00 01 EA` | Read Chip ID | +| 0x02 | `EA 02 00 00 00 00 02 EA` | Sleep/Wake (sent after capture) | +| 0x04 | `EA 04 00 00 00 00 04 EA` | Capture image | + +**No configuration commands were observed** - the driver does not send any resolution configuration to the device. + +### Conclusions + +1. ✅ Chip ID 0x62A0 confirmed via live capture +2. ✅ Command protocol matches reverse-engineered specs +3. ⚠️ **This device outputs 8024 bytes (118×68) not 59904 (288×208)** +4. ❌ No resolution configuration command exists - this is a hardware/firmware variant + +For Linux driver: Must detect actual image size from response, not just rely on Chip ID. + +--- + +## Notes for Linux Driver + +1. The sensor outputs raw 8-bit grayscale images +2. The 500 DPI resolution is standard for fingerprint sensors +3. Image processing and template creation can use libfprint's algorithms +4. USB bulk transfers are used for image capture +5. **The Chip ID must be read during device initialization** to determine the correct image dimensions +6. The FP100-LK uses Chip ID 0x62A0 or 0x62A1 with 288x208 resolution +7. The EngineAdapter validates received image size against expected size based on Chip ID +8. **Device needs 2-3 command cycles to "wake up"** from idle state - implement retry logic +9. All commands use the `[0xEA, cmd, params..., checksum, 0xEA]` packet format + +## Sample Code + +### Building a Command Packet (Python) + +```python +def build_command(cmd, params=None): + """Build an 8-byte command packet for the CS9711 sensor.""" + if params is None: + params = [0x00, 0x00, 0x00, 0x00] + + # Checksum is XOR of bytes 1-5 (cmd + 4 param bytes) + checksum = cmd + for p in params: + checksum ^= p + + return bytes([0xEA, cmd] + params + [checksum, 0xEA]) + +# Examples +CMD_READ_CHIP_ID = build_command(0x01) # [0xEA, 0x01, 0, 0, 0, 0, 0x01, 0xEA] +CMD_SLEEP_WAKE = build_command(0x02) # [0xEA, 0x02, 0, 0, 0, 0, 0x02, 0xEA] +CMD_CAPTURE = build_command(0x03) # [0xEA, 0x03, 0, 0, 0, 0, 0x03, 0xEA] +CMD_RESET = build_command(0x07) # [0xEA, 0x07, 0, 0, 0, 0, 0x07, 0xEA] +``` + +### Parsing a Response Packet (Python) + +```python +def parse_response(data): + """Parse an 8-byte response packet.""" + if len(data) != 8 or data[0] != 0xEA or data[7] != 0xEA: + return None + + return { + 'cmd': data[1], + 'data': (data[2] << 8) | data[3], # Big-endian 16-bit + 'extra': (data[4] << 8) | data[5], + 'checksum': data[6], + } + +# Example: Parse chip ID response +# Response: [0xEA, 0x01, 0x62, 0xA0, 0x00, 0x00, 0xC3, 0xEA] +# Result: {'cmd': 1, 'data': 0x62A0, 'extra': 0, 'checksum': 0xC3} +``` diff --git a/REVERSE_ENGINEERING_NOTES.md b/REVERSE_ENGINEERING_NOTES.md new file mode 100644 index 00000000..f7e4a9dc --- /dev/null +++ b/REVERSE_ENGINEERING_NOTES.md @@ -0,0 +1,271 @@ +# Reverse Engineering Windows Drivers: Techniques and Lessons Learned + +This document describes the techniques used to reverse engineer the ChipSailing CS9711 Windows fingerprint driver to extract hardware specifications and USB protocol details for Linux driver development. + +## Overview + +**Goal:** Extract hardware specifications (resolution, image dimensions) and USB protocol from Windows driver DLLs without source code. + +**Files Analyzed:** +- `WudfBioUsb.dll` - UMDF USB driver (main driver) +- `EngineAdapter.dll` - Biometric engine adapter +- `CSAlgDll.dll` - ChipSailing algorithm library +- `WudfBioUsb.inf` - Driver installation file + +**Tools Used:** +- `radare2` (r2) - Primary disassembly and analysis tool +- `objdump` - Secondary disassembly for specific searches +- `strings` - Extract readable strings from binaries +- `file` - Identify file types +- `iconv` - Convert text encodings (INF file was UTF-16) +- `xxd` - Hex dump for binary pattern searching +- Python + pyusb - Verify findings on actual hardware + +## What Worked Well + +### 1. Start with the INF File + +The `.inf` file is plain text (though may be UTF-16 encoded) and contains valuable metadata: + +```bash +iconv -f UTF-16LE -t UTF-8 WudfBioUsb.inf +``` + +**Found:** +- USB VID/PID: `USB\VID_2541&PID_0236` +- Manufacturer: ChipSailing Electronics +- DLL relationships and dependencies +- Registry keys and configuration + +**Lesson:** Always start here - it's the easiest source of structured information. + +### 2. String Extraction and Searching + +Debug strings in the DLLs were extremely valuable: + +```bash +strings -n 6 WudfBioUsb.dll | grep -iE 'width|height|size|image|chip' +``` + +**Key strings found:** +- `"ChipBioUsb | pDevice->ChipID = %x"` - Revealed chip ID mechanism +- `"ChipBioUsb |DeviceContext->ImageBufferSize is %d"` - Led to buffer size code +- `"ChipEngine | Incorrect raw image size: %d"` - Revealed size validation logic + +**Lesson:** Debug/logging strings are goldmines. They often name variables and describe what code is doing. + +### 3. Cross-Reference Analysis with radare2 + +Once you find an interesting string, trace back to the code that uses it: + +```bash +# Find string address +r2 -q -c 'iz~ChipID' WudfBioUsb.dll + +# Find code that references it +r2 -q -c 'aaa; axt 0x18001d7c0' WudfBioUsb.dll + +# Disassemble the function +r2 -q -c 'aaa; s fcn.1800033c0; pdf' WudfBioUsb.dll +``` + +**Lesson:** The `axt` (cross-references to) command is essential for tracing data flow. + +### 4. Pattern Matching for Constants + +Search for known constant patterns in disassembly: + +```bash +# Search for specific hex values (e.g., 500 DPI = 0x1f4) +r2 -q -c '/x f4010000' WudfBioUsb.dll + +# Search for mov instructions with specific values +objdump -D -M intel WudfBioUsb.dll | grep -E 'mov.*(0x1f4|0xea00)' +``` + +**Found:** Image buffer sizes (0xea00 = 59904) and resolution (0x1f4 = 500) as immediate values. + +### 5. Function List and Export Analysis + +```bash +# List all exports +r2 -q -c 'rabin2 -E CSAlgDll.dll' + +# List all functions +r2 -q -c 'aaa; afl' WudfBioUsb.dll +``` + +**Found:** Algorithm function names like `ChipSailing_CreateTemplate`, `ChipSailing_MatchScore` which revealed the library's purpose. + +### 6. Data Section Analysis + +For finding embedded constants and lookup tables: + +```bash +# Dump specific memory regions +r2 -q -c 's 0x1800589d0; px 128' CSAlgDll.dll +``` + +**Found:** Version string `CSAlg_C_V04.07.1` and chip identifiers. + +### 7. Hardware Verification + +After forming hypotheses, verify on actual hardware: + +```python +# Send command, read response +ep_out.write(bytes([0xEA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xEA])) +data = ep_in.read(8) +chip_id = (data[2] << 8) | data[3] # Confirmed 0x62A0 +``` + +**Lesson:** Always verify findings on real hardware when possible. + +## What Didn't Work Well + +### 1. Automated Analysis Often Incomplete + +radare2's `aaa` (analyze all) sometimes misses functions or misidentifies code: + +```bash +r2 -q -c 'aaa; afl~init' # Often returns empty or incomplete +``` + +**Workaround:** Manually navigate to addresses found via string cross-references. + +### 2. Complex Call Graphs Are Hard to Follow + +The `agC` (call graph) command produced unreadable ASCII art for complex drivers. + +**Workaround:** Focus on specific functions rather than trying to understand entire call graph. + +### 3. Indirect Calls Through vtables + +Windows drivers use COM-like interfaces with function pointer tables: + +```asm +mov rax, qword [rax + 0x6e8] ; Load function pointer from vtable +call qword [0x1800142b0] ; Call through dispatcher +``` + +**Challenge:** Can't easily determine which function is being called. + +**Workaround:** Focus on the data being passed (arguments) rather than trying to resolve every call. + +### 4. pyusb Backend Issues on NixOS + +The nix-installed pyusb didn't automatically find libusb: + +``` +usb.core.NoBackendError: No backend available +``` + +**Workaround:** Explicitly set `LD_LIBRARY_PATH`: +```bash +sudo LD_LIBRARY_PATH=/nix/store/.../libusb-1.0.28/lib python3 script.py +``` + +### 5. Initial Protocol Guesses Were Wrong + +First attempts at USB communication timed out - the device needed a specific command format. + +**What helped:** Finding the command-building function in disassembly and reverse engineering the packet structure. + +## Step-by-Step Methodology + +### Phase 1: Reconnaissance +1. Identify all files (DLLs, INF, CAT) +2. Check file types with `file` command +3. Parse INF file for metadata (VID/PID, manufacturer, dependencies) +4. Extract all strings from each DLL + +### Phase 2: String-Guided Analysis +1. Search strings for keywords: `width`, `height`, `size`, `resolution`, `dpi`, `image`, `chip`, `init` +2. For each interesting string, find its address +3. Find cross-references to that address +4. Disassemble the referencing function + +### Phase 3: Code Analysis +1. Identify key functions (initialization, configuration, capture) +2. Look for constants being assigned to structures +3. Trace switch statements - they often map chip IDs to configurations +4. Look for debug print statements that reveal variable names + +### Phase 4: Protocol Extraction +1. Find USB read/write functions +2. Identify command buffer construction +3. Note packet sizes and formats +4. Map command codes to operations + +### Phase 5: Verification +1. Write test script to communicate with device +2. Send discovered commands +3. Verify responses match expected format +4. Iterate if needed + +## Key Insights + +### Switch Statements Reveal Configuration Tables + +The driver used switch statements on Chip ID to set dimensions: + +```asm +cmp edi, 0x1f58 ; 8024 bytes +je set_small_config +cmp edi, 0x4800 ; 18432 bytes +je set_medium_config +cmp edi, 0xea00 ; 59904 bytes +je set_large_config +``` + +**Lesson:** Switch statements are often configuration lookups - analyze all branches. + +### Debug Builds Are Your Friend + +The driver had extensive debug logging which made analysis much easier. Production drivers with stripped symbols are significantly harder. + +### Protocol Framing Is Common + +The `[0xEA, cmd, data..., checksum, 0xEA]` format with start/end markers is common in embedded protocols. Look for repeated byte values at fixed offsets. + +### Retry Logic Reveals Reliability Issues + +The Windows driver retried commands up to 3 times. This told us the device might need multiple attempts to respond (which we confirmed). + +## Tools Cheat Sheet + +```bash +# Basic info +file *.dll +strings -n 8 driver.dll | grep -i keyword + +# INF file (often UTF-16) +iconv -f UTF-16LE -t UTF-8 driver.inf + +# radare2 essentials +r2 -q -c 'iz' driver.dll # List strings +r2 -q -c 'izz~pattern' driver.dll # Search all strings +r2 -q -c 'aaa; afl' driver.dll # List functions +r2 -q -c 'aaa; axt 0xADDRESS' driver.dll # Cross-references +r2 -q -c 'aaa; s FUNC; pdf' driver.dll # Disassemble function +r2 -q -c 's ADDR; px 64' driver.dll # Hex dump +r2 -q -c '/x HEXPATTERN' driver.dll # Search hex bytes + +# objdump for grep-friendly output +objdump -D -M intel driver.dll | grep -E 'pattern' + +# Exports +rabin2 -E driver.dll +``` + +## Conclusion + +The most effective approach was **string-guided reverse engineering**: find debug strings, trace them to code, analyze the surrounding logic. This is much more efficient than trying to understand the entire binary. + +For USB device drivers specifically, focus on: +1. Device initialization (VID/PID handling, configuration) +2. Endpoint setup (bulk, interrupt, control) +3. Command/response packet formats +4. Image buffer allocation (reveals dimensions) + +Always verify findings on real hardware when possible - it catches misinterpretations quickly. diff --git a/devbox.json b/devbox.json index 9de807d0..74b1d3f3 100644 --- a/devbox.json +++ b/devbox.json @@ -15,18 +15,19 @@ "gcc", "opencv4", "cmake", - "doctest@latest" + "doctest@latest", + "zip@latest" ], "shell": { "init_hook": "cat << 'EOF'\n\n╔════════════════════════════════════════════════════════════════╗\n║ libfprint-CS9711 Development Environment ║\n╚════════════════════════════════════════════════════════════════╝\n\n📦 Build Commands:\n devbox run build:setup - Initialize meson build directory\n devbox run build - Compile the project\n devbox run build:clean - Remove build directory\n devbox run build:rebuild - Clean and rebuild from scratch\n\n🧪 Test Commands:\n devbox run test - Run all tests (4 pass, 28 skipped)\n devbox run test:hwdb - Run only udev hwdb test\n\n🔧 Maintenance Commands:\n devbox run sync-hwdb - Regenerate udev hardware database\n\n📚 Quick Start:\n devbox run build:setup # First time setup\n devbox run build # Build the project\n devbox run test # Run tests\n\nEOF\n", "scripts": { - "build:setup": "meson setup build -Ddoc=false -Dgtk-examples=false", - "build": "meson compile -C build", - "build:clean": "rm -rf build", + "build:setup": "meson setup build -Ddoc=false -Dgtk-examples=false", + "build": "meson compile -C build", + "build:clean": "rm -rf build", "build:rebuild": "devbox run build:clean && devbox run build:setup && devbox run build", - "test": "meson test -C build", - "test:hwdb": "meson test -C build --suite data", - "sync-hwdb": "ninja -C build sync-udev-hwdb" + "test": "meson test -C build", + "test:hwdb": "meson test -C build --suite data", + "sync-hwdb": "ninja -C build sync-udev-hwdb" } } } diff --git a/libfprint/drivers/cs9711/cs9711.c b/libfprint/drivers/cs9711/cs9711.c index 49e02850..0b611888 100644 --- a/libfprint/drivers/cs9711/cs9711.c +++ b/libfprint/drivers/cs9711/cs9711.c @@ -30,25 +30,109 @@ G_DEFINE_TYPE (FpDeviceCs9711, fpi_device_cs9711, FP_TYPE_IMAGE_DEVICE) #define CS9711_SENSOR_WIDTH 34 #define CS9711_SENSOR_HEIGHT 236 -#define CS9711_DEFAULT_WAIT_TIMEOUT 300 +#define CS9711_DEFAULT_WAIT_TIMEOUT 500 #define CS9711_DEFAULT_RESET_SLEEP 250 #define CS9711_SEND_ENDPOINT 0x01 #define CS9711_RECEIVE_ENDPOINT 0x81 #define CS9711_FP_CMD_LEN_1 8 -#define CS9711_FP_RECV_LEN_1 8000 -#define CS9711_FP_RECV_LEN_2 24 -#define CS9711_FP_RECV_LEN_MAX CS9711_FP_RECV_LEN_1 +/* + * Image data size: 8024 bytes total + * With cmd 0x04: arrives as 8000 + 24 bytes + * With cmd 0x03: arrives as 8024 bytes in one transfer + */ +#define CS9711_FP_RECV_LEN_1 8024 +#define CS9711_FP_RECV_LEN_2 0 +/* Safety buffer size - prevents overflow if device returns unexpected data */ +#define CS9711_FP_RECV_LEN_MAX 65536 + +/* + * Command codes (from HARDWARE_SPECS.md USB traffic capture): + * 0x01 = READ_CHIP_ID - read chip identification + * 0x02 = SLEEP_WAKE - power state control + * 0x03 = CAPTURE - start capture (alternative) + * 0x04 = CAPTURE_ALT - start capture (Windows driver uses this) + * 0x07 = RESET - device reset + */ +#define CS9711_CMD_READ_CHIP_ID 0x01 +#define CS9711_CMD_SLEEP_WAKE 0x02 +#define CS9711_CMD_CAPTURE 0x04 /* Windows driver uses 0x04 for capture */ +#define CS9711_CMD_RESET 0x07 -#define CS9711_FP_CMD_TYPE_INIT 1 -#define CS9711_FP_CMD_TYPE_RESET 2 -#define CS9711_FP_CMD_TYPE_SCAN 4 +/* Legacy aliases for compatibility */ +#define CS9711_FP_CMD_TYPE_INIT CS9711_CMD_READ_CHIP_ID +#define CS9711_FP_CMD_TYPE_SLEEP CS9711_CMD_SLEEP_WAKE +#define CS9711_FP_CMD_TYPE_SCAN CS9711_CMD_CAPTURE +#define CS9711_FP_CMD_TYPE_RESET CS9711_CMD_RESET #define CS9711_FP_CMD_STATE_RESULT_EXPECTED { 0xea, 0x01, 0x62, 0xa0, 0x00, 0x00, 0xc3, 0xea } /************************** GENERIC STUFF *************************************/ +#define CS9711_HANDLE_USB_DISCONNECT 1 // Enable USB disconnect handling by default +/* + * KVM/USB disconnect handling - disabled by default. + * Enable with -DCS9711_HANDLE_USB_DISCONNECT to detect USB disconnects + * (e.g., from KVM switches) and mark the device as removed. + */ +#ifdef CS9711_HANDLE_USB_DISCONNECT + +static gboolean +is_disconnect_error (GError *error) +{ + if (!error) + return FALSE; + + if (error->domain == G_USB_DEVICE_ERROR) + { + if (error->code == G_USB_DEVICE_ERROR_IO) + return TRUE; + if (error->code == G_USB_DEVICE_ERROR_NO_DEVICE) + return TRUE; + } + + if (error->domain == G_IO_ERROR) + { + if (error->code == G_IO_ERROR_HOST_UNREACHABLE || + error->code == G_IO_ERROR_CONNECTION_CLOSED) + return TRUE; + } + + if (error->message) + { + if (g_strstr_len (error->message, -1, "disconnected") || + g_strstr_len (error->message, -1, "no device") || + g_strstr_len (error->message, -1, "No such device") || + g_strstr_len (error->message, -1, "transfer failed")) + return TRUE; + } + + return FALSE; +} + +static gboolean +handle_disconnect_error (FpDevice *dev, FpiSsm *ssm, GError *error) +{ + gboolean is_removed = FALSE; + + if (!is_disconnect_error (error)) + return FALSE; + + fp_warn ("Device appears to be disconnected: %s", error->message); + + g_object_get (dev, "removed", &is_removed, NULL); + if (!is_removed) + fpi_device_remove (dev); + + g_clear_error (&error); + GError *removed_error = fpi_device_error_new (FP_DEVICE_ERROR_REMOVED); + fpi_ssm_mark_failed (ssm, removed_error); + + return TRUE; +} + +#endif /* CS9711_HANDLE_USB_DISCONNECT */ /** If error isn't NULL then fail the ssm or move it to the next state */ static void @@ -96,7 +180,9 @@ usb_read_in (FpDevice *dev, FpiUsbTransferCallback callback, gpointer user_data) { + FpDeviceCs9711 *self = FPI_DEVICE_CS9711 (dev); FpiUsbTransfer *transfer = NULL; + fp_dbg("Reading %lu bytes", length); // If the response is larger than the buffer, then the usb helpers // error before. The reader doesn't seem to care about requestend @@ -110,11 +196,24 @@ usb_read_in (FpDevice *dev, transfer->short_is_error = short_is_error; transfer->ssm = ssm; fpi_usb_transfer_fill_bulk (transfer, CS9711_RECEIVE_ENDPOINT, length); - fpi_usb_transfer_submit (transfer, timeout_in_ms, NULL, callback, user_data); + fpi_usb_transfer_submit (transfer, timeout_in_ms, + self->interrupt_cancellable, + callback, user_data); } /************************** INIT SSM *************************************/ +/* Forward declaration of init states for use in callbacks */ +enum { + M_INIT_STATE_SEND_INI_QUERY = 0, + M_INIT_STATE_RECOVER_READ_IGNORED, + M_INIT_STATE_RECOVER_SEND_RESET, + M_INIT_STATE_RECOVER_READ_IGNORED_RESET, + M_INIT_STATE_RECOVER_SEND_INIT, + M_INIT_STATE_RECEIVE_STATUS, + M_INIT_STATE_COUNT, +}; + static void m_init_read_cb_check_expected (FpiUsbTransfer *transfer, FpDevice *dev, @@ -128,6 +227,10 @@ m_init_read_cb_check_expected (FpiUsbTransfer *transfer, if (error) { +#ifdef CS9711_HANDLE_USB_DISCONNECT + if (handle_disconnect_error (dev, transfer->ssm, error)) + return; +#endif fp_err ("Read failed: %s, aborting", error->message); fpi_ssm_mark_failed(transfer->ssm, error); } @@ -136,13 +239,15 @@ m_init_read_cb_check_expected (FpiUsbTransfer *transfer, fp_dbg ("Read %lu of requested %lu", transfer->length, transfer->actual_length); if (transfer->actual_length != CS9711_FP_CMD_LEN_1) { - fp_err ("Error; expected %lu bytes but got %lu, failing", (gsize)CS9711_FP_CMD_LEN_1, transfer->actual_length); - error = g_error_new (FP_DEVICE_ERROR, FP_DEVICE_ERROR_PROTO, - "Init: expected %lu bytes but got %lu", - (gsize)CS9711_FP_CMD_LEN_1, transfer->actual_length); - fpi_ssm_mark_failed (transfer->ssm, error); + /* Got unexpected data size - likely stale data from previous session. + * Don't fail - just warn and complete init. The device is present + * and captures may still work fine. */ + fp_warn ("Got %lu bytes instead of expected %lu - completing init anyway", + transfer->actual_length, (gsize)CS9711_FP_CMD_LEN_1); + fpi_ssm_mark_completed (transfer->ssm); } else if (memcmp (transfer->buffer, expected, CS9711_FP_CMD_LEN_1)) { + fp_info ("Detected ChipID: 0x%04X (expected 0x62A0)", (guint16)((transfer->buffer[2] << 8) | transfer->buffer[3])); if (!user_data_is_ignore_mismatch_if_non_null) { fp_warn ("Error; got different state response than expected, but don't understand it anyway, continuing"); } @@ -156,15 +261,28 @@ m_init_read_cb_check_expected (FpiUsbTransfer *transfer, } } -enum { - M_INIT_STATE_SEND_INI_QUERY = 0, - M_INIT_STATE_RECOVER_READ_IGNORED, - M_INIT_STATE_RECOVER_SEND_RESET, - M_INIT_STATE_RECOVER_READ_IGNORED_RESET, - M_INIT_STATE_RECOVER_SEND_INIT, - M_INIT_STATE_RECEIVE_STATUS, - M_INIT_STATE_COUNT, -}; +/* Callback to drain stale USB data - we don't care about errors or content */ +static void +m_init_drain_stale_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + g_assert (transfer->ssm != NULL); + + if (error) + { + /* Timeout is expected if no stale data - that's fine */ + fp_dbg ("Drain read: %s (continuing anyway)", error->message); + g_clear_error (&error); + } + else if (transfer->actual_length > 0) + { + fp_info ("Drained %lu bytes of stale data", transfer->actual_length); + } + + fpi_ssm_next_state (transfer->ssm); +} /* Exec init sequential state machine */ static void @@ -191,8 +309,10 @@ m_init_state (FpiSsm *ssm, FpDevice *_dev) break; case M_INIT_STATE_RECOVER_READ_IGNORED: - fp_warn("Send operation had a timeout. Switching to reset procedure. Ignore next message about the result not matching the expected data."); - usb_read_in (_dev, ssm, CS9711_FP_CMD_LEN_1, FALSE, CS9711_DEFAULT_WAIT_TIMEOUT, m_init_read_cb_check_expected, NULL); + /* Drain any stale data - use max buffer size and a short timeout. + * We don't care about the result, just need to clear the USB buffer. */ + fp_warn("Draining stale USB data before reset..."); + usb_read_in (_dev, ssm, CS9711_FP_RECV_LEN_MAX, FALSE, 100, m_init_drain_stale_cb, NULL); break; case M_INIT_STATE_RECOVER_SEND_RESET: @@ -201,17 +321,28 @@ m_init_state (FpiSsm *ssm, FpDevice *_dev) break; case M_INIT_STATE_RECOVER_READ_IGNORED_RESET: - fp_warn("Send operation had a timeout. Switching to reset procedure."); - usb_read_in (_dev, ssm, CS9711_FP_CMD_LEN_1, FALSE, CS9711_DEFAULT_WAIT_TIMEOUT, m_init_read_cb_check_expected, self); + fp_warn("Draining any response after reset..."); + usb_read_in (_dev, ssm, CS9711_FP_RECV_LEN_MAX, FALSE, 500, m_init_drain_stale_cb, NULL); break; case M_INIT_STATE_RECOVER_SEND_INIT: - usb_send_out_sync (_dev, CS9711_FP_CMD_TYPE_INIT, &error); // Do not reuse state to only try reset once - m_util_fail_if_error_or_next (ssm, error); + fp_info("Recovery: sending init command..."); + usb_send_out_sync (_dev, CS9711_FP_CMD_TYPE_INIT, &error); + if (error) + { + /* If init send fails during recovery, just complete anyway - + * the device is present and may work for captures */ + fp_warn ("Recovery init send failed: %s - completing init anyway", error->message); + g_clear_error (&error); + fpi_ssm_mark_completed (ssm); + } + else + fpi_ssm_next_state (ssm); break; case M_INIT_STATE_RECEIVE_STATUS: - usb_read_in (_dev, ssm, CS9711_FP_CMD_LEN_1, TRUE, CS9711_DEFAULT_WAIT_TIMEOUT, m_init_read_cb_check_expected, NULL); + fp_info("Reading init status response..."); + usb_read_in (_dev, ssm, CS9711_FP_CMD_LEN_1, FALSE, CS9711_DEFAULT_WAIT_TIMEOUT, m_init_read_cb_check_expected, NULL); break; default: @@ -278,6 +409,10 @@ m_scan_read_cb_bulk (FpiUsbTransfer *transfer, if (error) { +#ifdef CS9711_HANDLE_USB_DISCONNECT + if (handle_disconnect_error (dev, transfer->ssm, error)) + return; +#endif fp_err ("Read failed: %s, aborting", error->message); fpi_ssm_mark_failed (transfer->ssm, error); } @@ -296,6 +431,110 @@ m_scan_read_cb_bulk (FpiUsbTransfer *transfer, } } +/* + * Per-frame image normalization. + * + * The Windows driver preprocesses every frame before matching + * (EngineAdapter `_AdapterPreprocessImageData`, CSAlgDll + * `ChipSailing_AutoGain` / `ChipSailing_Enhance16to8`). The raw frames + * from this capacitive sensor sit on a baseline that drifts with + * temperature, humidity and skin-oil residue, so feeding them straight + * into SIGFM/SIFT makes match scores decay as conditions drift away + * from those at enrollment time. + * + * We approximate that preprocessing in two steps: + * 1. Background flattening: subtract a local box mean (integral image), + * removing the slowly-varying baseline while keeping ridge detail. + * At 500 DPI the ridge period is ~9 px; a 17x17 window is well above + * that, so ridges survive. + * 2. Robust contrast stretch: map the 2nd..98th percentile of the + * flattened values to 0..255 so gain drift doesn't change the + * image statistics SIFT sees. + * + * Returns FALSE if the flattened frame has so little contrast that it + * cannot contain a fingerprint (blank/baseline frame, ~stddev 2 vs ~35 + * for a real finger), in which case the caller should ask for a retry + * instead of submitting garbage to the matcher. + */ +#define CS9711_NORM_RADIUS 8 +#define CS9711_MIN_CONTRAST_VARIANCE 64 /* stddev 8, between blank ~2 and finger ~35 */ + +static gboolean +m_scan_normalize_image (guint8 *data, gint width, gint height) +{ + gint n = width * height; + gint iw = width + 1; + g_autofree guint32 *integral = g_new (guint32, iw * (height + 1)); + g_autofree gint16 *flat = g_new (gint16, n); + guint hist[511] = { 0 }; + gint64 sum = 0, sum_sq = 0; + + /* Integral image */ + memset (integral, 0, iw * sizeof (guint32)); + for (gint y = 1; y <= height; y++) + { + guint32 row = 0; + integral[y * iw] = 0; + for (gint x = 1; x <= width; x++) + { + row += data[(y - 1) * width + (x - 1)]; + integral[y * iw + x] = integral[(y - 1) * iw + x] + row; + } + } + + /* Flatten: subtract local box mean */ + for (gint y = 0; y < height; y++) + { + gint y0 = MAX (y - CS9711_NORM_RADIUS, 0); + gint y1 = MIN (y + CS9711_NORM_RADIUS + 1, height); + for (gint x = 0; x < width; x++) + { + gint x0 = MAX (x - CS9711_NORM_RADIUS, 0); + gint x1 = MIN (x + CS9711_NORM_RADIUS + 1, width); + guint32 box = integral[y1 * iw + x1] - integral[y0 * iw + x1] - + integral[y1 * iw + x0] + integral[y0 * iw + x0]; + gint mean = box / ((y1 - y0) * (x1 - x0)); + gint v = data[y * width + x] - mean; + + flat[y * width + x] = v; + hist[v + 255]++; + sum += v; + sum_sq += (gint64) v * v; + } + } + + /* Blank-frame gate (Windows: ChipSailing_IsBlankImage) */ + gint64 variance = (sum_sq - sum * sum / n) / n; + if (variance < CS9711_MIN_CONTRAST_VARIANCE) + { + fp_dbg ("Frame variance %" G_GINT64_FORMAT " below threshold %d - blank frame", + variance, CS9711_MIN_CONTRAST_VARIANCE); + return FALSE; + } + + /* Robust contrast stretch on 2nd..98th percentiles */ + gint lo = -255, hi = 255; + guint acc = 0, lo_count = n * 2 / 100, hi_count = n * 98 / 100; + for (gint i = 0; i < 511; i++) + { + acc += hist[i]; + if (acc <= lo_count) + lo = i - 255; + if (acc < hi_count) + hi = i - 254; + } + if (hi <= lo) + return FALSE; + + for (gint i = 0; i < n; i++) + { + gint v = (flat[i] - lo) * 255 / (hi - lo); + data[i] = CLAMP (v, 0, 255); + } + + return TRUE; +} + static int m_scan_submit_image (FpiSsm *ssm, FpImageDevice *dev) @@ -314,6 +553,12 @@ m_scan_submit_image (FpiSsm *ssm, img->data[dy * CS9711_WIDTH + dx] = self->image_buffer[y * CS9711_SENSOR_WIDTH + x]; } + if (!m_scan_normalize_image (img->data, CS9711_WIDTH, CS9711_HEIGHT)) + { + g_object_unref (img); + return 1; + } + img->flags = FPI_IMAGE_PARTIAL; fpi_image_device_image_captured (dev, img); @@ -346,16 +591,23 @@ m_scan_state (FpiSsm *ssm, FpDevice *_dev) break; case M_SCAN_GET_IMAGE_TAIL: - usb_read_in (_dev, ssm, CS9711_FP_RECV_LEN_2, TRUE, CS9711_DEFAULT_WAIT_TIMEOUT, m_scan_read_cb_bulk, M_SCAN_READ_CB_BULK_UD_SECOND_BLOCK); + /* All data arrives in first transfer with cmd 0x04 */ + fpi_ssm_next_state (ssm); break; case M_SCAN_SEND_POST_SCAN: - usb_send_out_sync (_dev, CS9711_FP_CMD_TYPE_RESET, &error); + usb_send_out_sync (_dev, CS9711_FP_CMD_TYPE_SLEEP, &error); m_util_fail_if_error_or_next (ssm, error); break; case M_SCAN_IMAGE_COMPLETE: - m_scan_submit_image (ssm, image_device); + if (m_scan_submit_image (ssm, image_device)) + { + /* Blank or low-contrast frame - ask the user to retry rather + * than letting the matcher fail on a garbage image. */ + fp_warn ("Discarding low-quality frame, requesting retry"); + fpi_image_device_retry_scan (image_device, FP_DEVICE_RETRY_CENTER_FINGER); + } fpi_image_device_report_finger_status (image_device, FALSE); fpi_ssm_mark_completed (ssm); break; @@ -382,6 +634,16 @@ dev_activate (FpImageDevice *dev) static void dev_deactivate (FpImageDevice *dev) { + FpDeviceCs9711 *self = FPI_DEVICE_CS9711 (dev); + + /* Cancel any in-flight read (e.g. a capture read still waiting for a + * finger). Leaving it pending would let it swallow the next session's + * init response, which is what the "drain stale data" recovery used to + * fight after the fact. */ + g_cancellable_cancel (self->interrupt_cancellable); + g_clear_object (&self->interrupt_cancellable); + self->interrupt_cancellable = g_cancellable_new (); + fpi_image_device_deactivate_complete (dev, NULL); } @@ -409,6 +671,7 @@ dev_open (FpImageDevice *dev) /* Initialize private structure */ memset(self->image_buffer, 0, CS9711_FRAME_SIZE); + self->interrupt_cancellable = g_cancellable_new (); /* Notify open complete */ fpi_image_device_open_complete (dev, error); @@ -417,8 +680,12 @@ dev_open (FpImageDevice *dev) static void dev_close (FpImageDevice *dev) { + FpDeviceCs9711 *self = FPI_DEVICE_CS9711 (dev); GError *error = NULL; + g_cancellable_cancel (self->interrupt_cancellable); + g_clear_object (&self->interrupt_cancellable); + /* Release usb interface */ g_usb_device_release_interface (fpi_device_get_usb_device (FP_DEVICE (dev)), 0, 0, &error); @@ -445,7 +712,7 @@ fpi_device_cs9711_class_init (FpDeviceCs9711Class *klass) FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); FpImageDeviceClass *img_class = FP_IMAGE_DEVICE_CLASS (klass); - g_assert ((CS9711_FRAME_SIZE) == (CS9711_FP_RECV_LEN_1 + CS9711_FP_RECV_LEN_2)); + g_assert ((CS9711_FRAME_SIZE) == CS9711_FP_RECV_LEN_1); dev_class->id = "cs9711"; dev_class->full_name = "Chipsailing CS9711Fingprint"; @@ -454,6 +721,11 @@ fpi_device_cs9711_class_init (FpDeviceCs9711Class *klass) dev_class->scan_type = FP_SCAN_TYPE_PRESS; dev_class->nr_enroll_stages = 15; + /* Disable software thermal throttling - this is a simple capacitive sensor + * without thermal concerns, and the default 3-minute limit causes spurious + * "Device disabled to prevent overheating" errors */ + dev_class->temp_hot_seconds = -1; + img_class->algorithm = FPI_PRINT_SIGFM; img_class->img_open = dev_open; img_class->img_close = dev_close; @@ -461,9 +733,12 @@ fpi_device_cs9711_class_init (FpDeviceCs9711Class *klass) img_class->deactivate = dev_deactivate; img_class->change_state = dev_change_state; - //TODO: Makes very marginal improvement, stick with default in case - // it changes with a better implementation in the future - // img_class->bz3_threshold = 24; + /* The default threshold (40) is BOZORTH3_DEFAULT_THRESHOLD, tuned for + * NBIS minutiae matching - not for SIGFM scores. Use the same value as + * elan, the other SIGFM driver in tree. SIGFM scores collapse + * quadratically as keypoint overlap shrinks, so an overly high + * threshold causes false rejections on this small (68x118) sensor. */ + img_class->score_threshold = 24; img_class->img_width = CS9711_WIDTH; img_class->img_height = CS9711_HEIGHT; diff --git a/libfprint/drivers/cs9711/cs9711.h b/libfprint/drivers/cs9711/cs9711.h index 832ce79c..868a9914 100644 --- a/libfprint/drivers/cs9711/cs9711.h +++ b/libfprint/drivers/cs9711/cs9711.h @@ -35,6 +35,11 @@ struct _FpDeviceCs9711 FpImageDevice parent; unsigned char image_buffer[CS9711_FRAME_SIZE]; + + /* Cancels in-flight USB transfers on deactivate so a pending capture + * read from a previous session cannot swallow responses meant for the + * next one (the main source of "stale data" during init). */ + GCancellable *interrupt_cancellable; }; G_DECLARE_FINAL_TYPE (FpDeviceCs9711, fpi_device_cs9711, FPI, DEVICE_CS9711, FpImageDevice) diff --git a/libfprint/sigfm/meson.build b/libfprint/sigfm/meson.build index 354d943d..ead9ad58 100644 --- a/libfprint/sigfm/meson.build +++ b/libfprint/sigfm/meson.build @@ -2,10 +2,12 @@ sigfm_sources = ['sigfm.cpp'] opencv = dependency('opencv4', required: true) -doctest = dependency('doctest', required: true) +doctest_dep = dependency('doctest', required: true) +doctest = doctest_dep.partial_dependency(compile_args: true, includes: true) libsigfm = static_library('sigfm', sigfm_sources, dependencies: [opencv], ) + sigfm_tests = executable('sigfm-tests', ['./tests.cpp'], dependencies: [doctest, opencv], link_with: [libsigfm])