bluetooth: show per-peripheral battery for split devices (ZMK)#2752
Open
dwright134 wants to merge 2 commits into
Open
bluetooth: show per-peripheral battery for split devices (ZMK)#2752dwright134 wants to merge 2 commits into
dwright134 wants to merge 2 commits into
Conversation
BlueZ's org.bluez.Battery1 (and therefore Quickshell's BluetoothDevice.battery) exposes only a single battery percentage per device, so split keyboards such as ZMK Corne boards only ever showed the central half's charge. Such devices report each half's charge on a separate GATT Battery Level (0x2A19) characteristic under the same device object; peripheral characteristics are tagged with a 0x2901 user-description descriptor (e.g. "Peripheral 0"). Read those extra characteristics over D-Bus (busctl) and expose them from BluetoothService as `peripheralBatteries`, keyed by device address, then render one entry per peripheral in the Bluetooth detail view next to the existing central battery. The central battery is intentionally skipped (it is already covered by Battery1), keeping the change purely additive: non-split devices are unaffected. Reads fall back to BlueZ's cached value when a half is asleep, so the last known level keeps showing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The peripheral (0x2901 user-description) label was read via `busctl get-property ... Value` first, falling back to `ReadValue` only when the result was empty. But busctl prints `ay 0` for an empty byte array, which is a non-empty string, so the `[ -n "$lv" ]` guard never triggered the ReadValue fallback. BlueZ does not populate that descriptor's cached Value until something issues a ReadValue on it, so on a cold cache (the normal state right after connect) the label came back empty and the peripheral was dropped — split keyboards showed only the central half again. Read the label via ReadValue first when connected (matching how the percentage is already read), then fall back to the cached Value, using an `"ay "[1-9]*` check so the empty `ay 0` case is detected properly. This also warms BlueZ's cache itself instead of depending on it already being warm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
|
Pushed a follow-up fix (b59462f): the split-peripheral battery label was read from BlueZ's cached Now the label is read via |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Split keyboards (ZMK Corne and similar) report the battery level of each half
separately, but the Bluetooth detail view only ever showed one number.
The cause is that BlueZ's
org.bluez.Battery1interface — whichQuickshell.Bluetooth'sBluetoothDevice.batterywraps — exposes a singlepercentage per device. A split keyboard is one bonded device: the central half
holds the radio and relays the peripheral half's charge as an additional
GATT Battery Service (
0x180F) on the same device object. BlueZ folds only oneof those into
Battery1, so the second half was invisible.This PR reads the extra
0x2A19(Battery Level) characteristics directly overD-Bus and surfaces them:
BluetoothServicegains aperipheralBatteriesmap (keyed by deviceaddress) populated by a small
sh/busctlProcess. It walks the GATT treeof each connected device and emits one
address|percent|labelline perBattery Level characteristic that carries a
0x2901user-descriptiondescriptor — which is exactly how ZMK tags peripheral batteries (e.g.
"Peripheral 0"). The label is read verbatim from that descriptor, so itgeneralizes to N peripherals (
Peripheral 0,Peripheral 1, …) and tomultiple keyboards.
BluetoothDetailrenders one extra entry per peripheral next to theexisting battery text via a
Repeater.Design notes:
already covered by
Battery1/device.battery), so non-split devices renderexactly as before —
peripheralBatteriesFor()returns[]for them.same
/org/bluez/.../dev_XXXobject path, so peripherals attach to the rightdevice by construction — there is no name-matching that could go wrong.
ReadValuethen fails fast with "Not connected", so the reader falls back toBlueZ's cached
Value(last known level) and best-effortStartNotifyto keepthat cache warm. Every
busctlcall is bounded with--timeout=5.connectedchanges, and every 120 s (guarded soruns can't overlap).
Type of change
Related issues
Screenshots / video
Bluetooth detail row for a ZMK Corne, before → after:
Before:
Connected • 90%After:
Checklist
sh -c+busctl,matching existing
Processusage in this service)central 90% + peripheral 83% render, clean shell reload, no QML warnings)
existing (untranslated) battery/signal text (
"• " + value + "%"); thelabel text comes from the device's own GATT descriptor
make lint-qml— please run in CI/devshell; my local Qt'sqmllintcouldn't parsepragma ComponentBehavior: Bound(bailed at line 2)so I could not get a clean local run