Compare commits

...
6 Commits
Author SHA1 Message Date
spoopyghosty0 c53d754007 FramePort 0.6.2 2026-10-03 19:29:24 -04:00
spoopyghosty0 a1662da17d Connecting a new Frame (Reddit feedback): a PC without SSH keys made paramiko end the key attempt with 'No authentication methods available' (SSHException), which stopped the loop before the password was tried and showed that raw message; now it's a failed login like any other, and the final error (FrameNotPaired) says to run the first-time setup or that the password was refused. Wizard: first-time setup is step 1, a 'not set up yet' callout with the setup button after such a failure. Failed installs/tests offer Report a problem. README/GAMES/CONTRIBUTING: share-a-config and report-a-problem links 2026-10-03 19:02:31 -04:00
spoopyghosty0 0141bad259 Desktop on the Steam Frame: SteamVR dashboard → Launch a program → Desktop, then System → Konsole (the Steam Deck's Power → Switch to Desktop doesn't apply) — README, INSTALL, setup wizard, CLI, bootstrap 2026-10-03 18:12:12 -04:00
spoopyghosty0 6ed95684dc FramePort 0.6.1: Linux ARM64 build (CI ubuntu-22.04-arm, PyInstaller one-folder fallback; the updater picks FramePort-linux-arm64.tar.gz on ARM); tested games list docs/GAMES.md generated from the catalog (scripts/compat_list.py) 2026-10-03 12:05:59 -04:00
spoopyghosty0 a6ca8385a9 COMPATIBILITY: no game counts (they change often); point to the catalog recipes 2026-10-03 11:38:56 -04:00
spoopyghosty0 210410f125 Catalog: Stremio VR works (confirmed by the owner with frame.unity_text_input + device.text_input_window); compatibility count 28/5/6 2026-10-03 11:37:59 -04:00
22 changed files with 296 additions and 44 deletions

No files matched your search

