Files
myscan/FINDINGS.md
T
erikandClaude Opus 5.5 e1c1012c1c Hama myScan Linux driver tools: scan mode, frame decoding, stitching
UVC extension-unit control (mouse/scan mode, LEDs, exposure), JPEG header
fix and embedded laser record decoding, session recorder, and a stitcher
that combines laser dead reckoning with phase correlation and ECC refinement.
FINDINGS.md holds the reverse-engineered protocol and laser calibration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:41:22 +00:00

16 KiB
Raw Blame History

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:

    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)

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

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.022a - 0.355b, dy = 0.324a + 0.002b (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> -> /page.png. Full 751-frame session stitches cleanly.