Skip to content

feat(bbm): Bluetooth Battery Meter integration - #1

Closed
silentone12725 wants to merge 18 commits into
mainfrom
feature/bbm-integration
Closed

silentone12725 wants to merge 18 commits into
mainfrom
feature/bbm-integration

Conversation

@silentone12725

Copy link
Copy Markdown
Owner

Summary

Integrates the Bluetooth Battery Meter (org.bbm) GJS daemon into
caelestia-shell, adding per-component battery indicators and device
controls to the Bluetooth bar popout and hover cards.

  • BbmService.qml — new singleton monitoring org.bbm D-Bus via
    gdbus; exposes deviceMap, batteriesFor(), togglesFor(),
    sendUIAction(), constants, and icon URL helpers
  • BbmExpandedRow.qml — expandable in-popout panel showing up to
    3 per-component batteries (level-coloured rings) and 2 toggle control
    groups (ANC, spatial audio, etc.)
  • BtDeviceHover.qml — hover card extended with BBM battery rows
    and toggle controls, falling back to native BT battery when BBM is
    unavailable
  • BbmIcon.qml — shared component for themed SVG device icons
    (Colouriser + ImageAnalyser layer effect), extracted from the two
    places that previously inlined it
  • BluetoothStatus.qml / Bar.qml — BBM-aware battery
    indicator in the status bar (rings, text, icon; configurable)
  • ServicesPage.qml / BtDeviceDefaultsPage.qml — nexus
    settings page listing paired devices with BBM icons and navigation to
    per-device settings
  • Nix — nix/bbm.nix wraps the daemon; hm-module.nix wires
    systemd user service and GSettings schema

Breaking changes / side effects

None. All BBM paths are guarded by BbmService.available; the UI
degrades gracefully to native BlueZ battery data when the daemon is not
running.

How to use

  1. Build and start the org.bbm daemon (see bbm/daemon/) or install
    via the home-manager module (nix/hm-module.nix)
  2. Reload the shell — BbmService picks up org.bbm automatically
  3. Connect a supported device → battery rings appear in the BT popout;
    click the device row to expand ANC / mode toggles

Test plan

  • node tests/bbm/test-bbm-logic.mjs → 35/35 pass
  • /usr/lib/qt6/bin/qmltestrunner -import tests/bbm/stubs -import tests/bbm -input tests/bbm/tst_BbmIcon.qml → 8/8 pass
  • With daemon running: BT popout shows battery rings and ANC toggle for a connected device
  • With daemon not running: popout shows native battery or hides battery section — no errors
  • Hover a BT device icon in bar → hover card shows BBM components when available
  • Nexus → Services → Bluetooth → "Device defaults" lists paired devices with correct icons
  • Toggle ANC pill → state updates on device
  • Disconnect / reconnect → BbmService re-initialises cleanly

silentone12725 and others added 18 commits August 20, 2026 15:01
Integrates the Bluetooth Battery Meter daemon (org.bbm D-Bus service)
into the Bluetooth popout. When the daemon is running and a supported
device is connected, a collapsible row appears beneath the device entry
showing:

- Per-component battery bars (L / R / case) with SVG icons, percentage,
  and a bolt icon when charging
- ANC / Noise Control toggle pills (Off / Transparency / NC) with the
  device's own icon alongside the label
- NC intensity sub-row (Low / Mid / High / Adaptive) for devices that
  support multiple cancellation levels, using the secondary accent to
  visually distinguish it from the mode row

The integration degrades gracefully: when the daemon is not running or
the device is not supported the Bluetooth popout renders exactly as
before. BbmService retries the D-Bus connection every 5 s so it
recovers automatically if the daemon starts later.

New files:
  services/BbmService.qml         – singleton owning org.bbm session-bus
                                    connection; GVariant text parser;
                                    icon URL helpers (batteryIconUrl /
                                    toggleIconUrl) with encodeURI so
                                    paths containing spaces work
  modules/bar/popouts/BbmExpandedRow.qml – expanded device row; inline
                                    BbmSvgIcon component uses QtQuick.
                                    Effects MultiEffect for theme-aware
                                    recolouring of symbolic SVG icons

Modified files:
  modules/bar/popouts/Bluetooth.qml – delegate changed from RowLayout
                                    to ColumnLayout; Loader added for
                                    BbmExpandedRow
  modules/ServiceLoader.qml        – eagerly instantiates BbmService so
                                    battery data is ready before the user
                                    first opens the Bluetooth popout

Requires: bbm-daemon ≥ commit that adds GetIconPath() and AncLevel*
fields to the org.bbm.Manager D-Bus interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ANC toggle pills are now icon-only (no text label) with Tokens.rounding.medium
  radius so they look like rounded-square chips rather than full pills
- NC intensity level pills also use Tokens.rounding.medium for visual consistency
- NC level section animates in/out (scale 0.92→1 + opacity 0→1) and is only
  shown when OptionsBoxVisible === 1 (i.e. NC mode is the active ANC state)