+9 -5
View File
@@ -40,6 +40,8 @@ jobs:
archive: FramePort-macos-arm64.zip
- os: ubuntu-22.04 # oldest supported: the bundle needs the build host's GLib/glibc or newer
archive: FramePort-linux-x64.tar.gz
- os: ubuntu-22.04-arm # Linux ARM64 (same base as x64)
archive: FramePort-linux-arm64.tar.gz
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
@@ -50,9 +52,9 @@ jobs:
- run: uv sync --extra dev
- id: flet
run: uv run python scripts/package.py
continue-on-error: ${{ runner.os == 'macOS' }}
- name: macOS fallback (PyInstaller bundle)
if: runner.os == 'macOS' && steps.flet.outcome == 'failure'
continue-on-error: ${{ runner.os == 'macOS' || runner.arch == 'ARM64' }}
- name: macOS / Linux ARM64 fallback (PyInstaller bundle)
if: (runner.os == 'macOS' || runner.arch == 'ARM64') && steps.flet.outcome == 'failure'
run: uv run --with pyinstaller python scripts/package.py --pyinstaller
# ---- signing: self-signed certificate on Windows, ad-hoc on macOS ----
@@ -95,7 +97,9 @@ jobs:
run: ditto -c -k --keepParent "$(find dist -maxdepth 3 -name '*.app' -type d | head -1)" ${{ matrix.archive }}
- name: Package (Linux)
if: runner.os == 'Linux'
run: tar czf ${{ matrix.archive }} -C dist --transform 's,^linux,FramePort,' linux
run: | # flet build -> dist/linux; the PyInstaller fallback -> dist/FramePort (one folder)
if [ -d dist/linux ]; then tar czf ${{ matrix.archive }} -C dist --transform 's,^linux,FramePort,' linux
else tar czf ${{ matrix.archive }} -C dist FramePort; fi
# self-update with this archive: unpack, verify the layout (+ the signer on Windows), run the real swap script
- name: Update smoke test
run: uv run python scripts/update_smoke.py ${{ matrix.archive }}
@@ -137,7 +141,7 @@ jobs:
git fetch --force origin "refs/tags/${tag}:refs/tags/${tag}"
whatsnew=$(git tag -l --format='%(contents:body)' "$tag")
[ -n "$whatsnew" ] || whatsnew=$(git tag -l --format='%(contents:subject)' "$tag")
{ echo "FramePort ${tag}: Windows (x64), macOS (Apple Silicon) and Linux (x64)."; echo;
{ echo "FramePort ${tag}: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64)."; echo;
if [ -n "$whatsnew" ] && [ "$(git cat-file -t "$tag")" = tag ]; then
echo "## What's new"; echo; echo "$whatsnew"; echo; fi
echo "Already have FramePort? It offers this update itself (Library → **Update now**)."
+8
View File
@@ -23,6 +23,14 @@ uv run ruff check . # lint
row and a unit test.
- Native binaries in `artifacts/` are rebuilt with `python native/build.py` (downloads the NDK).
## Adding a tested game
Tested a game on the Steam Frame? The easiest way is in FramePort: open the game → **…** → **Share working config…**.
It opens a prefilled GitHub issue with the exact recipe; after review a workflow turns it into
`catalog/games/<package>.yaml`. Without the app: [working config form](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml). Something doesn't run: the game's
**Report a problem…** (diagnostics zip, personal data removed) or the [problem report form](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml).
After catalog changes, regenerate the games list: `python scripts/compat_list.py`.
## Translating FramePort
The app's texts (screens, help, patch and setting descriptions) can be translated; the command line, logs and
diagnostics stay English. No programming needed:
+11 -3
View File
@@ -36,11 +36,12 @@ Install games that target the Meta Quest, Android, or general PCVR onto your **V
2. **Connect the Frame** (once):
1. In FramePort click **Steam Frame → Show setup command**. Keep FramePort open; the Frame and your computer must
be on the same Wi-Fi.
2. On the Frame press the **Steam button → Power → Switch to Desktop**.
3. Open the app menu (bottom-left corner), search for **Konsole** and open it.
2. On the Frame open the **SteamVR dashboard → Launch a program → Desktop**: the Linux desktop opens on a virtual
screen.
3. Open the app menu (bottom-left corner of that desktop) → **System → Konsole** (or search for Konsole).
4. Type the command FramePort shows exactly as shown (on-screen keyboard or any USB/Bluetooth keyboard) and press
**Enter**. This will run the following [bash setup script](bootstrap/bootstrap.sh).
5. After a few seconds the desktop closes by itself and the Frame returns to its normal view; that's expected. If
5. After a few seconds the desktop closes by itself (Steam restarts once); that's expected. If
Steam asks to install **Lepton**, confirm it. FramePort shows the Frame as connected within a minute. No
password needed.
3. **Add games → Scan a folder** with your game backups (APK + OBB, or PC VR game folders).
@@ -66,6 +67,12 @@ fix, so Steam's on-screen keyboard and Type on Frame work in them too.
If a game has already been tested with FramePort, it will automatically use the optimal game config. Otherwise, FramePort
will attempt to guess key patches. If you find a new config that works for an app you are testing, please consider submitting it to the community!
**[List of tested games](docs/GAMES.md)**
**Tested something? Share it.** In FramePort open the game → **…** → **Share working config…** (it fills in the
recipe for you) or **Report a problem…** (attaches a diagnostics zip with personal data removed). Without the app:
[share a working config](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml) · [report a problem](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml). Shared configs become built-in recipes for everyone.
## Built on
[OVRPort](https://github.com/Android-XR-Bridge/OVRPort) (Quest → OpenXR, originally
@@ -81,6 +88,7 @@ What FramePort adds itself: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
| [INSTALL.md](docs/INSTALL.md) | Install, connect, update, PC VR, files, problem reports |
| [FRAME_SETUP.md](docs/FRAME_SETUP.md) | What setup changes on the Frame, networks and firewalls, undoing it |
| [COMPATIBILITY.md](docs/COMPATIBILITY.md) | What runs and how well |
| [GAMES.md](docs/GAMES.md) | Tested games and how well they run |
| [PLAYBOOK.md](docs/PLAYBOOK.md) | Symptoms and fixes per game |
| [FRAME_RUNTIME.md](docs/FRAME_RUNTIME.md) | Steam Frame runtime facts |
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | How the code is organised |
+3 -2
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
# FramePort one-time Steam Frame setup. Run in the Frame's Desktop Mode terminal (Konsole):
# FramePort one-time Steam Frame setup. Run in the Frame's desktop terminal (SteamVR dashboard -> Launch a program
# -> Desktop, then System -> Konsole):
# curl -fsS <pc-ip>:<port>/<code> | bash
# (the FramePort app shows the exact line and serves this script while its Setup page is open).
#
@@ -128,6 +129,6 @@ if [[ ! -x "$DEVKIT_HELPER" || ! -f "$STEAM_CONFIG" ]] || \
echo "Couldn't turn it on automatically. Turn it on in Settings > System > Developer, then run this command again."
exit 1
fi
echo "Steam restarts to turn it on. That closes Desktop Mode in a few seconds and the Frame returns to its"
echo "Steam restarts to turn it on. That closes the desktop in a few seconds and the Frame returns to its"
echo "normal view; setup finishes on its own. If Steam asks to install Lepton, confirm it."
echo "Then return to FramePort on your PC: the Frame appears as connected within a minute."
+4 -3
View File
@@ -1,11 +1,12 @@
package: com.stremio.vrone
title: Stremio
status: issues
status: works
notes: 'Unity 6 VR media centre (its interface is web pages in Vuplex 3D WebView). Text fields closed at once on the
Frame (no system keyboard): frame.unity_text_input keeps them selected and device.text_input_window shows the
Android window so Steam''s on-screen keyboard (or Type on Frame from the PC) types into them; confirmed in the
headset by the owner (2026-10-03). Known issue: the add-ons page stays blank in VR (the add-on sync itself
succeeds). "Play in an external player" can''t reach another app: Lepton sends every link to Steam''s browser.'
headset by the owner (2026-10-03), who confirmed it working. Note: the add-ons page can stay blank in VR (the
add-on sync itself succeeds). "Play in an external player" can''t reach another app: Lepton sends every link to
Steam''s browser.'
details: 'Patches: frame.unity_text_input (TMP_InputField.TouchScreenKeyboardShouldBeUsed -> false,
isKeyboardUsingEvents -> true; uGUI InputField too), device.text_input_window (lepton-show-flatscreen).'
tested_version: 0.7.5
+2 -1
View File
@@ -1,6 +1,7 @@
# Compatibility
The built-in catalog has tested settings for 39 games (27 work, 6 work with known issues, 6 can't run on the Frame).
The built-in catalog has tested settings for games, each marked as working, working with known issues, or not
running on the Frame: see the [list of tested games](GAMES.md) (recipes in [catalog/games](../catalog/games)).
Other games get suggested patches from detection rules; each suggestion states its reason, and every patch can be
switched on or off under **Customize**: described in plain words, with **Show technical details** for the exact
effect of each patch.
+47
View File
@@ -0,0 +1,47 @@
# Tested games
Games tested on the Steam Frame with FramePort's recipes. Games not listed here may work too: FramePort suggests patches for them, and a working config can be shared from the app (**Share working config…**).
Tested a game? [Share a working config](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml) or [report a problem](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml) (in the app: the game's **…** menu does both and fills in the details).
Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.
| Game | Platform | Status | Notes |
|---|---|---|---|
| 4XVR Video Player | Quest | ✅ Works | |
| Asgard's Wrath 2 | Quest | ✅ Works | |
| BAM | Quest | ✅ Works | |
| BARTENDER VR SIMULATOR | Quest | ✅ Works | |
| Batman: Arkham Shadow | Quest | ✅ Works | |
| Carve Snowboarding | Quest | ✅ Works | |
| Demeter | Quest | ✅ Works | |
| Espire 2 | Quest | ✅ Works | |
| Genotype | Quest | ✅ Works | |
| H.U.N.T | Quest | ✅ Works | |
| In Death: Unchained | Quest | ✅ Works | |
| LEGO® Bricktales | Quest | ✅ Works | |
| Marvel's Deadpool VR | Quest | ✅ Works | |
| Medieval Dynasty New Settlement | Quest | ✅ Works | |
| Mobile Suit Gundam: Silver Phantom | Quest | ✅ Works | |
| Nano | Quest | ✅ Works | |
| NEX Player | Quest | ✅ Works | |
| NOPE CHALLENGE | Quest | ✅ Works | |
| Path of the Warrior | Quest | ✅ Works | |
| PowerWash Simulator VR | Quest | ✅ Works | |
| Rick and Morty: Virtual Rick-ality | PC VR | ✅ Works | |
| Robo Recall | Quest | ✅ Works | |
| Sniper Elite VR: Winter Warrior | Quest | ✅ Works | |
| Stremio | Quest | ✅ Works | |
| The Climb 2 | Quest | ✅ Works | |
| Toy Master | Quest | ✅ Works | |
| Under Cover | Quest | ✅ Works | |
| Wallace & Gromit in The Grand Getaway | Quest | ✅ Works | |
| Arcsmith | Quest | ⚠️ Works with issues | Right eye distorts during movement (unresolved; swap, tracking, Valve layers, depth and pacing ruled out). |
| Assassin's Creed Nexus | Quest | ⚠️ Works with issues | Some launch warning text is still upside down; the rest of the UI is fixed by flip emulation. |
| Phantom: Covert Ops | Quest | ⚠️ Works with issues | DLC/store button crashes (no Meta store). |
| Silhouette | Quest | ⚠️ Works with issues | Hand-tracking game; the Frame synthesizes hands from controllers, so it is janky. |
| Time Stall | Quest | ⚠️ Works with issues | Both eyes distort during movement (unresolved). |
| Espire 1: VR Operative (Quest Edition) | Quest | ❌ Doesn't run | Mesa GL driver crash during texture upload. |
| HITMAN 3 VR: Reloaded | Quest | ❌ Doesn't run | Vulkan driver crash (freedreno), even without Valve layers. |
| Journey of the Gods | Quest | ❌ Doesn't run | 32-bit only; the Frame has no AArch32. |
| Shadow Point | Quest | ❌ Doesn't run | 32-bit only; the Frame has no AArch32. |
| Sniper Elite VR | Quest | ❌ Doesn't run | GPU hang (zink: DEVICE LOST) even with MSAA off. |
| Sports Scramble (Santa Cruz) | Quest | ❌ Doesn't run | 32-bit only; the Frame has no AArch32. |
+5 -4
View File
@@ -9,6 +9,7 @@ runtime, the OVRPort CLI and apksigner into its data folder (Settings → Tools
| Windows 10/11 (x64) | `FramePort-windows-x64.zip` | `FramePort.exe` |
| macOS (Apple Silicon) | `FramePort-macos-arm64.zip` | `FramePort.app` |
| Linux (x64, GTK 3; Ubuntu 22.04 or newer) | `FramePort-linux-x64.tar.gz` | `FramePort/FramePort` |
| Linux (ARM64, GTK 3; Ubuntu 22.04 or newer) | `FramePort-linux-arm64.tar.gz` | `FramePort/FramePort` |
| Command line only (Python 3.11+) | `frameport-<version>-py3-none-any.whl` | `frameport --help` |
The command-line version installs from the wheel's release link with `uv tool install <link>` (or pipx / pip).
@@ -22,7 +23,7 @@ start shows a warning:
`FramePort-selfsigned.cer` (attached to each release) into *Trusted Root Certification Authorities* (Current User) to
show FramePort as the publisher; the certificate can only sign code. Remove it with `certmgr.msc`.
- **macOS:** right-click `FramePort.app` → **Open** → **Open** (once), or `xattr -dr com.apple.quarantine FramePort.app`.
- **Linux:** `tar xzf FramePort-linux-x64.tar.gz && ./FramePort/FramePort`.
- **Linux:** `tar xzf FramePort-linux-x64.tar.gz && ./FramePort/FramePort` (ARM64: `FramePort-linux-arm64.tar.gz`).
## Connecting the Steam Frame
@@ -30,12 +31,12 @@ The Frame and the computer must be on the same network.
1. In FramePort open **Steam Frame** and click **Show setup command**.
2. First time only, on the Frame:
1. Press the **Steam button → Power → Switch to Desktop**.
2. Open the app menu (bottom-left corner), search for **Konsole** and open it.
1. Open the **SteamVR dashboard → Launch a program → Desktop**: the Linux desktop opens on a virtual screen.
2. Open the app menu (bottom-left corner of that desktop) → **System → Konsole** (or search for Konsole).
3. Type the command FramePort shows exactly as shown (on-screen keyboard or any USB/Bluetooth keyboard) and press
**Enter**. It looks like `curl -fsS 192.168.1.20:8765/1a2b3c4d | bash`: your computer's address, then a
one-time code.
4. After a few seconds the desktop closes by itself and the Frame returns to its normal view; that's expected. If
4. After a few seconds the desktop closes by itself (Steam restarts once); that's expected. If
Steam asks to install **Lepton** (Valve's Android runtime), confirm it.
FramePort connects by itself within a minute. No password is needed. The command lets FramePort in and turns on
+1 -1
View File
@@ -2,7 +2,7 @@
- **Windows:** unzip `FramePort-windows-x64.zip`, run `FramePort.exe` (warning → **More info → Run anyway**).
- **macOS:** unzip `FramePort-macos-arm64.zip`, right-click `FramePort.app` → **Open**.
- **Linux:** `tar xzf FramePort-linux-x64.tar.gz && ./FramePort/FramePort`.
- **Linux:** `tar xzf FramePort-linux-x64.tar.gz && ./FramePort/FramePort` (ARM64: `FramePort-linux-arm64.tar.gz`).
Full guide: [docs/INSTALL.md](https://github.com/spoopyghosty0/frameport/blob/main/docs/INSTALL.md) · checksums in
`SHA256SUMS.txt`.
+46
View File
@@ -0,0 +1,46 @@
"""Write docs/GAMES.md: the tested games from the bundled catalog as a simple table to share.
Run after catalog changes: python scripts/compat_list.py"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from frameport.recommend import catalog # noqa: E402
STATUS = {"works": "✅ Works", "issues": "⚠️ Works with issues", "unsupported": "❌ Doesn't run"}
ORDER = {"works": 0, "issues": 1, "unsupported": 2}
ISSUES = "https://github.com/spoopyghosty0/frameport/issues/new?template="
SHARE, REPORT = ISSUES + "working-config.yml", ISSUES + "bug-report.yml"
def short(note: str) -> str:
"""The note's first sentence, without technical asides."""
first = re.split(r"(?<=[.!?])\s", " ".join((note or "").split()), maxsplit=1)[0]
return first if len(first) <= 160 else first[:157].rstrip() + "…"
def render() -> str:
entries = [e for e in catalog.load().values() if e.origin == "bundled" and e.status in STATUS]
entries.sort(key=lambda e: (ORDER[e.status], e.title.lower()))
rows = ["| Game | Platform | Status | Notes |", "|---|---|---|---|"]
for e in entries:
platform = "PC VR" if e.kind == "rift" else "Quest"
note = "" if e.status == "works" else short(e.notes).replace("|", "/")
rows.append(f"| {e.title} | {platform} | {STATUS[e.status]} | {note} |")
return ("# Tested games\n\n"
"Games tested on the Steam Frame with FramePort's recipes. Games not listed here may work too: FramePort "
"suggests patches for them, and a working config can be shared from the app (**Share working config…**).\n"
f"Tested a game? [Share a working config]({SHARE}) or [report a problem]({REPORT}) (in the app: the game's "
"**…** menu does both and fills in the details).\n"
"Generated from [catalog/games](../catalog/games) by `scripts/compat_list.py`.\n\n"
+ "\n".join(rows) + "\n")
if __name__ == "__main__":
out = ROOT / "docs" / "GAMES.md"
out.write_text(render(), encoding="utf-8")
print(f"wrote {out}")
+2
View File
@@ -70,6 +70,8 @@ def main() -> int:
# translations (data files PyInstaller doesn't pick up from the imports)
"--add-data", f"{ROOT / 'src/frameport/locales'}{sep}frameport/locales",
"--distpath", str(ROOT / "dist"), "--yes"]
if TARGET == "linux":
cmd.append("--onedir") # a folder: the bundle check sees the agent source, the archive the app
else:
# --yes: install the Flutter SDK etc. without prompting; --no-rich-output: plain logs (CI, Windows consoles)
cmd = ["flet", "build", TARGET, str(ROOT), "--project", "FramePort", "--product", "FramePort",
+1 -1
View File
@@ -1,2 +1,2 @@
# The FramePort version: the single source (pyproject reads it via hatch; CI checks a release tag matches it).
__version__ = "0.6.0"
__version__ = "0.6.2"
+2 -1
View File
@@ -405,7 +405,8 @@ def frame_pair(timeout: float = typer.Option(600, help="seconds to wait for the
from .frame.pairing import PairingServer
srv = PairingServer().start()
typer.echo("On the Frame, open Desktop Mode → Konsole and run:\n\n " + srv.one_liner + "\n")
typer.echo("On the Frame, open the SteamVR dashboard → Launch a program → Desktop, then System → Konsole, and "
"run:\n\n " + srv.one_liner + "\n")
end = time.time() + timeout
while time.time() < end and not srv.paired:
time.sleep(1)
+6 -2
View File
@@ -46,8 +46,12 @@ def explain(exc: BaseException) -> str:
low = msg.lower()
if paramiko and isinstance(exc, paramiko.ChannelException):
return tr("The connection to the Frame is busy. Try again in a moment.")
if "authentication failed" in low:
return msg # already says what to do (run the bootstrap / enter the password)
if "authentication failed" in low or "no authentication methods available" in low:
if "password was refused" in low:
return tr("The Frame refused that password. Check it, or run the first-time setup instead: Steam Frame → "
"Show setup command.")
return tr("This Frame isn't set up for FramePort yet. Run the first-time setup: Steam Frame → Show setup "
"command.")
if is_connection_error(exc):
return tr("Can't reach your Frame. Make sure it's on, awake and on the same network, then try again.")
if isinstance(exc, OSError) and exc.errno == errno.ENOSPC:
+26 -2
View File
@@ -24,6 +24,25 @@ from ..core.paths import agent_file, ssh_dir, user_data_dir, write_atomic
REMOTE_AGENT_DIR = ".local/share/frameport/agent"
class FrameNotPaired(ConnectionError):
"""SSH works but none of FramePort's ways in was accepted: the Frame hasn't run the first-time setup (or the
password was wrong). The message keeps "authentication failed" for older callers."""
def __init__(self, tried_password: bool, details: list[str]):
self.tried_password = tried_password
self.details = details
reason = ("the password was refused" if tried_password
else "this Frame doesn't know FramePort yet")
super().__init__(f"SSH authentication failed: {reason}. Run the first-time setup on the Frame (Steam Frame → "
"Show setup command)." + (f" ({'; '.join(details)})" if details else ""))
def _no_auth_methods(exc: BaseException) -> bool:
"""paramiko ends a key/agent attempt that had nothing to offer (no SSH keys on this PC, no agent) with a plain
SSHException, not an AuthenticationException: it's a failed login all the same, not a network problem."""
return isinstance(exc, paramiko.SSHException) and "no authentication methods available" in str(exc).lower()
def app_key() -> paramiko.Ed25519Key:
path = ssh_dir() / "id_ed25519"
if not path.exists():
@@ -283,12 +302,17 @@ class Frame:
except paramiko.AuthenticationException as exc:
client.close()
errors.append(f"{attempt}: {exc}")
except paramiko.SSHException as exc:
client.close()
if not _no_auth_methods(exc):
raise
# (Reddit, 2026-10-03: this ended the loop before the password was tried)
errors.append(f"{attempt}: {exc}")
except BaseException:
client.close()
raise
else:
raise ConnectionError("SSH authentication failed (" + "; ".join(errors) + "). Run the FramePort "
"bootstrap on the Frame or enter the steamos password.")
raise FrameNotPaired(bool(self.password), errors)
client.save_host_keys(str(known))
transport = client.get_transport()
transport.set_keepalive(15)
+9 -6
View File
@@ -97,6 +97,7 @@
"Allows very large pictures (8K video, theatres) instead of quitting.": "",
"Already patched: don't change the game's files": "",
"Already patched: install as is (skip patching)": "",
"Already set up: on your network": "",
"Also in your library: {value} version": "",
"Also remove FramePort's games and files from the Frame": "",
"An ordinary Android app or game (no VR). It's installed unchanged and runs in Lepton, Valve's Android container.": "",
@@ -403,7 +404,7 @@
"It has a known blocker on the Steam Frame.": "",
"It has issues: share…": "",
"it stopped at '{value}'": "",
"It trusts this app, turns on Developer Mode and installs Lepton if needed. Turning on Developer Mode closes Desktop Mode; the setup finishes on its own and FramePort connects by itself. You only do this once.": "",
"It trusts this app, turns on Developer Mode and installs Lepton if needed. Turning on Developer Mode closes the desktop; the setup finishes on its own and FramePort connects by itself. You only do this once.": "",
"It was removed from the library.": "",
"It works: share…": "",
"Java runtime": "",
@@ -487,13 +488,13 @@
"Needs Windows (or WSL on Windows)": "",
"Neutralises Meta XR Audio's Quest-only calls (Unity/Wwise).": "",
"New folder": "",
"New Frame? Run one command on it and FramePort does the rest.": "",
"New Frame? Run one command on it and FramePort does the rest. Did this once already? Your Frame appears under step 2.": "",
"New name": "",
"Next": "",
"No activity": "",
"No artwork could be downloaded from that {source} result; the current artwork stays. Try another one.": "",
"No folders to rescan yet: add games with Scan a folder": "",
"No Frames found. New Frame? Use first-time setup below. Otherwise make sure Developer Mode is on (Settings → System → Developer) and the Frame is on the same network.": "",
"No Frames found. New Frame? Use first-time setup (step 1). Otherwise make sure Developer Mode is on (Settings → System → Developer) and the Frame is on the same network.": "",
"No games found in that folder": "",
"No limit": "",
"No matching files": "",
@@ -528,11 +529,10 @@
"Older workaround for upside-down quads: rotates them 180° (quads are single-sided; prefer flip_emul).": "",
"On Frame": "",
"On the Frame, place Revive's runtime (FramePort's Revive, or the repack's bundled LibRevive) where the game's Oculus SDK looks for LibOVRRT so it can load a VR runtime (pure runtime substitution — what Revive's LoadLibrary hook does on Windows; that hook doesn't work under Proton on the Frame). It does NOT bypass the Oculus runtime signature check: a game that verifies the runtime's signature will still refuse it. Ignored on this PC (Revive's hook handles it there).": "",
"On the Frame: Steam button → Power → Switch to Desktop, open Konsole and run:": "",
"On the Frame: SteamVR dashboard → Launch a program → Desktop, then app menu → System → Konsole, and run:": "",
"On this PC": "",
"On this PC, launch the game through Revive's OpenVR backend, which talks straight to SteamVR — the original, most reliable Revive path. On by default for PC installs. Turn it off to use Revive's newer OpenXR backend instead. Ignored on the Frame (which always uses OpenXR).": "",
"On this PC, runs Revive through SteamVR (works with more headsets).": "",
"On your network": "",
"on: {names}": "",
"One PC VR game folder": "",
"Only FramePort's entry is removed. Your game files and anything installed stay.": "",
@@ -546,7 +546,7 @@
"Opens this player's storage on the Frame (Files tab): upload videos into the folder it lists": "",
"OpenXR runtime": "",
"Or paste text to type it in one go": "",
"Or turn on Developer Mode on the Frame (Settings → System → Developer): it then shows up above. Open Settings → Developer → Pair new host on the Frame, click Connect here and approve FramePort. That way needs no connection into this computer.": "",
"Or turn on Developer Mode on the Frame (Settings → System → Developer): it then shows up under step 2. Open Settings → Developer → Pair new host on the Frame, click Connect there and approve FramePort. That way needs no connection into this computer.": "",
"Override the guardian depth (0 = use guardian, min 1.5 m).": "",
"Override the guardian width (0 = use guardian, min 1.5 m).": "",
"OVRPort converts Quest games from Meta's own VR APIs to standard OpenXR. These are its optional patches.": "",
@@ -813,6 +813,7 @@
"The Frame is charging: continuing the installs.": "",
"the Frame isn't connected": "",
"The Frame isn't connected": "",
"The Frame refused that password. Check it, or run the first-time setup instead: Steam Frame → Show setup command.": "",
"The Frame went offline: {exc}": "",
"The Frame's battery is at {pct} % and not charging. Plug it in (a strong charger, not a PC port): long installs can drain it, and FramePort pauses them at {pause} %.": "",
"The Frame's battery is at {pct} %: installs paused so it doesn't switch off mid-upload. Plug it in; the queue continues by itself.": "",
@@ -837,6 +838,7 @@
"This computer's keyboard works as a keyboard on your Frame while this window is open. In the headset, select a text field (in a game or app, in Steam or on the desktop), then type here.": "",
"This deletes the {what} on the Frame. It can't be undone.": "",
"This folder is empty. Upload files with the buttons above.": "",
"This Frame isn't set up for FramePort yet. Run the first-time setup: Steam Frame → Show setup command.": "",
"This game can't run on the Steam Frame.": "",
"This game is marked \"Can't run\" on the Steam Frame:": "",
"This game's folder has several programs and FramePort isn't sure which one starts the game. Open the game (or right-click → Change executable) to pick it.": "",
@@ -969,6 +971,7 @@
"Your custom recipe": "",
"your Frame": "",
"Your Frame is back: continuing": "",
"Your Frame isn't set up for FramePort yet. Run the first-time setup once (step 1).": "",
"Your headset, what it's ready for and what's installed": "",
"Your own game files aren't touched.": "",
"Your own tags (filled) can be anything you like. Outlined tags are added automatically from the engine, VR API and store genres. Filter the library by either.": "",
+12 -2
View File
@@ -393,8 +393,13 @@ class FramePortApp:
else (lambda e: self.show_activity(True)))
elif job.state == "failed":
msg = tr("{title} failed: {error}").format(title=job.title, error=job.error)
self.toast(msg, error=True, action=tr("Details"),
on_action=lambda e: self.show_activity(True))
pkg = job.package
if job.kind in ("install", "test") and pkg: # one click to the diagnostics + prefilled issue
self.toast(msg, error=True, action=tr("Report a problem"),
on_action=lambda e: self.report_problem_dialog(pkg))
else:
self.toast(msg, error=True, action=tr("Details"),
on_action=lambda e: self.show_activity(True))
self.refresh_view()
if job.kind == "scan" and self.exe_queue:
self.page.run_thread(self.next_exe_choice)
@@ -1466,9 +1471,14 @@ class FramePortApp:
pass
if not quiet:
self.toast(tr("Connected to {label}").format(label=target.label))
self._not_paired = False
except Exception as exc: # noqa: BLE001
from ..frame.connection import FrameNotPaired
self.frame_state = "offline"
self.frame_info = None
# reachable, but it doesn't let FramePort in: the Frame page points at the first-time setup
self._not_paired = isinstance(exc, FrameNotPaired)
if not quiet:
self.toast(tr("Couldn't connect: {exc}").format(exc=explain(exc)), error=True)
if self.route[0] in ("frame", "library", "game", "welcome"):
+17 -9
View File
@@ -173,7 +173,7 @@ class FrameView:
devkit=f.source == "devkit")),
], spacing=T.S3), padding=ft.Padding(T.S3, T.px(8), T.S2, T.px(8)), bgcolor=T.SURFACE_2,
border_radius=T.RADIUS_SM))
found.controls = items or [C.body(tr("No Frames found. New Frame? Use first-time setup below. Otherwise "
found.controls = items or [C.body(tr("No Frames found. New Frame? Use first-time setup (step 1). Otherwise "
"make sure Developer Mode is on (Settings → System → Developer) and "
"the Frame is on the same network."))]
found.controls.append(C.ghost(tr("Search again"), ft.Icons.REFRESH_ROUNDED, lambda e: app.run_bg(discover)))
@@ -215,7 +215,8 @@ class FrameView:
discovery), which used to close it."""
line = app.pairing.one_liner
pair_box.controls = [
C.body(tr("On the Frame: Steam button → Power → Switch to Desktop, open Konsole and run:"), T.TEXT),
C.body(tr("On the Frame: SteamVR dashboard → Launch a program → Desktop, then app menu → System → "
"Konsole, and run:"), T.TEXT),
ft.Container(ft.Row([ft.Text(line, font_family="monospace", selectable=True, size=T.px(12),
color=T.TEXT, expand=True),
C.icon_btn(ft.Icons.CONTENT_COPY_ROUNDED, tr("Copy"), lambda e: app.copy(line))]),
@@ -225,7 +226,7 @@ class FrameView:
C.meta(tr("Waiting for your Frame… (code {code})").format(code=app.pairing.code))],
spacing=T.S2),
C.meta(tr("It trusts this app, turns on Developer Mode and installs Lepton if needed. Turning on "
"Developer Mode closes Desktop Mode; the setup finishes on its own and FramePort connects "
"Developer Mode closes the desktop; the setup finishes on its own and FramePort connects "
"by itself. You only do this once.")),
]
hint = getattr(app.pairing, "hint", "")
@@ -235,8 +236,8 @@ class FrameView:
weight=ft.FontWeight.W_600),
C.body(hint, T.TEXT),
C.meta(tr("Or turn on Developer Mode on the Frame (Settings → System → Developer): it then shows "
"up above. Open Settings → Developer → Pair new host on the Frame, click Connect here "
"and approve FramePort. That way needs no connection into this computer.")),
"up under step 2. Open Settings → Developer → Pair new host on the Frame, click Connect "
"there and approve FramePort. That way needs no connection into this computer.")),
], spacing=T.S2), "warn"))
if update:
pair_box.update()
@@ -252,7 +253,12 @@ class FrameView:
width=T.px(240), tooltip=C.tip(HELP["password"]), **style)
saved = saved_targets()
offline = None
if saved and app.frame_state == "offline":
if getattr(app, "_not_paired", False): # it answered, but doesn't know FramePort yet
offline = C.callout(ft.Row([
C.body(tr("Your Frame isn't set up for FramePort yet. Run the first-time setup once (step 1)."), T.TEXT,
expand=True),
C.primary(tr("Show setup command"), ft.Icons.TERMINAL_ROUNDED, pair)]), "warn")
elif saved and app.frame_state == "offline":
unreachable = tr("{label} ({host}) isn't reachable. Make sure it's switched on and on the same "
"network.").format(label=saved[0].label, host=saved[0].host)
offline = C.callout(ft.Row([C.body(unreachable, T.TEXT, expand=True),
@@ -280,10 +286,12 @@ class FrameView:
return ft.Column([
app.top_bar(tr("Connect your Steam Frame"), tr("FramePort installs games on the Frame over your network")),
*([offline] if offline else []),
step(1, tr("On your network"), tr("Frames in Developer Mode show up here."), found, help="developer_mode"),
step(2, tr("First-time setup"), tr("New Frame? Run one command on it and FramePort does the rest."),
C.secondary(tr("Show setup command"), ft.Icons.TERMINAL_ROUNDED, pair), pair_box,
step(1, tr("First-time setup"), tr("New Frame? Run one command on it and FramePort does the rest. Did this "
"once already? Your Frame appears under step 2."),
C.primary(tr("Show setup command"), ft.Icons.TERMINAL_ROUNDED, pair), pair_box,
help="first_time_setup"),
step(2, tr("Already set up: on your network"), tr("Frames in Developer Mode show up here."), found,
help="developer_mode"),
step(3, tr("Enter the address"), tr("If you know the Frame's address."),
ft.Row([addr, pw, C.primary(tr("Connect"),
on_click=lambda e: app.connect_manual(addr.value, pw.value))],
+4 -1
View File
@@ -18,6 +18,7 @@ import hashlib
import json
import logging
import os
import platform
import re
import shutil
import subprocess
@@ -39,7 +40,7 @@ REPO = REPO_URL.removeprefix("https://github.com/").strip("/")
LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest"
CHECK_EVERY = 6 * 3600 # seconds between automatic checks
ASSETS = {"win32": "FramePort-windows-x64.zip", "darwin": "FramePort-macos-arm64.zip",
"linux": "FramePort-linux-x64.tar.gz"}
"linux": "FramePort-linux-x64.tar.gz", "linux-arm64": "FramePort-linux-arm64.tar.gz"} # never rename
SUMS = "SHA256SUMS.txt"
@@ -75,6 +76,8 @@ def is_newer(candidate: str, current: str = __version__) -> bool:
def platform_asset() -> str | None:
key = "linux" if sys.platform.startswith("linux") else sys.platform
if key == "linux" and platform.machine().lower() in ("aarch64", "arm64"):
key = "linux-arm64" # the ARM64 bundle (from 0.6.0)
return ASSETS.get(key)
+71
View File
@@ -0,0 +1,71 @@
"""Logging in to the Frame (Reddit 2026-10-03: "Couldn't connect: no authentication methods available" even with the
right password). paramiko ends the key/agent attempt with a plain SSHException when this PC has no SSH keys; that must
not stop the password attempt, and the final error must say what to do."""
import paramiko
import pytest
from frameport.errors import explain
from frameport.frame import connection as C
class _Client:
"""Stands in for paramiko.SSHClient: `script` maps attempt kind -> exception (or None = success)."""
script: dict = {}
tried: list = []
def load_host_keys(self, path):
pass
def set_missing_host_key_policy(self, policy):
pass
def close(self):
pass
def save_host_keys(self, path):
pass
def connect(self, pkey=None, password=None, allow_agent=False, look_for_keys=False, **kw):
kind = "password" if password else "agent" if allow_agent else "app_key"
_Client.tried.append(kind)
exc = _Client.script.get(kind)
if exc is not None:
raise exc
@pytest.fixture
def fake_ssh(monkeypatch):
monkeypatch.setattr(C.paramiko, "SSHClient", _Client)
monkeypatch.setattr(C, "app_keys", lambda: ["key"])
_Client.tried = []
return _Client
NO_METHODS = paramiko.SSHException("No authentication methods available")
def test_no_ssh_keys_on_the_pc_still_tries_the_password(fake_ssh, monkeypatch):
fake_ssh.script = {"app_key": paramiko.AuthenticationException("Authentication failed."), "agent": NO_METHODS,
"password": paramiko.AuthenticationException("Authentication failed.")}
frame = C.Frame(C.FrameTarget("10.0.0.2"), password="secret")
with pytest.raises(C.FrameNotPaired) as err:
frame.connect()
assert fake_ssh.tried == ["app_key", "agent", "password"]
assert err.value.tried_password and "password was refused" in str(err.value)
assert "refused that password" in explain(err.value)
def test_unpaired_frame_without_password_explains_the_setup(fake_ssh):
fake_ssh.script = {"app_key": paramiko.AuthenticationException("Authentication failed."), "agent": NO_METHODS}
with pytest.raises(C.FrameNotPaired) as err:
C.Frame(C.FrameTarget("10.0.0.2")).connect()
assert "authentication failed" in str(err.value).lower() # app.connect's devkit fallback looks for this
assert "first-time setup" in explain(err.value) and "isn't set up" in explain(err.value)
assert explain(NO_METHODS).startswith("This Frame isn't set up")
def test_network_errors_still_end_the_attempts(fake_ssh):
fake_ssh.script = {"app_key": paramiko.SSHException("Error reading SSH protocol banner")}
with pytest.raises(paramiko.SSHException, match="banner"):
C.Frame(C.FrameTarget("10.0.0.2"), password="x").connect()
assert fake_ssh.tried == ["app_key"]
+2 -1
View File
@@ -37,6 +37,7 @@ def test_known_failures(exc, start):
def test_other_errors_keep_their_message():
auth = ConnectionError("SSH authentication failed (bad key). Run the FramePort bootstrap on the Frame.")
assert explain(auth) == str(auth) and not is_connection_error(auth) # already says what to do
# login refused: points at the first-time setup, and isn't a "Frame went away" (the queue mustn't wait for it)
assert explain(auth).startswith("This Frame isn't set up") and not is_connection_error(auth)
assert explain(AgentFailed("Lepton is not installed (Developer Mode → …)")).startswith("Lepton is not installed")
assert explain(RuntimeError()) == "RuntimeError"
+8
View File
@@ -246,3 +246,11 @@ def test_apply_reports_a_script_that_never_starts(monkeypatch, tmp_path):
monkeypatch.setattr(updates.subprocess, "Popen", lambda *a, **k: type("P", (), {"poll": lambda self: None})())
with pytest.raises(updates.UpdateError):
updates.apply(staged, installed, relaunch=False, pid=1, platform="linux")
def test_platform_asset_picks_the_arm64_linux_bundle(monkeypatch):
monkeypatch.setattr(updates.sys, "platform", "linux")
monkeypatch.setattr(updates.platform, "machine", lambda: "aarch64")
assert updates.platform_asset() == "FramePort-linux-arm64.tar.gz"
monkeypatch.setattr(updates.platform, "machine", lambda: "x86_64")
assert updates.platform_asset() == "FramePort-linux-x64.tar.gz"