- ScanSession: scan-mode context manager with bright-pixel auto-exposure and sensor colour gains; tools/scan.py scans on the scan button with live stitching - Read-only EEPROM access and calib.py parser (laser geometry, intrinsics, colour gains, black/white flat-field samples); flat-field correction in Stitcher - OV7740 sensor access via the MCU proxy; colour gains from EEPROM fix green cast - Stitcher: colour support, high-passed registration (robust on blank paper) - RE notes on the Mac app's colour, exposure, stitching and button handling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
405 lines
19 KiB
Markdown
405 lines
19 KiB
Markdown
# Hama myScan scanner mouse: reverse-engineering findings
|
||
|
||
Status as of 2026-10-02: **scanning works on Linux.** The stock `uvcvideo` and
|
||
`hid-generic` drivers handle the device. A small user-space tool switches the
|
||
mouse into scan mode (LED illumination plus per-frame sensor data) through the
|
||
UVC extension unit. Everything needed was reverse engineered from the
|
||
**Dacuda Scanner Mouse for Mac 2.4** package (see "The real driver" below).
|
||
What remains is software: auto-exposure and stitching frames into a page.
|
||
|
||
## Quick reference: how to scan
|
||
|
||
All access goes through UVC extension unit 6, selector 2 (5 bytes), using
|
||
`UVCIOC_CTRL_QUERY` on the video node. See `myscan/xu.py`.
|
||
|
||
```
|
||
bridge write : SET_CUR [0x00, reg, 0, value, 0]
|
||
bridge read : SET_CUR [0x80, reg, 0, 0, 0] then GET_CUR -> value in byte 3
|
||
```
|
||
|
||
Start scanning, exactly as `Hal::OVMouseManager::startVideo` does:
|
||
|
||
1. write `D8 = 0x00`, poll `D8` until it reads `0xFF` ("disable mouse", standby)
|
||
2. write `2D = 0x60` and `E7 = 0x28` (each one is read back to verify)
|
||
3. optionally write `E6` = exposure (`round(ms × 49.23)`, 0–255 ≈ 0–5.2 ms)
|
||
4. write `D8 = 0xE1`, poll until `0x99`; then stream MJPEG from `/dev/videoN`
|
||
|
||
Stop: write `D8 = 0xE0`, poll until `0x11` (normal mouse mode again; `0x11`
|
||
is also the idle value after plugging in).
|
||
|
||
Verified on the device: the acks come back exactly as expected (`FF`, `99`,
|
||
`11`), frames light up in proportion to `E6`, and each frame carries a live
|
||
sensor record (see "Embedded sensor record").
|
||
|
||
## Hardware probing results (device attached)
|
||
|
||
### USB identity
|
||
|
||
```
|
||
idVendor=274f idProduct=c001 bcdDevice=c6.16
|
||
Manufacturer: Omnivision Product: Scanner Mouse
|
||
```
|
||
|
||
High-speed USB 2.0, one configuration, max power 500 mA. It's a composite
|
||
device, and the kernel binds it without any extra driver:
|
||
|
||
| Interface | Class | Kernel driver | Notes |
|
||
|---|---|---|---|
|
||
| 0 | Video Control (UVC 1.00) | `uvcvideo` | Interrupt status EP 0x82; one vendor extension unit |
|
||
| 1 | Video Streaming | `uvcvideo` | Isochronous EP 0x81, 3×1020 B; MJPEG only |
|
||
| 2 | HID mouse | `hid-generic` | Interrupt EP 0x83 |
|
||
|
||
dmesg notes "UVC non compliance - GET_DEF(PROBE) not supported", which the
|
||
kernel works around automatically.
|
||
|
||
### Video format and the bogus JPEG header
|
||
|
||
- The only format is MJPEG, **640×296 at 30 fps** (`dwMaxVideoFrameBufferSize`
|
||
378,880 = 640×296×2).
|
||
- Each frame is a complete JPEG (FFD8 … FFD9, about 4.6–18 KB, YCbCr 4:2:2),
|
||
**but its SOF0 header claims 480×640**. Decoded as-is, the image looks
|
||
smeared and the data runs out after 400 rows, because 1500 MCUs of 16×8 at
|
||
480 px wide make 400 rows.
|
||
- **Fix:** rewrite the SOF0 height/width fields to 296/640 before decoding.
|
||
That gives clean 640×296 frames, with the 1500 MCUs filling 40 × 37.5 MCU
|
||
rows:
|
||
|
||
```python
|
||
i = raw.find(b'\xff\xc0')
|
||
raw = raw[:i+5] + (296).to_bytes(2,'big') + (640).to_bytes(2,'big') + raw[i+9:]
|
||
```
|
||
- The image is effectively grayscale (all channels equal).
|
||
|
||
Verified: a frame taken with the mouse lifted toward a window clearly shows
|
||
window blinds. A frame taken while sliding over printed text shows text
|
||
strokes (motion-blurred, because the exposure is long).
|
||
|
||
### Illumination (solved)
|
||
|
||
In mouse mode the LEDs are off and frames are almost black (mean ≈ 8/255).
|
||
In scan mode the LEDs are strobed for the exposure window set in bridge
|
||
register `E6`. The power-on default is `E6 = 5` (≈ 0.1 ms), which is still
|
||
nearly black. Measured brightness (mean / max) on a dark fabric mousepad:
|
||
|
||
| E6 | exposure | mean | max |
|
||
|---|---|---|---|
|
||
| 5 | 0.10 ms | 8.9 | 24 |
|
||
| 40 | 0.81 ms | 22.4 | 141 |
|
||
| 120 | 2.44 ms | 51.2 | 228 |
|
||
| 255 | 5.18 ms | 89.7 | 250 |
|
||
|
||
The Mac app runs a PID auto-exposure loop over `E6` (strings `pidGainP/I/D`,
|
||
`exposureTimeUsMin/Max`) and stores per-device calibration in the mouse's
|
||
EEPROM ("HardwareData": `exposureTimeUs`, `imagerGain`, laser scaling, …).
|
||
|
||
### UVC controls
|
||
|
||
- The Input Terminal (camera) and Processing Unit advertise **no** standard
|
||
controls, so there are no exposure, gain or brightness controls through V4L2.
|
||
- **Extension Unit ID 6, GUID `dd880f8a-1cba-4954-8a25-f7875967f0f7`, 3 controls**
|
||
(queried with `UVCIOC_CTRL_QUERY` on `/dev/video4`):
|
||
|
||
| Selector | Length | Info (GET/SET) | GET_CUR | Interpretation |
|
||
|---|---|---|---|---|
|
||
| 1 | 20 | 0x01 (GET only) | `020c0616 01070000 4f2701c0 0038f501 00774242` | Device info: contains VID/PID; `02 0c 06 16` matches bcdDevice c616 (firmware version/date?) |
|
||
| 2 | 5 | 0x03 (GET+SET) | `8f311c6900` (constant) | Unknown; possibly a register address/value or command register |
|
||
| 3 | 33 | 0x03 (GET+SET) | read stalls (EPROTO) | Probably a command/response buffer: write first, then read |
|
||
|
||
MIN/DEF are all zeros, MAX all 0xFF, RES 1, so they say nothing useful.
|
||
Selector 2 turned out to be the register-access channel and selector 3 a
|
||
buffer channel; see "The real driver" for the full protocol.
|
||
|
||
### HID mouse
|
||
|
||
Report descriptor (73 bytes): 5 buttons + 3 padding bits, int16 X, int16 Y,
|
||
int8 wheel, int8 AC Pan. There is no report ID, and reports are 7 bytes:
|
||
|
||
```
|
||
byte 0: buttons bit0 left, bit1 right, bit2 middle, bit3/4 side,
|
||
bit7 = SCAN BUTTON (not declared in the descriptor)
|
||
byte 1-2: dX (int16 LE) byte 3-4: dY (int16 LE) byte 5: wheel byte 6: pan
|
||
```
|
||
|
||
The scan button sits in the "padding" bits, so the kernel's input layer
|
||
ignores it. It can only be read through `/dev/hidrawN`. Motion reports stop
|
||
when the mouse is lifted (laser lift-off detection). The 16-bit deltas at up
|
||
to ~125 Hz give a position track that can seed the frame stitching.
|
||
|
||
### Tooling
|
||
|
||
- `myscan/xu.py`: extension-unit register access and the scan-mode state machine.
|
||
- `myscan/frame.py`: JPEG header fix plus embedded sensor-record decoder.
|
||
- `tools/scantest.py`: enter scan mode, grab frames, restore mouse mode.
|
||
- `tools/probe.py` records raw frames and HID reports side by side (with
|
||
`frames.csv` / `hid.csv` timelines). Run it as
|
||
`.venv/bin/python tools/probe.py -d 60 -o capture`.
|
||
- `capture/` holds a 60 s recording: resting on paper, sliding over text,
|
||
button presses, and lifted toward a window.
|
||
|
||
### Container passthrough (Incus, container `ai`)
|
||
|
||
```sh
|
||
incus config device add ai myscan-usb usb vendorid=274f productid=c001 mode=0666
|
||
incus config device add ai myscan-video4 unix-char source=/dev/video4 path=/dev/video4 mode=0666 required=false
|
||
incus config device add ai myscan-video5 unix-char source=/dev/video5 path=/dev/video5 mode=0666 required=false
|
||
incus config device add ai myscan-hidraw unix-char source=/dev/hidraw8 path=/dev/hidraw8 mode=0666 required=false
|
||
```
|
||
|
||
The node numbers can change when the mouse is replugged.
|
||
|
||
---
|
||
|
||
## The real driver: Dacuda Scanner Mouse for Mac 2.4
|
||
|
||
- **Source:** the Wayback Machine has `Dacuda-ScannerMouse-Mac-V2.4.0.411.dmg`
|
||
(484 MB, archived 2016-05-23), linked from Dacuda's archived "scanner mouse
|
||
get started" page. The matching Windows `Dacuda-ScannerMouse-Win-V2.4.0.413.exe`
|
||
was never archived. The DMG is saved in `vendor/`, with the unpacked app in
|
||
`vendor/mac/payload/Scanner_Mouse.app`.
|
||
- **Key binaries:**
|
||
- `Contents/Frameworks/UVCControl.framework`: Omnivision's camera-control
|
||
library (author path `/Developer/usr/hslee4/UVCControl_110619/`), with
|
||
full symbols, fat x86_64/i386/ppc. Classes `UVCCameraControl` and
|
||
`CameraControlAppDelegate`, C API `OVIF_*`, plus `DaMouseCIF.m`.
|
||
- `Contents/MacOS/Scanner Mouse` (i386 only, 33 MB, Qt5): the app. Its
|
||
hardware layer is `Hal::OVMouseManager`
|
||
(`libs/Hal/Driver/LuckyCat/OVMouseManager.cpp`), used through `HalFree`.
|
||
|
||
### UVCControl.framework: the transport layer
|
||
|
||
All requests are class requests to interface 0, unit 6
|
||
(`bmRequestType 0x21/0xA1`, `wIndex 0x0600`):
|
||
|
||
| Method | Request |
|
||
|---|---|
|
||
| `UVCWriteUSBReg:withValue:` | SET_CUR sel 2 `[0x00, reg, reg>>8, val, 0]` |
|
||
| `UVCReadUSBReg:withValue:` | SET_CUR sel 2 `[0x80, reg, …]`, then GET_CUR sel 2; value = byte 3 |
|
||
| `UVCWriteSensorReg:withValue:` | SET_CUR sel 2 `[0x01, reg, reg>>8, val, 0]` |
|
||
| `UVCReadSensorReg:withValue:` | SET_CUR sel 2 `[0x81, reg, …]`, then GET_CUR; value = byte 3 |
|
||
| `UVCWriteBuf:` / `UVCReadBuf:` | SET_CUR / GET_CUR sel 3, variable length |
|
||
| `UVCGetExtensionInfo:` | GET_CUR sel 1 |
|
||
|
||
`OVIF_SCCBRead/Write(id, reg, …)`: **id `0x88` is a pseudo-ID meaning "bridge
|
||
registers"**. Any other id is a real SCCB slave: the library first writes
|
||
bridge register `0xF1 = id`, then uses the sensor-register commands.
|
||
`OVIF_GetScanBtnStatus` reads bridge register `0xBC`, bit 0.
|
||
|
||
### Mouse-controller registers (bridge registers, the app's "SCCB 0x88")
|
||
|
||
| Reg | Use in the app |
|
||
|---|---|
|
||
| `D8` | Command/status: `00`→`FF` disable mouse (standby); `E1`→`99` start scan streaming; `E0`→`11` enable mouse; `ED`/`EC` security check (`performSecurityCheck`, challenge/response via `A1`–`A5`) |
|
||
| `2D` | Written `0x60` before starting a scan (default `0x20`) |
|
||
| `E7` | Written `0x28` before starting a scan (default already `0x28`) |
|
||
| `E6` | Exposure / LED on-time: `floor(ms × 49.23 + 0.5)` (`OVMouseManager::setExposureTime`) |
|
||
| `DC` | Laser readout position (`setLaserReadPosition`; refused while streaming) |
|
||
| `BC` | Bit 0 = scan button |
|
||
| `BD`/`BE`/`B8` | Proxy to the OV image sensor (SCCB 0x42): `BD` = register, `BE` = value, `B8` = 1 write / 2 read, with a status poll before |
|
||
|
||
Image-sensor registers used (via the proxy): `0x00` gain, `0x01`–`0x03` colour
|
||
gains, `0x16`/`0x19` window position (`setYBaseOffset`). These follow the
|
||
standard OV76xx/77xx register map.
|
||
|
||
### Embedded sensor record (per frame)
|
||
|
||
`OVJpegSrcPreProc` and `DaReadMouseData` in UVCControl. In scan mode the
|
||
firmware hides a 20-byte record in each JPEG's headers:
|
||
|
||
- SOF0 height/width bytes (4 bytes) carry: frame counter, plus 3 "mode" bytes.
|
||
The app restores them to height 296, width 640.
|
||
- Entries 49–63 (offset `0x36..0x44` from the `FF DB` marker) of the
|
||
**second** DQT carry 15 bytes. The app restores them to `0x19`.
|
||
- Each payload byte is decoded with `OVSPCal` using a 2-bit mode from the
|
||
SOF bytes and one bit from the first three DQT bytes. Implemented in
|
||
`myscan/frame.py`.
|
||
|
||
Record layout after decoding (field names from app strings
|
||
`laser1Dx/laser1Dy/laser1Sq/laser2…`):
|
||
|
||
| Bytes | Content |
|
||
|---|---|
|
||
| 0 | frame counter (increments by 1 per frame; verified) |
|
||
| 2–3, 4–5 | big-endian 16-bit: laser 1 dX, dY (presumably) |
|
||
| 8–9, 10–11 | big-endian 16-bit: laser 2 dX, dY (presumably) |
|
||
| 14, 15 | laser 1 / laser 2 surface quality (≈155–164 on fabric; verified to vary) |
|
||
| 16 | unknown |
|
||
| 17 | 5 flag bits (buttons / state) |
|
||
| 19 | set by the app when a frame was dropped (counter gap) |
|
||
|
||
In mouse mode the headers are plain (480×640, standard tables), so the record
|
||
decodes to constants.
|
||
|
||
---
|
||
|
||
# Original analysis of the supplied installer
|
||
|
||
## Device background
|
||
|
||
- **Product:** Hama myScan, Hama article no. 00052343. It is a 4-button, 1200 dpi
|
||
laser mouse with a built-in hand scanner (scan area about 2 × 4 cm, scan
|
||
resolutions 100/200/320/400 dpi).
|
||
- **Real manufacturer:** Dacuda AG (Zürich, Switzerland). Dacuda designed the
|
||
scanner-mouse hardware and its "SLAM Scan" stitching/OCR software and licensed
|
||
it to other brands.
|
||
- **Sister products from the same Dacuda platform:**
|
||
- LG LSM-100 "Smart Scan" mouse (sold with the LG Smart Scan software, Windows only)
|
||
- probably the IRIScan Mouse (I.R.I.S.)
|
||
- Products of this kind use a small CMOS camera to capture frames of the page.
|
||
The host software stitches the frames together using the mouse's position.
|
||
|
||
## The supplied file: `00052343_wxpvista78_v2_3_2.exe`
|
||
|
||
| Property | Value |
|
||
|---|---|
|
||
| Size | 2,046,568 bytes |
|
||
| Format | PE32, i386, GUI. A plain executable, not an installer archive. |
|
||
| Framework | wxWidgets, MSVC 9 (VC90 CRT) |
|
||
| Signed by | Dacuda (Comodo code-signing certificate, contact `lionel.roll@dacuda.com`) |
|
||
| PDB path | `D:\JENKINS\jobs\OnlineInstallerClient-Master\workspace\ScanMouse\bin\Release\HAM_Scanner_Mouse_Online_Installer.pdb` |
|
||
| Window title | "Scanner Mouse Driver Installer" |
|
||
|
||
### What it does
|
||
|
||
1. **Lists USB devices.** It opens the host controllers directly (`\\.\HCD%d`)
|
||
and walks the hub tree with `DeviceIoControl`, collecting each device's
|
||
descriptor (`wVendorID`, `wProductID`, `bDeviceClass`, …). With `-v`
|
||
(verbose) it writes the list to `%TEMP%/connectedDevices.txt`. Other
|
||
command-line flags are `-d` (developer) and `-h` (help).
|
||
2. **Asks the update server about every device.** The function at VA
|
||
`0x404c90` loops over **all** connected devices and appends each one to the
|
||
query (the `VID=` reference is at `0x404e9d`, `&PID=` at `0x404eba`):
|
||
|
||
```
|
||
http://updater.dacuda.com/onlineinstaller_list?VID=<vid>&PID=<pid>&VID=...
|
||
```
|
||
|
||
The path string `/onlineinstaller_list` is referenced at `0x403baa`.
|
||
**There is no local VID/PID filter.** The server decided which driver
|
||
matched the hardware.
|
||
3. **Downloads the real driver.** It saves the package as `\online.exe` in a
|
||
temp directory and runs it. The status text reads "Downloading Scanner
|
||
Mouse Driver (incl. 198 recognition languages)...".
|
||
|
||
If no matching device is found, it shows "Please connect the Scanner Mouse
|
||
and run the installer again."
|
||
|
||
### Conclusion
|
||
|
||
The executable contains **no scanning protocol, no device IDs and no image
|
||
handling**. There is nothing in it to build a Linux driver from.
|
||
|
||
## Status of download sources
|
||
|
||
| Source | Result (2026-10-02) |
|
||
|---|---|
|
||
| `http://updater.dacuda.com/onlineinstaller_list?...` | HTTP 404. The hostname now points at Google hosting (`ghs.googlehosted.com`); the service is gone. |
|
||
| `http://www.hama.de/scannermaus` (the download URL given in Hama's docs) | 404 |
|
||
| Hama product pages for 00052343 | 404 |
|
||
| Wayback Machine (CDX query for `updater.dacuda.com*`) | Archive was "Temporarily Offline". **Retry this.** |
|
||
| Web search for driver mirrors or existing Linux work | Nothing found. The LG LSM-100 was only ever supported on Windows. |
|
||
|
||
## Next steps (original plan; A.1–A.3 are now done, see the top of this file)
|
||
|
||
### A. Probe the hardware directly (needs the mouse connected)
|
||
|
||
1. Dump the USB descriptors (install `usbutils` first, then run `lsusb -v -d <vid>:<pid>`).
|
||
Look for interfaces besides the standard HID mouse: a vendor-specific class
|
||
(0xFF) with bulk/interrupt endpoints, a video-class interface (UVC), or a
|
||
second HID interface with large reports.
|
||
2. Check whether the scan button changes what the device reports or how it
|
||
enumerates.
|
||
3. Use pyusb/libusb to read from the extra endpoints and try to capture raw
|
||
sensor frames. Then work out the frame format (size, bit depth) by
|
||
rendering the frames as images.
|
||
4. If frames come through, write a Linux tool that grabs frames together with
|
||
the mouse position, stitches them, and optionally runs OCR with Tesseract.
|
||
|
||
### B. Find the real Windows driver (fastest route if it turns up)
|
||
|
||
- Retry the Wayback Machine for `updater.dacuda.com/*` and the Hama download pages.
|
||
- Look for the LG Smart Scan or IRIScan Mouse software. Being on the same
|
||
Dacuda platform, they probably share the USB protocol.
|
||
- Any old CD, any copy of `online.exe`, or an installed "Scanner Mouse"
|
||
program folder from a Windows machine would do. The protocol could then be
|
||
read out of the real driver.
|
||
- With a working Windows setup (a VM with USB passthrough is enough), capturing
|
||
traffic with Wireshark + USBPcap during a scan would show the protocol
|
||
directly.
|
||
|
||
## References
|
||
|
||
- Hama myScan manual: https://www.manualslib.de/manual/56025/Hama-116600.html
|
||
- Hama myScan review (PL): https://www.videotesty.pl/recenzja/1442/hama-myscan-myszi-skaner-w-jednym/
|
||
- Dacuda background: https://www.venturekick.ch/Worldwide-renowned-scanner-mice
|
||
- LG LSM-100 review: https://www.computing.co.uk/review/2122872/review-lg-lsm-100-scanner-mouse
|
||
- LG Scanner Mouse review: https://www.itpro.com/637726/lg-scanner-mouse-review
|
||
|
||
## Laser calibration & stitching (2026-10-02, recording scan_text/)
|
||
|
||
- Record v0/v1 = laser 1 cumulative (a, b) counts, v3/v4 = laser 2; signed 16-bit, wrapping. ~3 counts per frame pixel.
|
||
- Frame content shift: dx = -0.022*a - 0.355*b, dy = 0.324*a + 0.002*b (a, b = mean of both lasers).
|
||
- Rotation: dtheta ≈ -4.4e-4 rad * (b1 - b2) (fit against ORB rotation over 10-30 frame windows; corr ~0.88).
|
||
- At fast motion (~25 px/frame) the lasers disagree (one over-counts by ~40%), so stitch.py checks each step against
|
||
frame-to-frame phase correlation, then ECC-refines every frame against the canvas.
|
||
- Frames are transposed + mirrored relative to the page; output = flip(transpose(canvas), -1).
|
||
- `tools/stitch.py <dir>` -> <dir>/page.png. Full 751-frame session stitches cleanly.
|
||
|
||
## Per-device calibration in EEPROM (read 2026-10-02)
|
||
|
||
Read-only access is `XU.eeprom_read(addr, n)`, XU selector 3:
|
||
- `01 addr len`: set address and length.
|
||
- `02`: start the read.
|
||
- `03`: returns the next 32 bytes; repeat until done.
|
||
- `05`: end.
|
||
|
||
The write path uses commands 06/07/09 and is RSA-signed. It is not implemented.
|
||
|
||
Layout:
|
||
- `0x3000`: u16 blob length (19417 on this unit).
|
||
- `0x3002`: checksum.
|
||
- `0x3006`: a boost binary archive (i386 sizes).
|
||
- `0x7EB5`: 64-byte ID.
|
||
|
||
`myscan/calib.py` parses it. Values on this unit:
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| ROI | 0,0 – 640,295 |
|
||
| Laser scale (left, right) | 1.922e-5, 1.878e-5 m/count |
|
||
| Laser positions (camera frame) | (11.25, 53.8) mm and (33.55, −0.67) mm |
|
||
| Intrinsics | f = 737.3 px, principal point (317.5, 132.7); 5 distortion coefficients |
|
||
| `pixelSize` | 61.3 µm |
|
||
| `gainR/G/B` | 115 / 89 / 64 (sensor registers 1/2/3) |
|
||
|
||
Colour calibration is stored as point samples, not as NURBS coefficients:
|
||
- 827 black samples and 873 white samples.
|
||
- Each sample is `u16 x, u16 y, u32 3, u8 b, g, r` in raw-frame coordinates.
|
||
- The white surface matches the illumination falloff of real frames (correlation 0.91–0.95 per channel).
|
||
|
||
`FlatField` fits cubic B-spline surfaces to the samples. Its default correction divides each frame by the normalised white surface. The app's full `(in − black)/(white − black)·250` is only valid at factory exposure and gains.
|
||
|
||
Auto-exposure now regulates the 95th percentile of the brightest channel (target 200). The old mean-brightness target let green clip at about 233, which broke the linear correction.
|
||
|
||
### Image sensor access and colour gains
|
||
|
||
The image sensor is an **OV7740**: PID 0x77/0x42, MID 0x7F/0xA2.
|
||
|
||
Two access paths work, both only in scan mode (after D8 E1→99):
|
||
- **MCU proxy**, which is what the app uses:
|
||
- Read: poll until B8=0, write BD=reg, write B8=2, poll until B8=0, then read BE.
|
||
- Write: write BD=reg, BE=val, B8=1, then read back to verify.
|
||
- **Bridge path** (UVCControl `ReadSCCB`/`WriteSCCB`): set F1=0x42, then XU selector 2 command 0x81/0x01. The app never uses it.
|
||
|
||
In mouse mode B8 stays stuck at 2. In standby BE reads 0.
|
||
|
||
Colour gains: firmware leaves regs 1/2/3 (blue/red/green, 0x40 = 1×) at 64/64/64. The app writes the EEPROM's `gainR/G/B` = 115/89/64 to regs 1/2/3 in that order.
|
||
|
||
Measured frame B:G:R:
|
||
|
||
| Gains | B | G | R |
|
||
|---|---|---|---|
|
||
| Firmware default | 6.5 | 14.3 | 10.8 |
|
||
| EEPROM gains | 13.0 | 13.4 | 13.3 |
|
||
|
||
The EEPROM gains remove the green cast in hardware. `ScanSession(gains=...)` applies them; `scan.py` and `record.py` pass the EEPROM values.
|