mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 11:01:47 +01:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3e0ec01d4 | ||
|
|
b5d50a8d89 | ||
|
|
0c9bed1254 | ||
|
|
741735aa98 | ||
|
|
4133ea1c7d | ||
|
|
3aa510a0e0 | ||
|
|
637813024f | ||
|
|
c87e5c6e8a | ||
|
|
c9777851c9 | ||
|
|
af03081f27 | ||
|
|
ef99d4953b | ||
|
|
17a7b0b80e | ||
|
|
c38e7e8da4 | ||
|
|
fc00878a23 | ||
|
|
657b4449f7 | ||
|
|
30a65facde | ||
|
|
784118b71e | ||
|
|
e0115f356d |
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.40",
|
||||
"version": "0.4.41",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.40",
|
||||
"version": "0.4.41",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.40",
|
||||
"version": "0.4.41",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# What we have tested
|
||||
|
||||
Release baseline: Frame Control **v0.4.39**, 26 October 2026. Rows also identify verified post-release main changes explicitly. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
|
||||
Release baseline: Frame Control **v0.4.40**, 26 October 2026. Rows also identify verified post-release main changes explicitly. “Real Frame + physical PC” means an actual headset and a non-virtual computer. “VM only” means a Windows virtual machine, sometimes connected to the real Frame. A CI test or a simulated device is **not** a physical hardware test. The date/version column is the last relevant check, not a promise that every new release was retested on that platform. See each link for the exact procedure and limits.
|
||||
|
||||
| Feature | Real Frame + physical PC tested | VM only | Untested or limited | Last check and proof |
|
||||
|---|---|---|---|---|
|
||||
| Mac desktop release — tested on a real Mac | Physical MacBook Air (MacBookAir9,1), Intel Core i3, macOS 15.7.7 (24G720): official v0.4.39 x64 DMG verified, installed and launched. Native UI, preference persistence across restart (original file restored), bundled Python logs, update check, offline OBS Start/Stop and 116-entry Decky store checked. A 40-byte relay upload/readback/delete passed under coordinator/headset locks | No | No quarantined Safari/Chrome download or Gatekeeper override test; native Open at Login/menu automation unavailable. Direct frame.local had no route and the app refused a stale SSH identity. Relay transfer is not native Mac transfer acceptance. Real OBS/Framey VR unverified. Quit exposed a server cleanup race; fix #284 has physical-Mac regression coverage, not a released fix | 26 Oct, official v0.4.39 tag `fd0b0a9`: [Intel release evidence](evidence/mac-v0439-2026-10-26.md); [earlier ARM64 failure](evidence/mac-desktop-2026-10-26.md) |
|
||||
| Core title smoke test | Linux lxso2 installed and removed reserved native/Proton titles on the real Frame; all 13 steps passed. ARM64 and Windows titles launched | No | The x86_64 title could not launch because Steam Linux Runtime 4.0 was not installed. The headset was unworn; visible rendering and controller input were not checked. Other feature paths retain their separate limits below | 26 Oct, v0.4.40 candidate source `9aa26a8`, Frame OS 0.5.6 build `20261022.6153909`: [latest 13/13 smoke](evidence/v0440-candidate-frame-2026-10-26.md); [earlier tagged smoke](evidence/v0438-release-smoke-2026-10-26.md) |
|
||||
| Mac desktop release — tested on a real Mac | Physical MacBook Air (MacBookAir9,1), Intel Core i3, macOS 15.7.7 (24G720): official v0.4.39 x64 DMG verified, installed and launched. Native UI, preference persistence across restart (original file restored), bundled Python logs, update check, offline OBS Start/Stop and 116-entry Decky store checked. A 40-byte relay upload/readback/delete passed under coordinator/headset locks | No | No quarantined Safari/Chrome download or Gatekeeper override test; native Open at Login/menu automation unavailable. Direct frame.local had no route and the app refused a stale SSH identity. Relay transfer is not native Mac transfer acceptance. Real OBS/Framey VR unverified. Quit exposed a server cleanup race; fix #284 shipped in v0.4.40 with physical-Mac source regression coverage, but the tagged Intel installer was not launched on physical Intel hardware | 26 Oct, official v0.4.39 tag `fd0b0a9`: [Intel release evidence](evidence/mac-v0439-2026-10-26.md); [earlier ARM64 failure](evidence/mac-desktop-2026-10-26.md) |
|
||||
| Core title smoke test | Linux lxso2 ran the exact v0.4.40 tagged source against the real Frame; all 13 reserved native/Proton title steps passed and cleanup completed | No | The headset was unworn; visible rendering and controller input were not checked. Other feature paths retain their separate limits below | 26 Oct, exact v0.4.40 tag `a4d8073`, Frame OS 0.5.6 build `20261022.6153909`: [tagged release evidence](evidence/v0440-release-2026-10-26.md); [earlier candidate smoke](evidence/v0440-candidate-frame-2026-10-26.md) |
|
||||
| Windows desktop in the headset | No physical Windows PC test | Windows 11 VM sent a whole desktop to a real Frame; this used an intermediate fix, not the final native DLL | Physical GPU, capture and controller input; individual windows unavailable in bundled DXGI build | 24 Oct, v0.4.10 installer + candidate fix: [VM log](evidence/windows-vm-2026-10-24.md), [PC guide](pc-in-headset.md) |
|
||||
| Linux desktop in the headset | No | No | Native Linux PC capture is disabled; lxso2 lacked a desktop portal and PipeWire session. The separate Steam `-pipewire` helper is not Frame Control's PC capture | 24 Oct, v0.4.17: [host audit](evidence/linux-pc-host-lxso2-2026-10-24.md), [Steam helper PR #163](https://github.com/saphid/frame-control/pull/163) |
|
||||
| Mac desktop in the headset | A physical Mac sent its screen/window to a real Frame | No | Current v0.4.26 Mac installer and worn-headset controller input have not been repeated | 28 Sep, pre-v0.4 / Frame OS 0.4.1: [Mac guide and measurement](streaming.md#b-show-the-macs-desktop-inside-the-frame) |
|
||||
@@ -23,8 +23,8 @@ Release baseline: Frame Control **v0.4.39**, 26 October 2026. Rows also identify
|
||||
| Passthrough in recordings | Real room pixels in OBS and a production MP4 after safe SteamVR wake | No | **Yes** for the checked 0.5.6 mirror. Raw cameras are not opened. Other games/modes and Steam clips may differ | 25 Oct: [validation](evidence/obs-validation-2026-10-25.md), [recording guide](recording.md#does-record-include-passthrough) |
|
||||
| Open at login | No Frame needed; Linux autostart file toggled on a physical Linux host with an isolated config directory | No | A full login session and physical Windows/macOS host login have not been checked; OS API calls were unit tested | 25 Oct, v0.4.26: [PR #189](https://github.com/saphid/frame-control/pull/189) |
|
||||
| Local Frame Control extensions | Example panel displayed live Frame status in a browser preview; the real Frame served a read-only example in the experimental headset panel | No | No wearer-visible extension view; third-party extensions and physical Windows/macOS extension folders untested | 25 Oct, v0.4.29: [PR #194](https://github.com/saphid/frame-control/pull/194), [PR #201](https://github.com/saphid/frame-control/pull/201), [extension guide](extensions.md) |
|
||||
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 26 Oct, v0.4.29–0.4.39: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [latest candidate sample exercise](evidence/v0440-candidate-frame-2026-10-26.md), [earlier standby-gated exercise](evidence/framey-vr-standby-gate-2026-10-26.md), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. Post-release main after PR #286 classifies the 116 exact-version plugins as 1 works (Notebook default flow only), 49 partial, 55 broken and 11 not applicable; v0.4.39 shipped the earlier 1/45/59/11 catalog. Deck Shelves moved to partial after its exact chunked frontend rendered settings in local Chromium; its backend and Steam Home shelves remain unverified. Steamback, PlayTime and Deck Settings became partial after detached read-only library/user snapshots and isolated amd64 backend checks; their official ARM64 backends, automatic behavior and VR panels remain unverified. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Three official audio plugins had local real-backend controls and independent virtual-audio readbacks, but their ARM64 Frame audio sessions and physical output remain unverified. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.39: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [local audio evidence](evidence/framey-audio-backends-2026-10-26.md), [post-release library evidence](evidence/framey-library-context-2026-10-26.md), [compatibility table](framey-compatibility.md) |
|
||||
| Experimental Framey VR plugin panels and store | Unattended Frame OS 0.5.6 runs showed the Framey sample panel in stereo compositor and mirror captures. The latest standby-gated exercise restored the original power settings and released its shared lock. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; post-release PR #282 recorded 101 local Chromium initializers ready and 15 failed, up from 98/18 in v0.4.39. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.39: [latest unworn sample evidence](evidence/framey-vr-standby-gate-2026-10-26.md), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [post-release sample exercise](evidence/v0439-release-2026-10-26.md#post-release-framey-library-change-282), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Frame Control SteamVR panel | Frame OS 0.5.6 registered and served the opt-in overlay. A later unattended check safely woke SteamVR and found the panel in stereo compositor and mirror captures; it removed its temporary service and restored power settings | No | No person wore the headset for that check. Physical controller input, scrolling, VR keyboard, comfort and legibility remain unverified. The large status view has browser and packaging checks, not a wearer review | 26 Oct, v0.4.29–0.4.40: [panel PR #201](https://github.com/saphid/frame-control/pull/201), [unattended evidence](evidence/framey-vr-2026-10-25/README.md), [post-#292 unworn sample exercise](evidence/v0440-release-2026-10-26.md#post-tag-292-panel-regression), [earlier standby-gated exercise](evidence/framey-vr-standby-gate-2026-10-26.md), [worn checklist](framey-vr-check.md) |
|
||||
| Experimental Decky Loader manager | Frame OS 0.5.6 ARM64 user-service install/status/update/remove and managed ProtonDB install/update/remove passed earlier. A private Decky 3.3.0 service returned read-only ProtonDB and Steam Status setting RPCs; a later locked check verified the official Notebook default drawing/save/reopen flow and Simple Timer backend events in local Chromium | No | The loader remains opt-in. Installs/updates reject an archive whose Decky name collides with another installed plugin before download and under the headset transaction lock; this safety path has host/fake Frame tests, not a new real-headset collision test. v0.4.40 classifies the 116 exact-version plugins as 1 works (Notebook default flow only), 49 partial, 55 broken and 11 not applicable. Post-tag PRs #289 and #293 give 3 works / 48 partial / 54 broken / 11 N/A on current main: TabMaster is partial, and Volume Mixer and Mixer work for tested per-app silent-stream volume control only. Deck Shelves moved to partial after its exact chunked frontend rendered settings in local Chromium; its backend and Steam Home shelves remain unverified. Steamback, PlayTime and Deck Settings became partial after detached read-only library/user snapshots and isolated amd64 backend checks; their official ARM64 backends, automatic behavior and VR panels remain unverified. A read-only Steam bridge returned the Frame country, install folders and system information; IsThereAnyDeal selected that country in local Chromium, but its deployed HTTP endpoint and deal lookup remain unverified. An isolated ARM64 LocalSend core answered its info endpoint, but the official x86-only plugin archive and file transfers remain blocked. Volume Mixer and Mixer controlled a named silent stream through real ARM64 Decky with independent Pulse readbacks; Volume Boost default-sink reads matched, but real writes were denied. Audible output, Bluetooth playback, internal-node controls and worn VR remain unverified. Most official backends and VR controls remain unverified. Physical controller use and wearer-visible official panels remain unverified | 26 Oct, v0.4.31–0.4.40: [managed store PR #208](https://github.com/saphid/frame-control/pull/208), [name-collision PR #269](https://github.com/saphid/frame-control/pull/269), [catalog PR #266](https://github.com/saphid/frame-control/pull/266), [backend event evidence](evidence/framey-events-backend-2026-10-26.json), [Steam read PR #274](https://github.com/saphid/frame-control/pull/274), [LocalSend evidence PR #275](https://github.com/saphid/frame-control/pull/275), [local audio evidence](evidence/framey-audio-backends-2026-10-26.md), [ARM64 audio evidence](evidence/framey-audio-arm64-2026-10-26.md), [post-release library evidence](evidence/framey-library-context-2026-10-26.md), [compatibility table](framey-compatibility.md) |
|
||||
| Experimental Framey VR plugin panels and store | Unattended Frame OS 0.5.6 runs showed the Framey sample panel in stereo compositor and mirror captures. The latest standby-gated exercise restored the original power settings and released its shared lock. Synthetic OpenVR clicks changed a live sample setting, read its Decky backend file and confirmed it after a fresh iframe reopen | No | This was the repository sample, not an official store plugin. Physical controllers, native scrolling, VR keyboard, wearer comfort and legibility remain unverified. CSS Loader and SteamGridDB exact archives passed fake Frame install/remove and panel packing; this does not establish theme or artwork controls in VR. Panel-local backend events and React toasts have local/real-backend checks, but no wearer review. Pause Games and Tailscale Control settings render in local Chromium; their live actions are unverified. The 116-plugin catalog had 60 fake installs/removals and 56 expected blocks; v0.4.40 recorded 101 local Chromium initializers ready and 15 failed, up from 98/18 in v0.4.39. Notebook is marked works only for its default drawing/save/reopen flow | 26 Oct, v0.4.38–0.4.40: [post-#294 unworn sample and scroll evidence](evidence/framey-scroll-probe-2026-10-26.md#post-294-exact-main-sample-check), [backend event PR #270](https://github.com/saphid/frame-control/pull/270), [settings/cache PR #271](https://github.com/saphid/frame-control/pull/271), [post-release sample exercise](evidence/v0439-release-2026-10-26.md#post-release-framey-library-change-282), [worn checklist](framey-vr-check.md) |
|
||||
|
||||
Release notes must preserve these limits until a newer recorded check changes them. When a feature is retested, add its date, exact app/Frame version, host type and proof here. The older [v0.3.1 matrix](support-matrix.html) is a historical snapshot and does not describe current coverage.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Headset smoke: unavailable x86 runtime is a skip
|
||||
|
||||
The v0.4.41 tagged smoke result recorded 13/13 passed even though its x86_64 launch outcome was `runtime missing`: Steam had acknowledged the title but had not started it. This follow-up separates an unavailable optional runtime from a verified launch. It keeps the smoke usable on a Frame without Steam Linux Runtime 4 while making the missing coverage explicit in its console summary and saved JSON.
|
||||
|
||||
Focused local `test_smoke_cleanup.py` ran **22 tests successfully**. The new regression starts an x86_64 title cycle with a fresh missing-runtime Steam log, verifies `skipped=true` and `outcome=runtime missing`, and verifies that the title is still removed. A result test checks separate passed/skipped/failed counts and a zero exit code when an optional runtime is absent but nothing fails.
|
||||
|
||||
The revised `scripts/frame-smoke.sh` then ran on the real Frame from this branch. It acquired its unique owner in `/tmp/frame-test.lock` before any headset read. Locked preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build `20261022.6153909`, the result was **12 passed / 1 skipped / 0 failed**. ARM64 and Proton test executables ran; x86_64 was explicitly skipped because Steam Linux Runtime 4 was not installed. Both title cleanup steps passed. The command exited 0, including release of its owned shared lock. The detailed result remains private at `tests/smoke/results/2026-10-26T050711Z-20261022.6153909.json` in the test checkout.
|
||||
|
||||
The headset was in standby and nobody wore it for this test. Title smoke does not prove wearer-visible pixels, physical controller input, official Decky panels, audio output or comfort. It also does not prove x86_64 runtime execution on this Frame; that path needs the optional runtime present. The hourly worn check remains enabled.
|
||||
@@ -0,0 +1,384 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"timeUtc": "2026-10-26T04:05:15.145624+00:00",
|
||||
"baseCommit": "56bde24",
|
||||
"frameOS": "0.5.6",
|
||||
"decky": {
|
||||
"version": "v3.3.0",
|
||||
"sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6"
|
||||
},
|
||||
"scope": "Local Chromium opaque Framey panels, production SSH/CDP bridge and actual unmodified official backends in ARM64 Decky; real user audio session, silent test stream only; no worn VR",
|
||||
"plugins": [
|
||||
{
|
||||
"id": 38,
|
||||
"name": "Volume Mixer",
|
||||
"version": "1.2.1-1",
|
||||
"sha256": "11b259ad234d0e97cfef4e6a2af3f1edfb0908d5482765af56b86b0a4601c5f3"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"name": "Volume Boost",
|
||||
"version": "0.0.7",
|
||||
"sha256": "2cc576a7f60751306d690268e227df626e59d3733680ced753de4c116629a9cc"
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"name": "Mixer",
|
||||
"version": "0.1.0",
|
||||
"sha256": "4b3546170e5fbd5f9afad3b24555498ee51caac7cec9e4c86321b18cdd4b5106"
|
||||
}
|
||||
],
|
||||
"readOnlySession": {
|
||||
"timeUtc": "2026-10-26T04:03:54.509107+00:00",
|
||||
"before": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"volumeMixerLogExists": false,
|
||||
"defaultSinkVolume": {
|
||||
"code": 0,
|
||||
"stdout": "Volume: front-left: 0 / 0% / -inf dB, front-right: 0 / 0% / -inf dB\n balance 0.00\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"pactlInfoExit": 0,
|
||||
"pwDumpExit": 0
|
||||
},
|
||||
"after": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"volumeMixerLogExists": false,
|
||||
"defaultSinkVolume": {
|
||||
"code": 0,
|
||||
"stdout": "Volume: front-left: 0 / 0% / -inf dB, front-right: 0 / 0% / -inf dB\n balance 0.00\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"pactlInfoExit": 0,
|
||||
"pwDumpExit": 0
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"id": 38,
|
||||
"method": "get_sink_inputs",
|
||||
"args": [
|
||||
{}
|
||||
],
|
||||
"legacy": true,
|
||||
"version": "1.2.1-1",
|
||||
"sha256": "11b259ad234d0e97cfef4e6a2af3f1edfb0908d5482765af56b86b0a4601c5f3",
|
||||
"result": {
|
||||
"success": true,
|
||||
"streamCount": 1,
|
||||
"volumes": [
|
||||
1
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "left"
|
||||
}
|
||||
],
|
||||
"legacy": true,
|
||||
"version": "0.0.7",
|
||||
"sha256": "2cc576a7f60751306d690268e227df626e59d3733680ced753de4c116629a9cc",
|
||||
"result": {
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "right"
|
||||
}
|
||||
],
|
||||
"legacy": true,
|
||||
"version": "0.0.7",
|
||||
"sha256": "2cc576a7f60751306d690268e227df626e59d3733680ced753de4c116629a9cc",
|
||||
"result": {
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"method": "get_settings",
|
||||
"args": [],
|
||||
"legacy": false,
|
||||
"version": "0.1.0",
|
||||
"sha256": "4b3546170e5fbd5f9afad3b24555498ee51caac7cec9e4c86321b18cdd4b5106",
|
||||
"result": {
|
||||
"result": {
|
||||
"filters": [],
|
||||
"boostLimit": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"method": "get_playbacks",
|
||||
"args": [],
|
||||
"legacy": false,
|
||||
"version": "0.1.0",
|
||||
"sha256": "4b3546170e5fbd5f9afad3b24555498ee51caac7cec9e4c86321b18cdd4b5106",
|
||||
"result": {
|
||||
"count": 6,
|
||||
"internalAudioNodeCount": 5,
|
||||
"steamStreamCount": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"fixedLogAbsent": true
|
||||
},
|
||||
"lockReleased": true
|
||||
},
|
||||
"liveSilentStream": {
|
||||
"before": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"volumeMixerLogExists": false,
|
||||
"defaultSinkVolume": {
|
||||
"code": 0,
|
||||
"stdout": "Volume: front-left: 0 / 0% / -inf dB, front-right: 0 / 0% / -inf dB\n balance 0.00\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"pactlInfoExit": 0,
|
||||
"pwDumpExit": 0
|
||||
},
|
||||
"after": {
|
||||
"battery": [
|
||||
100
|
||||
],
|
||||
"uid": 1000,
|
||||
"arch": "aarch64",
|
||||
"decky": "failed",
|
||||
"panel": "inactive",
|
||||
"volumeMixerLogExists": false,
|
||||
"defaultSinkVolume": {
|
||||
"code": 0,
|
||||
"stdout": "Volume: front-left: 0 / 0% / -inf dB, front-right: 0 / 0% / -inf dB\n balance 0.00\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"pactlInfoExit": 0,
|
||||
"pwDumpExit": 0
|
||||
},
|
||||
"source": "paplay --raw --format=s16le --rate=48000 --channels=2 --client-name=FrameySilentARM64 --stream-name=\"Framey ARM64 silent verification\" /dev/zero",
|
||||
"writes": [
|
||||
{
|
||||
"id": 38,
|
||||
"method": "set_input_volume",
|
||||
"args": [
|
||||
{
|
||||
"index": 31400,
|
||||
"volume": 0.5
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentReadback": [
|
||||
{
|
||||
"index": 31400,
|
||||
"name": "FrameySilentARM64",
|
||||
"volume": {
|
||||
"front-left": {
|
||||
"value": 32768,
|
||||
"value_percent": "50%",
|
||||
"db": "-18.06 dB"
|
||||
},
|
||||
"front-right": {
|
||||
"value": 32768,
|
||||
"value_percent": "50%",
|
||||
"db": "-18.06 dB"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"method": "set_input_volume",
|
||||
"args": [
|
||||
{
|
||||
"index": 31400,
|
||||
"volume": 0.45
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": true,
|
||||
"success": true
|
||||
},
|
||||
"independentReadback": [
|
||||
{
|
||||
"index": 31400,
|
||||
"name": "FrameySilentARM64",
|
||||
"volume": {
|
||||
"front-left": {
|
||||
"value": 29491,
|
||||
"value_percent": "45%",
|
||||
"db": "-20.81 dB"
|
||||
},
|
||||
"front-right": {
|
||||
"value": 29491,
|
||||
"value_percent": "45%",
|
||||
"db": "-20.81 dB"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"method": "set_volume",
|
||||
"args": [
|
||||
199,
|
||||
37
|
||||
],
|
||||
"result": true,
|
||||
"independentReadback": [
|
||||
{
|
||||
"index": 31400,
|
||||
"name": "FrameySilentARM64",
|
||||
"volume": {
|
||||
"front-left": {
|
||||
"value": 24248,
|
||||
"value_percent": "37%",
|
||||
"db": "-25.91 dB"
|
||||
},
|
||||
"front-right": {
|
||||
"value": 24248,
|
||||
"value_percent": "37%",
|
||||
"db": "-25.91 dB"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 144,
|
||||
"method": "set_volume",
|
||||
"args": [
|
||||
199,
|
||||
38
|
||||
],
|
||||
"result": true,
|
||||
"independentReadback": [
|
||||
{
|
||||
"index": 31400,
|
||||
"name": "FrameySilentARM64",
|
||||
"volume": {
|
||||
"front-left": {
|
||||
"value": 24904,
|
||||
"value_percent": "38%",
|
||||
"db": "-25.21 dB"
|
||||
},
|
||||
"front-right": {
|
||||
"value": 24904,
|
||||
"value_percent": "38%",
|
||||
"db": "-25.21 dB"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"volumeMixerReopen": {
|
||||
"id": 38,
|
||||
"method": "get_sink_inputs",
|
||||
"args": [
|
||||
{}
|
||||
],
|
||||
"result": {
|
||||
"result": [
|
||||
{
|
||||
"name": "FrameySilentARM64",
|
||||
"index": 31400,
|
||||
"volume": 0.3800048828125
|
||||
}
|
||||
],
|
||||
"success": true
|
||||
}
|
||||
},
|
||||
"mixerReadAfterChange": {
|
||||
"id": 144,
|
||||
"method": "get_playbacks",
|
||||
"args": [],
|
||||
"result": [
|
||||
{
|
||||
"id": 199,
|
||||
"name": "FrameySilentARM64",
|
||||
"mediaName": "Framey ARM64 silent verification",
|
||||
"volume": 38
|
||||
}
|
||||
]
|
||||
},
|
||||
"volumeBoostReads": [
|
||||
{
|
||||
"id": 58,
|
||||
"method": "get_volume_state",
|
||||
"args": [
|
||||
{
|
||||
"channel": "left"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"result": 0,
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"cleanup": {
|
||||
"baseAbsent": true,
|
||||
"unitAbsent": true,
|
||||
"fixedLogAbsent": true
|
||||
},
|
||||
"lockReleased": true
|
||||
},
|
||||
"independentCleanupAudit": {
|
||||
"testPathsAbsent": true,
|
||||
"units": {
|
||||
"framey-audio-kp0z857q58-32363fd7": "MainPID=0\nLoadState=not-found\nActiveState=inactive",
|
||||
"framey-audio-kp0z857q58-c08f3f5d": "MainPID=0\nLoadState=not-found\nActiveState=inactive",
|
||||
"framey-audio-kp0z857q58-c08f3f5d-silence": "MainPID=0\nLoadState=not-found\nActiveState=inactive"
|
||||
},
|
||||
"silentStreamsAbsent": true
|
||||
},
|
||||
"limitations": [
|
||||
"Physical audible output, default-sink writes, boost above 100%, Bluetooth playback controls and worn VR unverified.",
|
||||
"Mixer also exposes internal Frame PipeWire nodes and Reset; neither was changed or invoked.",
|
||||
"Private Decky/installed-plugin outer status fixture; real RPC used production framey_panel.call_method over SSH and live Steam CEF, not a deployed headset HTTP endpoint test.",
|
||||
"Mixer filter save/restart was verified on amd64 in the earlier evidence, not repeated on ARM64."
|
||||
],
|
||||
"catalogRefresh": {
|
||||
"timeUtc": "2026-10-26T04:11:59.449535+00:00",
|
||||
"visibleCount": 116,
|
||||
"idsVersionsDigestsUnchanged": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
# Official audio plugins on the ARM64 Frame
|
||||
|
||||
Volume Mixer 1.2.1-1 (38) and Mixer 0.1.0 (144) now qualify as **works for
|
||||
per-app volume control**. Their exact official, unmodified Python backends ran
|
||||
in published Decky 3.3.0 ARM64 on Frame OS 0.5.6. Local Chromium rendered the
|
||||
actual Framey panels and changed only a deliberately created silent playback
|
||||
stream in the Frame's real user audio session. Independent Pulse readbacks
|
||||
confirmed the changes. This is useful backend behavior, not initializer-only
|
||||
or fake-backend evidence.
|
||||
|
||||
Volume Boost 0.0.7 (58) remains **partial**. Its real ARM64 default-sink reads
|
||||
matched independent `pactl` reads, and its controls rendered. Default-sink writes
|
||||
were denied throughout the real-Frame run. Its earlier slider/numeric write
|
||||
checks used an isolated amd64 virtual sink.
|
||||
|
||||
[Digest-pinned observations and cleanup](framey-audio-arm64-2026-10-26.json)
|
||||
record both runs. No physical audible output or worn-VR result is claimed.
|
||||
|
||||
| Plugin | Real Frame result | Remaining limits |
|
||||
| --- | --- | --- |
|
||||
| Volume Mixer | `pulsectl`/`ravel` import; real Pulse session discovery; silent-stream slider click 100 → 50%, ArrowDown → 45%; reopen reads back the 38% subsequently set by Mixer | Bluetooth discovery/playback commands, audible output and worn-VR input unverified. The range input rounds the reopened 0.38000488 backend value to its configured step; no extra write occurred. |
|
||||
| Volume Boost | `pactl` default sink and stereo channel parsing; both backend reads return 0 and agree with independent 0% / 0% sink reads; local panel shows 0 | No real default-sink writes or boosts; no audible output or worn-VR result. Zero alone is not proof: upstream returns 0 when its command fails. |
|
||||
| Mixer | `pw-dump`/`wpctl` available; real playbacks/settings reads; exact silent-stream numeric input 37%, ArrowUp → 38%, independently read back | Exposes internal Frame PipeWire nodes as well as apps. Internal-node controls and Reset were not exercised. Filter persistence passed on amd64 earlier, not ARM64. Audible output and worn-VR input unverified. |
|
||||
|
||||
## Runtime and transport
|
||||
|
||||
The normal atomic `/tmp/frame-test.lock` was acquired separately for each run.
|
||||
An existing owner would abort testing; the lock was never stolen. The headset
|
||||
was not woken, and no power setting was changed.
|
||||
|
||||
The actual Frame UID is 1000. The legacy plugins' hard-coded
|
||||
`XDG_RUNTIME_DIR=/run/user/1000` therefore reaches this Frame's existing audio
|
||||
session. Volume Mixer's bundled pure Python wrappers successfully loaded the
|
||||
system ARM64 Pulse and D-Bus libraries. No native rebuild was needed. It also
|
||||
sets `HOME=/home/deck` inside its own plugin process; this test does not establish
|
||||
that arbitrary Deck paths work on the Frame.
|
||||
|
||||
The upstream `PluginLoader-arm64` release was verified against GitHub's
|
||||
published SHA-256 and ELF machine 183 before transfer. Two private, bounded,
|
||||
removable user-service installs used their own Decky plugins/settings bases.
|
||||
Neither service was enabled. The normal managed Decky service was already
|
||||
failed, and the normal headset-panel service inactive; both states remained
|
||||
unchanged. A pre-existing `/tmp/VolumeMixer-decky.log` would have aborted the
|
||||
run because the upstream plugin truncates that fixed path during import.
|
||||
|
||||
The local relay served actual `ui/headset.html`, vendored React, `framey-host.js`
|
||||
and digest-checked official frontend bytes in an opaque `allow-scripts` iframe.
|
||||
Only outer status/install responses were fixtures. Plugin RPC used production
|
||||
`framey_panel.call_method`, SSH through lxso2, `frame_steam.Page`, the live Steam
|
||||
CEF `DeckyBackend.call`, and actual Decky plugin subprocesses. Private install
|
||||
paths were redirected to the temporary base. No second Decky websocket client
|
||||
was attached. This does not verify the deployed headset HTTP endpoint or a VR
|
||||
compositor interaction.
|
||||
|
||||
## Safe control check
|
||||
|
||||
The first run was read-only. Independent `pactl info`, default-sink volume and
|
||||
`pw-dump` commands exited successfully, so empty/zero fallback values were not
|
||||
accepted on their own. Volume Mixer saw a live Steam stream; Mixer also saw
|
||||
Frame spatialization, echo-cancellation and ALSA internal nodes.
|
||||
|
||||
For the second run, a separate task-owned transient user service ran:
|
||||
|
||||
```sh
|
||||
paplay --raw --format=s16le --rate=48000 --channels=2 \
|
||||
--client-name=FrameySilentARM64 \
|
||||
--stream-name='Framey ARM64 silent verification' /dev/zero
|
||||
```
|
||||
|
||||
This sends zero-valued samples to the existing user server. It is an explicit
|
||||
silent client, not the earlier private null-server fixture. No server, device,
|
||||
default-route or physical-sink configuration was changed. The relay resolved
|
||||
its live stream IDs before every write and allowed only this exact application
|
||||
name, bounded to 80% volume. The spatialization node with a similar prefix was
|
||||
excluded. All Volume Boost writes, Mixer Reset/settings writes and Bluetooth
|
||||
commands were denied. Existing streams and internal nodes were never write
|
||||
targets. This broader real-session check supersedes the earlier suggested
|
||||
null-server-only ARM64 follow-up; that older fixture could not prove access to
|
||||
the Frame's real session.
|
||||
|
||||
Every browser-triggered volume RPC was followed by independent
|
||||
`pactl --format=json list sink-inputs` channel readback. Only the named silent
|
||||
stream's rows are published. Background streams can appear/disappear naturally;
|
||||
no whole-session stability claim is made.
|
||||
|
||||
## Cleanup and catalog interpretation
|
||||
|
||||
Both runs removed their private Decky units/bases and the test-created fixed
|
||||
plugin log. The live run also stopped its transient silent client. Decky's main
|
||||
process exited during shutdown but left children waiting; those remaining
|
||||
processes were killed only within the test service's systemd cgroup before
|
||||
cleanup completed. Both locks were released after cleanup. An independent
|
||||
fresh-lock audit confirmed all three test units unloaded/inactive with PID 0,
|
||||
private paths absent, and no named silent stream. Battery was 100 → 100 and
|
||||
independent default-sink reads stayed 0% / 0% in both runs.
|
||||
|
||||
The canonical store data and generated compatibility table now report
|
||||
**3 works / 48 partial / 54 broken / 11 N/A**. “Works” describes the tested
|
||||
per-app control behavior; read each row's remaining limits. Historical
|
||||
[fake-Frame archive checks](framey-module-lookup-2026-10-26.md) cover transport
|
||||
with plugin execution stubbed, and the
|
||||
[earlier local audio checks](framey-audio-backends-2026-10-26.md) cover isolated
|
||||
amd64 controls/filter persistence. Neither is relabelled as a real ARM64 test.
|
||||
|
||||
## Repository checks
|
||||
|
||||
The full Python suite passed 1,621 tests (16 skips), and the focused Framey
|
||||
suite passed all 43 tests. `scripts/framey-compatibility-doc.py --check` and
|
||||
`git diff --check` passed. A fresh official-store fetch at 04:11:59 UTC found
|
||||
all 116 IDs, versions and digests unchanged. This PR changes catalog/evidence
|
||||
and generated documentation; it adds no backend shim or plugin patch.
|
||||
@@ -1,5 +1,9 @@
|
||||
# Official audio plugins: local real-backend checks
|
||||
|
||||
Historical amd64 run. The [later ARM64 Frame check](framey-audio-arm64-2026-10-26.md)
|
||||
supersedes the ARM64 gaps and follow-up plan below; it promotes the two per-app
|
||||
mixers to works for their verified scope. Volume Boost remains partial.
|
||||
|
||||
Volume Mixer 1.2.1-1 (38), Volume Boost 0.0.7 (58) and Mixer 0.1.0 (144)
|
||||
passed useful audio controls in local Chromium with their **unmodified official
|
||||
Python backends running in actual Decky 3.3.0**. All three remain **partial**:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Unworn Framey scroll transport probe, 26 October 2026
|
||||
|
||||
The previous `scripts/framey-vr-check.py --exercise` runs reported `scrollY=0` before and after one injected smooth-scroll event. That was inconclusive because the repository sample may fit inside the iframe. This probe changed only the **test script**: it created a temporary 160-pixel-high scroll region with 716 pixels of content, moved the native OpenVR pointer over it, and measured its own `scrollTop` and captured wheel events. No production Framey panel code changed.
|
||||
|
||||
Two local diagnostic versions ran on the real Frame from base main `784118b` after PR #292. The final version in this PR tried OpenVR `VREvent_ScrollSmooth` (309) and `VREvent_ScrollDiscrete` (305), using Valve's documented `VREvent_Scroll_t` ABI. The overlay listing showed the temporary Frame Control dashboard visible with `+smoothscroll`; a trusted native pointer move reached the iframe. Neither scroll event reached Chromium's capture listener and the region stayed at `scrollTop=0`. This establishes only that `IVROverlayView_003.PostOverlayEvent` did not deliver these synthetic scroll events in this run. A physical controller may follow a different path.
|
||||
|
||||
| Check | First probe | Final probe |
|
||||
| --- | --- | --- |
|
||||
| Unique shared lock and standby gate | Acquired; activity 3, battery 99% | Acquired; activity 3, battery 100% |
|
||||
| Scroll region | 160 px viewport, 716 px content | 160 px viewport, 716 px content |
|
||||
| Native pointer | Moved over region | Trusted pointer move reached iframe |
|
||||
| Injected scroll | Smooth 309 | Smooth 309, then discrete 305 |
|
||||
| Wheel events / offset | 0 / 0 → 0 | 0 / 0 → 0 |
|
||||
| Other sample checks | Passed | Passed |
|
||||
| Cleanup | Power restored to `false/5`, recovery timer removed, owned lock released; battery 99% | Same cleanup; battery 99% |
|
||||
|
||||
Both runs captured changed stereo compositor frames and a `/dev/video99` mirror frame, saved only in private folders `/tmp/framey-native-scroll-probe/` and `/tmp/framey-native-scroll-events/`. No room pixels are committed. The final run's `result.json` reported `passed=true`, `cleanupErrors=[]`, `powerRecoveryRemoved=true` and `lockReleased=true`. Here `passed` covers the sample setting, quick action, slider, text and cleanup checks; **native scroll did not pass**. The final immediate SteamVR activity was 0 after the temporary wake; original power settings were restored and read back. No person wore the headset, and physical controller scrolling, rays, haptics, keyboard, optical readability and comfort remain unverified. The hourly worn-check schedule remains enabled.
|
||||
|
||||
## Post-#294 exact-main sample check
|
||||
|
||||
After PR #294 merged, the **unmodified** `scripts/framey-vr-check.py --exercise` from exact main `c38e7e8da459e44a9a717d15e6a07ab8a8e8ef8d` exited 0 on the real Frame. Locked preflight found activity 3 standby, proximity 6.3125 and battery 99%. The sample saved `{"enabled": true}` through trusted synthetic OpenVR input and the live Decky backend, read the file independently, then reopened a fresh iframe with the value retained. Quick action, slider value 50 and text entry completed; compositor frames changed and the mirror capture returned a frame. The old scroll diagnostic still reported no offset movement.
|
||||
|
||||
The run removed its temporary services and recovery timer, restored/read back power `false/5`, reported no cleanup errors, and released only its owned lock. Its immediate activity was 0 after the temporary wake. Private captures/result are under `/tmp/framey-post294/` outside Git. This was one setting activation, so it does not prove the new rapid-double-input guard on physical controllers. No person wore the headset.
|
||||
@@ -0,0 +1,45 @@
|
||||
# v0.4.40 tag and post-tag Frame checks, 26 October 2026
|
||||
|
||||
The v0.4.40 annotated tag dereferences to main merge commit `a4d8073e3199671873d304b80a6acc8cdbd33866` from PR #291. The tag contains the 0.4.40 package and lockfile version. PR #291 passed all reported exact-head checks before merge, including Linux, Windows, macOS, iOS, fake Frame e2e and installer preview builds.
|
||||
|
||||
## Real Frame, tagged source
|
||||
|
||||
From an exact `v0.4.40` checkout, `scripts/frame-smoke.sh` acquired its unique `/tmp/frame-test.lock` owner before any headset read. Locked preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%. On SteamOS 0.5.6 build `20261022.6153909`, all **13/13** reserved title steps passed: ARM64, x86_64 and Windows executable install, launch and remove, plus final cleanup. The command exited 0, which includes successful removal of its own lock. Its detailed result remains private under `tests/smoke/results/2026-10-26T035641Z-20261022.6153909.json` in the test checkout.
|
||||
|
||||
## Real Frame, next main source
|
||||
|
||||
After #289 and #290 merged, `scripts/framey-vr-check.py --exercise` ran from main commit `56bde24be1d38ac060e8ff287455182765e48564` under its own unique shared lock. The locked preflight found activity 3, proximity 6.25, battery 99%, dashboard already visible, and original SteamVR power values `false` and `5`.
|
||||
|
||||
The repository sample panel appeared in the stereo compositor; before/after compositor hashes differed and `/dev/video99` returned a mirror frame. Synthetic OpenVR pointer events were trusted by the panel. A real Decky 3.3.0 backend saved the sample setting from off to on; an independent settings-file read returned `{"enabled": true}`, and a fresh iframe reopen showed on. The quick action, slider value 50 and text entry path completed. Native scroll was attempted but its measured offset stayed 0. This exercise did not run an official store plugin.
|
||||
|
||||
The command exited 0. It removed test services and temporary profile/plugin paths, restored and read back the original power values, removed its recovery timer, reported no cleanup errors, found battery still 99%, and released its own lock. Its immediate final SteamVR activity was 0 after the temporary wake; the saved standby power behavior was restored. Private room and UI captures remain outside Git.
|
||||
|
||||
No person wore the headset. A stereo screenshot, changed pixels, trusted synthetic input and mirror frame do **not** prove wearer-visible passthrough, physical controller usability, VR keyboard behavior, native scrolling, optical legibility or comfort. The [worn checklist](../framey-vr-check.md#three-minute-worn-checklist) and hourly check remain necessary. Official TabMaster, Deck Shelves, ProtonDB Badges and other store panels were not exercised in this run.
|
||||
|
||||
## Downloaded tagged installers
|
||||
|
||||
The draft release's GitHub digest for `Frame-Control-linux-x86_64.AppImage` is `sha256:cded3c660e41eb04e835e28057da81345075c4dbe9a009f397aea1da59655b3d`. A fresh download matched it. After setting the download's executable bit, `scripts/check-appimage-launch.py` launched that exact file under isolated Xvfb: its bundled server answered HTTP 200 with `FrameControl/1`, then stopped on shutdown.
|
||||
|
||||
The draft release's GitHub digest for `Frame-Control-mac-arm64.zip` is `sha256:98f6d118d0db0cba10c3e4bfcd618953f10ba1c35246ce26041ecd39a1d3aff0`. Fresh local and physical-Mac copies matched. On a reachable physical arm64 Mac running macOS 26.5.2, a private temporary extraction reported bundle version 0.4.40; `lipo -verify_arch arm64` passed for the app executable, bundled Python, adb and Mac viewer. Its bundled Python imported `http.server`, `ssl` and `sqlite3` and reported OpenSSL 3.5.8. The temporary directory was removed and checked absent.
|
||||
|
||||
That Mac already had a person's Frame Control app running, so no tagged GUI was launched, replaced or quit. The physical Intel Air was unreachable from this worker; the tagged Intel artifact's build check and bundle-architecture CI are separate from a physical Intel runtime test. No macOS Gatekeeper browser-download path was tested.
|
||||
|
||||
## Tagged workflow and checksum gate
|
||||
|
||||
The [tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40266393941) ran at `a4d8073e3199671873d304b80a6acc8cdbd33866` and passed all six jobs: draft, Apple Silicon Mac, Intel Mac, Windows, Linux and checksums. Its Linux job launched the built AppImage under Xvfb; its Mac jobs checked bundled binary architectures and Python imports; its Windows job installed the Setup executable, compared installed resources and ran the bundled Python PC-library tests.
|
||||
|
||||
The draft has exactly ten installers and `SHA256SUMS`, each with a GitHub SHA-256 digest. Regenerating the checksum file from the ten asset digests produced bytes identical to the downloaded `SHA256SUMS`; its own digest matches `7457da8f292bc1b2b74cb989b4d179858055380721b26e23ccd595994701d1f8`.
|
||||
|
||||
A fresh `Frame-Control-mac-x64.zip` download matched GitHub digest `52f257fc0dfc1b41e89ab0c7a551d667af4cc46f800e4f57e2689d47c856977c`. Independent ZIP inspection found x86_64 Mach-O app, bundled Python 3.12 and Mac viewer executables. This is packaging evidence, not a physical Intel Mac launch.
|
||||
|
||||
## Publication and updater readback
|
||||
|
||||
`scripts/publish-release.sh --dry-run v0.4.40` passed the exact tag, ten installer digests and regenerated `SHA256SUMS` gate. The release script then published [v0.4.40](https://github.com/saphid/frame-control/releases/tag/v0.4.40) at 2026-10-26 04:14:35 UTC. A direct GitHub latest-release read returned `draft=false`, `tag_name=v0.4.40`, ten installers, `SHA256SUMS` and `update.json`. A fresh download of the public `update.json` parsed as version 0.4.40, its stable tag page, ten assets with SHA-256 digests and the unworn-test limits in its notes. This release contains the tagged `a4d8073` commit, not later main changes.
|
||||
|
||||
## Post-tag #292 panel regression
|
||||
|
||||
PR #292 passed all 11 hosted checks on exact head `e0115f356d3b33369a61cfba034b080a7764559f` and merged as `784118b71e3463156574988121f47354d87358c7`. From that exact main commit, `python3 scripts/framey-vr-check.py --exercise --output /tmp/framey-v0440-post292` exited 0 on the real Frame. Its unique `/tmp/frame-test.lock` owner was acquired before any read and released on exit. Preflight: battery 99%, proximity 6.25, SteamVR activity 3 standby, dashboard already visible, saved power `false/5`.
|
||||
|
||||
The temporary repository sample appeared in stereo compositor and mirror captures; compositor frame hashes changed. Trusted synthetic OpenVR pointer input saved a Decky setting to `{"enabled": true}`, independently read from the settings file and retained on a fresh iframe reopen. A quick action, slider value 50 and CDP text entry after OpenVR focus completed. Native scroll remained at offset 0. The test reported `passed=true`, no cleanup errors, removed its recovery timer, restored/read back power `false/5`, battery 99%, and released only its own lock. The private result and room captures remain under `/tmp/framey-v0440-post292/` outside Git.
|
||||
|
||||
This exercises one quick-action activation. It does not prove the new rapid duplicate-tap guard with physical controllers. No person wore the headset; no official Decky plugin panel was tested. The hourly worn-headset schedule remains enabled for wearer-visible passthrough, controller, scrolling, keyboard and comfort checks.
|
||||
@@ -0,0 +1,7 @@
|
||||
# v0.4.41 candidate: real Frame title smoke, 26 October 2026
|
||||
|
||||
Source commit `c87e5c6` bumps the app and lockfile to 0.4.41 on top of main after PRs #289, #290, #292–#295. It includes the exact official ARM64 audio evidence from #293. The release tag and installers do not exist at this candidate stage.
|
||||
|
||||
`scripts/frame-smoke.sh` acquired its unique owner in `/tmp/frame-test.lock` before any headset read. Locked preflight found SteamVR activity 3 (standby), proximity 6.25 and battery 99%. On Frame OS 0.5.6 build `20261022.6153909`, all **13/13** steps passed: existing reserved-title cleanup, devkit identity/status, install/launch/remove for ARM64, x86_64 and Windows test executables, and final cleanup. The command exited 0, which includes successfully releasing only its own lock. Its detailed result remains private under `tests/smoke/results/2026-10-26T043229Z-20261022.6153909.json` in the candidate checkout.
|
||||
|
||||
No person wore the headset. This checks title management and process cleanup, not wearer-visible rendering, physical controllers, official plugin VR panels, audio output or installed desktop packages. After PR #294, a separate exact-main unworn Framey sample exercise passed live setting persistence, synthetic OpenVR input, stereo compositor change and cleanup; it did not prove rapid physical controller taps or native scroll. The [v0.4.40 publication evidence](v0440-release-2026-10-26.md) records the previous tagged installer gates. New v0.4.41 tagged installers and checksums must be verified before publication.
|
||||
@@ -4,7 +4,7 @@ Snapshot: 2026-10-25T23:19:48.854504+00:00; 116 current visible official plugins
|
||||
|
||||
This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review.
|
||||
|
||||
**Works: 1 · Partial: 50 · Broken: 54 · N/A: 11.**
|
||||
**Works: 3 · Partial: 48 · Broken: 54 · N/A: 11.**
|
||||
|
||||
- **Works** requires verified useful plugin behavior with its real backend. Read the reason for the verified scope and remaining limits; it does not imply worn-VR verification.
|
||||
- **Partial** means only a subset or a local fixture scope was verified, often just frontend/settings initialization. The remaining backend/device functionality is unverified; read the reason for the exact scope.
|
||||
@@ -27,7 +27,7 @@ Open the browser audit URL in local Chromium, run `runAudit()` and inspect/downl
|
||||
|
||||
The current published digests are pinned in the JSON; if the store changed, retain/download those exact archives for reproduction. Audit downloads permit larger ZIPs for inspection; production installer limits remain 50 MiB ZIP / 150 MiB unpacked / 30 MiB per member.
|
||||
|
||||
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. These three stay partial: real ARM64 Frame audio dependencies/session mapping remain unverified because the shared headset lock was occupied. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. Junk-Store loading/error views render, but its real backend cannot execute the extracted startup script, so it stays broken. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. TabMaster/Junk-Store ran in actual isolated amd64 Decky; no ARM64 or worn-VR result is claimed. Beyond the documented checks, official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, install folders and system information, plus IsThereAnyDeal's country dropdown in local Chromium. The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md).
|
||||
After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. Volume Boost stays partial: default-sink reads matched, but real sink writes were denied. Audible output, Bluetooth playback and worn VR remain unverified. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. A [local module lookup follow-up](evidence/framey-module-lookup-2026-10-26.md) supplies a finite registry of Framey-local controls and CSS. TabMaster QAM/docs now render; Steam library-tab editing remains unavailable. Junk-Store loading/error views render, but its real backend cannot execute the extracted startup script, so it stays broken. SteamGridDB still requires Steam context-menu internals and DeckSP still fails its published MobX fallback. TabMaster/Junk-Store ran in actual isolated amd64 Decky; no ARM64 or worn-VR result is claimed. Beyond the documented checks, official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, install folders and system information, plus IsThereAnyDeal's country dropdown in local Chromium. The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md).
|
||||
|
||||
## Every current plugin
|
||||
|
||||
@@ -57,7 +57,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 35: MoonDeck 1.13.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: moondeck/python/externals/yarl/_quoting_c.cpython-313-x86_64-linux-gnu.so. Frontend initializes; native CPython wheels and executables in the published archive are x86_64. | static archive, local browser, fake-Frame |
|
||||
| 36: SteamGridDB 1.8.1 | 1 / 2 | broken | Local module mapping is available; frontend still requires the Steam LibraryContextMenu implementation, which has no target in the isolated Framey panel. | static archive, local browser, fake-Frame |
|
||||
| 37: Discord Status 1.5.0 | 1 / 2 | broken | Detached app overviews are now available, but RunningApps/lifetime observations and Steam artwork helpers still lack a live mapping. Discord activity integration remains unavailable/unverified. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 38: Volume Mixer 1.2.1-1 | legacy / legacy | partial | Local Chromium discovers and changes a private silent stream through the actual Decky 3.3.0 Linux backend, with independent Pulse readback. Frame ARM64 libraries/audio session and Bluetooth playback controls remain unverified. | static archive, local browser, fake-Frame, isolated Linux backend |
|
||||
| 38: Volume Mixer 1.2.1-1 | legacy / legacy | works | Per-app stream discovery and slider/keyboard volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, with independent Pulse readback of a silent test stream. Bluetooth playback, audible output and worn-VR controls unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 40: Controller Tools 2.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: ControllerTools/bin/backend. Frontend audit failed: Audit blocks WebSocket. | static archive, local browser, fake-Frame |
|
||||
| 43: Tailscale Control 0.1.3 | legacy / legacy | partial | Settings render locally after the SuspensefulImage fallback. Tailscale daemon/state, connection and exit-node changes unverified; bundled localhost QR asset remains unavailable. No network settings were changed. | static archive, local browser, fake-Frame |
|
||||
| 44: DeckMTP 1.0.5 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: DeckMTP/bin/umtprd. | static archive, local browser, fake-Frame |
|
||||
@@ -68,7 +68,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 52: Game Theme Music 1.7.1-1 | 1 / 2 | broken | Frontend audit failed: Cannot read properties of null (reading 'type'). | static archive, local browser, fake-Frame |
|
||||
| 53: Shotty 0.1.0 | legacy / legacy | broken | Library snapshot fixes the frontend allApps crash. Actual published Decky 3.3.0 amd64 backend fails importing click.get_app_dir (ModuleNotFoundError); the archive does not bundle click. Screenshot aggregation/rescue was not run. | static archive, local browser, fake-Frame, real Frame SSH |
|
||||
| 56: MangoPeel 0.0.7 | 1 / 2 | partial | Initializer succeeds; gamescope/MangoHud actions and backend are unverified. | static archive, local browser, fake-Frame |
|
||||
| 58: Volume Boost 0.0.7 | legacy / legacy | partial | Local Chromium sync/separate sliders, numeric volume entry and cache reopen pass through the actual Decky 3.3.0 backend on a private virtual stereo sink. Frame pactl/default sink, boost above 100% and physical audio remain unverified. | static archive, local browser, fake-Frame, isolated Linux backend |
|
||||
| 58: Volume Boost 0.0.7 | legacy / legacy | partial | Real Decky 3.3.0 ARM64 default-sink reads match independent Frame pactl channel volumes; controls render locally. Sliders/numeric writes passed on an isolated amd64 virtual sink only. Real default-sink writes, boost above 100%, audible output and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 59: Web Browser 1.4.3 | legacy / legacy | broken | Frontend audit failed: deckyFrontendLib.getReactRoot is not a function. | static archive, local browser, fake-Frame |
|
||||
| 60: DeckyFileServer 1.2.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: DeckyFileServer/bin/backend. | static archive, local browser, fake-Frame |
|
||||
| 61: TabMaster 2.15.1 | 1 / 2 | partial | Local Chromium rendered the QAM and navigable Overview/Filters documentation with an actual Decky 3.3.0 amd64 backend. Library-tab editing remains unavailable: Steam login/library patches and MobX reactions have no target. ARM64 and worn VR unverified. | static archive, local browser, fake-Frame |
|
||||
@@ -149,7 +149,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say
|
||||
| 141: DeckyClocker 1.0.0 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 142: MuteOnSleep 1.0.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
| 143: Desktop Apps 0.1.0 | 1 / 2 | partial | Initializer succeeds; Flatpak app discovery and Steam shortcut changes unverified. | static archive, local browser, fake-Frame |
|
||||
| 144: Mixer 0.1.0 | 1 / 2 | partial | Local Chromium numeric stream volume and filter save/reopen pass through the actual Decky 3.3.0 Linux backend; filters survive its restart. Frame pw-dump/wpctl, real audio session and worn VR remain unverified. | static archive, local browser, fake-Frame, isolated Linux backend |
|
||||
| 144: Mixer 0.1.0 | 1 / 2 | works | Per-app discovery and numeric volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, independently read back on a silent test stream. Filters persist on amd64; ARM64 filter persistence, Reset, internal audio nodes, audible output and worn VR unverified. | static archive, local browser, fake-Frame, isolated Linux backend, real Frame SSH |
|
||||
| 145: VARTA 1.0.21 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame |
|
||||
|
||||
## API, UI and backend inventory
|
||||
|
||||
@@ -201,7 +201,9 @@ an optional `settings` React element under a **Settings** tab and an optional
|
||||
Framey shows tabs only when both views exist. For one-tap headset controls, return
|
||||
`frameyQuickActions: [{label: 'Refresh data', onClick: refreshData}]` from the
|
||||
plugin initializer. Framey displays up to eight actions and reports a pending,
|
||||
success, or failure state. These descriptor fields are Framey additions; Decky
|
||||
success, or failure state. It ignores extra taps while an action is pending,
|
||||
and a failed action can be retried. Invalid action descriptors do not count
|
||||
toward the eight-action limit. These descriptor fields are Framey additions; Decky
|
||||
ignores them. Use a plugin backend method through `@decky/api` for work needing
|
||||
persistent state, and keep a quick action safe to repeat.
|
||||
See the [Decky API compatibility table](frame-plugin-author.md#framey-decky-api-compatibility)
|
||||
|
||||
@@ -53,7 +53,12 @@ iframe through its own debugging target, and reads control geometry. OpenVR's
|
||||
actual gamescope overlay. The check verifies delivery to the intended element
|
||||
and a trusted pointer event, toggles the sample, reads its real Decky settings
|
||||
file, reopens a fresh iframe, and checks the saved value. It also clicks the
|
||||
Settings/Panel tabs and quick action. A smooth-scroll event is attempted and its measured scroll offset is reported.
|
||||
Settings/Panel tabs and quick action. The scroll diagnostic creates a temporary
|
||||
160-pixel-high region with more than 700 pixels of content, moves the native
|
||||
pointer over it, then attempts OpenVR smooth and discrete scroll events. It
|
||||
reports region geometry, trusted pointer events, wheel events and `scrollTop`.
|
||||
No wheel event or offset change means this synthetic injection path remains
|
||||
unverified; it does not establish whether a physical controller can scroll.
|
||||
Temporary controls exercise the shipped
|
||||
`DFL.SliderField` and `DFL.TextField`; their values remain UI-only. Text uses
|
||||
CDP `Input.insertText` after OpenVR pointer focus. This is synthetic input,
|
||||
|
||||
+16
-12
@@ -176,19 +176,21 @@ switch while SSH is down:
|
||||
|
||||
## Headset smoke test
|
||||
|
||||
**Documented shared-device procedure:** before a test installs, launches or
|
||||
stops an application, acquire `ssh frame 'mkdir /tmp/frame-test.lock'`. If it
|
||||
fails, leave that lock alone and continue offline work. Only the thread that
|
||||
acquired it releases it with `ssh frame 'rmdir /tmp/frame-test.lock'`, after
|
||||
cleanup. Keep each device session to a few minutes.
|
||||
**Documented shared-device procedure:** before any headset read or a test that
|
||||
installs, launches or stops an application, atomically create
|
||||
`/tmp/frame-test.lock` on the Frame and write a unique token to its `owner`
|
||||
file. If creation fails, leave that lock alone and continue offline work. Only
|
||||
the owner that verifies its token may remove its `owner` file and lock directory
|
||||
after cleanup. Keep each device session to a few minutes.
|
||||
|
||||
Check battery capacity and charging state under `/sys/class/power_supply`
|
||||
before and after; keep capacity above 20%. Stop only processes started by the
|
||||
test, remove temporary installs and profiles, and restore the prior dashboard
|
||||
state. Leave Steam and SteamVR running. Do not reboot or change global settings.
|
||||
Record the build, actual interaction results, cleanup and any unworn-headset
|
||||
limits alongside screenshots or logs. These are caller responsibilities; the
|
||||
smoke script below does not acquire this shared lock itself.
|
||||
limits alongside screenshots or logs. The smoke script below acquires and
|
||||
releases its own tokenized shared lock; other device tests follow this
|
||||
procedure themselves.
|
||||
|
||||
[Packaged Desktop Capture evidence, 2026-10-24](evidence/desktop-capture-packaged-2026-10-24.md)
|
||||
records v0.4.17 GUI button captures of a real mapped nested desktop on SteamOS
|
||||
@@ -220,17 +222,19 @@ removes three tiny titles built from bytes by `tests/smoke/tiny_programs.py`
|
||||
(an ARM64 and an x86-64 static Linux program that sleep for ten seconds, and
|
||||
an x86-64 `.exe` that exits at once). A launch passes only with fresh evidence:
|
||||
the ARM64 program running, the `.exe` started (its process or Steam's log),
|
||||
and the x86-64 program running or Steam logging that its runtime isn't
|
||||
installed, which is what the Frame does today. Steam's log lines about each
|
||||
title are kept.
|
||||
and the x86-64 program running. If Steam logs that the optional x86-64 runtime
|
||||
is not installed, that launch is **skipped**, with the reason and log saved in
|
||||
the result. It does not prove that the x86-64 title ran. Steam's log lines about
|
||||
each title are kept.
|
||||
|
||||
Everything it installs is removed again, also after a failure: the titles and
|
||||
their Steam shortcuts, a paired key, and `~/devkit-utils` if it wasn't there
|
||||
before (if it was, it stays, synced to this checkout as Frame Control always
|
||||
does). A cleanup that fails counts as a failed step. Results go to
|
||||
`tests/smoke/results/<time>-<BUILD_ID>.json` (not committed) with a summary on
|
||||
screen; it exits 0 when every step passed, 1 if one failed, 2 if the headset
|
||||
isn't reachable.
|
||||
screen. The summary counts passed, skipped and failed steps separately. It
|
||||
exits 0 when no step failed (including a skipped optional runtime), 1 if a step
|
||||
failed, or 2 if the headset isn't reachable.
|
||||
|
||||
Smoke process cleanup runs before and after title removal and during the
|
||||
initial/final leftover sweeps. It targets only the reserved `fc_smoke_arm64`,
|
||||
|
||||
@@ -54,7 +54,7 @@ def render(data):
|
||||
'A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer '
|
||||
'through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, '
|
||||
'independent server readbacks and Mixer filter persistence across a runtime restart passed. '
|
||||
'These three stay partial: real ARM64 Frame audio dependencies/session mapping remain unverified because the shared headset lock was occupied. '
|
||||
'An [ARM64 audio follow-up](evidence/framey-audio-arm64-2026-10-26.md) verified the unmodified official backends in actual Decky 3.3.0 on the Frame user audio session. Volume Mixer and Mixer now qualify as works for per-app volume: Chromium controls changed only a named silent stream, with independent Pulse readbacks. Volume Boost stays partial: default-sink reads matched, but real sink writes were denied. Audible output, Bluetooth playback and worn VR remain unverified. '
|
||||
'A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs '
|
||||
'and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot '
|
||||
'to render Deck Settings game selection, Steamback\'s empty save panel and PlayTime statistics/settings. '
|
||||
|
||||
@@ -260,6 +260,11 @@ print('{}')
|
||||
except Exception as exc:
|
||||
report['hiddenOverlayError'] = str(exc)
|
||||
raise RuntimeError('Frame Control overlay stayed hidden after focus retry; no input sent')
|
||||
if args.exercise:
|
||||
lines = remote(VRCMD + ' --overlays').splitlines()
|
||||
report['testOverlayListing'] = [line.strip() for index, line in enumerate(lines)
|
||||
if line.startswith("'" + KEY + "'") or
|
||||
(index and lines[index - 1].startswith("'" + KEY + "'"))]
|
||||
if args.wake or args.exercise:
|
||||
shot = remote.python((ROOT/'ui/frame_vrshot.py').read_text(), timeout=20)['path']
|
||||
try:
|
||||
|
||||
@@ -93,9 +93,11 @@ class Overlay:
|
||||
event = Event(kind, 0xFFFFFFFF, 0, Data(mouse=Mouse(x, y, button)))
|
||||
C.CFUNCTYPE(None, C.c_uint64, C.POINTER(Event))(view[2])(self.handle, C.byref(event))
|
||||
|
||||
def post_scroll(self, xdelta, ydelta):
|
||||
def post_scroll(self, xdelta, ydelta, kind=309):
|
||||
if kind not in (305, 309):
|
||||
raise ValueError('Only OpenVR discrete and smooth scroll events are allowed')
|
||||
view = self.interface(b'FnTable:IVROverlayView_003')
|
||||
event = Event(309, 0xFFFFFFFF, 0, Data(scroll=Scroll(xdelta, ydelta, 0, 1)))
|
||||
event = Event(kind, 0xFFFFFFFF, 0, Data(scroll=Scroll(xdelta, ydelta, 0, 1)))
|
||||
C.CFUNCTYPE(None, C.c_uint64, C.POINTER(Event))(view[2])(self.handle, C.byref(event))
|
||||
|
||||
def image_hash(self):
|
||||
|
||||
@@ -46,7 +46,7 @@ def exercise(profile, plugin_id, folder):
|
||||
|
||||
def text(): return panel.eval('document.body.innerText')
|
||||
|
||||
def click(selector):
|
||||
def point(selector):
|
||||
# Account for CSS pixels, iframe position, border, HiDPI scaling, and
|
||||
# OpenVR's bottom-left mouse origin; read geometry after scrolling.
|
||||
panel.eval(f'document.querySelector({json.dumps(selector)}).scrollIntoView({{block:"center"}})')
|
||||
@@ -58,6 +58,10 @@ def exercise(profile, plugin_id, folder):
|
||||
y = sy - (outer['y'] + pos['y']) * sy / outer['h']
|
||||
if not 0 <= x <= sx or not 0 <= y <= sy:
|
||||
raise RuntimeError('Control is outside the visible viewport')
|
||||
return x, y
|
||||
|
||||
def click(selector):
|
||||
x, y = point(selector)
|
||||
count = panel.eval('__vrClickCount')
|
||||
vr.post_mouse(300, x, y)
|
||||
vr.post_mouse(301, x, y, 1)
|
||||
@@ -113,12 +117,39 @@ def exercise(profile, plugin_id, folder):
|
||||
# CDP only types text; OpenVR supplies pointer focus into the field.
|
||||
command(parent, 'Input.insertText', {'text':'framey-ci'})
|
||||
result['textValue'] = wait(lambda:panel.eval('document.querySelector("#vr-controls input[type=text]").value'), lambda v:v == 'framey-ci')
|
||||
panel.eval('window.scrollTo(0,0)')
|
||||
result['scrollBefore'] = panel.eval('scrollY')
|
||||
vr.post_scroll(0,-3)
|
||||
time.sleep(.5)
|
||||
result['scrollAfter'] = panel.eval('scrollY')
|
||||
# The sample alone may be shorter than its iframe. Use an owned,
|
||||
# visibly scrollable region so a zero offset is a meaningful result.
|
||||
panel.eval('''(()=>{
|
||||
const box=document.createElement('div');box.id='vr-scroll-probe';
|
||||
box.style.cssText='height:160px;overflow-y:scroll;border:2px solid #72baf0';
|
||||
box.innerHTML='<div style="height:700px;padding:8px">Scroll probe</div>';
|
||||
document.body.append(box);
|
||||
window.__vrWheelEvents=[];
|
||||
document.addEventListener('wheel',e=>__vrWheelEvents.push({trusted:e.isTrusted,
|
||||
deltaY:e.deltaY,target:e.target?.id||e.target?.tagName}),{capture:true,passive:true});
|
||||
return true;
|
||||
})()''')
|
||||
result['scrollGeometry'] = panel.eval('(()=>{let e=document.getElementById("vr-scroll-probe");return {height:e.clientHeight,content:e.scrollHeight}})()')
|
||||
if result['scrollGeometry']['content'] <= result['scrollGeometry']['height']:
|
||||
raise RuntimeError('Scroll diagnostic did not create overflow')
|
||||
x, y = point('#vr-scroll-probe')
|
||||
panel.eval('window.__vrScrollPointer=[];document.addEventListener("pointermove",e=>__vrScrollPointer.push({trusted:e.isTrusted,target:e.target?.id||e.target?.tagName}),{capture:true});true')
|
||||
vr.post_mouse(300, x, y)
|
||||
time.sleep(.2)
|
||||
result['scrollPointer'] = panel.eval('__vrScrollPointer')
|
||||
result['scrollBefore'] = panel.eval('document.getElementById("vr-scroll-probe").scrollTop')
|
||||
result['scrollAttempts'] = []
|
||||
for kind in (309, 305):
|
||||
vr.post_scroll(0,-3,kind)
|
||||
time.sleep(.5)
|
||||
offset = panel.eval('document.getElementById("vr-scroll-probe").scrollTop')
|
||||
result['scrollAttempts'].append({'eventType':kind,'offset':offset})
|
||||
if offset != result['scrollBefore']:
|
||||
break
|
||||
result['scrollAfter'] = result['scrollAttempts'][-1]['offset']
|
||||
result['scrollEvents'] = panel.eval('__vrWheelEvents')
|
||||
result['nativeScrollMoved'] = result['scrollAfter'] != result['scrollBefore']
|
||||
panel.eval('document.getElementById("vr-scroll-probe").remove()')
|
||||
panel.eval('document.getElementById("vr-controls").remove()')
|
||||
# Scroll saved status into the visible iframe for compositor evidence.
|
||||
panel.eval('document.querySelector("[role=status]").scrollIntoView({block:"center"})')
|
||||
|
||||
@@ -7,7 +7,8 @@ const testSource=source.replace(/\n start\(\);\n\}\)\(\);\s*$/, '\n window.__s
|
||||
assert.notEqual(testSource,source);
|
||||
const states=[];
|
||||
const React={Fragment:'fragment',Component:class {},createElement:(type,props,...children)=>({type,props:props||{},children}),
|
||||
useState:value=>[value,next=>states.push(next)], useEffect(){},isValidElement:value=>!!value?.type};
|
||||
useState:value=>[value,next=>states.push(next)], useRef:value=>({current:value}),
|
||||
useEffect(){},isValidElement:value=>!!value?.type};
|
||||
class Element {constructor(){this.children=[];this.textContent='';this.attributes={};}
|
||||
append(...children){this.children.push(...children);}setAttribute(key,value){this.attributes[key]=value;}
|
||||
remove(){this.removed=true;}}
|
||||
@@ -98,13 +99,27 @@ const plugin={content:{type:'content'},settings:{type:'settings'},titleView:{typ
|
||||
{label:'invalid'},...Array.from({length:9},(_,i)=>({label:`Action ${i}`,onClick:()=>{}}))]};
|
||||
const tree=window.__surface({plugin});
|
||||
const buttons=nodes(tree,'button');
|
||||
assert.equal(buttons.length,9); // Panel, Settings, and seven valid actions inside the first eight entries.
|
||||
assert.equal(buttons.length,10); // Panel, Settings, and eight valid actions after filtering.
|
||||
assert.equal(nodes(tree,'nav')[0].props['aria-label'],'Plugin views');
|
||||
assert.equal(nodes(tree,'section')[0].props['aria-label'],'Plugin quick actions');
|
||||
assert.equal(nodes(tree,'header').length,1);
|
||||
await buttons[2].props.onClick();
|
||||
assert.equal(called,1);
|
||||
assert.deepEqual(states.slice(-3),[0,'Refresh data…','Fresh',null].slice(-3));
|
||||
let actionCount=0, settleAction;
|
||||
const guarded=window.__surface({plugin:{content:{type:'content'},frameyQuickActions:[{
|
||||
label:'Save setting',onClick:()=>{actionCount++;return new Promise((resolve,reject)=>{
|
||||
settleAction={resolve,reject};});}}]}});
|
||||
const saveButton=nodes(guarded,'button')[0];
|
||||
const firstSave=saveButton.props.onClick();
|
||||
await saveButton.props.onClick(); // React has not rendered disabled state yet.
|
||||
assert.equal(actionCount,1,'rapid second tap must not repeat a backend mutation');
|
||||
settleAction.reject(Error('offline'));
|
||||
await firstSave;
|
||||
const retrySave=saveButton.props.onClick();
|
||||
assert.equal(actionCount,2,'a failed action must release the synchronous lock');
|
||||
settleAction.resolve('Saved');
|
||||
await retrySave;
|
||||
const settingsOnly=window.__surface({plugin:{settings:{type:'settings'}}});
|
||||
assert.equal(nodes(settingsOnly,'nav').length,0);
|
||||
assert.equal(nodes(settingsOnly,'settings').length,1);
|
||||
|
||||
+26
-10
@@ -27,7 +27,8 @@ rechecked around deletion; shared Proton services are never stopped by name.
|
||||
|
||||
Usage: python3 tests/smoke/frame_smoke.py [--pair] (or scripts/frame-smoke.sh)
|
||||
Env: FRAME_ALIAS (default frame), FRAME_SMOKE_RESULTS (default tests/smoke/results)
|
||||
Exit status: 0 all passed, 1 a step failed, 2 the headset isn't reachable.
|
||||
Exit status: 0 no step failed (optional runtime may skip a launch), 1 a step
|
||||
failed, 2 the headset isn't reachable.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
@@ -125,21 +126,32 @@ class Smoke:
|
||||
self.installed = set()
|
||||
|
||||
def step(self, name, fn, *args):
|
||||
"""Run one step; record ok/failed, its detail and how long it took."""
|
||||
"""Run one step; record passed/skipped/failed and how long it took."""
|
||||
t0 = time.monotonic()
|
||||
rec = {'step': name}
|
||||
try:
|
||||
detail = fn(*args)
|
||||
rec.update(ok=True, detail=detail)
|
||||
except Skipped as e:
|
||||
rec.update(ok=False, skipped=True, reason=str(e), detail=e.detail)
|
||||
except Exception as e: # a failed step is a result, not the end of the run
|
||||
rec.update(ok=False, error=f'{type(e).__name__}: {e}', trace=traceback.format_exc(limit=3))
|
||||
rec['seconds'] = round(time.monotonic() - t0, 1)
|
||||
self.steps.append(rec)
|
||||
print(f" {'ok ' if rec['ok'] else 'FAIL'} {name} ({rec['seconds']} s)"
|
||||
+ ('' if rec['ok'] else f": {rec['error']}"), flush=True)
|
||||
label = 'ok ' if rec['ok'] else 'SKIP' if rec.get('skipped') else 'FAIL'
|
||||
message = '' if rec['ok'] else f": {rec.get('reason') or rec['error']}"
|
||||
print(f" {label} {name} ({rec['seconds']} s){message}", flush=True)
|
||||
return rec
|
||||
|
||||
|
||||
class Skipped(Exception):
|
||||
"""A prerequisite was unavailable; preserve its evidence without claiming a launch."""
|
||||
|
||||
def __init__(self, reason, detail):
|
||||
super().__init__(reason)
|
||||
self.detail = detail
|
||||
|
||||
|
||||
def ssh(cmd, timeout=60):
|
||||
return frame_android.ssh(cmd, timeout=timeout)
|
||||
|
||||
@@ -194,8 +206,8 @@ def steam_log_lines(*patterns):
|
||||
|
||||
# What a launch must show, per kind. The x86-64 runtime isn't installed on the
|
||||
# Frame, so Steam acknowledges that launch and logs "... is not installed"
|
||||
# instead (docs/sideloading.md, 2026-09-26): recorded, not a failure.
|
||||
EXPECTED = {'arm64': ('running',), 'x86_64': ('running', 'runtime missing'), 'exe': ('running', 'started')}
|
||||
# instead (docs/sideloading.md, 2026-09-26): record a skip, not a launch pass.
|
||||
EXPECTED = {'arm64': ('running',), 'x86_64': ('running',), 'exe': ('running', 'started')}
|
||||
|
||||
|
||||
def title_cycle(smoke, kind, folder):
|
||||
@@ -229,10 +241,12 @@ def title_cycle(smoke, kind, folder):
|
||||
outcome, new = 'runtime missing', new + missing
|
||||
elif any('started devkit game' in line or 'chdir' in line for line in new):
|
||||
outcome = 'started'
|
||||
if outcome in EXPECTED[kind]: # else keep looking: the log can come before the process
|
||||
if outcome in EXPECTED[kind] or (kind == 'x86_64' and outcome == 'runtime missing'):
|
||||
break
|
||||
time.sleep(0.5)
|
||||
detail = {'outcome': outcome, 'processes': procs, 'steam_log': new[-12:]}
|
||||
if kind == 'x86_64' and outcome == 'runtime missing':
|
||||
raise Skipped('Steam Linux Runtime 4 is not installed; x86_64 title did not launch', detail)
|
||||
if outcome not in EXPECTED[kind]:
|
||||
raise AssertionError(f"Steam acknowledged the launch, but {outcome} (expected "
|
||||
f"{' or '.join(EXPECTED[kind])}): {json.dumps(detail)[:400]}")
|
||||
@@ -396,14 +410,16 @@ def run_smoke(args, before):
|
||||
'rm -rf ~/devkit-utils ~/.devkit-utils.frame-control && echo removed')
|
||||
subprocess.run(['rm', '-rf', folder])
|
||||
passed = sum(s['ok'] for s in smoke.steps)
|
||||
skipped = sum(s.get('skipped', False) for s in smoke.steps)
|
||||
failed = len(smoke.steps) - passed - skipped
|
||||
RESULTS.mkdir(exist_ok=True)
|
||||
out = RESULTS / f"{started.replace(':', '')}-{build}.json"
|
||||
out.write_text(json.dumps({'started': started, 'alias': ALIAS, 'preflight': before,
|
||||
'os_release': release, 'paired': args.pair,
|
||||
'passed': passed, 'failed': len(smoke.steps) - passed,
|
||||
'passed': passed, 'skipped': skipped, 'failed': failed,
|
||||
'devkit_utils_there_before': had_utils, 'steps': smoke.steps}, indent=1))
|
||||
print(f'==> {passed}/{len(smoke.steps)} steps passed on build {build}; results in {out}')
|
||||
return 0 if passed == len(smoke.steps) else 1
|
||||
print(f'==> {passed} passed, {skipped} skipped, {failed} failed on build {build}; results in {out}')
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -105,6 +105,9 @@ class Safety(unittest.TestCase):
|
||||
overlay = importlib.util.module_from_spec(overlay_spec); overlay_spec.loader.exec_module(overlay)
|
||||
self.assertEqual(ctypes.sizeof(overlay.Event), 60)
|
||||
self.assertEqual(overlay.Event.data.offset, 12)
|
||||
self.assertEqual(ctypes.sizeof(overlay.Scroll), 16)
|
||||
with self.assertRaises(ValueError):
|
||||
object.__new__(overlay.Overlay).post_scroll(0, -3, 308)
|
||||
original = check.frame_headset.bundle()
|
||||
patched = check.debug_bundle(original, '/tmp/owned-profile')
|
||||
import tarfile
|
||||
|
||||
@@ -95,6 +95,60 @@ class RealProcesses(unittest.TestCase):
|
||||
|
||||
|
||||
class Safety(unittest.TestCase):
|
||||
def test_missing_x86_runtime_skips_launch_but_still_removes_title(self):
|
||||
smoke = frame_smoke.Smoke()
|
||||
gid = 'fc_smoke_x86_64'
|
||||
with mock.patch.object(frame_smoke.tiny_programs, 'write', return_value=Path('/tmp/tiny')), \
|
||||
mock.patch.object(frame_smoke.frame_titles, 'install', return_value={
|
||||
'runtime': 'SteamLinuxRuntime_4'}), \
|
||||
mock.patch.object(frame_smoke.frame_titles, 'list_titles', side_effect=[
|
||||
[{'id': gid}], []]), \
|
||||
mock.patch.object(frame_smoke.frame_titles, 'launch') as launch, \
|
||||
mock.patch.object(frame_smoke.frame_titles, 'remove') as remove, \
|
||||
mock.patch.object(frame_smoke, 'steam_log_lines', side_effect=[
|
||||
[], [], [], ['Tool 4183110 Steam Linux Runtime 4.0 is found, but is not installed']]), \
|
||||
mock.patch.object(frame_smoke, 'ssh', return_value=''), \
|
||||
mock.patch.object(frame_smoke, 'stop_titles', return_value={'stopped_pids': []}):
|
||||
frame_smoke.title_cycle(smoke, 'x86_64', '/tmp')
|
||||
self.assertEqual([step['step'] for step in smoke.steps],
|
||||
['x86_64: install', 'x86_64: launch', 'x86_64: remove'])
|
||||
self.assertTrue(smoke.steps[0]['ok'])
|
||||
self.assertFalse(smoke.steps[1]['ok'])
|
||||
self.assertTrue(smoke.steps[1]['skipped'])
|
||||
self.assertEqual(smoke.steps[1]['detail']['outcome'], 'runtime missing')
|
||||
self.assertTrue(smoke.steps[2]['ok'])
|
||||
self.assertNotIn(gid, smoke.installed)
|
||||
launch.assert_called_once_with(gid)
|
||||
remove.assert_called_once_with(gid)
|
||||
|
||||
def test_optional_runtime_skip_is_counted_separately_in_saved_result(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
output = Path(root) / 'results'
|
||||
folder = Path(root) / 'titles'
|
||||
folder.mkdir()
|
||||
def cycle(smoke, kind, _folder):
|
||||
if kind == 'x86_64':
|
||||
def missing():
|
||||
raise frame_smoke.Skipped('optional runtime absent', {'outcome': 'runtime missing'})
|
||||
smoke.step(f'{kind}: launch', missing)
|
||||
else:
|
||||
smoke.step(f'{kind}: launch', lambda: {'outcome': 'running'})
|
||||
with mock.patch.object(frame_smoke, 'RESULTS', output), \
|
||||
mock.patch.object(frame_smoke, 'os_release', return_value={
|
||||
'BUILD_ID': 'fake', 'VERSION_ID': 'fake', 'VARIANT_ID': 'fake'}), \
|
||||
mock.patch.object(frame_smoke, 'ssh', return_value='yes'), \
|
||||
mock.patch.object(frame_smoke, 'cleanup', return_value={}), \
|
||||
mock.patch.object(frame_smoke, 'properties', return_value={}), \
|
||||
mock.patch.object(frame_smoke, 'status', return_value={}), \
|
||||
mock.patch.object(frame_smoke, 'title_cycle', side_effect=cycle), \
|
||||
mock.patch.object(frame_smoke.tempfile, 'mkdtemp', return_value=str(folder)):
|
||||
self.assertEqual(frame_smoke.run_smoke(mock.Mock(pair=False),
|
||||
{'activity': 3, 'proximity': [], 'battery': [100]}), 0)
|
||||
result = json.loads(next(output.glob('*-fake.json')).read_text())
|
||||
self.assertEqual((result['passed'], result['skipped'], result['failed']), (6, 1, 0))
|
||||
self.assertEqual(next(s for s in result['steps'] if s['step'] == 'x86_64: launch')
|
||||
['detail']['outcome'], 'runtime missing')
|
||||
|
||||
def test_non_test_ids_rejected_before_scanning(self):
|
||||
for ids in ([], ['portal2'], ['fc_smoke_exe/../portal2'], ['fc_smoke_custom']):
|
||||
with self.subTest(ids=ids), self.assertRaises(ValueError):
|
||||
|
||||
@@ -2990,14 +2990,15 @@
|
||||
"https://github.com/ldo/dbussy",
|
||||
"https://github.com/mk-fg/python-pulse-control"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Local Chromium discovers and changes a private silent stream through the actual Decky 3.3.0 Linux backend, with independent Pulse readback. Frame ARM64 libraries/audio session and Bluetooth playback controls remain unverified.",
|
||||
"status": "works",
|
||||
"reason": "Per-app stream discovery and slider/keyboard volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, with independent Pulse readback of a silent test stream. Bluetooth playback, audible output and worn-VR controls unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame",
|
||||
"isolated Linux backend"
|
||||
"isolated Linux backend",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 38,
|
||||
@@ -4030,13 +4031,14 @@
|
||||
"https://github.com/saumya-banthia/volume-boost.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Local Chromium sync/separate sliders, numeric volume entry and cache reopen pass through the actual Decky 3.3.0 backend on a private virtual stereo sink. Frame pactl/default sink, boost above 100% and physical audio remain unverified.",
|
||||
"reason": "Real Decky 3.3.0 ARM64 default-sink reads match independent Frame pactl channel volumes; controls render locally. Sliders/numeric writes passed on an isolated amd64 virtual sink only. Real default-sink writes, boost above 100%, audible output and worn VR unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame",
|
||||
"isolated Linux backend"
|
||||
"isolated Linux backend",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 58,
|
||||
@@ -13272,14 +13274,15 @@
|
||||
"https://github.com/allmazz/decky-volume-mixer",
|
||||
"https://github.com/allmazz/decky-volume-mixer.git"
|
||||
],
|
||||
"status": "partial",
|
||||
"reason": "Local Chromium numeric stream volume and filter save/reopen pass through the actual Decky 3.3.0 Linux backend; filters survive its restart. Frame pw-dump/wpctl, real audio session and worn VR remain unverified.",
|
||||
"status": "works",
|
||||
"reason": "Per-app discovery and numeric volume changes pass in local Chromium through real Decky 3.3.0 ARM64 on the Frame audio session, independently read back on a silent test stream. Filters persist on amd64; ARM64 filter persistence, Reset, internal audio nodes, audible output and worn VR unverified.",
|
||||
"blockInstall": false,
|
||||
"verification": [
|
||||
"static archive",
|
||||
"local browser",
|
||||
"fake-Frame",
|
||||
"isolated Linux backend"
|
||||
"isolated Linux backend",
|
||||
"real Frame SSH"
|
||||
],
|
||||
"browser": {
|
||||
"id": 144,
|
||||
|
||||
+8
-4
@@ -482,18 +482,22 @@
|
||||
const [route, setRoute] = R.useState(currentRoute);
|
||||
R.useEffect(() => {routeListeners.add(setRoute); return () => routeListeners.delete(setRoute);}, []);
|
||||
const [running, setRunning] = R.useState(null);
|
||||
const runningRef = R.useRef(false);
|
||||
const [message, setMessage] = R.useState('');
|
||||
const actions = Array.isArray(plugin.frameyQuickActions) ? plugin.frameyQuickActions.slice(0, 8)
|
||||
const actions = Array.isArray(plugin.frameyQuickActions) ? plugin.frameyQuickActions
|
||||
.filter(action => action && typeof action.label === 'string' && action.label.length <= 80 &&
|
||||
typeof action.onClick === 'function') : [];
|
||||
typeof action.onClick === 'function').slice(0, 8) : [];
|
||||
async function runAction(action, index) {
|
||||
if (running !== null) return;
|
||||
// React state may not render between two controller clicks. Lock before
|
||||
// awaiting a backend action so one tap cannot start a second mutation.
|
||||
if (runningRef.current) return;
|
||||
runningRef.current = true;
|
||||
setRunning(index); setMessage(`${action.label}…`);
|
||||
try {
|
||||
const result = await action.onClick();
|
||||
setMessage(typeof result === 'string' && result.length < 200 ? result : `${action.label} done`);
|
||||
} catch (failure) { setMessage(`${action.label} failed: ${failure.message || failure}`); }
|
||||
finally { setRunning(null); }
|
||||
finally { runningRef.current = false; setRunning(null); }
|
||||
}
|
||||
return R.createElement(R.Fragment, null,
|
||||
plugin.titleView && R.createElement('header', null, plugin.titleView),
|
||||
|
||||
Reference in new issue
Block a user