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>
81 lines
2.7 KiB
Python
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
|