mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 04:04:09 +02:00
Gaze learning limit: 55 degrees, past it a quick check; a solid panel
Drops the quick check's second step (3629681): the user didn't want it, and it
re-entered itself every tick, re-placing and redrawing the panel, which
flickered in the headset.
POINTER_GAZE_NUDGE_MAX is the one learning limit, now 55 degrees by default:
half of the 109 the Frame shows across. The helper measures the correction
itself (raw gaze to click), not the mouse's path; past the limit it isn't
learned and the helper sends "recheck" for the quick check, for the mouse and
keyboard alike. ft-gazed's own limits (8 and 25) follow the setting.
Test, our tracker's 409 clicks since its Sep 29 calibration: a one-dot check
set from any one of them puts the next 2 minutes' clicks within 15 degrees (99%
within 4.2) and the next 10 minutes' within 25, so it gets well under 55.
The panel's dot is still and its capture ring fills in quarters, so it's drawn
again only a few times per dot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
362968198b
commit
8998fc1ddd
9 files changed
+76
-195
No files matched your search
+2
-2
@@ -29,8 +29,8 @@ Gaze as an input method for the whole desktop, without replacing anything of Ste
|
||||
Looks down past the screens (under 20 degrees down, on no Frametop screen: a glance at the keyboard) aren't sent, so the pointer stays where it was instead of following you down, and eyes lost there aren't counted. It drops blinks (both eyes closing or lost), smooths with a fixation lock, and sends the result to the pointer helper 90 times a second. It follows SteamVR's eye tracking log, and when the headset goes back on (SteamVR starts its eye model over, and the error moves), older lessons count less, so the first few after relearn the offset.
|
||||
- The pointer helper's **gaze mode** (off by default: the Gaze page of Frametop Input Settings, `gaze/ft-gazectl on`, `POINTER_GAZE=1` in `~/.config/frametop.conf`, or a mouse button or key combination mapped to "Gaze pointer on/off") works like MAGIC pointing (Zhai et al., 1999). The pointer goes where you look. Move the mouse and it's the mouse's, from where the gaze put it, for the last bit. Look well away (5 degrees) and the gaze takes it back. A press isn't sent at once: the pointer stops where the gaze put it, and if that's wrong, drag it onto what you meant with the button still held; the click happens where you let go. To drag something, hold the press still for half a second first (`POINTER_GAZE_HOLD`), then move. Outside games the pointer stays on while gaze mode is on, until a controller is picked up. The dot shows all the time (`POINTER_GAZE_DOT=moving`: only while the mouse moves it, while a press is held, and as a pulse when you click). Gaze mode works with the mouse and the keyboard, not the controllers ([docs/gaze-controllers.md](../docs/gaze-controllers.md) explains why).
|
||||
- **Keyboard clicks** (Meta+J left, Meta+K right; other key combinations on the Keyboard page of Input Settings): tap to click where you look. A quick tap (let go within 0.25 s, `POINTER_KEY_TAP`) clicks where the dot was when you pressed, whatever your head did, and tells the gaze service it was right there. Hold instead, and the dot stays put in your view: turn your head until it sits on what you meant, and let go to click there (the correction is a lesson, as with the mouse, but up to 30 degrees whatever `POINTER_GAZE_NUDGE_MAX` says: a keyboard correction is always meant). Hold still for half a second to press for real, then turn your head to drag. With Meta+J held, Meta+K presses where the dot is now, so you can correct first and then drag; let go of either to drop.
|
||||
- **Learning from nudges:** if the mouse took the pointer from the gaze and moved it a little (0.2 to 15 degrees, `POINTER_GAZE_NUDGE_MAX`) before you clicked, or you dragged a held press that far, you were nudging it onto what you looked at. The helper sends that as a lesson, from the raw gaze when the mouse took over to where you clicked, and ft-gazed learns it. So using it is what calibrates it. The raw gaze is one ft-gazed sent, so it also finds when that look was, and what each eye read then. With SteamVR, each eye learns its own error. With our tracker, the look goes to it as a click, like the probe's, and it relearns how the headset sits on your face. After the headset was off, your first nudge and click there resets that (the probe's one-dot check does the same). Nudges past `POINTER_GAZE_NUDGE_MAX` aren't learned, by the helper or by ft-gazed (they look like using the mouse, not correcting the gaze), but a keyboard click's correction is always meant, so one past it opens a quick check instead. One lesson moves the whole correction by only a third of what it measured (more near where it was taken), since in the first live test one 6 degree lesson moved everything and put the next target 7 degrees off. `ft-gazectl status` shows the lessons, and `ft-gazectl forget` drops them.
|
||||
- **Checks and calibration in the headset** (`gaze/gazecheck.py`, shown by `gaze/panel/ft-gazepanel`, a panel fixed to the headset that ft-gazed runs): a one-dot quick check opens when you put the headset on (SteamVR's tracker sees your eyes for 3 s after none for 3 s; its "HMD on" log line can't say, since it repeats every minute or so and can stay on for hours with nobody in the headset), when our tracker asks for a click (its "reseat", when the headset may sit differently), at most once every 2 minutes, and from Quick check on the Gaze page. Look at the dot: it takes your gaze once it has held still for 0.6 s (the steadiness counts, not where the tracker puts it, so it works however far off it is), or at once with a left click or Meta+J; a right click or Meta+K closes it, and ignoring it changes nothing. In gaze mode, a second step follows: the dot stays where it is in the room, the gaze has the pointer again, and you put the pointer on the dot as you'd correct a click (the mouse, or Meta+J and your head). That click clicks nothing; it's learned whatever its size, and if the pointer was more than `POINTER_GAZE_NUDGE_MAX` (15 degrees) off, the dot comes back fixed to your view for another capture, up to 3 times. A right click or Meta+K skips the step; ignored, it closes after 20 s. If the first 3 lessons after it are still over 2 degrees off, five dots follow. The full calibration (Calibrate on the Gaze page, or by itself when gaze mode comes on without one) is the probe's: three rounds, dark, medium and bright, of the middle and a ring around it, in a panel 64 degrees wide, with Frametop's screens hidden. Quitting it while there's still no calibration turns gaze mode off; turning it on again reopens it. For our tracker a check is a click and the calibration is its own (calib-point per dot); for SteamVR's, a check is a lesson for each eye and the calibration replaces calibration.json, and the lessons start over. Checks go to `checks.jsonl`.
|
||||
- **Learning from nudges:** if the mouse took the pointer from the gaze and moved it (0.2 degrees or more, and the correction within `POINTER_GAZE_NUDGE_MAX`, 55 degrees: half of the 109 the headset shows across) before you clicked, or you dragged a held press that far, you were nudging it onto what you looked at. The helper sends that as a lesson, from the raw gaze when the mouse took over to where you clicked, and ft-gazed learns it. So using it is what calibrates it. The raw gaze is one ft-gazed sent, so it also finds when that look was, and what each eye read then. With SteamVR, each eye learns its own error. With our tracker, the look goes to it as a click, like the probe's, and it relearns how the headset sits on your face. After the headset was off, your first nudge and click there resets that (the probe's one-dot check does the same). A correction past `POINTER_GAZE_NUDGE_MAX` isn't learned: the helper asks ft-gazed for the quick check instead ("recheck", after its 2-minute cooldown). Tested on our tracker's 409 clicks since its Sep 29 calibration: a one-dot check set from any one of them put the next 2 minutes' clicks within 15 degrees (99% within 4.2) and the next 10 minutes' within 25 (the far ones after the headset moved), so the check gets back well under it. The limit used to be 8 degrees, and live on 2026-10-01 our tracker was 12 off after the headset went on, so every correction was dropped. One lesson moves the whole correction by only a third of what it measured (more near where it was taken), since in the first live test one 6 degree lesson moved everything and put the next target 7 degrees off. `ft-gazectl status` shows the lessons, and `ft-gazectl forget` drops them.
|
||||
- **Checks and calibration in the headset** (`gaze/gazecheck.py`, shown by `gaze/panel/ft-gazepanel`, a panel fixed to the headset that ft-gazed runs): a one-dot quick check opens when you put the headset on (SteamVR's tracker sees your eyes for 3 s after none for 3 s; its "HMD on" log line can't say, since it repeats every minute or so and can stay on for hours with nobody in the headset), when our tracker asks for a click (its "reseat", when the headset may sit differently), at most once every 2 minutes, and from Quick check on the Gaze page. Look at the dot: it takes your gaze once it has held still for 0.6 s (the steadiness counts, not where the tracker puts it, so it works however far off it is), or at once with a left click or Meta+J; a right click or Meta+K closes it, and ignoring it changes nothing. It also runs when a click's correction was past `POINTER_GAZE_NUDGE_MAX`. The dot is still and the ring fills in quarters, so the panel is drawn again only a few times per dot. If the first 3 lessons after it are still over 2 degrees off, five dots follow. The full calibration (Calibrate on the Gaze page, or by itself when gaze mode comes on without one) is the probe's: three rounds, dark, medium and bright, of the middle and a ring around it, in a panel 64 degrees wide, with Frametop's screens hidden. Quitting it while there's still no calibration turns gaze mode off; turning it on again reopens it. For our tracker a check is a click and the calibration is its own (calib-point per dot); for SteamVR's, a check is a lesson for each eye and the calibration replaces calibration.json, and the lessons start over. Checks go to `checks.jsonl`.
|
||||
- Nothing writes to SteamVR, its eye tracker, or its files: ft-gaze maps the eye tracker's shared memory read-only. With no fresh gaze (a blink, the service stopped, the headset off), the pointer stays where it is, and the mouse works as always.
|
||||
|
||||
Lessons are logged to `pointer-lessons.jsonl`: the raw gaze, the true direction, the correction at the time, and how far off it was.
|
||||
|
||||
+11
-13
@@ -45,7 +45,7 @@ was when the mouse took over, and where the pointer was when you clicked (you we
|
||||
there). The gap is the tracker's error there. The raw gaze is the one sent, so it also says
|
||||
when that look was (the history of what was sent), and so what each eye read then:
|
||||
- SteamVR: each eye learns its own error, unless the gaze was more than
|
||||
POINTER_GAZE_NUDGE_MAX degrees (frametop.conf, 15 by default; the helper's limit too)
|
||||
POINTER_GAZE_NUDGE_MAX degrees (frametop.conf, 55 by default: the helper's limit too)
|
||||
past the correction (then it wasn't a nudge onto what you looked at);
|
||||
- our tracker: unless it was more than POINTER_GAZE_NUDGE_MAX off, the look goes to it as
|
||||
a click ("click T YAW PITCH" on @ft_eyes), as the
|
||||
@@ -72,8 +72,8 @@ tracker's shared memory read-only.
|
||||
|
||||
Control socket: abstract unix datagram "@ft_gazed":
|
||||
lesson <rhy> <rhp> <thy> <thp> from the pointer helper (see above)
|
||||
calverify <rhy> <rhp> <thy> <thp> the same, the quick check's second step (gazecheck.py)
|
||||
recheck <deg> a keyboard correction past POINTER_GAZE_NUDGE_MAX: a quick check
|
||||
recheck <deg> from the pointer helper: a click's correction was past
|
||||
POINTER_GAZE_NUDGE_MAX, so the quick check (gazecheck.py)
|
||||
status reply: one JSON object
|
||||
forget drop what the lessons taught (the calibration stays)
|
||||
reload read calibration.json and the settings again
|
||||
@@ -126,7 +126,7 @@ SOURCES = ("action", "mmap1", "mmap2", "left", "right") # the ones with a calib
|
||||
SIDES = ("left", "right") # ft-gaze's order, and the sources for each eye alone
|
||||
TRACKERS = ("steam", "own")
|
||||
BIASES = ("auto", "left", "right")
|
||||
NUDGE_MAX = 15.0 # degrees: POINTER_GAZE_NUDGE_MAX's default, the largest lesson taken
|
||||
NUDGE_MAX = 55.0 # degrees: POINTER_GAZE_NUDGE_MAX's default, the largest lesson taken
|
||||
HISTORY = 12.0 # seconds of the gaze sent, to find a lesson's look (the helper sends it up to 10 s later)
|
||||
LOOK = 0.3 # seconds of samples before that moment make the look (the probe's fixation)
|
||||
RETRY = 3.0 # seconds before starting ft-gaze again
|
||||
@@ -166,7 +166,7 @@ def read_settings():
|
||||
tracker = conf.get("GAZE_TRACKER", "steam")
|
||||
bias = conf.get("GAZE_EYE", "auto")
|
||||
try:
|
||||
nudge = min(max(float(conf.get("POINTER_GAZE_NUDGE_MAX", NUDGE_MAX)), 1.0), 30.0) # the helper's range
|
||||
nudge = min(max(float(conf.get("POINTER_GAZE_NUDGE_MAX", NUDGE_MAX)), 1.0), 110.0) # the helper's range
|
||||
except ValueError:
|
||||
nudge = NUDGE_MAX
|
||||
return tracker if tracker in TRACKERS else "steam", bias if bias in BIASES else "auto", nudge
|
||||
@@ -320,18 +320,16 @@ class Service:
|
||||
eyes.append((statistics.median(e[0] for e in seen), statistics.median(e[1] for e in seen)) if seen else None)
|
||||
return t, eyes
|
||||
|
||||
def lesson(self, rhy, rhp, thy, thp, limit=True):
|
||||
"""Learn a lesson; limit=False takes it whatever its size (the quick check's own)."""
|
||||
def lesson(self, rhy, rhp, thy, thp):
|
||||
kind = self.kind
|
||||
limit = self.nudge_max if limit else math.inf
|
||||
rec = {"time": time.time(), "kind": kind, "raw": [rhy, rhp], "true": [thy, thp], "wear": self.steam.worn()}
|
||||
if kind == "source":
|
||||
dy, dp = thy - rhy, thp - rhp # the whole error there
|
||||
cy, cp = self.correction(self.source, rhy, rhp)
|
||||
left = math.hypot(dy - cy, dp - cp)
|
||||
rec.update(source=self.source, model=self.mode, correction=[cy, cp], lesson_deg=left)
|
||||
if left > limit:
|
||||
rec["refused"] = f"more than {limit} deg past the correction"
|
||||
if left > self.nudge_max:
|
||||
rec["refused"] = f"more than {self.nudge_max:g} deg past the correction"
|
||||
else:
|
||||
self.lives[self.source].add({"time": rec["time"], "hy": rhy, "hp": rhp, "dy": dy, "dp": dp,
|
||||
"wy": 1.0, "wp": 1.0, "how": "pointer"}, self.models[self.source], self.mode)
|
||||
@@ -344,8 +342,8 @@ class Service:
|
||||
eyes=eyes, weights=[round(w, 3) for w in weights.weights()])
|
||||
if t is None:
|
||||
rec["refused"] = "that gaze isn't in the last few seconds sent"
|
||||
elif left > limit:
|
||||
rec["refused"] = f"more than {limit} deg off"
|
||||
elif left > self.nudge_max:
|
||||
rec["refused"] = f"more than {self.nudge_max:g} deg off"
|
||||
if "refused" in rec:
|
||||
return self.taken(rec)
|
||||
if kind == "own":
|
||||
@@ -675,7 +673,7 @@ class Service:
|
||||
elif words[:1] == ["forget"]:
|
||||
self.forget_lessons()
|
||||
reply = "ok"
|
||||
elif words[:1] and words[0] in ("quickcal", "calibrate", "calaccept", "calquit", "calverify", "recheck"):
|
||||
elif words[:1] and words[0] in ("quickcal", "calibrate", "calaccept", "calquit", "recheck"):
|
||||
reply = self.checks.command(words)
|
||||
elif words[:1] == ["eyes"] and len(words) == 2:
|
||||
try:
|
||||
|
||||
+15
-76
@@ -8,17 +8,9 @@ directions: look at each one.
|
||||
stay on for hours with nobody in the headset. It also opens when our own tracker asks
|
||||
for a click (its "reseat": the headset may sit differently on your face now), at most
|
||||
once every QUICK_COOLDOWN, and on "quickcal" (Frametop Input Settings, or a mouse
|
||||
button or key combination mapped to Gaze quick check). Ignored, it closes after
|
||||
QUICK_TIMEOUT and changes nothing.
|
||||
Then, in gaze mode, its second step: the dot stays where it is in the room (the
|
||||
panel's "lock"), the gaze has the pointer again, and you bring the pointer onto the
|
||||
dot as you'd correct a click, with the mouse or a keyboard click and the head (the
|
||||
helper's "calpanel 2": that click clicks nothing). The helper sends it as
|
||||
"calverify", and it's learned whatever its size. If the pointer was more than
|
||||
POINTER_GAZE_NUDGE_MAX (15 degrees) off, the first step runs again, up to RECHECKS
|
||||
times. A right click or Meta+K skips the step; ignored, it closes after
|
||||
VERIFY_TIMEOUT. A keyboard click's correction past POINTER_GAZE_NUDGE_MAX outside a
|
||||
check ("recheck", from the helper) opens a quick check too.
|
||||
button or key combination mapped to Gaze quick check), and when a click's correction
|
||||
was past POINTER_GAZE_NUDGE_MAX (55 degrees; the helper's "recheck"): the tracker is
|
||||
far off. Ignored, it closes after QUICK_TIMEOUT and changes nothing.
|
||||
five the middle and four around it, when the first FIVE_COUNT lessons after a quick check
|
||||
were all over FIVE_LIMIT degrees off: the quick check didn't fix it.
|
||||
full the calibration, as the gaze probe's: three rounds, dark, medium and bright (pupil
|
||||
@@ -76,8 +68,6 @@ CHECK_SPREAD = 1.0 # degrees
|
||||
ACCEPT_SPREAD = 2.5 # degrees: a capture asked for (calaccept) takes this much
|
||||
DOT_TIMEOUT = 8.0 # seconds a dot of five or full waits; then it's skipped
|
||||
QUICK_TIMEOUT = 6.0
|
||||
VERIFY_TIMEOUT = 20.0 # seconds the quick check's second step waits for its click
|
||||
RECHECKS = 3
|
||||
QUICK_COOLDOWN = 120.0
|
||||
DON_DELAY = 3.0 # seconds of eyes after AWAY_MIN without: the headset went on
|
||||
AWAY_MIN = 3.0
|
||||
@@ -312,7 +302,7 @@ class Checks:
|
||||
now = time.monotonic()
|
||||
self.check = {"kind": kind, "reason": reason, "own": own, "dots": check_dots(kind, own), "i": 0,
|
||||
"started": now, "shown": now, "run": [], "accept": False, "done_at": None, "tries": 0,
|
||||
"skipped": 0, "captured": 0, "points": {}, "verify_at": None, "rechecks": 0}
|
||||
"skipped": 0, "captured": 0, "points": {}}
|
||||
log(f"{kind} check: {reason}")
|
||||
if kind == "full":
|
||||
st = ask(SCREENS, "state", 0.5).split()
|
||||
@@ -338,6 +328,7 @@ class Checks:
|
||||
else:
|
||||
self.to_panel("text Look at the dot")
|
||||
self.to_panel(f"dot {yaw:.3f} {pitch:.3f} look")
|
||||
self.last_progress = None
|
||||
c["shown"] = time.monotonic()
|
||||
c["run"], c["accept"], c["done_at"] = [], False, None
|
||||
|
||||
@@ -348,7 +339,7 @@ class Checks:
|
||||
if self.away and self.back_since is None:
|
||||
self.back_since = self.seen_at
|
||||
c = self.check
|
||||
if not c or c["done_at"] or c["verify_at"]:
|
||||
if not c or c["done_at"]:
|
||||
return
|
||||
if c["own"]:
|
||||
src = s["src"].get("own") or {}
|
||||
@@ -373,10 +364,13 @@ class Checks:
|
||||
run.append((now, s, g[0], g[1]))
|
||||
window = [p for p in run if p[0] >= now - CHECK_WINDOW]
|
||||
held = now - run[0][0]
|
||||
if now - self.last_progress > 0.07:
|
||||
# The ring fills in quarters: each step is a new picture for the panel to upload, so
|
||||
# few of them keep it solid.
|
||||
progress = math.floor(min(1.0, held / CHECK_WINDOW) * 4) / 4
|
||||
if progress != self.last_progress:
|
||||
yaw, pitch, _ = c["dots"][c["i"]]
|
||||
self.to_panel(f"dot {yaw:.3f} {pitch:.3f} capture {min(1.0, held / CHECK_WINDOW):.2f}")
|
||||
self.last_progress = now
|
||||
self.to_panel(f"dot {yaw:.3f} {pitch:.3f} capture {progress:.2f}")
|
||||
self.last_progress = progress
|
||||
if c["accept"] and held >= 0.3 and len(window) >= 10:
|
||||
sd, _ = spread([(p[2], p[3]) for p in window])
|
||||
if sd <= ACCEPT_SPREAD:
|
||||
@@ -493,9 +487,6 @@ class Checks:
|
||||
if c["captured"]:
|
||||
log(f"{kind} check done ({c['captured']} of {len(c['dots'])} dots)")
|
||||
self.after_quick = [] if kind == "quick" else None
|
||||
if kind == "quick" and self.gaze_on:
|
||||
self.verify()
|
||||
return
|
||||
self.close()
|
||||
return
|
||||
n = len(c["dots"])
|
||||
@@ -526,45 +517,6 @@ class Checks:
|
||||
log(f"calibration ({c['captured']} of {n} dots): {mode}, saved")
|
||||
self.close()
|
||||
|
||||
def verify(self):
|
||||
"""The quick check's second step (see the top): bring the pointer onto the dot."""
|
||||
c = self.check
|
||||
yaw, pitch, _ = c["dots"][0]
|
||||
c["verify_at"] = time.monotonic()
|
||||
self.to_panel("lock")
|
||||
self.to_panel(f"dot {yaw:.3f} {pitch:.3f} look")
|
||||
self.to_panel("title Now put the pointer on the dot")
|
||||
self.to_panel("text Right click or Meta+K: skip")
|
||||
self.to_helper("calpanel 2")
|
||||
|
||||
def on_verify(self, rhy, rhp, thy, thp):
|
||||
svc = self.svc
|
||||
c = self.check
|
||||
rec = svc.lesson(rhy, rhp, thy, thp, limit=False)
|
||||
off = rec.get("lesson_deg", math.hypot(thy - rhy, thp - rhp))
|
||||
self.log_check({"time": time.time(), "check": "verify", "raw": [rhy, rhp], "true": [thy, thp], "off": off,
|
||||
"own": c["own"], "accepted": "refused" not in rec, "reply": rec.get("refused")})
|
||||
log(f"quick check: the pointer was {off:.1f} deg off the dot"
|
||||
+ (f" (not learned: {rec['refused']})" if "refused" in rec else ""))
|
||||
if off <= svc.nudge_max:
|
||||
self.close()
|
||||
elif c["rechecks"] < RECHECKS:
|
||||
c["rechecks"] += 1
|
||||
log(f"more than {svc.nudge_max:.0f} deg: the quick check again ({c['rechecks']} of {RECHECKS})")
|
||||
self.recapture()
|
||||
else:
|
||||
self.close(f"still more than {svc.nudge_max:.0f} deg off after {RECHECKS} more tries: "
|
||||
"the full calibration should help (Calibrate on the Gaze page)")
|
||||
|
||||
def recapture(self):
|
||||
"""The quick check's first step again, in the same check."""
|
||||
c = self.check
|
||||
now = time.monotonic()
|
||||
c.update(i=0, started=now, verify_at=None, tries=0, done_at=None, captured=0, skipped=0)
|
||||
self.to_panel("show quick") # fixed to the headset again
|
||||
self.to_helper("calpanel 1")
|
||||
self.show_dot()
|
||||
|
||||
def close(self, why=None):
|
||||
if not self.check:
|
||||
return
|
||||
@@ -615,16 +567,8 @@ class Checks:
|
||||
if cmd == "calquit":
|
||||
self.quit()
|
||||
return "ok"
|
||||
if cmd == "calverify" and len(words) == 5:
|
||||
if not self.check or not self.check["verify_at"]:
|
||||
return "error no quick check is waiting for that"
|
||||
try:
|
||||
self.on_verify(*map(float, words[1:]))
|
||||
except ValueError:
|
||||
return "error bad numbers"
|
||||
return "ok"
|
||||
if cmd == "recheck" and len(words) == 2:
|
||||
self.auto_quick(f"a keyboard correction was {words[1]} deg")
|
||||
self.auto_quick(f"a click was corrected {words[1]} deg")
|
||||
return "ok"
|
||||
return "error unknown command"
|
||||
|
||||
@@ -655,10 +599,6 @@ class Checks:
|
||||
if not self.eyes_seen(EYES_GONE):
|
||||
self.close("the headset came off")
|
||||
return
|
||||
if c["verify_at"]:
|
||||
if now - c["verify_at"] > VERIFY_TIMEOUT:
|
||||
self.close("the pointer wasn't put on the dot")
|
||||
return
|
||||
if c["done_at"]:
|
||||
return
|
||||
if c["kind"] == "quick" and now - c["started"] > QUICK_TIMEOUT:
|
||||
@@ -676,7 +616,7 @@ class Checks:
|
||||
if now - self.gaze_heard > 5:
|
||||
self.gaze_on = None # the helper isn't answering
|
||||
if self.check:
|
||||
self.to_helper("calpanel 2" if self.check["verify_at"] else "calpanel 1")
|
||||
self.to_helper("calpanel 1")
|
||||
if self.want_full_until:
|
||||
cal = self.calibrated()
|
||||
if cal is not None or now > self.want_full_until:
|
||||
@@ -705,8 +645,7 @@ class Checks:
|
||||
"last_quick_s": round(time.monotonic() - self.last_quick) if self.last_quick else None}
|
||||
if c:
|
||||
st["check"] = {"kind": c["kind"], "dot": c["i"] + 1, "dots": len(c["dots"]), "captured": c["captured"],
|
||||
"skipped": c["skipped"], "reason": c["reason"], "verifying": bool(c["verify_at"]),
|
||||
"rechecks": c["rechecks"]}
|
||||
"skipped": c["skipped"], "reason": c["reason"]}
|
||||
return st
|
||||
|
||||
def stop(self):
|
||||
|
||||
@@ -12,15 +12,12 @@
|
||||
//
|
||||
// Control socket: abstract unix datagram "@ft_gazepanel" (--socket NAME); a sender with an
|
||||
// address gets "ok" or "error ...":
|
||||
// show quick|full the panel, empty, in front of you (fixed to the headset)
|
||||
// lock stay where it is in the room now, until the next show (the
|
||||
// quick check's second step: the head carries the pointer onto
|
||||
// the dot, so the dot mustn't move with it)
|
||||
// show quick|full the panel, empty, in front of you
|
||||
// hide
|
||||
// bg <0..1> the background's brightness (full)
|
||||
// dot <yaw> <pitch> <state> [<progress 0..1>]
|
||||
// the dot, head-relative degrees (yaw +left, pitch +up); state:
|
||||
// look (pulsing), capture (a ring filling to progress), done,
|
||||
// look, capture (a ring filling to progress), done,
|
||||
// fail, off
|
||||
// title <text> / text <text> a line at the top / at the bottom (empty to clear)
|
||||
// ping
|
||||
@@ -125,7 +122,6 @@ struct Panel {
|
||||
bool dotOn = false;
|
||||
double dotYaw = 0, dotPitch = 0, progress = 0;
|
||||
std::string state = "off";
|
||||
Clock::time_point stateAt = Clock::now();
|
||||
};
|
||||
|
||||
void Blend(Panel &p, int x, int y, double r, double g, double b, double a) {
|
||||
@@ -211,12 +207,10 @@ void Draw(Panel &p) {
|
||||
double x, y;
|
||||
ToPixel(p, p.dotYaw, p.dotPitch, x, y);
|
||||
const double core = 0.22 * pxPerDeg;
|
||||
const double t = std::chrono::duration<double>(Clock::now() - p.stateAt).count();
|
||||
if (p.state == "look") {
|
||||
// A ring that draws in toward the dot, again and again, and a still centre to look at.
|
||||
const double phase = std::fmod(t * 1.4, 1.0);
|
||||
const double rr = (1.6 - 1.1 * phase) * pxPerDeg;
|
||||
Disc(p, x, y, rr, rr - 0.12 * pxPerDeg, ink, ink, ink, 0.55 * (1 - phase) + 0.2);
|
||||
// Still, so the panel looks solid and is drawn again only when something changes.
|
||||
const double rr = 0.75 * pxPerDeg;
|
||||
Disc(p, x, y, rr, rr - 0.12 * pxPerDeg, ink, ink, ink, 0.6);
|
||||
Disc(p, x, y, core, 0, ink, ink, ink, 1);
|
||||
} else if (p.state == "capture") {
|
||||
const double rr = 0.75 * pxPerDeg;
|
||||
@@ -232,7 +226,6 @@ void Draw(Panel &p) {
|
||||
}
|
||||
}
|
||||
|
||||
bool Animating(const Panel &p) { return p.dotOn && (p.state == "look"); }
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -300,7 +293,6 @@ int main(int argc, char **argv) {
|
||||
};
|
||||
std::fprintf(stderr, "ft-gazepanel running: @%s, %.2f m\n", sockName.c_str(), distance);
|
||||
|
||||
Clock::time_point lastDraw{};
|
||||
while (!g_stop) {
|
||||
char buf[512];
|
||||
sockaddr_un from{};
|
||||
@@ -320,24 +312,12 @@ int main(int argc, char **argv) {
|
||||
place();
|
||||
ov->ShowOverlay(h);
|
||||
visible = dirty = true;
|
||||
} else if (!std::strcmp(buf, "lock")) {
|
||||
vr::TrackedDevicePose_t pose{};
|
||||
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &pose, 1);
|
||||
if (pose.bPoseIsValid) {
|
||||
// The headset's pose, moved out to the panel: where place() puts it now.
|
||||
vr::HmdMatrix34_t m = pose.mDeviceToAbsoluteTracking;
|
||||
for (int r = 0; r < 3; ++r) m.m[r][3] -= float(distance) * m.m[r][2];
|
||||
ov->SetOverlayTransformAbsolute(h, vr::TrackingUniverseStanding, &m);
|
||||
} else {
|
||||
reply = "error no headset pose";
|
||||
}
|
||||
} else if (!std::strcmp(buf, "hide")) {
|
||||
ov->HideOverlay(h);
|
||||
visible = false;
|
||||
} else if (std::sscanf(buf, "bg %lf", &a) == 1) {
|
||||
p.bg = std::clamp(a, 0.0, 1.0), dirty = true;
|
||||
} else if (std::sscanf(buf, "dot %lf %lf %15s %lf", &a, &b, state, &c) >= 3) {
|
||||
if (p.state != state || a != p.dotYaw || b != p.dotPitch) p.stateAt = Clock::now();
|
||||
p.dotYaw = a, p.dotPitch = b, p.state = state, p.progress = c;
|
||||
p.dotOn = std::strcmp(state, "off") != 0, dirty = true;
|
||||
} else if (!std::strncmp(buf, "title", 5)) {
|
||||
@@ -359,11 +339,9 @@ int main(int argc, char **argv) {
|
||||
vr::VRSystem()->AcknowledgeQuit_Exiting();
|
||||
g_stop = true;
|
||||
}
|
||||
const auto now = Clock::now();
|
||||
if (visible && (dirty || (Animating(p) && now - lastDraw > std::chrono::milliseconds(40)))) {
|
||||
if (visible && dirty) {
|
||||
Draw(p);
|
||||
ov->SetOverlayRaw(h, p.px.data(), uint32_t(p.w), uint32_t(p.h), 4);
|
||||
lastDraw = now;
|
||||
dirty = false;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(visible ? 10 : 50));
|
||||
|
||||
Reference in new issue
Block a user