Files
DeeJanuz--frametop/gaze/tracker/lab/ft-eyes-replay
T
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

72 lines
2.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""ft-eyes-replay: play a recording into the shared-frame layout, as ft-eyegrab --share
would, at the recorded pace, to test ft-eyes without the headset.
Usage: lab/py lab/ft-eyes-replay NAME [PATH] [--from S] [--for S]
NAME is a recording (a bare name is in eyes_lab.CAPTURES). PATH defaults to
/dev/shm/frametop-eyes-cams-replay; run ft-eyes with FT_EYES_CAMS=PATH (and FT_EYES_GAZE=...
so it doesn't overwrite the live output).
"""
import mmap
import os
import struct
import sys
import time
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent))
from eyes_lab import capture # noqa: E402
W, H, SLOTS = 512, 400, 8
ESIZE = 64 + W * H
def main(argv):
cap = capture(argv[0])
path = argv[1] if len(argv) > 1 and not argv[1].startswith("--") else "/dev/shm/frametop-eyes-cams-replay"
start = float(argv[argv.index("--from") + 1]) if "--from" in argv else 0.0
length = float(argv[argv.index("--for") + 1]) if "--for" in argv else 1e9
idx = np.array([[float(v) for v in l.split()] for l in (cap / "index.txt").read_text().splitlines()
if len(l.split()) == 4])
frames = np.memmap(cap / "frames.raw", dtype=np.uint8, mode="r").reshape(-1, H, W)
size = 64 + 2 * SLOTS * ESIZE
if os.path.exists(path):
os.unlink(path)
fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
os.ftruncate(fd, size)
mm = mmap.mmap(fd, size)
os.close(fd)
struct.pack_into("<6I2QI", mm, 0, 0x31434546, 1, W, H, SLOTS, ESIZE, 0, 0, os.getpid())
count = [0, 0]
t0 = idx[0, 3] + start
wall0 = time.monotonic()
try:
for i in range(len(frames)):
t = idx[i, 3]
if t < t0:
continue
if t - t0 > length:
break
delay = (t - t0) - (time.monotonic() - wall0)
if delay > 0:
time.sleep(delay)
cam = int(idx[i, 2])
n = count[cam]
off = 64 + (cam * SLOTS + n % SLOTS) * ESIZE
seq = struct.unpack_from("<Q", mm, off)[0]
struct.pack_into("<Q", mm, off, seq + 1)
mm[off + 64:off + 64 + W * H] = frames[i].tobytes()
struct.pack_into("<dQII", mm, off + 8, t, n, cam, int(idx[i, 1]))
struct.pack_into("<Q", mm, off, seq + 2)
count[cam] = n + 1
struct.pack_into("<Q", mm, 24 + 8 * cam, n + 1)
finally:
os.unlink(path)
print(f"replayed {sum(count)} frames")
if __name__ == "__main__":
main(sys.argv[1:])