Warn in Input Settings when SteamVR hasn't loaded the pointer driver

When SteamVR crashes, safe mode can block the newest add-on, and then it
skips the ft_pointer driver at every start. The cursor still moves (the
pointer helper draws it), but clicks, scrolling and mapped actions go
through the driver's virtual controller, so none of them do anything,
and nothing said why (#4).

Input Settings now shows a warning at the top of every page with the
fix: Manage Add-Ons in SteamVR's settings, unblock ft_pointer, restart
SteamVR. It reads steamvr.vrsettings (~/.config/openvr/config on the
Frame) for blocked_by_safe_mode, a disabled driver, or SteamVR's own
safe mode. When none of those is set but SteamVR is running (the helper
answers) and @ft_pointer doesn't, it says SteamVR runs without the
driver: unblocked but not restarted yet, or not installed. The driver
ignores the "ping" it sends. It checks at startup and every 30 minutes,
since the driver only changes when SteamVR restarts.

Fixes #4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-29 10:04:17 -06:00
1 parent b99fb97f32
commit 51e7e0fa85
2 files changed
+94 -7

No files matched your search

+53
View File
@@ -13,6 +13,10 @@ to the input relay over its control socket (@frametop_relay):
- Gaze: the pointer's gaze mode (@ft_pointer_helper "gaze") and the gaze service
(gaze/ft-gazed, @ft_gazed: status, forget, reload).
- Bluetooth: paired devices, and re-applying the Bluetooth LE fixes after pairing.
- A warning on every page when SteamVR won't load the ft_pointer driver (blocked after a
crash, disabled, or SteamVR in safe mode) or hasn't loaded it (@ft_pointer doesn't answer
while SteamVR runs): the cursor still moves, but no click lands. Checked at startup and
every 30 minutes.
Rules go to ~/.config/frametop-input.json and pointer settings to
~/.config/frametop.conf; then the relay (and through it the helper) reloads.
Launch with input-settings/ft-input-settings (host wrapper).
@@ -36,6 +40,10 @@ CONF_PATH = os.path.expanduser("~/.config/frametop.conf")
RELAY = "\0frametop_relay"
HELPER = "\0ft_pointer_helper"
GAZED = "\0ft_gazed"
DRIVER = "\0ft_pointer" # the ft_pointer driver's control socket, bound while SteamVR has it loaded
# SteamVR's settings; older installs keep them under Steam's config.
VRSETTINGS_PATHS = [os.path.expanduser("~/.config/openvr/config/steamvr.vrsettings"),
os.path.expanduser("~/.steam/steam/config/steamvr.vrsettings")]
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GAZE_PROBE = os.path.join(REPO, "gaze", "probe", "ft-gazeprobe")
BTN_MISC = 0x100
@@ -157,6 +165,29 @@ def host(*cmd):
return subprocess.CompletedProcess(cmd, 1, "", str(e))
def driver_block():
"""Why SteamVR won't load the ft_pointer driver: "blocked" (safe mode blocked it after a
crash), "disabled" (turned off in Manage Add-Ons), "safemode" (SteamVR in safe mode, every
add-on off) or "" (nothing stops it). SteamVR reads these at startup."""
for path in VRSETTINGS_PATHS:
if os.path.exists(path):
settings = read_json(path)
break
else:
return ""
section = lambda name: settings.get(name) if isinstance(settings.get(name), dict) else {}
if not isinstance(settings, dict):
return ""
driver = section("driver_ft_pointer")
if driver.get("blocked_by_safe_mode") is True:
return "blocked"
if driver.get("enable") is False:
return "disabled"
if section("steamvr").get("enableSafeMode") is True:
return "safemode"
return ""
class Backend(QObject):
devicesChanged = Signal()
mappingsChanged = Signal()
@@ -164,6 +195,7 @@ class Backend(QObject):
bluetoothChanged = Signal()
controllersChanged = Signal()
gazeChanged = Signal()
driverChanged = Signal()
activity = Signal(str) # device id
captured = Signal(int, str) # code, name
capturedController = Signal(str, str) # button, label
@@ -184,6 +216,7 @@ class Backend(QObject):
self._gaze_prev = None # the status before, for rates
self._gaze_at = 0.0
self._gaze_mode = None # the helper's gaze mode: True, False, None (no answer)
self._driver_block = "" # set by _check_driver
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.sock.bind("") # autobind an abstract address the relay can reply to
self.sock.setblocking(False)
@@ -194,6 +227,11 @@ class Backend(QObject):
self.rewatch = QTimer(interval=50000, timeout=lambda: self._send("watch 60"))
self.rewatch.start()
self.reload_timer = QTimer(singleShot=True, interval=400, timeout=lambda: self._send("reload"))
# The driver only changes with a SteamVR restart, so a rare check is enough. The first one
# waits a moment for the helper's vrstatus answer, which tells us SteamVR is up.
self.driver_timer = QTimer(interval=30 * 60 * 1000, timeout=self._check_driver)
self.driver_timer.start()
QTimer.singleShot(3000, self._check_driver)
self._refresh()
self._send("watch 60")
self.refreshBluetooth()
@@ -225,6 +263,14 @@ class Backend(QObject):
self._gaze_mode = None
self.gazeChanged.emit()
def _check_driver(self):
block = driver_block()
if not block and self._vr and not self._send("ping", DRIVER):
block = "unloaded" # SteamVR runs (the helper answers) without the driver
if block != self._driver_block:
self._driver_block = block
self.driverChanged.emit()
def _read(self):
while True:
try:
@@ -270,6 +316,13 @@ class Backend(QObject):
self._capture_id = ""
self.captured.emit(code, self.codeName(code))
# --- SteamVR driver ---
@Property(str, notify=driverChanged)
def driverBlock(self):
"""driver_block(), or "unloaded": SteamVR runs without the driver (unblocked since it
started, or not installed)."""
return self._driver_block
# --- devices ---
@Property(bool, notify=devicesChanged)
def relayRunning(self):
+41 -7
View File
@@ -25,6 +25,30 @@ Kirigami.ApplicationWindow {
]
}
// Why SteamVR has no ft_pointer driver, and how to get it back. Clicks, scrolling and mapped
// actions all go through that driver, so every page shows this as (part of) its header.
component DriverWarning: Kirigami.InlineMessage {
readonly property string fix: "SteamVR Settings > Startup / Shutdown > Manage Add-Ons"
readonly property string restart: ", then restart SteamVR (or reboot the headset)."
visible: backend.driverBlock !== ""
position: Kirigami.InlineMessage.Position.Header
type: backend.driverBlock === "unloaded" ? Kirigami.MessageType.Warning : Kirigami.MessageType.Error
text: ({
blocked: "SteamVR blocked the Frametop pointer driver (ft_pointer) after a crash, so mouse clicks "
+ "and scrolling do nothing (the cursor still moves). To fix it, open " + fix
+ ", press Unblock next to ft_pointer" + restart,
disabled: "The Frametop pointer driver (ft_pointer) is turned off in SteamVR, so mouse clicks and "
+ "scrolling do nothing (the cursor still moves). To fix it, open " + fix
+ ", turn ft_pointer on" + restart,
safemode: "SteamVR is in safe mode, so it loads no add-ons, the Frametop pointer driver (ft_pointer) "
+ "included: mouse clicks and scrolling do nothing (the cursor still moves). To fix it, turn "
+ "safe mode off in SteamVR and check " + fix + restart,
unloaded: "SteamVR is running without the Frametop pointer driver (ft_pointer), so mouse clicks and "
+ "scrolling do nothing. If you just unblocked it, restart SteamVR (or reboot the headset). "
+ "Otherwise check " + fix + ", or reinstall it with pointer/driver/install.sh."
})[backend.driverBlock] || ""
}
function show(page) {
pageStack.clear()
pageStack.push(page)
@@ -47,13 +71,18 @@ Kirigami.ApplicationWindow {
Kirigami.ScrollablePage {
title: "Devices"
header: Kirigami.InlineMessage {
visible: !backend.relayRunning || !backend.pointerMode
position: Kirigami.InlineMessage.Position.Header
type: backend.relayRunning ? Kirigami.MessageType.Information : Kirigami.MessageType.Error
text: !backend.relayRunning
? "The input relay isn't running (frametop-input-relay.service)."
: "Pointer mode is off (POINTER=0): pointer devices act as a plain mouse."
header: ColumnLayout {
spacing: 0
DriverWarning { Layout.fillWidth: true }
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: !backend.relayRunning || !backend.pointerMode
position: Kirigami.InlineMessage.Position.Header
type: backend.relayRunning ? Kirigami.MessageType.Information : Kirigami.MessageType.Error
text: !backend.relayRunning
? "The input relay isn't running (frametop-input-relay.service)."
: "Pointer mode is off (POINTER=0): pointer devices act as a plain mouse."
}
}
ListView {
@@ -171,6 +200,7 @@ Kirigami.ApplicationWindow {
Kirigami.ScrollablePage {
id: bpage
title: "Buttons"
header: DriverWarning {}
actions: [
Kirigami.Action {
text: "Remove all"
@@ -310,6 +340,7 @@ Kirigami.ApplicationWindow {
Kirigami.ScrollablePage {
id: cpage
title: "Controllers"
header: DriverWarning {}
actions: [
Kirigami.Action {
text: "Remove all"
@@ -478,6 +509,7 @@ Kirigami.ApplicationWindow {
id: pointerPage
Kirigami.ScrollablePage {
title: "Pointer"
header: DriverWarning {}
actions: [
Kirigami.Action {
text: "Recenter"
@@ -549,6 +581,7 @@ Kirigami.ApplicationWindow {
Kirigami.ScrollablePage {
id: gpage
title: "Gaze"
header: DriverWarning {}
property var status: backend.gazeStatus
actions: [
Kirigami.Action {
@@ -703,6 +736,7 @@ Kirigami.ApplicationWindow {
id: bluetoothPage
Kirigami.ScrollablePage {
title: "Bluetooth"
header: DriverWarning {}
actions: [
Kirigami.Action { text: "Refresh"; icon.name: "view-refresh"; onTriggered: backend.refreshBluetooth() },
Kirigami.Action {