From e3fef31ba33ff65860cf0d243d4d52e67ddc9233 Mon Sep 17 00:00:00 2001 From: danny Date: Fri, 2 Jan 2026 11:02:46 +0100 Subject: [PATCH] Replace inline Bluetooth pairing script with external reusable bash script for improved maintainability and compatibility. Refactor Bluetooth panel/device logic to use adapter-based APIs. --- Bin/dev/BluetoothConnectionScript.sh | 59 +++++++++++++++++++++ Helpers/BluetoothScripts.js | 42 --------------- Modules/Panels/Bluetooth/BluetoothPanel.qml | 28 +++++----- Services/Networking/BluetoothService.qml | 32 +++++++---- 4 files changed, 96 insertions(+), 65 deletions(-) create mode 100755 Bin/dev/BluetoothConnectionScript.sh delete mode 100644 Helpers/BluetoothScripts.js diff --git a/Bin/dev/BluetoothConnectionScript.sh b/Bin/dev/BluetoothConnectionScript.sh new file mode 100755 index 000000000..d74a7b73c --- /dev/null +++ b/Bin/dev/BluetoothConnectionScript.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +# BluetoothConnectionScript.sh +# Pairs, trusts, and attempts to connect to a Bluetooth device using bluetoothctl. +# Usage: BluetoothConnectionScript.sh + +set -euo pipefail + +if [[ ${#} -lt 4 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +addr=$1 +pair_wait_seconds=$2 +attempts=$3 +interval_sec=$4 + +if [[ -z "${addr}" || ${#addr} -lt 7 ]]; then + echo "Invalid Bluetooth address: '${addr}'" >&2 + exit 2 +fi + +# Launch bluetoothctl session to pair, trust, and try to connect repeatedly. +{ + echo 'agent KeyboardDisplay' + echo 'default-agent' + echo 'power on' + echo "pair ${addr}" + # Give time for potential confirmation prompt; send 'yes' optimistically (no-op if not needed) + sleep 1 + echo 'yes' + # Mark device trusted + echo "trust ${addr}" + # Attempt multiple connects within the session + for i in $(seq 1 "${attempts}"); do + echo "connect ${addr}" + sleep "${interval_sec}" + done + echo 'quit' +} | bluetoothctl & + +# Wait up to pair_wait_seconds for pairing to complete +for i in $(seq 1 "${pair_wait_seconds}"); do + if bluetoothctl info "${addr}" | grep -q 'Paired: yes'; then + break + fi + sleep 1 +done + +# Check connection state for ~attempts*interval_sec seconds total +for i in $(seq 1 "${attempts}"); do + if bluetoothctl info "${addr}" | grep -q 'Connected: yes'; then + exit 0 + fi + sleep "${interval_sec}" +done + +exit 1 diff --git a/Helpers/BluetoothScripts.js b/Helpers/BluetoothScripts.js deleted file mode 100644 index aeb23ccc3..000000000 --- a/Helpers/BluetoothScripts.js +++ /dev/null @@ -1,42 +0,0 @@ -.pragma library - -var pairAndConnectScript = (addr, pairWaitSeconds, attempts, intervalSec) => { - // Produces a shell script that pairs, trusts and attempts to connect repeatedly. - return ` - addr='${addr}' - { - echo 'agent KeyboardDisplay' - echo 'default-agent' - echo 'power on' - echo "pair $addr" - # Give time for potential confirmation prompt; send 'yes' optimistically (no-op if not needed) - sleep 1 - echo 'yes' - # Mark device trusted - echo "trust $addr" - # Attempt multiple connects within the session - for i in $(seq 1 ${attempts}); do - echo "connect $addr" - sleep ${intervalSec} - done - echo 'quit' - } | bluetoothctl & - - # Wait up to ${pairWaitSeconds}s for pairing to complete - for i in $(seq 1 ${pairWaitSeconds}); do - if bluetoothctl info "$addr" | grep -q 'Paired: yes'; then - break - fi - sleep 1 - done - - # Check connection state for ~${attempts * intervalSec}s total - for i in $(seq 1 ${attempts}); do - if bluetoothctl info "$addr" | grep -q 'Connected: yes'; then - exit 0 - fi - sleep ${intervalSec} - done - exit 1 - `; -}; diff --git a/Modules/Panels/Bluetooth/BluetoothPanel.qml b/Modules/Panels/Bluetooth/BluetoothPanel.qml index c531cd3cd..fe8fc853e 100644 --- a/Modules/Panels/Bluetooth/BluetoothPanel.qml +++ b/Modules/Panels/Bluetooth/BluetoothPanel.qml @@ -156,9 +156,9 @@ SmartPanel { label: I18n.tr("bluetooth.panel.connected-devices") headerMode: "layout" property var items: { - if (!BluetoothService.adapter || !Bluetooth.devices) + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) return []; - var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected); + var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && dev.connected); filtered = BluetoothService.dedupeDevices(filtered); return BluetoothService.sortDevices(filtered); } @@ -173,9 +173,9 @@ SmartPanel { tooltipText: I18n.tr("tooltips.connect-disconnect-devices") headerMode: "layout" property var items: { - if (!BluetoothService.adapter || !Bluetooth.devices) + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) return []; - var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted)); + var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted)); filtered = BluetoothService.dedupeDevices(filtered); return BluetoothService.sortDevices(filtered); } @@ -189,9 +189,9 @@ SmartPanel { label: I18n.tr("bluetooth.panel.available-devices") headerMode: "filter" property var items: { - if (!BluetoothService.adapter || !Bluetooth.devices) + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) return []; - var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted); + var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted); // Optionally hide devices without a meaningful name when the filter is enabled if (Settings.data && Settings.data.ui && Settings.data.ui.bluetoothHideUnnamedDevices) { filtered = filtered.filter(function (dev) { @@ -268,12 +268,12 @@ SmartPanel { // Empty state when no devices NBox { visible: { - if (!Bluetooth.devices || BluetoothService.scanningActive) + if (!(BluetoothService.adapter && BluetoothService.adapter.devices) || BluetoothService.scanningActive) return false; - var availableCount = Bluetooth.devices.values.filter(dev => { - return dev && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0); - }).length; + var availableCount = BluetoothService.adapter.devices.values.filter(dev => { + return dev && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0); + }).length; return (availableCount === 0); } Layout.fillWidth: true @@ -321,13 +321,13 @@ SmartPanel { Layout.fillWidth: true Layout.preferredHeight: columnScanning.implicitHeight + Style.marginM * 2 visible: { - if (!Bluetooth.devices || !BluetoothService.scanningActive) { + if (!(BluetoothService.adapter && BluetoothService.adapter.devices) || !BluetoothService.scanningActive) { return false; } - var availableCount = Bluetooth.devices.values.filter(dev => { - return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0); - }).length; + var availableCount = BluetoothService.adapter.devices.values.filter(dev => { + return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0); + }).length; return (availableCount === 0); } diff --git a/Services/Networking/BluetoothService.qml b/Services/Networking/BluetoothService.qml index ffbbad864..9614ada55 100644 --- a/Services/Networking/BluetoothService.qml +++ b/Services/Networking/BluetoothService.qml @@ -9,7 +9,6 @@ import qs.Commons import qs.Services.UI import "." import "../../Helpers/BluetoothUtils.js" as BluetoothUtils -import "../../Helpers/BluetoothScripts.js" as BluetoothScripts QtObject { id: root @@ -105,7 +104,21 @@ QtObject { restoreDiscoveryTimer.stop(); root._discoveryWasRunning = false; } catch (_) {} + + // Prefer Quickshell API if available, fall back to bluetoothctl + try { + if (adapter) { + if (active && adapter.startDiscovery !== undefined) { + adapter.startDiscovery(); + } else if (!active && adapter.stopDiscovery !== undefined) { + adapter.stopDiscovery(); + } + } + } catch (e1) {} + + // Always issue bluetoothctl as a compatibility fallback btExec(["bluetoothctl", "scan", active ? "on" : "off"]); + if (active && durationMs && durationMs > 0) { manualScanTimer.interval = durationMs; manualScanTimer.restart(); @@ -119,22 +132,22 @@ QtObject { function toggleDiscovery() { if (!adapter) return; - setScanActive(!root.ctlDiscovering, scanAutoStopMs); + setScanActive(!root.scanningActive, scanAutoStopMs); } // Auto-stop manual discovery after a short window property Timer manualScanTimer: Timer { repeat: false onTriggered: { - // Stop scan via bluetoothctl if currently active - if (root.ctlDiscovering) { + // Stop scan if currently active + if (root.scanningActive) { root.setScanActive(false, 0); } } } - // Exposed scanning flag for UI button state, driven by bluetoothctl state - readonly property bool scanningActive: (root.ctlDiscovering === true) || manualScanTimer.running + // Exposed scanning flag for UI button state; reflects adapter discovery when available + readonly property bool scanningActive: ((adapter && adapter.discovering) ? true : (root.ctlDiscovering === true)) || manualScanTimer.running function init() { Logger.i("Bluetooth", "Service started"); @@ -414,9 +427,10 @@ QtObject { const totalPauseMs = (pairWait * 1000) + (attempts * intervalSec * 1000) + 2000; _pauseDiscoveryFor(totalPauseMs); - // Auto-confirm pairing with bluetoothctl ("yes"). Build script via helper. - const script = BluetoothScripts.pairAndConnectScript(addr, pairWait, attempts, intervalSec); - btExec(["sh", "-c", script]); + // Prefer external dev script for pairing/connecting; executed detached + const scriptPath = Quickshell.shellDir + "/Bin/dev/BluetoothConnectionScript.sh"; + // Use bash explicitly to avoid relying on executable bit in all environments + btExec(["bash", scriptPath, String(addr), String(pairWait), String(attempts), String(intervalSec)]); } // --- Helper to run bluetoothctl and scripts with consistent error logging ---