qemu: structural hash relative to the background, taps that land on the pixel

imgtools.structural() marked a cell occupied when its grey exceeded an
absolute 10/255, and the WardenOS page background is grey 16: every cell
of every region read occupied and no structural check could ever fail
(#19). Occupancy is now grey deviating from the crop's own median by more
than DEVIATION_THRESHOLD, or edge energy above EDGE_THRESHOLD. Measured on
real captures: a switch knob left/right differs in 240 of 256 cells (was
0), a dark card reads its icon and text and nothing else. Every committed
reference is recaptured with flare-edge tools/flow-run-all.sh --capture.

qmp.py to_axis() truncated the pixel-to-axis conversion and LVGL's
evdev calibration truncates on the way back, so many pixels landed one
short (130 -> 5924 -> 129) and a tap could miss the control hit had just
confirmed at that pixel (#21). It now rounds up to the smallest axis value
that truncates to the requested pixel; test_qmp_drive.py asserts the round
trip for every pixel at three panel sizes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
This commit is contained in:
Noah
2026-09-08 13:53:59 -06:00
co-authored by Claude Fable 5.1
parent f809c6e5a3
commit bd5c5af9db
3 changed files with 56 additions and 8 deletions
+12 -2
View File
@@ -97,6 +97,7 @@ A step that names an unknown command is a FATAL error rather than a skip: a
silently-ignored line in a scenario is a test that proves nothing.
"""
import json
import math
import os
import re
import shutil
@@ -157,9 +158,18 @@ def btn_ev(down):
def to_axis(px, size):
"""Panel pixels -> the tablet's absolute axis, clamped to the panel."""
"""Panel pixels -> the tablet's absolute axis, clamped to the panel.
LVGL's evdev driver maps the axis back with integer truncation
(lv_evdev.c _evdev_calibrate: px = axis * (width - 1) / AXIS_MAX), so the
axis value must be the SMALLEST one that truncates to px, i.e. rounded up.
Truncating here as well composed two floors and landed one pixel short for
many values (130 -> 5924 -> 129), which is how a tap could miss the control
that `hit` at the same pixel had just confirmed (flare-edge #148 triage).
tools/touch-inject writes the pixel itself on a panel, so hardware never
had this seam."""
px = max(0, min(size - 1, int(px)))
return int(px * AXIS_MAX / (size - 1))
return min(AXIS_MAX, math.ceil(px * AXIS_MAX / (size - 1)))
def do_tap(s, f, ax, ay, hold=0.2):