diff --git a/input-settings/ft_input_settings.py b/input-settings/ft_input_settings.py index c17d93b..43d0c0f 100644 --- a/input-settings/ft_input_settings.py +++ b/input-settings/ft_input_settings.py @@ -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): diff --git a/input-settings/main.qml b/input-settings/main.qml index 8493dc2..21edb1f 100644 --- a/input-settings/main.qml +++ b/input-settings/main.qml @@ -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 { @@ -684,6 +717,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 {