mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 03:00:06 +02:00
The gaze service ran ft-gaze and our own eye tracker all the time: with gaze mode off, ft-eyes still took about 60% of a core, and ft-eyegrab, ft-gaze and ft-gazed 3 to 4% each. Now ft-gaze and our tracker run only while gaze mode is on and someone wears the headset, while a check or the calibration is open or asked for, or under a "wake" lease, which the Gaze page of Frametop Input Settings renews while it's open. 30 s after the last use they stop, and the frame grabber idles with our tracker. - The pointer helper answers "gaze ? headset" with worn|away (SteamVR's activity level for the headset); an older helper answers it as before, and the service then goes by gaze mode alone. - A quick check, calibration, or fit check asked for while idle wakes the tracker and opens once it sends; the automatic calibration waits quietly while it starts. - Status has "awake" and "idle" (why), and the Gaze page shows it. - gaze/test/idle-test.py runs the service with a fake helper and ft-gaze, offline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
5.8 KiB
Python
Executable File
159 lines
5.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.
|
|
tmp = tempfile.mkdtemp(prefix="ft-gaze-idle-test-")
|
|
FAKE = os.path.join(tmp, "ft-gaze")
|
|
with open(FAKE, "w") as f:
|
|
f.write('''import json, select, sys, time
|
|
while True:
|
|
if select.select([sys.stdin], [], [], 1 / 90)[0] and not sys.stdin.read(1):
|
|
break
|
|
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], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
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()
|
|
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)
|
|
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.rmdir(tmp)
|
|
os._exit(1 if failures else 0)
|