mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-25 05:02:44 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c53d754007 | ||
|
|
a1662da17d | ||
|
|
0141bad259 | ||
|
|
6ed95684dc | ||
|
|
a6ca8385a9 | ||
|
|
210410f125 |
No files matched your search
@@ -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**)."
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
@@ -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}")
|
||||
@@ -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,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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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"):
|
||||
|
||||
@@ -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))],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user