From e87019da41289e33bb7e82568edfec887b36bc97 Mon Sep 17 00:00:00 2001 From: 1jehuang Date: Sun, 28 Dec 2025 05:29:38 -0800 Subject: [PATCH 1/4] Add D-Bus interface for battery monitoring Implements a D-Bus service (me.kavishdevar.librepods) that exposes AirPods battery information at /battery. This allows external tools like waybar to query battery levels without parsing BLE data directly. The interface exposes: - Battery levels for left, right, case, and headset - Charging status for each component - Availability status for each component - Device name and connection status - GetBatteryInfo() method returning all data as a map This enables integration with status bars and other system monitors. --- linux/CMakeLists.txt | 1 + linux/dbusadaptor.hpp | 111 ++++++++++++++++++++++++++++++++++++++++++ linux/main.cpp | 28 ++++++++++- 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 linux/dbusadaptor.hpp diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 629abd85..c3fb5d12 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -41,6 +41,7 @@ qt_add_executable(librepods thirdparty/QR-Code-generator/qrcodegen.hpp QRCodeImageProvider.hpp eardetection.hpp + dbusadaptor.hpp media/playerstatuswatcher.cpp media/playerstatuswatcher.h systemsleepmonitor.hpp diff --git a/linux/dbusadaptor.hpp b/linux/dbusadaptor.hpp new file mode 100644 index 00000000..fdefb3f7 --- /dev/null +++ b/linux/dbusadaptor.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include "battery.hpp" +#include "deviceinfo.hpp" + +class BatteryDBusAdaptor : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "me.kavishdevar.librepods.Battery") + + // Battery levels (0-100) + Q_PROPERTY(int LeftLevel READ leftLevel NOTIFY BatteryChanged) + Q_PROPERTY(int RightLevel READ rightLevel NOTIFY BatteryChanged) + Q_PROPERTY(int CaseLevel READ caseLevel NOTIFY BatteryChanged) + Q_PROPERTY(int HeadsetLevel READ headsetLevel NOTIFY BatteryChanged) + + // Charging status + Q_PROPERTY(bool LeftCharging READ leftCharging NOTIFY BatteryChanged) + Q_PROPERTY(bool RightCharging READ rightCharging NOTIFY BatteryChanged) + Q_PROPERTY(bool CaseCharging READ caseCharging NOTIFY BatteryChanged) + Q_PROPERTY(bool HeadsetCharging READ headsetCharging NOTIFY BatteryChanged) + + // Availability (connected/detected) + Q_PROPERTY(bool LeftAvailable READ leftAvailable NOTIFY BatteryChanged) + Q_PROPERTY(bool RightAvailable READ rightAvailable NOTIFY BatteryChanged) + Q_PROPERTY(bool CaseAvailable READ caseAvailable NOTIFY BatteryChanged) + Q_PROPERTY(bool HeadsetAvailable READ headsetAvailable NOTIFY BatteryChanged) + + // Device info + Q_PROPERTY(QString DeviceName READ deviceName NOTIFY DeviceChanged) + Q_PROPERTY(bool Connected READ connected NOTIFY DeviceChanged) + +public: + BatteryDBusAdaptor(Battery *battery, DeviceInfo *deviceInfo, QObject *parent) + : QDBusAbstractAdaptor(parent), m_battery(battery), m_deviceInfo(deviceInfo) + { + setAutoRelaySignals(true); + + // Connect battery signals to our relay + connect(m_battery, &Battery::batteryStatusChanged, this, [this]() { + emit BatteryChanged(); + }); + + connect(m_deviceInfo, &DeviceInfo::batteryStatusChanged, this, [this]() { + emit BatteryChanged(); + }); + + connect(m_deviceInfo, &DeviceInfo::deviceNameChanged, this, [this]() { + emit DeviceChanged(); + }); + } + + // Battery levels + int leftLevel() const { return m_battery->getLeftPodLevel(); } + int rightLevel() const { return m_battery->getRightPodLevel(); } + int caseLevel() const { return m_battery->getCaseLevel(); } + int headsetLevel() const { return m_battery->getHeadsetLevel(); } + + // Charging status + bool leftCharging() const { return m_battery->isLeftPodCharging(); } + bool rightCharging() const { return m_battery->isRightPodCharging(); } + bool caseCharging() const { return m_battery->isCaseCharging(); } + bool headsetCharging() const { return m_battery->isHeadsetCharging(); } + + // Availability + bool leftAvailable() const { return m_battery->isLeftPodAvailable(); } + bool rightAvailable() const { return m_battery->isRightPodAvailable(); } + bool caseAvailable() const { return m_battery->isCaseAvailable(); } + bool headsetAvailable() const { return m_battery->isHeadsetAvailable(); } + + // Device info - connected if device name is set and any battery is available + QString deviceName() const { return m_deviceInfo->deviceName(); } + bool connected() const { + return !m_deviceInfo->deviceName().isEmpty() && + (leftAvailable() || rightAvailable() || headsetAvailable()); + } + +public slots: + // Method to get all battery info at once (useful for waybar) + QVariantMap GetBatteryInfo() + { + QVariantMap info; + info["left_level"] = leftLevel(); + info["left_charging"] = leftCharging(); + info["left_available"] = leftAvailable(); + info["right_level"] = rightLevel(); + info["right_charging"] = rightCharging(); + info["right_available"] = rightAvailable(); + info["case_level"] = caseLevel(); + info["case_charging"] = caseCharging(); + info["case_available"] = caseAvailable(); + info["headset_level"] = headsetLevel(); + info["headset_charging"] = headsetCharging(); + info["headset_available"] = headsetAvailable(); + info["device_name"] = deviceName(); + info["connected"] = connected(); + return info; + } + +signals: + void BatteryChanged(); + void DeviceChanged(); + +private: + Battery *m_battery; + DeviceInfo *m_deviceInfo; +}; diff --git a/linux/main.cpp b/linux/main.cpp index 94d341ea..bce48237 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -30,6 +30,8 @@ #include "ble/bleutils.h" #include "QRCodeImageProvider.hpp" #include "systemsleepmonitor.hpp" +#include "dbusadaptor.hpp" +#include using namespace AirpodsTrayApp::Enums; @@ -149,7 +151,31 @@ class AirPodsTrayApp : public QObject { bool isEnabled = true; // Ability to disable the feature } CrossDevice; - void initializeDBus() { } + void initializeDBus() { + // Create D-Bus adaptor for battery info + new BatteryDBusAdaptor(m_deviceInfo->getBattery(), m_deviceInfo, this); + + // Register on session bus + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + if (!sessionBus.isConnected()) { + LOG_ERROR("Cannot connect to D-Bus session bus"); + return; + } + + // Register service + if (!sessionBus.registerService("me.kavishdevar.librepods")) { + LOG_ERROR("Cannot register D-Bus service: " << sessionBus.lastError().message()); + return; + } + + // Register object + if (!sessionBus.registerObject("/battery", this)) { + LOG_ERROR("Cannot register D-Bus object: " << sessionBus.lastError().message()); + return; + } + + LOG_INFO("D-Bus service registered: me.kavishdevar.librepods at /battery"); + } bool isAirPodsDevice(const QBluetoothDeviceInfo &device) { From 185fa531ab253d4145c50ba26517d8a5c293fd3b Mon Sep 17 00:00:00 2001 From: 1jehuang Date: Sun, 28 Dec 2025 06:04:33 -0800 Subject: [PATCH 2/4] Add AirPods Pro 3 support - Add AirPodsPro3 to AirPodsModel enum - Add model number mappings (A3063, A3064, A3065) - Add Bluetooth model ID (0x3F20) for BLE detection - Update icon mappings to use AirPods Pro icons --- linux/ble/blemanager.cpp | 3 ++- linux/enums.h | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/linux/ble/blemanager.cpp b/linux/ble/blemanager.cpp index d75a2a62..9e00f1a9 100644 --- a/linux/ble/blemanager.cpp +++ b/linux/ble/blemanager.cpp @@ -18,7 +18,8 @@ AirpodsTrayApp::Enums::AirPodsModel getModelName(quint16 modelId) {0x1F20, AirPodsModel::AirPodsMaxUSBC}, {0x0E20, AirPodsModel::AirPodsPro}, {0x1420, AirPodsModel::AirPodsPro2Lightning}, - {0x2420, AirPodsModel::AirPodsPro2USBC} + {0x2420, AirPodsModel::AirPodsPro2USBC}, + {0x3F20, AirPodsModel::AirPodsPro3} }; return modelMap.value(modelId, AirPodsModel::Unknown); diff --git a/linux/enums.h b/linux/enums.h index 815415db..2c3708c3 100644 --- a/linux/enums.h +++ b/linux/enums.h @@ -30,6 +30,7 @@ namespace AirpodsTrayApp AirPodsPro, AirPodsPro2Lightning, AirPodsPro2USBC, + AirPodsPro3, AirPodsMaxLightning, AirPodsMaxUSBC, AirPods4, @@ -63,7 +64,10 @@ namespace AirpodsTrayApp {"A3054", AirPodsModel::AirPods4}, {"A3056", AirPodsModel::AirPods4ANC}, {"A3055", AirPodsModel::AirPods4ANC}, - {"A3057", AirPodsModel::AirPods4ANC}}; + {"A3057", AirPodsModel::AirPods4ANC}, + {"A3063", AirPodsModel::AirPodsPro3}, + {"A3064", AirPodsModel::AirPodsPro3}, + {"A3065", AirPodsModel::AirPodsPro3}}; return modelNumberMap.value(modelNumber, AirPodsModel::Unknown); } @@ -82,6 +86,7 @@ namespace AirpodsTrayApp case AirPodsModel::AirPodsPro: case AirPodsModel::AirPodsPro2Lightning: case AirPodsModel::AirPodsPro2USBC: + case AirPodsModel::AirPodsPro3: return {"podpro.png", "podpro_case.png"}; case AirPodsModel::AirPodsMaxLightning: case AirPodsModel::AirPodsMaxUSBC: From 939f7bacdcb9ce7cb9e9feaf7489fbc22ce960ed Mon Sep 17 00:00:00 2001 From: 1jehuang Date: Sun, 28 Dec 2025 06:17:31 -0800 Subject: [PATCH 3/4] Add additional Bluetooth model ID (0x2720) for AirPods Pro 3 Discovered via BLE scanning that AirPods Pro 3 can also broadcast with model ID 0x2720 in addition to 0x3F20. This ensures the model is correctly identified regardless of which ID is used. --- linux/ble/blemanager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/linux/ble/blemanager.cpp b/linux/ble/blemanager.cpp index 9e00f1a9..91bb1873 100644 --- a/linux/ble/blemanager.cpp +++ b/linux/ble/blemanager.cpp @@ -19,6 +19,7 @@ AirpodsTrayApp::Enums::AirPodsModel getModelName(quint16 modelId) {0x0E20, AirPodsModel::AirPodsPro}, {0x1420, AirPodsModel::AirPodsPro2Lightning}, {0x2420, AirPodsModel::AirPodsPro2USBC}, + {0x2720, AirPodsModel::AirPodsPro3}, {0x3F20, AirPodsModel::AirPodsPro3} }; From 42bbf3f663aba006867ebd13b8eb88988dff75a7 Mon Sep 17 00:00:00 2001 From: 1jehuang Date: Sun, 28 Dec 2025 07:14:04 -0800 Subject: [PATCH 4/4] Add --no-tray option to disable system tray icon Useful when using external status bars (like waybar) that query battery info via D-Bus instead of relying on the tray icon. Usage: librepods --hide --no-tray --- linux/main.cpp | 15 ++++++++++----- linux/trayiconmanager.cpp | 7 +++++-- linux/trayiconmanager.h | 3 ++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/linux/main.cpp b/linux/main.cpp index bce48237..73972190 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -51,17 +51,17 @@ class AirPodsTrayApp : public QObject { Q_PROPERTY(bool hearingAidEnabled READ hearingAidEnabled WRITE setHearingAidEnabled NOTIFY hearingAidEnabledChanged) public: - AirPodsTrayApp(bool debugMode, bool hideOnStart, QQmlApplicationEngine *parent = nullptr) + AirPodsTrayApp(bool debugMode, bool hideOnStart, bool noTray = false, QQmlApplicationEngine *parent = nullptr) : QObject(parent), debugMode(debugMode), m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp")) - , m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), parent(parent) + , m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), m_noTray(noTray), parent(parent) , m_deviceInfo(new DeviceInfo(this)), m_bleManager(new BleManager(this)) , m_systemSleepMonitor(new SystemSleepMonitor(this)) { QLoggingCategory::setFilterRules(QString("librepods.debug=%1").arg(debugMode ? "true" : "false")); LOG_INFO("Initializing LibrePods"); - // Initialize tray icon and connect signals - trayManager = new TrayIconManager(this); + // Initialize tray icon and connect signals (skip if --no-tray) + trayManager = new TrayIconManager(this, noTray); trayManager->setNotificationsEnabled(loadNotificationsEnabled()); connect(trayManager, &TrayIconManager::trayClicked, this, &AirPodsTrayApp::onTrayIconActivated); connect(trayManager, &TrayIconManager::openApp, this, &AirPodsTrayApp::onOpenApp); @@ -1008,6 +1008,7 @@ private slots: AutoStartManager *m_autoStartManager; int m_retryAttempts = 3; bool m_hideOnStart = false; + bool m_noTray = false; DeviceInfo *m_deviceInfo; BleManager *m_bleManager; SystemSleepMonitor *m_systemSleepMonitor = nullptr; @@ -1060,18 +1061,22 @@ int main(int argc, char *argv[]) { bool debugMode = false; bool hideOnStart = false; + bool noTray = false; for (int i = 1; i < argc; ++i) { if (QString(argv[i]) == "--debug") debugMode = true; if (QString(argv[i]) == "--hide") hideOnStart = true; + + if (QString(argv[i]) == "--no-tray") + noTray = true; } QQmlApplicationEngine engine; qmlRegisterType("me.kavishdevar.Battery", 1, 0, "Battery"); qmlRegisterType("me.kavishdevar.DeviceInfo", 1, 0, "DeviceInfo"); - AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine); + AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, noTray, &engine); engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp); // Expose PHONE_MAC_ADDRESS environment variable to QML for placeholder in settings diff --git a/linux/trayiconmanager.cpp b/linux/trayiconmanager.cpp index 738feecf..70c5e960 100644 --- a/linux/trayiconmanager.cpp +++ b/linux/trayiconmanager.cpp @@ -11,7 +11,7 @@ using namespace AirpodsTrayApp::Enums; -TrayIconManager::TrayIconManager(QObject *parent) : QObject(parent) +TrayIconManager::TrayIconManager(QObject *parent, bool noTray) : QObject(parent), m_noTray(noTray) { // Initialize tray icon trayIcon = new QSystemTrayIcon(QIcon(":/icons/assets/airpods.png"), this); @@ -24,7 +24,10 @@ TrayIconManager::TrayIconManager(QObject *parent) : QObject(parent) trayIcon->setContextMenu(trayMenu); connect(trayIcon, &QSystemTrayIcon::activated, this, &TrayIconManager::onTrayIconActivated); - trayIcon->show(); + // Only show tray icon if not disabled + if (!noTray) { + trayIcon->show(); + } } void TrayIconManager::showNotification(const QString &title, const QString &message) diff --git a/linux/trayiconmanager.h b/linux/trayiconmanager.h index 25c15304..65ecf323 100644 --- a/linux/trayiconmanager.h +++ b/linux/trayiconmanager.h @@ -13,7 +13,7 @@ class TrayIconManager : public QObject Q_PROPERTY(bool notificationsEnabled READ notificationsEnabled WRITE setNotificationsEnabled NOTIFY notificationsEnabledChanged) public: - explicit TrayIconManager(QObject *parent = nullptr); + explicit TrayIconManager(QObject *parent = nullptr, bool noTray = false); void updateBatteryStatus(const QString &status); @@ -51,6 +51,7 @@ private slots: QAction *caToggleAction; QActionGroup *noiseControlGroup; bool m_notificationsEnabled = true; + bool m_noTray = false; void setupMenuActions();