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.

This commit is contained in:
danny
2026-01-02 11:02:46 +01:00
parent ba45c67d93
commit e3fef31ba3
4 changed files with 96 additions and 65 deletions
+59
View File
@@ -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 <addr> <pairWaitSeconds> <attempts> <intervalSec>
set -euo pipefail
if [[ ${#} -lt 4 ]]; then
echo "Usage: $0 <addr> <pairWaitSeconds> <attempts> <intervalSec>" >&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
-42
View File
@@ -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
`;
};
+14 -14
View File
@@ -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);
}
+23 -9
View File
@@ -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 ---