Map the Frame controllers' buttons; make gaze clicks correctable

The Frame controllers aren't input devices on the host, so the pointer
helper reads them with SteamVR input (vrbuttons.h, actions/), one action
set per button, and sends presses to the relay, which does the mapped
action like for a mouse button. Only mapped buttons are taken, at an
overlay-global priority (SteamVR's experimental "Enable global input from
overlays"), and only outside games unless In games is on. Frametop Input
Settings gets a Controllers page to map them.

Gaze mode: a left press while the gaze has the pointer isn't sent at
once. The pointer stops, you drag it onto what you meant with the button
held, and the release clicks there; a press held still for
POINTER_GAZE_HOLD (0.5 s) becomes a real press, for drags. The dot shows
only while the mouse moves it (POINTER_GAZE_SHOW), while a press is held,
and as a pulse per click. Outside games the pointer stays on while gaze
mode is on. ft-gazed takes a third of each lesson's offset instead of all
of it: in the first live test one 6 degree lesson moved everything and put
the next target 7 degrees off. Frametop Input Settings gets a Gaze page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-28 16:26:24 -06:00
1 parent 3070fd5776
commit 0d50efec45
13 files changed
+1426 -54

No files matched your search

+17 -5
View File
@@ -69,6 +69,18 @@ LESSON_MAX = 8.0 # degrees past the correction
RETRY = 3.0 # seconds before starting ft-gaze again
class PointerLessons(LiveCorrection):
"""LiveCorrection, with the offset held back: one lesson moves the whole correction by a
third of what it measured, not all of it (two alike, half; three, three fifths). In the
probe, clicks came thick and fast on a stale calibration, where the error was mostly one
offset. Here lessons are few and the calibration is often fresh: in the first live test a
6 degree lesson shifted everything 6 degrees, and the next target, 10 degrees away and
1.4 off before, was 7.1 off (3.6 with this). Near the lesson, the kernel still takes up
most of it (5.1 of the 6 degrees)."""
RIDGE = [2.0] + LiveCorrection.RIDGE[1:]
def log(msg):
print(f"ft-gazed: {msg}", file=sys.stderr, flush=True)
@@ -80,7 +92,7 @@ class Service:
self.base = Correction()
self.mode = DEFAULT_MODEL
self.cal_mtime = None
self.live = LiveCorrection()
self.live = PointerLessons()
self.dirty = False
self.load_calibration()
self.load_lessons()
@@ -91,7 +103,7 @@ class Service:
self.fix = Fixation(radius=1.0)
self.opens = (deque(maxlen=90), deque(maxlen=90)) # left, right
self.vergence = deque(maxlen=90)
self.counts = {"samples": 0, "sent": 0, "blinks": 0, "one_eye": 0, "dropped": 0, "lessons": 0, "refused": 0}
self.counts = {"samples": 0, "sent": 0, "blinks": 0, "one_eye": 0, "dropped": 0, "lessons_taken": 0, "refused": 0}
self.last_sample = 0.0
self.last = None
@@ -124,7 +136,7 @@ class Service:
try:
d = json.loads(LESSONS.read_text())
if d.get("source") == self.source:
self.live.samples = d.get("samples", [])[-LiveCorrection.KEEP:]
self.live.samples = d.get("samples", [])[-PointerLessons.KEEP:]
except (OSError, ValueError):
pass
log(f"{len(self.live.samples)} lessons")
@@ -152,7 +164,7 @@ class Service:
else:
self.live.add({"time": rec["time"], "hy": rhy, "hp": rhp, "dy": dy, "dp": dp, "wy": 1.0, "wp": 1.0,
"how": "pointer"}, self.base, self.mode)
self.counts["lessons"] += 1
self.counts["lessons_taken"] += 1
self.dirty = True
try:
with open(LESSON_LOG, "a") as f:
@@ -293,7 +305,7 @@ class Service:
elif words[:1] == ["status"]:
reply = json.dumps(self.status())
elif words[:1] == ["forget"]:
self.live = LiveCorrection()
self.live = PointerLessons()
self.live.wear_time = self.steam.worn()
self.save_lessons()
reply = "ok"