Commit Graph
17 Commits
Author SHA1 Message Date
DeeJanuzandClaude Opus 5.5 580004bb00 Gaze checks: the headset going on is eyes coming back
Steam's eyetracking.txt writes "HMD on" every minute or so with nobody in the
headset, and SteamVR said it was worn for 12 hours straight, so the quick check
opened about 40 times an hour at an empty headset. It now opens when SteamVR's
tracker sees eyes for 3 s after none for 3 s, and closes when they're gone 2 s.

The log reader also drops repeated "HMD on" lines and sub-second offs, so
lessons stop counting as from an older wear every minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:21:21 -06:00
DeeJanuzandClaude Opus 5.5 c4c10e6d92 Gaze checks and calibration in a panel fixed to the headset
ft-gazepanel (gaze/panel) is a SteamVR overlay that stays put in your view and
shows dots at known head-relative directions; the gaze service runs it and
drives it (gaze/gazecheck.py):
- a one-dot quick check 3 s after the headset goes on, when our tracker asks
  for a click (reseat), at most every 2 minutes, and from Quick check on the
  Gaze page; five dots follow if the next 3 lessons are still over 2 degrees off
- the full calibration (the probe's three rounds, dark to bright) when gaze mode
  comes on without one, or from Calibrate; quitting it with still no calibration
  turns gaze mode off
Each dot takes the gaze once it has held still for 0.6 s; a left click or Meta+J
takes it at once, a right click or Meta+K closes the panel (the pointer helper
hides its dot and passes those presses on while "calpanel" lasts). Our tracker
gets clicks and its own calibration; SteamVR's gets lessons per eye, or a new
calibration.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 22:12:26 -06:00
DeeJanuzandClaude Opus 5.5 e16b7588e2 Gaze mode is a mouse feature: drop its controller parts
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>
2026-09-30 21:11:24 -06:00
Nikita Koptelov c3375d32d3 Start Frametop's SteamVR clients only once SteamVR is up (#6)
The pointer and gaze services need steamvr.service to be running (Requisite=), and ft-pointer, ft-screens, and ft-gaze connect as a background app before switching to overlay, so they never start a vrserver of their own. One started from the dev container never finds the headset, which left a reboot stuck in a loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 51b4e79318)
2026-09-30 10:02:33 -06:00
DeeJanuzandClaude Opus 5.5 ed75614f62 Bring our own eye tracker into the gaze folder, run by the gaze service
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>
2026-09-30 10:00:07 -06:00
DeeJanuzandClaude Opus 5.5 5565a25444 Let the gaze pointer use our own eye tracker, and weight the eyes
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>
2026-09-30 09:11:54 -06:00
DeeJanuzandClaude Opus 5.5 76f3cdd2dc Ask for one look at a centre dot after the headset was off
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>
2026-09-29 23:08:07 -06:00
DeeJanuzandClaude Opus 5.5 c37bee27c9 Keep the Own tracker's calibration in reach, and let it learn after a re-seat
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>
2026-09-29 22:51:37 -06:00
DeeJanuzandClaude Opus 5.5 b76a8c50e0 Spread the Own tracker's calibration dots over the practice area
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>
2026-09-29 22:04:06 -06:00
DeeJanuzandClaude Opus 5.5 30a9d15072 Add our own eye tracker as a gaze source in ft-gaze and the probe
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>
2026-09-29 21:38:17 -06:00
DeeJanuzandClaude Opus 5.5 f01c84f9a3 Use the other eye when the tracker loses one, and add a headset fit check
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>
2026-09-28 22:07:41 -06:00
DeeJanuzandClaude Opus 5.5 0d50efec45 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>
2026-09-28 16:26:24 -06:00
DeeJanuzandClaude Opus 5.5 3070fd5776 Keep the gaze going when the tracker loses one eye
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>
2026-09-27 22:54:02 -06:00
DeeJanuzandClaude Opus 5.5 f1c385aa59 Add ft-gazed, the gaze service, and ft-gazectl
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>
2026-09-27 22:51:24 -06:00
DeeJanuzandClaude Opus 5.5 1d0211dc5e Move the gaze calibration code into gazecal.py
The correction models, filters, blink filter, and SteamVR log reader move
out of the probe so the gaze service can use them too. Two additions on the
way: the log reader follows the headset going on and off (SteamVR starts its
eye model over each time it goes on), and LiveCorrection counts lessons from
before the last time it went on less (OLD_WEAR), so the first few after
relearn the offset while the shape is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:51:24 -06:00
DeeJanuzandClaude Opus 5.5 526dfa3511 Report each eye's own direction from ft-gaze
Both mmap sets now add "eyes": the left and right eye's head-relative
direction, so the eyes can be calibrated separately (each has its own offset
between where the tracker says it points and where it looks). Nothing uses
it yet; it gets logged with every sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:51:24 -06:00
DeeJanuzandClaude Opus 5.5 9ecd9bb19d Add gaze: eye tracking as pointer input, with a probe to calibrate it
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>
2026-09-27 22:51:13 -06:00