Compare commits

..
Author SHA1 Message Date
SAPHID 0219cd10f0 Read SteamVR activity from the status row 2026-10-26 11:18:03 +00:00
Alex Southwell b0c0599f45 Merge pull request #321 from saphid/docs/v0445-release-evidence
Record v0.4.45 release verification
2026-10-26 11:13:32 +00:00
SAPHID 1dcca6d13a Record v0.4.45 release verification 2026-10-26 11:04:13 +00:00
6 changed files with 141 additions and 7 deletions

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.
+61
View File
@@ -0,0 +1,61 @@
# v0.4.45 tagged release, 26 October 2026
The annotated `v0.4.45` tag dereferences to main merge commit
`212f0af35c86eb651d5d23bfe0b1e6bf50c528b8` from PR #320. The app and
lockfile both report 0.4.45. [Post-merge main checks](https://github.com/saphid/frame-control/actions/runs/40309949732)
passed all seven jobs, including iOS and fake Frame end-to-end tests. The
[tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40310897979)
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum
jobs. The tagged Linux builder launched its AppImage under isolated Xvfb;
that does not verify a physical display, FUSE mount or headset connection.
## Frame and package gates
The [candidate evidence](v0445-candidate-2026-10-26.md) records two
shared-lock real Frame smokes: one on the combined main code and one after
the 0.4.45 version bump. Each finished **12 passed / 1 optional x86 launch
skipped / 0 failed**, with owned cleanup and lock release. The optional
x86_64 launch was skipped because Steam Linux Runtime 4 is absent. The
candidate also records the unworn sample Framey exercise on the combined
code. Synthetic OpenVR input, setting persistence and scroll-button
movement passed; native synthetic wheel scrolling did not. The only change
after the version-bumped smoke was its evidence paragraph.
The draft had ten installers plus `SHA256SUMS`, each with a GitHub SHA-256
digest. A fresh downloaded checksum list contained exactly those ten names
and matching digests; its bytes matched GitHub's digest
`d1d84f03c09881417610337cc48f0025535c8b0d227d55147f1f2b99592625fb`.
`scripts/publish-release.sh --dry-run v0.4.45` passed the exact remote tag,
installer and checksum gates after the release notes were attached.
Freshly downloaded packages passed these independent checks:
| Package | Result |
| --- | --- |
| Linux x86_64 AppImage | SHA-256 `478ea6535979f049930351d5c0abba14e3bcc4bcb5e695ee7ee945a5fd5bf72f`; launched under isolated Xvfb, bundled server returned HTTP 200 with `FrameControl/1`, and stopped on exit. |
| Mac arm64 ZIP | SHA-256 `0204b445bc35596170d85f7a1e05e793f1c1b247f3129fadad6f8c92730cf3e2`; ZIP CRC valid, bundle version 0.4.45, main Mach-O CPU arm64. |
| Mac x64 ZIP | SHA-256 `88ded8bc492501f467a9c51156661f3a25b0dc6347660caf0d2e230bda0f984e`; ZIP CRC valid, bundle version 0.4.45, main Mach-O CPU x86_64. |
| Windows x64 Setup | SHA-256 `c512d322b2022a233ed26aaa03e038b9a735a2df654ec7a3ebefc1ba86b04334`; PE/Nullsoft installer format. The tagged Windows CI also checked its packaged files and Python libraries. |
| Linux arm64 DEB | SHA-256 `d08cc96dd18b1a602bd0af33a8e6d1f380d94195aec0f83b2898098956aeedc3`; package version 0.4.45 and architecture arm64. |
Tagged Mac and Windows packages were not launched on physical machines in
this release gate. Earlier Intel Mac acceptance covers older bytes.
## Publication and limits
`scripts/publish-release.sh v0.4.45` published
[v0.4.45](https://github.com/saphid/frame-control/releases/tag/v0.4.45) at
2026-10-26 11:02:08 UTC. GitHub's latest-release API returned
`draft=false`, `prerelease=false`, the expected tag, ten installers,
`SHA256SUMS` and `update.json`. A fresh public latest-download of
`update.json` reported version 0.4.45, the stable release page, ten hashed
assets and the worn-VR limits in its notes.
The official exact-version Decky catalog remains **4 works / 49 partial /
52 broken / 11 N/A** out of 116. AlarMe's tested silent timer, preset and
stopwatch workflows do not establish its audio, reminders, sleep controls,
closed-panel notifications or VR panel. The real Frame exercise used the
repository sample, not an official plugin. No wearer confirmation was
available. Physical controller use, optical readability, official-plugin
VR rendering, audible output and the VR keyboard remain unverified. The
hourly shared-lock worn-check schedule stays enabled.
+10 -3
View File
@@ -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
+5 -2
View File
@@ -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'
+16 -2
View File
@@ -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]}
+21
View File
@@ -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)