Files
myscan/myscan/frame.py
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

81 lines
2.7 KiB
Python

"""Decode Hama myScan / Dacuda scanner-mouse MJPEG frames.
The firmware hides a per-frame sensor record in the JPEG headers (reverse
engineered from OVJpegSrcPreProc / DaReadMouseData in Omnivision's
UVCControl.framework shipped with the Dacuda Scanner Mouse Mac app):
* SOF0 height/width bytes carry 4 data bytes; the true size is 640x296.
* Entries 0x36..0x44 (relative to the FF marker) of the second DQT carry
15 more data bytes; the true table value there is 0x19.
"""
import struct
from dataclasses import dataclass
WIDTH, HEIGHT = 640, 296
def _spcal(v, mode, bit):
# OVSPCal: undo the encoding that keeps quant-table bytes legal.
if mode == 2:
r = max(v - 1, 0) * 4 + 2
elif mode == 3:
r = max(v - 1, 0) * 4 + 3
elif mode == 1:
r = v * 4 + 1
else:
r = v * 4
return ((r & 0xFB) | ((bit & 1) << 2)) & 0xFF
@dataclass
class SensorRecord:
counter: int
v: tuple # six big-endian 16-bit values; [2] and [5] are not transmitted
b14: int
b15: int
b16: int
flags: int # five flag bits (byte 17)
raw: bytes
def fix_and_extract(jpeg: bytes):
"""Return (clean_jpeg, SensorRecord | None)."""
buf = bytearray(jpeg)
s = bytearray(32)
have = 0
dqt = 0
i = 0
n = min(len(buf), 40000)
while i < n - 1:
if buf[i] == 0xFF:
m = buf[i + 1]
if m == 0xD8:
have |= 1
elif m == 0xC0:
have |= 2
s[0], s[0x14], s[0x15], s[0x16] = buf[i + 5:i + 9]
buf[i + 5:i + 9] = struct.pack('>HH', HEIGHT, WIDTH)
elif m == 0xDB:
dqt += 1
if dqt == 2:
have |= 4
d = buf[i + 0x36:i + 0x45]
for dst, src in zip((0x18, 0x19, 0x1A, 2, 3, 4, 5, 8, 9, 10, 11, 14, 15, 16, 17), d):
s[dst] = src
buf[i + 0x36:i + 0x45] = b'\x19' * 15
if have == 7:
break
i += 1
if have != 7:
return bytes(buf), None
# Each data byte gets a 2-bit mode from bytes 0x14..0x16 and one bit from 0x18..0x1a.
plan = [(2, 0x14, 6, 0x18, 5), (3, 0x14, 4, 0x18, 4), (4, 0x14, 2, 0x18, 3), (5, 0x14, 0, 0x18, 2),
(8, 0x15, 6, 0x18, 1), (9, 0x15, 4, 0x19, 5), (10, 0x15, 2, 0x19, 4), (11, 0x15, 0, 0x19, 3),
(14, 0x16, 6, 0x19, 2), (15, 0x16, 4, 0x19, 1), (16, 0x16, 2, 0x1A, 5), (17, 0x16, 0, 0x1A, 4)]
for dst, mreg, msh, breg, bsh in plan:
s[dst] = _spcal(s[dst], (s[mreg] >> msh) & 3, s[breg] >> bsh)
vals = tuple((s[2 * k + 2] << 8) | s[2 * k + 3] for k in range(6))
rec = SensorRecord(s[0], vals, s[14], s[15], s[16], s[17] & 0x1F, bytes(s[:20]))
return bytes(buf), rec