From 07de29d58a9b47d72188edfc8908e20828777b3a Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Sat, 26 Sep 2026 08:49:06 +1000 Subject: [PATCH] Skip the Frame-side status probe test on Windows; new README and docs Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 362 ++++++++++++++++++------------------------ app/package.json | 9 +- docs/frame-control.md | 133 ++++++++++++++++ docs/img/icon.png | Bin 0 -> 38506 bytes docs/scripts.md | 103 ++++++++++++ tests/test_server.py | 2 + 6 files changed, 395 insertions(+), 214 deletions(-) create mode 100644 docs/frame-control.md create mode 100644 docs/img/icon.png create mode 100644 docs/scripts.md diff --git a/README.md b/README.md index 3eae27b..0a08a9e 100644 --- a/README.md +++ b/README.md @@ -1,253 +1,196 @@ -# Steam Frame ↔ Mac +
-**Frame Control** is a Mac app for managing a Valve Steam Frame (standalone VR -headset: SteamOS 3, Arch-based, arm64, Snapdragon 8 Gen 3) over SSH: live -headset view, battery and status, your Steam library, Android (Lepton) apps, -file and clipboard transfer. This repo also holds the scripts behind it and -field notes on how the Frame's software works, all aimed at as little typing -on the headset's virtual keyboard as possible. +Frame Control icon -It's an unofficial hobby project, not affiliated with Valve. +# Frame Control -Status: written 2026-09-25 and checked against a real Frame the same day -(SteamOS 0.3.0, variant `vr`, build 20260922). The **Frame Control** Mac app -and most scripts are **verified** on the device. The scripts table below marks -each one, and [docs/open-questions.md](docs/open-questions.md#verified-on-device-2026-09-25) -lists what's still unchecked. +**Manage your Valve Steam Frame from your computer.**
+See what the headset sees, install games and Android apps, move files and text +across, and check battery and status, all over SSH. -## Trying it out +[![Latest release](https://img.shields.io/github/v/release/saphid/steam-frame?label=release&color=1a9fff)](https://github.com/saphid/steam-frame/releases/latest) +[![Platforms](https://img.shields.io/badge/macOS%20%7C%20Windows%20%7C%20Linux-2a475e?label=runs%20on)](#install) +[![Checks](https://img.shields.io/github/actions/workflow/status/saphid/steam-frame/checks.yml?branch=main&label=checks)](https://github.com/saphid/steam-frame/actions/workflows/checks.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-66c0f4)](LICENSE) -You need: +[**Download**](#install) · [Features](#features) · [Set up the headset](#set-up-the-headset) · [Feedback](#feedback) · [Docs](#going-further) -- A Steam Frame with **Developer Mode** on (next section; it's a toggle). -- A Mac with Apple Silicon (M1 or later). Tested on macOS 26. There's no Intel - build. -- `python3` on the Mac (`xcode-select --install` provides it). -- Optional: `adb` for Android apps (`brew install android-platform-tools`). +
-Steps: +Frame Control showing the headset view, battery and status, and the Steam library -1. Download the DMG from the - [latest release](https://github.com/saphid/steam-frame/releases/latest), - open it and drag **Frame Control** to Applications. -2. The app isn't notarized (no paid Apple developer account), so macOS will - say it's damaged or can't be checked. Clear the download quarantine once: - ```sh - xattr -dr com.apple.quarantine "/Applications/Frame Control.app" - ``` -3. Open it. With no `frame` SSH alias yet, it offers to run the connection - setup in Terminal. That asks for the Developer Mode password once, then - uses a key from then on. +Unofficial hobby project, not affiliated with Valve. Free and open source. -**Feedback:** please open a -[GitHub issue](https://github.com/saphid/steam-frame/issues) with what you -tried, your SteamOS build (Steam Settings → System) and the server log -(**Frame → Show Server Log**, at `~/Library/Logs/Frame Control/server.log`). -Features are marked **verified** or not below; the unverified ones are the -most useful to hear about. +
-**What it changes on your Frame:** only what you click. Installs go to your -user account (`--user` Flatpaks, Lepton instances, Steam downloads), and -nothing needs `sudo` except the power buttons. On the Mac it adds a `Host -frame` entry to `~/.ssh/config` and a key at `~/.ssh/id_ed25519_frame`. +--- -## Minimum typing on the headset +## Features -Valve's own developer docs say SSH, ADB, and RDP are all turned on through a -**UI toggle**. You don't need a terminal, `passwd`, or `systemctl`. The only -thing you type on the headset is a password you choose. + + + + + + + + + + + + + + + + + +
-On the Frame: +**👓 Headset view**
+Live video of what the lenses show (about 30 fps), or a still of both eyes. +Zoom, pan, full screen, save as PNG. -1. **Steam Settings → System → Enable Developer Mode** (a toggle, no typing). -2. Scroll down to the **Developer** section and click **Set User Password**. - Type a password. **This is the only thing you type on the headset.** Pick - something short, because you'll type it once more on the Mac and then - never again. -3. (Optional, no typing) Note the IP address from **Quick Settings** or - **Steam Settings → Internet**, in case `frame.local` doesn't resolve. -4. (Optional) Check **Steam Settings → System → Hostname**. Leaving it as - `frame` means the scripts work without any extra setup. +
-On the Mac: +**🔋 Battery and status**
+Charge, charging watts and time left, storage, memory, temperature, Wi-Fi, and +what's running. -To use the scripts from a checkout instead of the app: +
-```sh -git clone https://github.com/saphid/steam-frame.git && cd steam-frame -./scripts/connect.sh # or: ./scripts/connect.sh 192.168.1.50 -ssh frame # passwordless from now on -``` +**🎮 Steam games**
+Everything you own with its Steam Frame rating. Install onto the headset with +live progress, and search the store. -`connect.sh` does four things: +
-- finds the headset (`frame.local`, then `frame`, or the IP/host you pass in) -- creates a dedicated key (`~/.ssh/id_ed25519_frame`) -- adds a `Host frame` block to `~/.ssh/config` -- runs `ssh-copy-id`, which asks for the Developer Mode password once +**🤖 Android apps**
+About 4,500 F-Droid apps rated for the Frame. One click installs each as its +own app in your Steam library. -Run `./scripts/connect.sh --harden` later if you want to turn off SSH password -logins. +
-Sources: [Valve: Setting up your Steam Frame for development](https://partner.steamgames.com/doc/steamhardware/steamframe/setup), -[Valve: Steam Frame Debugging](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging) -(both **confirmed on Steam Frame**, Valve official). +**📁 Files and clipboard**
+Drag files onto the window to send them. Send text or your clipboard straight +to the headset's desktop. -**Fallback, only if the Developer Mode toggle doesn't give you SSH.** From the -Mac, run `./scripts/serve-bootstrap.sh`. It prints a one-liner of about 30 -characters, like `curl -fsS mac.local:8765|bash`, to type into Konsole on the -Frame's Linux desktop. The script it serves installs your Mac's public key and -enables `sshd`. See [docs/ssh.md](docs/ssh.md#fallback-bootstrap-one-liner). +
-## Recommended options +**📸 Screenshots**
+Browse the shots you take in the headset and save them to your Pictures folder. -| Goal | Recommended | Confidence | +
+ +**🧩 Flatpaks and display**
+Install desktop apps like Moonlight or VLC, and set each Android app's +resolution and text size. + +
+ +**⚡ One-click tools**
+SSH, SFTP, Steam Link, remote desktop, volume, sleep, restart and shut down. + +
+ +Nothing is installed on the Frame for any of this: the app uses what SteamOS +already ships. [How each feature works](docs/frame-control.md). + +## Install + +| | Download | Needs | |---|---|---| -| Shell on the Frame | `ssh frame` (user `steamos`) | Confirmed (Valve docs) | -| **See/control the Frame from the Mac** | **Steam Link for macOS → connect to `frame`** (Valve names this). Alternatives: RDP to `xrdp` with Microsoft *Windows App* for the Linux desktop, or `adb`/`scrcpy` for the Android (Lepton) layer only | Steam Link and xrdp confirmed on Frame; the Mac RDP client is inferred | -| **Show the Mac's desktop inside the Frame** | **macOS Screen Sharing (built-in VNC) → Remmina (Flatpak, aarch64) on the Frame's Linux desktop**, installed over SSH | Inferred: each piece is documented, but the combination hasn't been tested on a Frame | -| File transfer | `scp` / `rsync` over the `frame` alias (`scripts/push.sh`) | **Verified** (rsync is on the image) | -| Paste Mac clipboard into the headset | `scripts/paste-to-frame.sh` (`pbpaste` → `ssh` → Klipper over D-Bus), or the clipboard sync in an RDP session | **Verified** (script); RDP untested | +| **macOS** (Apple Silicon) | [Frame-Control-mac-arm64.dmg](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-mac-arm64.dmg) | Python 3 (`xcode-select --install`) | +| **Windows** 10 / 11 (x64) | [Frame-Control-Setup-x64.exe](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-Setup-x64.exe) · [portable .zip](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-win-x64.zip) | Nothing extra: Python is bundled | +| **Linux** (x64) | [AppImage](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-x86_64.AppImage) · [.deb](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-amd64.deb) | `python3` and `ssh` (most desktops have both) | +| **Linux** (arm64) | [AppImage](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-arm64.AppImage) · [.deb](https://github.com/saphid/steam-frame/releases/latest/download/Frame-Control-linux-arm64.deb) | same | -Details: [docs/ssh.md](docs/ssh.md), [docs/streaming.md](docs/streaming.md), -[docs/file-transfer.md](docs/file-transfer.md), -[docs/open-questions.md](docs/open-questions.md). For how the Frame's software -fits together, see [docs/how-the-frame-works.md](docs/how-the-frame-works.md). +Optional: `adb` for Android apps +([macOS](https://formulae.brew.sh/formula/android-platform-tools) `brew install android-platform-tools` · +Windows `winget install Google.PlatformTools` · Linux `sudo apt install adb`). -## Windows anywhere in the room +
+macOS: the app isn't notarized -The in-headset Linux desktop is a single 1280×800 panel, and its windows can't -leave it. Each Steam app, though, gets its own SteamVR panel. That also works -for any Linux app tagged with an app id of its own: +There's no paid Apple developer account behind it, so macOS says the app is +damaged or can't be checked. Drag it to Applications, then clear the download +quarantine once: ```sh -./scripts/panel-on-frame.sh konsole -./scripts/panel-on-frame.sh mac-screen # the Mac's screen, in its own panel +xattr -dr com.apple.quarantine "/Applications/Frame Control.app" ``` -Then use the SteamVR dashboard's **Float in World**, **Move** and **Size** -controls to place each panel. See [docs/panels.md](docs/panels.md). +The first time, macOS also asks to allow local network access (for SSH) and +control of Terminal (for the password prompts). +
-## Frame Control (Mac app) +
+Windows: SmartScreen warning -As of 2026-09-25 no other Mac app manages the Frame end to end. -[Stream Frame](https://streamframe.app/) (macOS 14+, free) records and screenshots -the headset over SSH. [FrameDrop](https://framedropvr.com) sideloads but is -Windows-only. Steam Link views the headset. **Frame Control** is a Mac app over -the scripts below. Install it from the DMG (see [Mac app](#mac-app)), or run -the same UI in a browser without packaging: +The installer isn't code-signed, so Windows SmartScreen may say it protected +your PC. Choose **More info → Run anyway**. The portable `.zip` avoids the +installer: unzip it anywhere and run `Frame Control.exe`. +
+ +
+Linux: running the AppImage ```sh -./scripts/frame-ui.sh # opens http://127.0.0.1:47810 in its own window +chmod +x Frame-Control-linux-*.AppImage && ./Frame-Control-linux-*.AppImage ``` -![Frame Control](docs/img/frame-control.png) +If it complains about FUSE, install `libfuse2` (Ubuntu 24.04+: `libfuse2t64`), +or run it with `--appimage-extract-and-run`. Sending the clipboard needs +`wl-clipboard` (Wayland) or `xclip` (X11). +
-- **Headset view**: what the lenses show, as SteamVR composites it (the room, - floating panels, dashboard and controllers). Shows the left eye, like pointing - a camera into one lens, or both eyes, as a single shot; saves as PNG. **Live** - is 720p video at about 30 fps: `ffmpeg` on the Frame encodes SteamVR's - headset-view device (`/dev/video99`) to H.264 over SSH, and the page decodes - it with WebCodecs. Live video is one eye; Capture still gets both. The viewer fits the whole frame; zoom with − / + (or scroll, or - double-click), drag to pan, `0` to fit, `F` for full screen. Capture uses OpenVR's `IVRScreenshots` API through Python `ctypes` - (`ui/frame_vrshot.py`). Nothing extra is installed on the Frame (SteamOS ships `ffmpeg`). **Desktop panel** - captures gamescope's flat layer instead. -- Battery with charging state: charge rate in watts, time to full or empty, - charger type and wattage (for example USB-C PD 20 W), and battery temperature -- Storage, memory, temperature, Wi-Fi, uptime, and whether SteamVR, the desktop, - Lepton and xrdp are running -- Library shelf with Steam cover art and a Play button (`steam://rungameid`) -- **Get games**: every game you own with its Steam Frame rating (Verified, - Playable, Unsupported, Unknown). Install on Frame downloads it to the headset - with live progress. Search the Steam store with prices and Frame ratings; Buy - opens the store page in your browser, or Store on Frame opens it in the - headset. It drives the Frame's own Steam client through its DevTools port; - see `docs/steam-games.md` -- Volume and mute (`wpctl`) -- **Android apps**: search about 4,500 F-Droid apps rated for the Frame, install - one with a click as its own Lepton instance (it keeps its data and shows in the - Steam library), then launch, stop, test or remove it. **Report an APK** records whether any APK - worked (F-Droid or not: pick a file, type a package, or use an installed app). Your - reports are saved on your Mac and change the verdicts you see. They aren't - uploaded anywhere: the shared database is maintainer-only for now (see - `compat-db/README.md`) -- **Android display**: pick a running Lepton instance (by the app in it) and set - its resolution (Native 1920×1080, or Sharp 2560×1440 with density scaled to - match), UI scale (Smaller / Default / Larger, or an exact dpi) and text size - (0.85–1.3×) over ADB (`wm size`, `wm density`, `font_scale`). Reset puts all three - back. Whether the settings survive the app relaunching is untested -- Drag and drop files to `~/Downloads`; `.apk` files install as their own Android app -- Send typed text, or the Mac clipboard, to the Frame clipboard -- Install and remove Flatpaks (quick picks: Moonlight, Firefox, VLC, Remmina) -- One-click SSH or SFTP in Terminal, Steam Link, and Windows App (RDP). - Sleep, restart and shut down open Terminal because SteamOS asks for the - sudo password over SSH. +## Set up the headset -The server is Python stdlib only and listens on 127.0.0.1. It rejects requests -with a non-local `Host` header, and any `/api/` request without a custom -header, so other websites can't drive it or read captures. It keeps a single multiplexed SSH connection open, so -status and each capture take about 0.3s. Headset captures are deleted from the -Frame as soon as they're copied, because they show everything on screen, -including anything private. The look follows the Steam client: its palette, -Motiva Sans (loaded from Valve's CDN), portrait library capsules and green -Play buttons. **Verified on the Frame 2026-09-25:** status and charging details, -both capture modes (headset view while in use, and a blank frame in standby, -which the UI labels), clipboard, volume, file push, and input validation. **Not yet exercised from the UI:** Launch, Flatpak -install/remove, APK drop, and the power buttons. Each of these calls a -command or script that was verified separately. +You type one password on the headset, once. Everything else happens on your +computer. -### Mac app +1. **On the Frame:** Steam Settings → System → **Enable Developer Mode**, then + in the Developer section, **Set User Password**. Pick something short: + you'll type it once more on your computer and then never again. +2. **On your computer:** open Frame Control. It offers to **Set Up + Connection**, which finds the headset, creates an SSH key, and asks for that + password once in a terminal window. If it can't find the Frame, type the + IP address from the Frame's Quick Settings. +3. That's it. The app now reaches the headset whenever it's awake and on the + same network. For anywhere else, see [Tailscale](docs/tailscale.md). -`app/` wraps the same UI as a standalone Mac app (Electron). The app bundles -`ui/`, `scripts/`, `frame/android/` and the rated catalogue from `apk-catalog/`. -It starts `ui/server.py` on a free loopback port and shows it in its own window. -The server stops when you quit the app. A prebuilt DMG for Apple Silicon is -attached to each [GitHub release](https://github.com/saphid/steam-frame/releases). +**What it changes:** only what you click. Installs go to your user account on +the Frame (`--user` Flatpaks, Lepton instances, Steam downloads), and nothing +needs `sudo` except the power buttons. On your computer it adds a `Host frame` +entry to `~/.ssh/config` and a key at `~/.ssh/id_ed25519_frame`. -```sh -cd app -npm install -npm run dist # → app/dist/Frame Control--arm64.dmg (and a .zip) -npm start # run from the checkout without packaging -``` +## Feedback -Open the DMG and drag **Frame Control** to Applications. You need `python3` on -the Mac (Xcode Command Line Tools or Homebrew). The app reads `PATH` from your -login shell, so Homebrew's `rsync` and `adb` work when you launch it from -Finder. Each time it starts while there's no `frame` SSH alias, the app offers -to run `connect.sh` in Terminal. **Frame → Set Up Connection…** does the same -at any time. The Frame menu also shows the server log at -`~/Library/Logs/Frame Control/server.log`. Installing APKs needs `adb` -(`brew install android-platform-tools`). The F-Droid ratings are bundled with the app. +This is a first public test, so reports are really useful, especially from +Windows and Linux. Please [open an issue](https://github.com/saphid/steam-frame/issues/new) +with: -The build is ad-hoc signed and not notarized. A copy you build yourself opens -normally. A copy downloaded from GitHub Releases is quarantined; clear it with -`xattr -dr com.apple.quarantine "/Applications/Frame Control.app"`. The first -time you use them, macOS asks to allow local network access (for SSH) and -control of Terminal (for SSH and power actions). **Verified 2026-09-25:** -installed from the DMG, launched from Finder, connected to the Frame, and -showed live status and the library. +- what you tried and what happened +- your computer's OS and your SteamOS build (Steam Settings → System) +- the server log: **Frame → Show Server Log** in the app -## Scripts +## Going further -| Script | Runs on | Purpose | -|---|---|---| -| `scripts/tailscale-on-frame.sh` | Mac → Frame | Install Tailscale in `~` as a userspace user service so `frame` works from anywhere; `--uninstall` (**verified** on the LAN) | -| `scripts/connect.sh` | Mac | Discover, set up key and `~/.ssh/config`, copy key, optional `--harden` (**verified**; `--harden` untested) | -| `scripts/install-apps.sh` | Mac → Frame | Install Flatpaks (Remmina, Moonlight, …) on the Frame over SSH as `--user` (**verified** with Remmina) | -| `scripts/paste-to-frame.sh` | Mac → Frame | Send the Mac clipboard (or stdin) to the Frame clipboard (**verified**) | -| `scripts/install-apk.sh` | Mac → Frame | Install APKs, each as its own persistent Lepton instance with a Steam library shortcut (`--dev`: old ADB path into Lepton Development) (**verified**; see [docs/apks.md](docs/apks.md)) | -| `scripts/panel-on-frame.sh` | Mac → Frame | Start an app as its own floating VR panel, outside the desktop (**verified**: overlays created; in-headset placement not yet checked) | -| `scripts/run-on-frame.sh` | Mac → Frame | Start an app on the headset desktop, e.g. `mac-screen` opens Remmina straight into the Mac (**verified**) | -| `scripts/frame-ui.sh` | Mac | Start the Frame Control web UI (`ui/server.py`) and open it (**verified**) | -| `scripts/apk-catalog.sh` | Mac | Refresh the rated F-Droid catalogue that Frame Control's Android section shows (**verified**) | -| `scripts/compat-db-backup.sh` | Mac | Maintainer-only: back up the shared compatibility database locally and to Google Drive (**verified**) | -| `scripts/push-vr-video.sh` | Mac → Frame | Upload VR180/360 videos to `~/Videos/VR`, linked into DeoVR's Proton prefix; `--launch` starts DeoVR (**verified**: upload and link; in-headset playback of local files not yet checked). See [docs/vr-video.md](docs/vr-video.md) | -| `scripts/push.sh` | Mac → Frame | `rsync` files to `~/Downloads` (or a given path) on the Frame (**verified**) | -| `scripts/serve-bootstrap.sh` | Mac | Fallback: serve `bootstrap-on-frame.sh` with your public key embedded | -| `scripts/bootstrap-on-frame.sh` | Frame | Fallback: install the key and enable `sshd` | +This repo also holds the scripts behind the app and field notes on how the +Frame's software fits together, all checked against a real headset and labelled +**verified** or **inferred**. -## Security notes +| | | +|---|---| +| [Frame Control in detail](docs/frame-control.md) | Every feature, how it works, per-platform notes, building | +| [Scripts and headset setup](docs/scripts.md) | The command-line helpers, minimum typing, streaming options, floating panels | +| [How the Frame works](docs/how-the-frame-works.md) | SteamVR → gamescope → Plasma, verified facts, debugging | +| [Android apps (Lepton)](docs/apks.md) | Sideloading, the rated F-Droid catalogue, per-app instances | +| [Steam games](docs/steam-games.md) · [VR video](docs/vr-video.md) · [WebXR in Chromium](docs/webxr-chromium.md) | Installing and buying, watching VR180/360, the Chromium build | +| [SSH](docs/ssh.md) · [Streaming](docs/streaming.md) · [Files](docs/file-transfer.md) · [Panels](docs/panels.md) · [Tailscale](docs/tailscale.md) | Topic notes | +| [Open questions](docs/open-questions.md) | What's still unchecked | + +
+Security notes - With Developer Mode on, `sshd`, ADB and xrdp are all reachable on your LAN. Each running Lepton (Android) instance opens its own ADB port in 5555–5599, @@ -256,7 +199,7 @@ showed live status and the library. networks only, and turn Developer Mode off when you don't need it. - Frame Control reaches ADB and the Steam client's DevTools port (Frame loopback `127.0.0.1:8080`) only through SSH tunnels. The compatibility - database key lives in the macOS Keychain and is never written to the repo. + database key (maintainer-only) is never written to the repo. - `steamos` has `sudo`, protected by the same Developer Mode password. Once you've switched to key auth, a short password still protects `sudo` and RDP, so pick one that isn't trivially guessable. @@ -264,19 +207,18 @@ showed live status and the library. use Tailscale: `scripts/tailscale-on-frame.sh` (no sudo). In its userspace mode **every** Frame port is reachable from your tailnet, including Steam's DevTools on loopback 8080; see [docs/tailscale.md](docs/tailscale.md). +
## Development ```sh -python3 -m unittest discover -s tests # server guards, validation, Steam helpers; no headset needed -cd app && npm install && npm run dist # build the DMG +python3 -m unittest discover -s tests # server tests; no headset needed +cd app && npm install && npm start # run the app from the checkout ``` -GitHub Actions runs the tests on Python 3.9, which is the oldest `python3` the app -may find (Xcode Command Line Tools), plus syntax checks for every script and the -Electron main process (`.github/workflows/checks.yml`). Anything that touches the -headset is verified by hand against a real Frame, and the docs label it -**verified** or **inferred**. +The server is Python stdlib only; the app is Electron. GitHub Actions runs the +tests on macOS, Windows and Linux, and a `v*` tag builds all three installers +into the release. See [building](docs/frame-control.md#building). ## License diff --git a/app/package.json b/app/package.json index ee78fbd..930c114 100644 --- a/app/package.json +++ b/app/package.json @@ -75,7 +75,8 @@ "extendInfo": { "NSAppleEventsUsageDescription": "Frame Control opens Terminal for SSH sessions and for power actions that need the Developer Mode password.", "NSLocalNetworkUsageDescription": "Frame Control connects to your Steam Frame over SSH on the local network." - } + }, + "artifactName": "Frame-Control-mac-${arch}.${ext}" }, "dmg": { "title": "Frame Control ${version}" @@ -94,7 +95,7 @@ "icon": "build/icon.png", "executableName": "frame-control", "synopsis": "Manage a Valve Steam Frame over SSH", - "artifactName": "Frame-Control-${version}-${arch}.${ext}", + "artifactName": "Frame-Control-linux-${arch}.${ext}", "desktop": { "entry": { "StartupWMClass": "frame-control" @@ -113,7 +114,7 @@ "zip" ], "icon": "build/icon.png", - "artifactName": "Frame-Control-${version}-win-${arch}.${ext}", + "artifactName": "Frame-Control-win-${arch}.${ext}", "extraResources": [ { "from": "build/python-win", @@ -128,7 +129,7 @@ "oneClick": false, "perMachine": false, "allowToChangeInstallationDirectory": true, - "artifactName": "Frame-Control-Setup-${version}.${ext}" + "artifactName": "Frame-Control-Setup-${arch}.${ext}" } }, "homepage": "https://github.com/saphid/steam-frame", diff --git a/docs/frame-control.md b/docs/frame-control.md new file mode 100644 index 0000000..e3df8ee --- /dev/null +++ b/docs/frame-control.md @@ -0,0 +1,133 @@ +# Frame Control in detail + +What each part of the app does, how it works, and what has been checked on a +real Frame. For installing it, see the [README](../README.md#install). + +As of 2026-09-25 no other desktop app manages the Frame end to end. +[Stream Frame](https://streamframe.app/) (macOS 14+, free) records and screenshots +the headset over SSH. [FrameDrop](https://framedropvr.com) sideloads but is +Windows-only. Steam Link views the headset. + +You can also run the same UI in a browser without the app, from a checkout on +macOS or Linux: + +```sh +./scripts/frame-ui.sh # opens http://127.0.0.1:47810 in its own window +``` + +## Features + +- **Headset view**: what the lenses show, as SteamVR composites it (the room, + floating panels, dashboard and controllers). Shows the left eye, like pointing + a camera into one lens, or both eyes, as a single shot; saves as PNG. **Live** + is 720p video at about 30 fps: `ffmpeg` on the Frame encodes SteamVR's + headset-view device (`/dev/video99`) to H.264 over SSH, and the page decodes + it with WebCodecs. Live video is one eye; Capture still gets both. The viewer + fits the whole frame; zoom with − / + (or scroll, or double-click), drag to + pan, `0` to fit, `F` for full screen. Capture uses OpenVR's `IVRScreenshots` + API through Python `ctypes` (`ui/frame_vrshot.py`). Nothing extra is + installed on the Frame (SteamOS ships `ffmpeg`). **Desktop panel** captures + gamescope's flat layer instead. +- **Screenshots** you take in the headset with Steam's shortcut: browse them and + save them to `~/Pictures/SteamFrame`. +- **Battery** with charging state: charge rate in watts, time to full or empty, + charger type and wattage (for example USB-C PD 20 W), and battery temperature. +- **Status**: storage, memory, temperature, Wi-Fi, uptime, and whether SteamVR, + the desktop, Lepton and xrdp are running. +- **Library** shelf with Steam cover art and a Play button (`steam://rungameid`). +- **Get games**: every game you own with its Steam Frame rating (Verified, + Playable, Unsupported, Unknown). Install on Frame downloads it to the headset + with live progress. Search the Steam store with prices and Frame ratings; Buy + opens the store page in your browser, or Store on Frame opens it in the + headset. It drives the Frame's own Steam client through its DevTools port; + see [steam-games.md](steam-games.md). +- **Volume** and mute (`wpctl`). +- **Android apps**: search about 4,500 F-Droid apps rated for the Frame, install + one with a click as its own Lepton instance (it keeps its data and shows in the + Steam library), then launch, stop, test or remove it. **Report an APK** records + whether any APK worked (F-Droid or not: pick a file, type a package, or use an + installed app). Your reports are saved on your computer and change the verdicts + you see. They aren't uploaded anywhere: the shared database is maintainer-only + for now (see [compat-db/README.md](../compat-db/README.md)). Needs `adb`, and + `aapt2` for reading APK files. +- **Android display**: pick a running Lepton instance (by the app in it) and set + its resolution (Native 1920×1080, or Sharp 2560×1440 with density scaled to + match), UI scale (Smaller / Default / Larger, or an exact dpi) and text size + (0.85–1.3×) over ADB (`wm size`, `wm density`, `font_scale`). Reset puts all + three back. Whether the settings survive the app relaunching is untested. +- **Transfer**: drag and drop files to `~/Downloads`; `.apk` files install as + their own Android app. Send typed text, or your computer's clipboard, to the + Frame clipboard. +- **Flatpaks**: install and remove them (quick picks: Moonlight, Firefox, VLC, + Remmina). +- **One-click tools**: SSH or SFTP in a terminal window, Steam Link, and remote + desktop (Windows App on macOS, Remote Desktop on Windows, Remmina or FreeRDP on + Linux). Sleep, restart and shut down open a terminal window because SteamOS + asks for the sudo password over SSH. + +## How it works + +`app/` is an Electron shell. It starts `ui/server.py` on a free loopback port +and shows it in its own window; the server stops when you quit the app. The +app bundles `ui/`, `scripts/`, `frame/android/` and the rated catalogue from +`apk-catalog/`, and on Windows an embedded Python too. + +The server is Python stdlib only and listens on 127.0.0.1. It rejects requests +with a non-local `Host` header, and any `/api/` request without a custom +header, so other websites can't drive it or read captures. Everything reaches +the headset through the `frame` SSH alias. On macOS and Linux it keeps one +multiplexed SSH connection open, so status and each capture take about 0.3 s. +Windows' OpenSSH can't share a connection, so there each request connects on +its own and the app is a little slower. What differs between the three +systems lives in `ui/frame_host.py`. + +Headset captures are deleted from the Frame as soon as they're copied, because +they show everything on screen, including anything private. The look follows +the Steam client: its palette, Motiva Sans (loaded from Valve's CDN), portrait +library capsules and green Play buttons. + +**Verified on the Frame 2026-09-25 (macOS app):** status and charging details, +both capture modes (headset view while in use, and a blank frame in standby, +which the UI labels), clipboard, volume, file push, and input validation. +**Not yet exercised from the UI:** Launch, Flatpak install/remove, APK drop, +and the power buttons. Each of these calls a command that was verified +separately. + +## Per-platform notes + +**macOS.** The app reads `PATH` from your login shell, so Homebrew's `rsync`, +`adb` and Python work when you launch it from Finder. Set Up Connection runs +`scripts/connect.sh` in Terminal. The log is at +`~/Library/Logs/Frame Control/server.log`. The build is ad-hoc signed and not +notarized: a downloaded copy is quarantined until you run +`xattr -dr com.apple.quarantine "/Applications/Frame Control.app"`. The first +time you use them, macOS asks to allow local network access (for SSH) and +control of Terminal (for SSH and power actions). + +**Windows.** Python is bundled; `ssh` is Windows' built-in OpenSSH client +(Settings → System → Optional features, if it's been removed). Set Up +Connection runs `ui/frame_connect.py` in a console window. Copies use `scp` +because Windows has no `rsync`. The installer isn't code-signed, so SmartScreen +warns on first run: choose **More info → Run anyway**. The log is at +`%APPDATA%\Frame Control\logs\server.log`. + +**Linux.** Needs `python3` (3.8 or later) and `ssh`, which most desktops +have. The AppImage runs anywhere; the `.deb` pulls both in on Debian and +Ubuntu. Set Up Connection runs `ui/frame_connect.py` in your terminal emulator +(GNOME Terminal, Konsole, xterm and others). Sending the clipboard needs +`wl-clipboard` (Wayland) or `xclip` (X11). The log is at +`~/.config/Frame Control/logs/server.log`. + +## Building + +```sh +cd app +npm install +npm start # run from the checkout without packaging +npm run dist # macOS: dist/*.dmg and .zip (Apple Silicon) +npm run dist:win # Windows: installer and .zip (fetches the embedded Python first) +npm run dist:linux # Linux: AppImage and .deb, x64 and arm64 +``` + +Pushing a `v*` tag builds all three in GitHub Actions and attaches them to the +release (`.github/workflows/release.yml`). diff --git a/docs/img/icon.png b/docs/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b4e5a57a580e65ad048a8b4552e4eeacd43c3425 GIT binary patch literal 38506 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≺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 L^da(r