mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-27 20:02:07 +01:00
Read SteamVR activity from the status row
This commit is contained in:
1 parent
b0c0599f45
commit
0219cd10f0
5 files changed
+80
-7
No files matched your search
@@ -0,0 +1,28 @@
|
||||
# Read-only Framey activity gate, 26 October 2026
|
||||
|
||||
At 11:12 UTC, a read-only probe acquired `/tmp/frame-test.lock` by atomic
|
||||
`mkdir` with a random owner token before reading the Frame. SteamVR
|
||||
returned two `vrcmd --stats` rows: performance metrics first and the status
|
||||
row second. The status row reported activity level **1**. The original
|
||||
`framey-worn-check.py` looked only at row zero and reported unknown activity;
|
||||
`framey-vr-check.py` used the same row-zero assumption for its wake gate. The
|
||||
fixed gates select the unique status row and fail closed if it is missing,
|
||||
duplicated or malformed.
|
||||
|
||||
The first locked read found 90% battery, proximity raw 222.25, no active
|
||||
managed headset panel service and no managed Decky plugin. A subsequent locked
|
||||
read found activity 1 and proximity raw 223.625. No reliable wearer
|
||||
confirmation was available. No wake, service install, settings write or input
|
||||
was attempted.
|
||||
|
||||
While activity was 1, a separate owned-lock read captured four seconds from
|
||||
`/dev/video99` into a private 640×360 H.264 MP4. It decoded successfully and
|
||||
contained active video imagery. The inspected sample did **not** establish
|
||||
room passthrough, Frame Control panel visibility, ProtonDB Badges UI or
|
||||
controller interaction. The private recording and decoded frame were removed
|
||||
after inspection; no room or media images are committed. Each read released
|
||||
only its owner-token lock. The hourly worn-check schedule remains enabled.
|
||||
|
||||
The local focused safety suite covers a metrics row before the status row,
|
||||
awake activity, ambiguous/missing status and foreign-lock refusal. A genuine
|
||||
worn visual and physical-controller check remains outstanding.
|
||||
@@ -90,6 +90,15 @@ def settings(state):
|
||||
return pause, timeout
|
||||
|
||||
|
||||
def steamvr_activity(rows):
|
||||
"""Read the unique status row; metrics can precede it in --stats output."""
|
||||
if not isinstance(rows, list):
|
||||
return None
|
||||
levels = [row.get('activity_level') for row in rows
|
||||
if isinstance(row, dict) and row.get('operation') == 'status']
|
||||
return levels[0] if len(levels) == 1 and type(levels[0]) is int else None
|
||||
|
||||
|
||||
def diagnostic_source():
|
||||
return (ROOT / 'scripts/framey_openvr.py').read_text() + '''
|
||||
import json
|
||||
@@ -183,9 +192,7 @@ def main(argv=None):
|
||||
if not before['battery'] or min(before['battery']) < 20:
|
||||
raise RuntimeError('Battery unknown or below 20%')
|
||||
if args.wake or args.exercise:
|
||||
stats = before.get('stats')
|
||||
if (not isinstance(stats, list) or not stats or
|
||||
not isinstance(stats[0], dict) or stats[0].get('activity_level') != 3):
|
||||
if steamvr_activity(before.get('stats')) != 3:
|
||||
raise RuntimeError('Frame is awake or SteamVR activity is unknown; active session untouched')
|
||||
if args.wake or args.exercise:
|
||||
# Independent systemd timer restores power even if the controller is
|
||||
|
||||
@@ -44,8 +44,11 @@ for item in pathlib.Path('/sys/class/power_supply').glob('*/capacity'):
|
||||
power.append({'name':item.parent.name,'percent':int(item.read_text().strip())})
|
||||
except (OSError,ValueError): pass
|
||||
stats=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--stats'],capture_output=True,text=True,timeout=5)
|
||||
try: activity=json.loads(stats.stdout)[0]['activity_level'] if stats.returncode == 0 else None
|
||||
except (ValueError,KeyError,IndexError,TypeError): activity=None
|
||||
try:
|
||||
rows=json.loads(stats.stdout) if stats.returncode == 0 else None
|
||||
levels=[row.get('activity_level') for row in rows if isinstance(row,dict) and row.get('operation')=='status'] if isinstance(rows,list) else []
|
||||
activity=levels[0] if len(levels)==1 and type(levels[0]) is int else None
|
||||
except (ValueError,TypeError): activity=None
|
||||
panels=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--overlays'],capture_output=True,text=True,timeout=5)
|
||||
panel=next((line.strip() for line in panels.stdout.splitlines() if 'valve.steam.desktopgame.2001947813' in line),None)
|
||||
base=pathlib.Path.home()/'.local/share/frame-control/decky/plugins'
|
||||
|
||||
@@ -15,6 +15,18 @@ spec.loader.exec_module(check)
|
||||
|
||||
|
||||
class Safety(unittest.TestCase):
|
||||
def test_status_row_can_follow_metrics_and_ambiguous_status_fails_closed(self):
|
||||
self.assertEqual(check.steamvr_activity([
|
||||
{'operation':'performance','frame_presents':12},
|
||||
{'operation':'status','activity_level':3}]), 3)
|
||||
self.assertEqual(check.steamvr_activity([
|
||||
{'operation':'performance','frame_presents':12},
|
||||
{'operation':'status','activity_level':1}]), 1)
|
||||
self.assertIsNone(check.steamvr_activity([
|
||||
{'operation':'status','activity_level':3},
|
||||
{'operation':'status','activity_level':1}]))
|
||||
self.assertIsNone(check.steamvr_activity([{'activity_level':3}]))
|
||||
|
||||
def test_overlay_wait_is_bounded_and_stops_when_visible(self):
|
||||
from unittest.mock import Mock
|
||||
remote = Mock()
|
||||
@@ -61,7 +73,7 @@ class Safety(unittest.TestCase):
|
||||
def test_power_restore_after_partial_setter_failure(self):
|
||||
state = {'battery':[99], 'pause':'power.pauseCompositorOnStandby=true',
|
||||
'timeout':'power.turnOffScreensTimeout=17.000000','dashboardVisible':False,
|
||||
'stats':[{'activity_level':3}], 'proximity':[6.25]}
|
||||
'stats':[{'operation':'status','activity_level':3}], 'proximity':[6.25]}
|
||||
with tempfile.TemporaryDirectory() as folder, patch.object(check, 'Remote') as factory:
|
||||
remote = factory.return_value
|
||||
remote.python.return_value = state
|
||||
@@ -78,7 +90,9 @@ class Safety(unittest.TestCase):
|
||||
self.assertIn('rmdir /tmp/frame-test.lock', commands[-1])
|
||||
|
||||
def test_awake_or_unknown_activity_refuses_wake_and_install(self):
|
||||
for stats in ([{'activity_level':1}], [{'activity_level':2}], [], None):
|
||||
for stats in ([{'operation':'status','activity_level':1}],
|
||||
[{'operation':'performance'}, {'operation':'status','activity_level':2}],
|
||||
[], None):
|
||||
state = {'battery':[99], 'pause':'power.pauseCompositorOnStandby=true',
|
||||
'timeout':'power.turnOffScreensTimeout=17.000000',
|
||||
'dashboardVisible':False, 'stats':stats, 'proximity':[0.0]}
|
||||
|
||||
@@ -3,7 +3,9 @@ import sandbox # noqa: F401
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -61,3 +63,22 @@ class WornGate(unittest.TestCase):
|
||||
|
||||
def test_screenshot_palette_counts_distinct_colors(self):
|
||||
self.assertEqual(worn.color_count('0 #ff00aa 1 #ff00aa 2 #00ff00'), 2)
|
||||
|
||||
def test_probe_reads_activity_from_status_after_metrics(self):
|
||||
rows = [{'operation':'performance','frame_presents':12},
|
||||
{'operation':'status','activity_level':1}]
|
||||
def run(argv, **_):
|
||||
if argv[-1] == '--stats':
|
||||
return SimpleNamespace(returncode=0, stdout=json.dumps(rows))
|
||||
return SimpleNamespace(returncode=1, stdout='')
|
||||
for current, expected in (
|
||||
(rows, 1),
|
||||
([{'operation':'performance','frame_presents':12}], None),
|
||||
(rows + [{'operation':'status','activity_level':3}], None),
|
||||
):
|
||||
with self.subTest(rows=current):
|
||||
rows = current
|
||||
out = io.StringIO()
|
||||
with patch('subprocess.run', side_effect=run), contextlib.redirect_stdout(out):
|
||||
exec(worn.PROBE, {})
|
||||
self.assertEqual(json.loads(out.getvalue())['activity'], expected)
|
||||
Reference in new issue
Block a user