diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-20 11:45:08 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-20 11:45:08 +0200 |
| commit | 59c4a841b7107f6b21bcd1c8f120c8caf1bd2d7c (patch) | |
| tree | 868891e386c2e7597a3364aaed8c752c7a245469 /desktop/modules | |
| parent | eba0c13ad2f50964b6f88950366cb659c0d8b055 (diff) | |
| download | quickshell-59c4a841b7107f6b21bcd1c8f120c8caf1bd2d7c.tar.gz quickshell-59c4a841b7107f6b21bcd1c8f120c8caf1bd2d7c.zip | |
feat(desktop): read gamepad battery from power_supply
BluetoothRow only read BluetoothDevice.battery, which is BlueZ's
org.bluez.Battery1. A gamepad never publishes that interface: a Sony pad
exposes its charge as a kernel power_supply instead, so the percentage never
appeared.
Read every power_supply whose name ends in a MAC on a 30s timer while
anything is connected, keyed by address. BlueZ still wins when it has a
value.
Diffstat (limited to 'desktop/modules')
| -rw-r--r-- | desktop/modules/bluetooth/BluetoothModule.qml | 55 | ||||
| -rw-r--r-- | desktop/modules/bluetooth/BluetoothRow.qml | 12 | ||||
| -rw-r--r-- | desktop/modules/bluetooth/README.md | 16 |
3 files changed, 81 insertions, 2 deletions
diff --git a/desktop/modules/bluetooth/BluetoothModule.qml b/desktop/modules/bluetooth/BluetoothModule.qml index 4fd600f..6c4f587 100644 --- a/desktop/modules/bluetooth/BluetoothModule.qml +++ b/desktop/modules/bluetooth/BluetoothModule.qml @@ -33,6 +33,61 @@ Module { readonly property bool anyConnected: connectedDevices.length > 0 + // Battery for devices BlueZ does not publish through org.bluez.Battery1. + // Gamepads are the case: the Sony pads expose their charge as a kernel + // power_supply rather than a BlueZ interface, so BluetoothDevice.battery + // reads nothing for them. The DualSense driver names the supply + // "ps-controller-battery-<mac>"; the parser takes any supply whose name + // ends in a MAC, so the separator ("-" here, "_" under hid-sony) and the + // driver do not matter. This map is keyed by lowercase MAC and holds 0-100. + property var batteryPct: ({}) + function batteryFor(address) { + if (!address) return null; + const v = mod.batteryPct[address.toLowerCase()]; + return v === undefined ? null : v; + } + + function refreshBatteries() { + batProc.running = false; + batProc.running = true; + } + + // Reads every power_supply once. A supply earns a line only when its name + // ends in "xx:xx:xx:xx:xx:xx", which excludes the system battery (BAT0) + // and AC adapter while catching the controller batteries. The MAC is the + // trailing 17 characters; the separator before it is not assumed. + property Process batProc: Process { + command: ["sh", "-c", + "for d in /sys/class/power_supply/*; do n=${d##*/}; " + + "case $n in *??:??:??:??:??:??) " + + "[ -r \"$d/capacity\" ] && printf '%s %s\\n' \"$n\" \"$(cat \"$d/capacity\")\";; " + + "esac; done"] + stdout: StdioCollector { + onStreamFinished: { + const map = {}; + for (const line of text.trim().split("\n")) { + const i = line.lastIndexOf(" "); + if (i <= 0) continue; + const pct = parseInt(line.slice(i + 1), 10); + if (!isNaN(pct)) map[line.slice(0, i).slice(-17).toLowerCase()] = pct; + } + mod.batteryPct = map; + } + } + } + + // ponytail: 30s poll, matches how slowly a gamepad charge moves. Move to a + // uevent watch only if a lagging percentage is ever noticed. + property Timer batTimer: Timer { + interval: 30000 + repeat: true + running: mod.anyConnected + onTriggered: mod.refreshBatteries() + } + + onAnyConnectedChanged: if (mod.anyConnected) mod.refreshBatteries() + Component.onCompleted: mod.refreshBatteries() + // Null for the first ~2s, then filled. Safe navigation everywhere. icon: mod.adapter && mod.adapter.enabled ? "\uf293" : "\uf05e" diff --git a/desktop/modules/bluetooth/BluetoothRow.qml b/desktop/modules/bluetooth/BluetoothRow.qml index a7aecd0..297dc02 100644 --- a/desktop/modules/bluetooth/BluetoothRow.qml +++ b/desktop/modules/bluetooth/BluetoothRow.qml @@ -20,6 +20,14 @@ Rectangle { required property var bt required property var device + // BlueZ's org.bluez.Battery1 pushes a live value when the device reports + // one. A gamepad does not, so fall back to the module's power_supply map. + // Null means neither source knows, and the field stays hidden. + readonly property var battPct: { + if (row.device.batteryAvailable) return Math.round((row.device.battery ?? 0) * 100); + return row.bt.batteryFor(row.device.address); + } + implicitHeight: col.implicitHeight + 16 radius: 8 color: device.connected ? Qt.alpha(Theme.accent, 0.10) : Qt.alpha(Theme.surface, 0.35) @@ -49,8 +57,8 @@ Rectangle { Text { anchors.verticalCenter: parent.verticalCenter - visible: row.device.batteryAvailable ?? false - text: Math.round((row.device.battery ?? 0) * 100) + "%" + visible: row.battPct !== null + text: row.battPct + "%" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.overlay } diff --git a/desktop/modules/bluetooth/README.md b/desktop/modules/bluetooth/README.md index 2de634a..5a79060 100644 --- a/desktop/modules/bluetooth/README.md +++ b/desktop/modules/bluetooth/README.md @@ -10,6 +10,22 @@ then the connected devices, the paired ones, and anything found while scanning. A paired device offers Connect, Trust and Forget; an unpaired one offers Pair. +## Battery has two sources + +The row shows a percentage when it can find one, from either of two places: + +- `BluetoothDevice.battery` / `batteryAvailable`, which is BlueZ's + `org.bluez.Battery1`. Audio devices push this live. Nothing to poll. +- A kernel `power_supply`, read by the module on a 30s timer while anything is + connected. Gamepads are the reason: a Sony pad publishes no `Battery1`, it + exposes the charge as `/sys/class/power_supply/ps-controller-battery-<mac>` + (DualSense) or `sony_controller_battery_<mac>` (DS4). The module reads every + supply whose name ends in a MAC and keys it by the address, so the driver + and its separator do not matter. + +BlueZ wins when both exist. Neither source knowing the device leaves the field +empty, never a zero. + ## Pairing is the one shell-out Quickshell ships no BlueZ pairing agent and no generic D-Bus module, so a |
