Steam reads the Frame controllers itself, outside SteamVR's bindings, and every
press and release it sees takes SteamVR out of laser mode, so controller clicks
at the gaze can't be done cleanly (docs/gaze-controllers.md, from the gaze-first
branch's tests).
- Gaze precision and Gaze drag take a mouse button or a key combination only;
the controller source, its aim steering, and POINTER_PRECISION_GAIN,
POINTER_PRECISION_DEADZONE and POINTER_GAZE_DRAG_GAIN are gone.
- The gaze actions (including Gaze pointer on/off) can't be mapped to controller
buttons: the relay ignores them there and doesn't ask the helper for those
buttons, and Input Settings no longer offers them.
- Last used wins in gaze mode too: picking up a controller hands it the laser,
as docs/design.md already said.
- The gaze dot shows all the time (POINTER_GAZE_DOT=always, the default;
moving brings back the old behaviour), with a switch on the Gaze page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frame-eyes, a separate project until now, becomes gaze/tracker:
- ft-eyegrab (was fe-bufprobe) copies the eye-camera frames, read-only, out of
SteamVR's eyetracking process. It runs as the system service
frametop-eyegrab.service, which gaze/tracker/install.sh installs to
/etc/frametop with sudo. It keeps only CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, and
CAP_CHOWN, and copies frames only while /dev/shm/frametop-eyes-want is fresh,
holding none of the tracker's buffers otherwise.
- ft-eyes (was fe-trackd) runs under ft-gazed in the dev container, with
build/venv's pinned numpy and OpenCV: while Eye tracker is Own tracker, or on
the probe's lease ("eyes SECONDS"). No sudo password or fe-live script at
run time any more.
- lab/ holds the research tools (ft-eyes-score, -e2e, -record, -replay,
-session) and findings.md. Recordings live outside the repo, in
~/.local/share/frametop/eyes/captures; .gitignore catches stray frame dumps.
Its socket is now @ft_eyes, its output /dev/shm/frametop-eyes-gaze, and its state
~/.local/state/frametop/gaze/eyes. On practice1 -> practice2 the whole live path
(ft-eyes-e2e) gives 1.30 deg median and 3.18 for the worst tenth, as before the
move (1.30, 3.21).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frametop Input Settings' Gaze page gets two settings, saved in frametop.conf and
read again by ft-gazed when the file changes: Eye tracker (GAZE_TRACKER: SteamVR's
or our own, frame-eyes' fe-trackd) and Eye bias (GAZE_EYE: auto, left, right).
ft-gazed now combines the eyes, each calibrated on its own: SteamVR's set 2 eyes
with the probe's Left eye and Right eye calibrations, or our tracker's eyes as
they come. Without per-eye calibrations, or with --source, it keeps the older
one-source path. A pointer nudge finds its look from the raw gaze the helper
echoes back; with our tracker it goes to fe-trackd as a click.
The bias leans instead of choosing (gazecal.EyeWeights): on 306 live clicks the
eyes' errors partly cancelled, both together 0.65 deg off against 0.96 and 1.11
for either alone. Left or Right counts that eye twice; auto weights each eye by
its RMS miss at its last 20 nudges, since the calibration's fit picked the wrong
eye on SteamVR's test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Own tracker, the probe polls its status each second. After the headset was off
(or the tracker restarted), it shows one dot at the centre until you look at it and press
(S skips): that click resets both eyes' shifts, so the first real clicks aren't 10-17
degrees off. The screen-to-direction geometry the calibration used is shared with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The calibration dots for the Own tracker go out to the Calibration ring angle each way on
an oval, not to the window's corners, which were too far to look at while facing the
centre. The Own tracker learns from drags up to 25 degrees (after taking the headset off
and on, its first clicks were 11-17 degrees off, and the 6-degree limit blocked them). The
probe starts on the Own tracker when it's running, and the calibration header names the
tracker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its fit goes wrong past its dots, and the ring (limited by the window's height) never
reached the sides, where frame-eyes' worst practice clicks were. With the Own tracker
the calibration now uses the centre, corners, and side middles of the practice area.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ft-gaze reads frame-eyes' /dev/shm/frame-eyes-gaze and reports it as the source "own",
with each eye's gaze, where each lands on the screen, and its slip. The probe gets a
SteamVR / Own tracker toggle. With Own tracker on, it hides SteamVR's gaze and draws a
red dot per eye, its calibration fits fe-trackd's own calibration, and practice clicks
teach fe-trackd instead of the probe's correction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SteamVR's combined gaze keeps going on one eye, but it holds the lost
eye's yaw, so the gaze moves half as far sideways as the eyes do.
ft-gaze now reads each eye's tracking uncertainty and raw measurement
from eye-server.mmap. When the tracker loses an eye, ft-gazed takes the
gaze from the other one, plus the offset that eye usually shows
against both, learned while both are seen. On a recording, one eye
alone came out a median 0.8 degrees from both eyes' gaze.
Glances down at the keyboard, past every screen, aren't sent. The
pointer stays put, and eyes lost there don't count as lost.
The gaze probe gets a Headset fit mode. It shows per-eye tracking,
openness and confidence, maps where each eye gets lost, gives hints,
and has a guided check. The settings app opens it from the Gaze page
and shows how often each eye is lost. The probe can also test each eye
alone, and its side panel now collapses to a title bar so the dot
isn't hidden behind it.
Snapping to UI elements is deferred; the mouse drag is the correction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Live, the tracker had lost the left eye (openness 0, set 2's eyes 9 to 14
degrees apart) and ft-gazed dropped 96% of samples as blinks. A blink is now
both eyes closing, each judged against its own recent good readings, and the
service defaults to mmap set 1, SteamVR's combined gaze, which keeps going
with one eye. With set 2, one-eye samples are still dropped, since its
combined direction is off by half of what the lost eye reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ft-gazed runs ft-gaze, drops blinks and dropouts, smooths with a fixation
lock, applies the probe's calibration plus what the pointer has taught
since, and sends the corrected gaze to the pointer helper at 90 Hz. The
helper sends lessons back (a nudge before a click), which are learned and
saved. It only reads the eye tracker; nothing of SteamVR's is written.
gaze/run.sh installs it as a user service that starts with SteamVR;
ft-gazectl turns gaze mode on and off and shows the service's status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ft-gaze reads the Frame's eye tracker (SteamVR's eyetracking action and the
two gaze sets in eye-server.mmap, read only) and prints where the gaze lands
on the Frametop screens. ft-gazeprobe is a GTK playground on top of it: a
Vision Pro style calibration run, an accuracy test, refining the correction
model from logged points, and click, drag, and snap practice that learn the
tracker's error on the fly. It also follows SteamVR's eyetracking log for
restarts and the clicks SteamVR calibrates itself from, which it keeps only
in memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>