Require proximity telemetry before worn Framey input

This commit is contained in:
SAPHID committed 2026-10-26 11:45:51 +00:00
1 parent f5213b7c44
commit 385ae8290b
4 files changed
+76 -10

No files matched your search

@@ -0,0 +1,20 @@
# Interactive Framey worn-check proximity gate, 26 October 2026
The interactive `scripts/framey-worn-check.py` already required an explicit
operator wear confirmation and a unique `/tmp/frame-test.lock` owner before
reading SteamVR activity. It did not read proximity. This change makes its
locked preflight read the raw proximity sensor and stop before its interaction
window when the reading is missing or invalid. It checks activity, proximity
and battery again before input. The same owned-lock cleanup applies on failure.
The focused local gate suite passes 9/9 tests, including missing and nonfinite
proximity, proximity disappearing between reads, metrics preceding the SteamVR status row,
standby, low battery, no operator confirmation and a foreign lock. No headset
input was sent for this change. A separate owner-locked read-only Frame check
found SteamVR activity 3 (standby), proximity raw 3.0625 and battery 93%; it
released its lock without a worn test.
The raw proximity value is not calibrated as proof of wear. The operator's
in-person confirmation remains required; awake activity, proximity and trusted
synthetic input alone do not prove wearer-visible pixels or physical controller
usability. The hourly worn-check schedule remains enabled.
+4 -2
View File
@@ -112,8 +112,10 @@ the same-plugin navigation regression without a headset.
interaction window, use the physical controller to open the sample and
toggle **Example setting**. Confirm the laser aims where expected and the
script records a fresh input/panel event and visible stereo output. The flag
is the operator’s statement; sensor activity and trusted pointers can also
be produced synthetically and cannot identify controller hardware.
is the operator’s statement. The script requires valid locked SteamVR activity
and proximity readings before input; proximity alone cannot establish wear.
Sensor activity and trusted pointers can also be produced synthetically and
cannot identify controller hardware.
3. **1:15–2:15:** Use the controller to open Settings, return to Panel, invoke
**Read saved setting**, close/reopen the plugin, and confirm the value
remains. Try controller scroll, a slider and a harmless text field in an
+14 -4
View File
@@ -36,13 +36,19 @@ def remote(command, stdin=None, timeout=15):
PROBE = r'''
import json, pathlib, subprocess
import json, math, pathlib, subprocess
power=[]
for item in pathlib.Path('/sys/class/power_supply').glob('*/capacity'):
try:
if (item.parent/'type').read_text().strip() == 'Battery':
power.append({'name':item.parent.name,'percent':int(item.read_text().strip())})
except (OSError,ValueError): pass
proximity=[]
for item in pathlib.Path('/sys/bus/iio/devices').glob('iio:device*/in_proximity_raw'):
try:
value=float(item.read_text().strip())
if math.isfinite(value) and value >= 0: proximity.append(value)
except (OSError,ValueError): pass
stats=subprocess.run(['/opt/steamvr/bin/linuxarm64/vrcmd','--stats'],capture_output=True,text=True,timeout=5)
try:
rows=json.loads(stats.stdout) if stats.returncode == 0 else None
@@ -62,7 +68,7 @@ if base.is_dir() and not base.is_symlink():
if isinstance(p.get('storeId'),int): plugins.append({'id':p['storeId'],'name':manifest['name']})
except (OSError,ValueError,KeyError): pass
service=subprocess.run(['systemctl','--user','is-active','frame-control-headset.service'],capture_output=True,text=True)
print(json.dumps({'battery':power,'activity':activity,'panel':panel,
print(json.dumps({'battery':power,'proximity':proximity,'activity':activity,'panel':panel,
'panelService':service.returncode==0,'plugins':plugins}))
'''
@@ -120,11 +126,15 @@ def main(worn_confirmed=False):
raise RuntimeError('Battery capacity unavailable or below 20%; no device session started')
if before['activity'] is None or before['activity'] >= 3:
raise RuntimeError('SteamVR is in standby; no worn-headset check started')
if not before.get('proximity'):
raise RuntimeError('Proximity reading unavailable; no worn-headset check started')
if not before['panelService'] or not before['plugins']:
raise RuntimeError('Install and open the Frame Control panel and a Framey plugin before this check')
after_lock = probe()
if after_lock['activity'] >= 3 or min(x['percent'] for x in after_lock['battery']) < 20:
raise RuntimeError('Activity or battery changed after acquiring the lock')
if (not isinstance(after_lock.get('activity'), int) or after_lock['activity'] >= 3 or
not after_lock.get('proximity') or not after_lock.get('battery') or
min(x['percent'] for x in after_lock['battery']) < 20):
raise RuntimeError('Activity, proximity or battery changed after acquiring the lock')
state = json.dumps({'nonce':token,'started':time.time()})
code = "import pathlib; p=pathlib.Path.home()/'.local/share/frame-control/headset/worn-check.json'; p.write_text(" + repr(state) + ")"
remote('python3 -c ' + shlex.quote(code))
+38 -4
View File
@@ -32,7 +32,7 @@ class WornGate(unittest.TestCase):
self.assertIn('trusted pointer events alone do not prove',out.getvalue())
def test_standby_is_read_only_under_owned_lock_and_releases_it(self):
state = {'battery':[{'percent':99}], 'activity':3, 'panelService':True,
state = {'battery':[{'percent':99}], 'activity':3, 'proximity':[6.25], 'panelService':True,
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
code, report, remote, probe = self.run_gate(state)
self.assertEqual(code, 1)
@@ -43,7 +43,7 @@ class WornGate(unittest.TestCase):
self.assertIn('rmdir /tmp/frame-test.lock', remote.call_args_list[1].args[0])
def test_low_battery_releases_owned_lock(self):
state = {'battery':[{'percent':19}], 'activity':0, 'panelService':True,
state = {'battery':[{'percent':19}], 'activity':0, 'proximity':[6.25], 'panelService':True,
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
code, report, remote, probe = self.run_gate(state)
self.assertEqual(code, 1)
@@ -52,7 +52,7 @@ class WornGate(unittest.TestCase):
self.assertEqual(remote.call_count, 2)
def test_foreign_lock_prevents_all_sensor_reads_and_is_never_removed(self):
state = {'battery':[{'percent':99}], 'activity':0, 'panelService':True,
state = {'battery':[{'percent':99}], 'activity':0, 'proximity':[6.25], 'panelService':True,
'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
code, report, remote, probe = self.run_gate(state, RuntimeError('File exists'))
self.assertEqual(code, 1)
@@ -64,6 +64,29 @@ 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_missing_proximity_stops_before_panel_or_input_and_releases_lock(self):
state = {'battery':[{'percent':99}], 'activity':0, 'proximity':[],
'panelService':True, 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
code, report, remote, probe = self.run_gate(state)
self.assertEqual(code, 1)
self.assertIn('Proximity reading unavailable', report)
probe.assert_called_once()
self.assertEqual(remote.call_count, 2)
self.assertIn('rmdir /tmp/frame-test.lock', remote.call_args_list[-1].args[0])
def test_proximity_disappearing_on_second_read_stops_before_input(self):
first = {'battery':[{'percent':99}], 'activity':0, 'proximity':[200.0],
'panelService':True, 'plugins':[{'id':14,'name':'ProtonDB Badges'}], 'panel':None}
second = {**first, 'proximity':[]}
out = io.StringIO()
with patch.object(worn, 'probe', side_effect=[first, second]) as probe, \
patch.object(worn, 'remote') as remote, contextlib.redirect_stdout(out):
code = worn.main(worn_confirmed=True)
self.assertEqual(code, 1)
self.assertIn('Activity, proximity or battery changed', out.getvalue())
self.assertEqual(probe.call_count, 2)
self.assertEqual(remote.call_count, 2)
def test_probe_reads_activity_from_status_after_metrics(self):
rows = [{'operation':'performance','frame_presents':12},
{'operation':'status','activity_level':1}]
@@ -81,4 +104,15 @@ class WornGate(unittest.TestCase):
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)
result = json.loads(out.getvalue())
self.assertEqual(result['activity'], expected)
self.assertIsInstance(result['proximity'], list)
def test_probe_discards_nonfinite_proximity(self):
def glob(_path, pattern):
return [SimpleNamespace(read_text=lambda: 'nan')] if pattern == 'iio:device*/in_proximity_raw' else []
with patch('pathlib.Path.glob', autospec=True, side_effect=glob), \
patch('subprocess.run', return_value=SimpleNamespace(returncode=1, stdout='')), \
contextlib.redirect_stdout(io.StringIO()) as out:
exec(worn.PROBE, {})
self.assertEqual(json.loads(out.getvalue())['proximity'], [])