aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules')
-rw-r--r--desktop/modules/bluetooth/BluetoothModule.qml55
-rw-r--r--desktop/modules/bluetooth/BluetoothRow.qml12
-rw-r--r--desktop/modules/bluetooth/README.md16
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