mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 03:00:06 +02:00
ft-gaze computed and printed all six sources for every sample: about 1.3 KB of JSON a line
with our tracker (practice2), 120 KB a second through podman's stdio relay for ft-gazed to
json.loads 90 times a second. It also read SteamVR's gaze action for every sample outside
games (UpdateActionState and GetEyeTrackingDataRelativeToNow, two calls into vrserver, 180 a
second), though with our tracker ft-gazed only uses own and mmap1.
- ft-gaze takes --sources LIST (action, mmap1, mmap2, left, right, own, and eye for the EYE
object; all by default, so the probe and ft-eyes-session are unchanged), and with
--watch-stdin a line "sources LIST" on stdin switches them. A source left out isn't read
and prints as {"ok":0} ("eye" as null), so every line keeps the same keys. An older
ft-gaze ignores both, and prints everything as before.
- ft-gazed asks for what it reads: own,mmap1 with our tracker; left,right,mmap1 with
SteamVR's eyes; the source plus mmap1 and mmap2 on the older one-source path. While a
check or the calibration runs or waits to open, all of them, since checks record every
source (the calibration fits the action's correction too) and the fit check reads "eye".
It switches as soon as that changes, well inside the check's 0.45 s settle.
- So the action is read only during checks, or with --source action.
On recorded samples, a line with own and mmap1 is about 700 bytes instead of 1,300
(practice2), and one with left, right and mmap1 about 550 instead of 940 (test1).
gaze/test/idle-test.py now checks that ft-gaze starts with every source for a check and is
then switched to those in use, without the action or own; all its checks pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
6.8 KiB
Python
Executable File
179 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Offline test of the gaze service idling (gaze/ft-gazed, gaze/gazecheck.py): ft-gaze runs only
|
|
while the gaze is in use, and a check asked for while it idles waits for the tracker.
|
|
|
|
Runs ft-gazed's Service with its sockets renamed, a fake pointer helper (answers "gaze ?
|
|
headset" as the test says), and a fake ft-gaze (prints samples, quits when its stdin closes).
|
|
SteamVR's tracker is the one in use, so our own isn't started; the panel isn't either. Nothing
|
|
reaches the live gaze service, the pointer helper, or SteamVR, so it's safe next to them.
|
|
|
|
gaze/test/idle-test.py
|
|
"""
|
|
import importlib.machinery
|
|
import importlib.util
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
GAZE = os.path.join(HERE, "..")
|
|
sys.path.insert(0, GAZE)
|
|
loader = importlib.machinery.SourceFileLoader("ftgazed", os.path.join(GAZE, "ft-gazed"))
|
|
gazed = importlib.util.module_from_spec(importlib.util.spec_from_loader("ftgazed", loader))
|
|
loader.exec_module(gazed)
|
|
import gazecheck # noqa: E402 (the module ft-gazed imported)
|
|
|
|
tag = f"ft_gaze_idle_test_{os.getpid()}"
|
|
gazed.ME = f"\0{tag}_gazed"
|
|
gazed.POINTER = gazecheck.POINTER = f"\0{tag}_helper"
|
|
gazecheck.SCREENS = f"\0{tag}_screens"
|
|
gazecheck.PANEL_PROG = gazed.REPO / "nonexistent-panel" # "isn't built": no panel
|
|
gazed.IDLE_AFTER, gazed.WAKE_SETTLE = 1.0, 3.0
|
|
gazed.read_settings = lambda: ("steam", "steam", "auto", 55.0)
|
|
logs = []
|
|
gazed.log = gazecheck.log = lambda msg: logs.append(msg)
|
|
|
|
# The fake ft-gaze: 90 samples a second, both eyes seen, until its stdin closes. It logs the
|
|
# sources it was asked for: "argv LIST" (--sources), then "line LIST" for each "sources LIST".
|
|
tmp = tempfile.mkdtemp(prefix="ft-gaze-idle-test-")
|
|
FAKE = os.path.join(tmp, "ft-gaze")
|
|
SOURCES_LOG = os.path.join(tmp, "sources.log")
|
|
with open(FAKE, "w") as f:
|
|
f.write('''import json, os, select, sys, time
|
|
log = open(sys.argv[1], "a", buffering=1)
|
|
log.write("argv " + (sys.argv[sys.argv.index("--sources") + 1] if "--sources" in sys.argv else "-") + "\\n")
|
|
buf = b""
|
|
while True:
|
|
if select.select([sys.stdin], [], [], 1 / 90)[0]:
|
|
data = os.read(0, 4096)
|
|
if not data:
|
|
break
|
|
buf += data
|
|
while b"\\n" in buf:
|
|
line, buf = buf.split(b"\\n", 1)
|
|
if line.startswith(b"sources "):
|
|
log.write("line " + line[8:].decode() + "\\n")
|
|
eye = {"hy": 1.0, "hp": 2.0}
|
|
print(json.dumps({"t": time.monotonic(), "src": {"mmap1": {"hy": 1.0, "hp": 2.0, "unc": [0.001, 0.001],
|
|
"open": [0.8, 0.8]}, "left": eye, "right": eye}}), flush=True)
|
|
''')
|
|
started = []
|
|
|
|
|
|
def start_helper(self):
|
|
"""ft-gaze, straight from here instead of the dev container."""
|
|
import selectors
|
|
self.proc = subprocess.Popen([sys.executable, FAKE, SOURCES_LOG, "--sources", self.wanted_sources()],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
self.proc_sources = self.wanted_sources()
|
|
os.set_blocking(self.proc.stdout.fileno(), False)
|
|
os.set_blocking(self.proc.stderr.fileno(), False)
|
|
self.sel.register(self.proc.stdout, selectors.EVENT_READ, "stdout")
|
|
self.sel.register(self.proc.stderr, selectors.EVENT_READ, "stderr")
|
|
self.buf = b""
|
|
started.append(time.monotonic())
|
|
|
|
|
|
gazed.Service.start_helper = start_helper
|
|
|
|
# The fake pointer helper.
|
|
helper_state = {"reply": "ok off worn"}
|
|
helper = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
helper.bind(gazed.POINTER)
|
|
helper.settimeout(0.2)
|
|
|
|
|
|
def answer():
|
|
while True:
|
|
try:
|
|
data, addr = helper.recvfrom(512)
|
|
except socket.timeout:
|
|
continue
|
|
except OSError:
|
|
return
|
|
if data.startswith(b"gaze ?") and addr:
|
|
helper.sendto(helper_state["reply"].encode(), addr)
|
|
|
|
|
|
threading.Thread(target=answer, daemon=True).start()
|
|
svc = gazed.Service(None, False, gazed.POINTER)
|
|
threading.Thread(target=svc.run, daemon=True).start()
|
|
ctl = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
ctl.bind("")
|
|
ctl.settimeout(2)
|
|
|
|
|
|
def ask(cmd):
|
|
ctl.sendto(cmd.encode(), gazed.ME)
|
|
return ctl.recv(4096).decode()
|
|
|
|
|
|
failures = []
|
|
|
|
|
|
def check(label, got, want):
|
|
ok = got == want
|
|
print(("ok " if ok else "FAIL ") + label + ("" if ok else f": got {got!r}, want {want!r}"), flush=True)
|
|
if not ok:
|
|
failures.append(label)
|
|
|
|
|
|
def wait(cond, seconds):
|
|
end = time.monotonic() + seconds
|
|
while time.monotonic() < end:
|
|
if cond():
|
|
return True
|
|
time.sleep(0.05)
|
|
return cond()
|
|
|
|
|
|
time.sleep(2.5)
|
|
check("gaze mode off: idle, no ft-gaze", (svc.awake, svc.proc is None), (False, True))
|
|
check("status says why", ask("status").count('"idle": "gaze mode is off"'), 1)
|
|
|
|
helper_state["reply"] = "ok on worn"
|
|
check("gaze mode on, headset worn: awake within 2 s", wait(lambda: svc.awake and svc.proc is not None, 2.5), True)
|
|
check("samples come in", wait(lambda: svc.counts["samples"] > 20, 2), True)
|
|
|
|
helper_state["reply"] = "ok on away"
|
|
check("headset off: still awake for IDLE_AFTER", wait(lambda: not svc.awake, 0.5), False)
|
|
check("then idle, ft-gaze stopped", wait(lambda: not svc.awake and svc.proc is None, 3.5), True)
|
|
check("why: nobody wears it", ask("status").count('"idle": "nobody is wearing the headset"'), 1)
|
|
|
|
helper_state["reply"] = "ok on"
|
|
check("an older helper (no headset word): awake with gaze mode on", wait(lambda: svc.awake, 2.5), True)
|
|
helper_state["reply"] = "ok off worn"
|
|
check("gaze mode off again: idle", wait(lambda: not svc.awake and svc.proc is None, 4.5), True)
|
|
|
|
check("wake lease", ask("wake 2"), "ok")
|
|
check("wake: awake at once", (svc.awake, wait(lambda: svc.proc is not None, 1)), (True, True))
|
|
check("lease over (2 s + IDLE_AFTER): idle", wait(lambda: not svc.awake, 4.5), True)
|
|
|
|
time.sleep(0.5)
|
|
logs.clear()
|
|
open(SOURCES_LOG, "w").close()
|
|
count = len(started)
|
|
check("quick check while idle: queued, waking", ask("quickcal"), "ok waking the eye tracker first")
|
|
check("it woke", wait(lambda: svc.awake and len(started) > count, 1.5), True)
|
|
check("once the tracker sends, it ran (and says why it couldn't open)",
|
|
wait(lambda: any("quickcal, asked for while idle: the panel isn't running" in m for m in logs), 3), True)
|
|
check("nothing left pending", svc.checks.pending, None)
|
|
sources = open(SOURCES_LOG).read().split("\n")
|
|
check("ft-gaze started with every source for the check", sources[0], "argv all")
|
|
in_use = svc.wanted_sources()
|
|
check("then only those in use (SteamVR's tracker: not the action, not own)",
|
|
(f"line {in_use}" in sources, in_use != "all", "action" in in_use, "own" in in_use), (True, True, False, False))
|
|
check("idle again after", wait(lambda: not svc.awake, 3), True)
|
|
|
|
print("FAILED: " + ", ".join(failures) if failures else "all passed", flush=True)
|
|
svc.running = False
|
|
time.sleep(0.7)
|
|
os.remove(FAKE)
|
|
os.remove(SOURCES_LOG)
|
|
os.rmdir(tmp)
|
|
os._exit(1 if failures else 0)
|