Pause gesture: look the controllers up every 30 s, not every 3 s

The gesture reader fetched vrserver's /input/getstate.json over HTTP every 3 seconds, the
whole time the relay runs, to notice a controller's root path changing when the 3D mouse
takes or gives back its hand role.

It now looks them up when it connects, when a message comes from a device path it doesn't
know (at most every 3 s; the device is read from the message with two string searches, not
a JSON parse of all 160 a second), 1.5 s after the relay's 3D mouse connects or lets go (the
relay tells it through GamePause.controllers_changed), and otherwise every 30 s. The keys
test's pause stub gets the new method.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-03 09:21:09 -06:00
1 parent 83850bb1b7
commit 597551cf23
4 files changed
+44 -5

No files matched your search

+34 -2
View File
@@ -65,6 +65,18 @@ RESUME_DELAY = 5.0 # after a VR game ends: loading the next one can end one sce
GAME_STALE = 12.0 # the helper repeats "vrgame" every 5 s; silence this long means no game (or no SteamVR)
CHORD_WINDOW = 0.3 # a two-button gesture's buttons go down within this of each other
DOUBLE_WINDOW = 0.7 # a double press's second press comes within this of the first
LOOKUP_EVERY = 30.0 # the controllers are looked up again this often (and sooner when they may have changed)
LOOKUP_GAP = 3.0 # but not more often than this for a message from a device it doesn't know
def device_of(text):
"""A web socket message's "device", without parsing all of it (there are about 160 a second)."""
at = text.find('"device"')
if at < 0:
return None
start = text.find('"', text.find(":", at) + 1)
end = text.find('"', start + 1)
return text[start + 1:end] if 0 <= start < end else None
def gesture_of(rules, buttons):
@@ -137,6 +149,12 @@ class ControllerWatch(threading.Thread):
self.changed = threading.Event()
self.connected = False
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.lookup_at = None # look the controllers up again then (see recheck)
def recheck(self, delay=1.5):
"""The controllers' root paths may change soon: the 3D mouse's virtual controller is taking
or giving back its hand role. SteamVR takes a moment to hand the role over."""
self.lookup_at = time.monotonic() + delay
def set_gesture(self, spec):
"""(buttons, presses), or None for no gesture."""
@@ -176,15 +194,22 @@ class ControllerWatch(threading.Thread):
ws.open(f"frametop_pause_{os.getpid()}")
sides = {} # root path -> side
next_poll = 0.0
last_poll = 0.0
while True:
g = self.current()
if g is None:
return
now = time.monotonic()
lookup_at = self.lookup_at
if lookup_at is not None and now >= lookup_at:
self.lookup_at = None
next_poll = now
if now >= next_poll:
# A controller connects later, or changes its root path when the 3D mouse's
# virtual controller takes or gives back its hand role.
next_poll = now + 3.0
# virtual controller takes or gives back its hand role. Each lookup is an HTTP
# request to vrserver, so it's at connect, every LOOKUP_EVERY, when a message
# comes from a device not on the list, and when the relay says (recheck).
next_poll, last_poll = now + LOOKUP_EVERY, now
found = vrws.controllers()
if found != sides:
for path in sides.keys() - found.keys():
@@ -199,6 +224,9 @@ class ControllerWatch(threading.Thread):
f"controller{'s' if len(sides) != 1 else ''} on SteamVR's web socket")
wanted = {b: (b.split("/", 1)[0], f"/input/{b.split('/', 1)[1]}/click") for b in g.buttons}
for text in ws.messages(timeout=1.0):
device = device_of(text)
if device and device not in sides:
next_poll = min(next_poll, last_poll + LOOKUP_GAP) # its root path changed, maybe
# About 160 messages a second, nearly all capacitive sensing: parse only the
# few that carry one of the gesture's buttons.
if not any(c in text for _, c in wanted.values()):
@@ -470,6 +498,10 @@ class GamePause:
return max(0.05, self.resume_at - now)
return 1.0 if self.game else 3600.0
def controllers_changed(self):
"""The 3D mouse took or gave back its hand role, which changes a controller's root path."""
self.watch.recheck()
def helper_started(self):
"""The pointer helper (re)started, so SteamVR did too, maybe with the gaze service."""
if self.paused:
+8 -1
View File
@@ -785,7 +785,8 @@ def main():
# pointer: the 3D mouse while it's in use, pointer_conf: the one the config asks for (they
# differ while Frametop is paused).
state = {"pointer": None, "pointer_conf": None, "rules": {}, "share_keys": False,
"desktop_until": 0.0, "typing_applied": None, "vr_capture_until": 0.0, "vr_bind_retry": False}
"desktop_until": 0.0, "typing_applied": None, "vr_capture_until": 0.0, "vr_bind_retry": False,
"pointer_holding": False}
def pause_changed(paused):
"""Frametop paused or resumed (game_pause.py): the relay's own part."""
@@ -1292,6 +1293,12 @@ def main():
if added:
apply_roles()
# The 3D mouse connecting or letting go moves a hand role, so the pause gesture's reader
# looks the controllers up again (game_pause.py).
holding = bool(state["pointer_conf"] and state["pointer_conf"].active)
if holding != state["pointer_holding"]:
state["pointer_holding"] = holding
pause.controllers_changed()
ready, _, _ = select.select(list(nodes) + [control], [], [],
min(volume.timeout(now, pointer.timeout() if pointer else 0.5), pause.timeout(now)))
now = time.monotonic()
+1 -1
View File
@@ -56,7 +56,7 @@ class StubPause:
def configure(self, *args):
pass
game_state = tick = helper_started = configure
game_state = tick = helper_started = controllers_changed = configure
stub = {}