- Card background uses StyledRect with m3surfaceContainer + Tokens.rounding.large
- All icons theme-aware via MultiEffect.colorization: 1.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch BbmIcon from raw MultiEffect to Colouriser (which applies brightness
pre-correction based on sourceColor.hslLightness) so dark SVG fills (#2e3436)
are normalised to near-white before the colorizationColor is applied.
Without this, dark source pixels stayed dark regardless of the theme token.

- Battery icons: 16px, m3onSurface colour (correctly follows light/dark theme)
- Toggle pill icons: 18px, same colour semantics
- Pill size grows to accommodate 18px icons with padding
- Bluetooth popout width: 300 → 360 to fit Low/Mid/High/Adaptive row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Battery row now shows when Battery1Level > 0 (was Battery2Level > 0),
  so single-battery devices (headphones, neckbands, ear sticks) display
  their battery component instead of a blank row
- BbmIcon hides itself on Image.Error so a missing/broken SVG path never
  shows Qt's broken-image placeholder (fixes stray '+' artifact)
- Remove redundant Layout.rightMargin from BbmExpandedRow; parent device
  ColumnLayout now owns 2px horizontal margins on both sides
- Bluetooth.qml device delegate: Layout.leftMargin/rightMargin = 2px each
  so the device list has 2px breathing room from the panel edges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hardcoded sourceColor: "#2e3436" with ImageAnalyser.dominantColour,
mirroring ColouredIcon.qml. The analyser reads the actual rendered pixels
to derive the dominant source colour, so Colouriser's brightness compensation
is always accurate regardless of which BBM SVG variant is loaded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BbmExpandedRow.qml:
- Pill implicitHeight/implicitWidth now derived from pillIcon.size instead
  of repeating the magic number 18 in three places
- Remove font.pixelSize: 12 from bolt MaterialIcon (inherits from context)

BbmService.qml:
- Extract D-Bus dest/path/interface to readonly property constants (_dest,
  _path, _iface) and reference them across all 5 gdbus commands and the
  signal monitor regex — single edit point if the bus name ever changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BbmService.qml spawns gdbus (from glib) to communicate with the org.bbm
D-Bus service. Declare it explicitly so the Nix-wrapped binary has it in
PATH on NixOS. callPackage injects pkgs.glib automatically by name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bundle bbm/daemon + bbm/lib + bbm/icons from Bluetooth-Battery-Meter
- Add nix/bbm.nix derivation: compiles schemas, wraps gjs as bbm-daemon binary
- Expose bbm-daemon package in flake.nix
- Add programs.caelestia.bbm option to hm-module.nix with auto-starting
  systemd user service (After=bluetooth.target, PartOf=graphical-session)
- CMakeLists: install bbm/ alongside shell QML dirs for non-Nix installs
- BbmService.qml: fix _parseDeviceData to handle GVariant type-annotated
  integers ("int32 65", "uint32 3", etc.) — battery levels and toggle states
  were stored as raw strings, breaking level filters and state comparisons
- Bluetooth.qml: add matching leftMargin=extraSmall wherever rightMargin
  was set, making horizontal padding symmetric

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bluetooth.qml: change device row margins from hardcoded 2px to
  Tokens.padding.extraSmall so they match title/toggle/device-count
  margins and all horizontal padding is uniform
- BbmService.qml: prefix gdbus monitor with stdbuf -oL to force
  line-buffering — without it, gdbus switches to block-buffered stdout
  when piped (not a tty) and DeviceChanged signals queue silently in a
  4-8KB buffer before flushing, causing battery/ANC state to appear
  frozen despite the daemon emitting updates in real time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DataHandler rewire on reconnect — see BBM repo commit for full detail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract BbmIcon inline component to shared BbmIcon.qml
- Add batteriesFor(), togglesFor() helpers to BbmService singleton
- Add STATUS_CHARGING and WIDGET_ANC_LEVEL constants to BbmService
- Replace hardcoded retryTimer interval with _retryIntervalMs constant
- Fix _init() race: start monitor before listProc via Qt.callLater
- Remove redundant batteryIconUrl visibility check in BtDeviceHover
- batteryColorForLevel kept local in each file (Colours singleton not
  safely importable from within qs.services without circular dep risk)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tests/bbm/test-bbm-logic.mjs — 35 Node.js tests covering:
  - BbmService constants (STATUS_CHARGING, WIDGET_ANC_LEVEL, _retryIntervalMs)
  - batteriesFor(): null safety, level=0 filter, charging flag, all 3 slots,
    icon defaults, mixed levels, no mutation
  - togglesFor(): null safety, invisible/empty-button filter, widgetId per
    slot, field defaults, ordering
  - batteryColorForLevel(): lerp boundaries at 0/15/40/100, midpoints,
    NaN/negative/over-100 clamping, string input

tests/bbm/tst_BbmIcon.qml — 8 Qt6 QML tests via qmltestrunner covering:
  - BbmIcon loads without Quickshell runtime (using stubs)
  - default size=18 drives implicitWidth/implicitHeight
  - size property is reactive
  - url and colour properties settable
  - empty url does not crash

Run with:
  node tests/bbm/test-bbm-logic.mjs
  /usr/lib/qt6/bin/qmltestrunner \
    -import tests/bbm/stubs -import tests/bbm \
    -input tests/bbm/tst_BbmIcon.qml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lists all paired devices with BBM icon fallback and navigation to
per-device settings subpage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds blank separators between QML object sections and fixes section
ordering to match the Qt coding conventions (id → properties →
functions → bindings → child objects).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant