From 8a69dbe173d9a73c780eac64c98c3a0d3c93d61b Mon Sep 17 00:00:00 2001 From: Tim Gromeyer <58736434+tim-gromeyer@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:07:30 +0200 Subject: [PATCH 1/3] [Linux] Move all device related properties to new class (#135) * Clean up code * Move all device releated properties to new class --- linux/BasicControlCommand.hpp | 27 ++-- linux/CMakeLists.txt | 1 + linux/airpods_packets.h | 13 +- linux/battery.hpp | 2 + linux/deviceinfo.hpp | 209 +++++++++++++++++++++++++ linux/main.cpp | 285 +++++++++------------------------- 6 files changed, 314 insertions(+), 223 deletions(-) create mode 100644 linux/deviceinfo.hpp diff --git a/linux/BasicControlCommand.hpp b/linux/BasicControlCommand.hpp index 747068e..8fe3f3e 100644 --- a/linux/BasicControlCommand.hpp +++ b/linux/BasicControlCommand.hpp @@ -18,22 +18,12 @@ namespace ControlCommand return packet; } - // Parse activated/not activated - inline std::optional parseActive(const QByteArray &data) + inline std::optional parseActive(const QByteArray &data) { if (!data.startsWith(ControlCommand::HEADER)) return std::nullopt; - quint8 statusByte = static_cast(data.at(7)); - switch (statusByte) - { - case 0x01: // Enabled - return true; - case 0x02: // Disabled - return false; - default: - return std::nullopt; - } + return static_cast(data.at(7)); } } @@ -54,6 +44,19 @@ struct BasicControlCommand // Basically returns the byte at the index 7 static std::optional parseState(const QByteArray &data) + { + switch (ControlCommand::parseActive(data).value_or(0x00)) + { + case 0x01: // Enabled + return true; + case 0x02: // Disabled + return false; + default: + return std::nullopt; + } + } + + static std::optional getValue(const QByteArray &data) { return ControlCommand::parseActive(data); } diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 186b108..cae68ef 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -23,6 +23,7 @@ qt_add_executable(applinux BluetoothMonitor.h autostartmanager.hpp BasicControlCommand.hpp + deviceinfo.hpp ) qt_add_qml_module(applinux diff --git a/linux/airpods_packets.h b/linux/airpods_packets.h index e7a4db1..58e1361 100644 --- a/linux/airpods_packets.h +++ b/linux/airpods_packets.h @@ -13,6 +13,7 @@ namespace AirPodsPackets // Noise Control Mode Packets namespace NoiseControl { + using NoiseControlMode = AirpodsTrayApp::Enums::NoiseControlMode; static const QByteArray HEADER = ControlCommand::HEADER + 0x0D; static const QByteArray OFF = ControlCommand::createCommand(0x0D, 0x01); static const QByteArray NOISE_CANCELLATION = ControlCommand::createCommand(0x0D, 0x02); @@ -21,7 +22,6 @@ namespace AirPodsPackets static const QByteArray getPacketForMode(AirpodsTrayApp::Enums::NoiseControlMode mode) { - using NoiseControlMode = AirpodsTrayApp::Enums::NoiseControlMode; switch (mode) { case NoiseControlMode::Off: @@ -36,6 +36,17 @@ namespace AirPodsPackets return QByteArray(); } } + + inline std::optional parseMode(const QByteArray &data) + { + char mode = ControlCommand::parseActive(data).value_or(CHAR_MAX); + if (mode < static_cast(NoiseControlMode::MinValue) || + mode > static_cast(NoiseControlMode::MaxValue)) + { + return std::nullopt; + } + return static_cast(mode - 1); + } } // One Bud ANC Mode diff --git a/linux/battery.hpp b/linux/battery.hpp index 6211901..a8a95a2 100644 --- a/linux/battery.hpp +++ b/linux/battery.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include #include diff --git a/linux/deviceinfo.hpp b/linux/deviceinfo.hpp new file mode 100644 index 0000000..4bbabde --- /dev/null +++ b/linux/deviceinfo.hpp @@ -0,0 +1,209 @@ +#pragma once + +#include +#include +#include "battery.hpp" +#include "enums.h" + +using namespace AirpodsTrayApp::Enums; + +class DeviceInfo : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString batteryStatus READ batteryStatus WRITE setBatteryStatus NOTIFY batteryStatusChanged) + Q_PROPERTY(QString earDetectionStatus READ earDetectionStatus WRITE setEarDetectionStatus NOTIFY earDetectionStatusChanged) + Q_PROPERTY(int noiseControlMode READ noiseControlModeInt WRITE setNoiseControlModeInt NOTIFY noiseControlModeChangedInt) + Q_PROPERTY(bool conversationalAwareness READ conversationalAwareness WRITE setConversationalAwareness NOTIFY conversationalAwarenessChanged) + Q_PROPERTY(int adaptiveNoiseLevel READ adaptiveNoiseLevel WRITE setAdaptiveNoiseLevel NOTIFY adaptiveNoiseLevelChanged) + Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged) + Q_PROPERTY(Battery *battery READ getBattery CONSTANT) + Q_PROPERTY(bool primaryInEar READ isPrimaryInEar WRITE setPrimaryInEar NOTIFY primaryChanged) + Q_PROPERTY(bool secondaryInEar READ isSecondaryInEar WRITE setSecondaryInEar NOTIFY primaryChanged) + Q_PROPERTY(bool oneBudANCMode READ oneBudANCMode WRITE setOneBudANCMode NOTIFY oneBudANCModeChanged) + Q_PROPERTY(AirPodsModel model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(bool adaptiveModeActive READ adaptiveModeActive NOTIFY noiseControlModeChangedInt) + Q_PROPERTY(QString podIcon READ podIcon NOTIFY modelChanged) + Q_PROPERTY(QString caseIcon READ caseIcon NOTIFY modelChanged) + Q_PROPERTY(bool leftPodInEar READ isLeftPodInEar NOTIFY primaryChanged) + Q_PROPERTY(bool rightPodInEar READ isRightPodInEar NOTIFY primaryChanged) + +public: + explicit DeviceInfo(QObject *parent = nullptr) : QObject(parent), m_battery(new Battery(this)) {} + + QString batteryStatus() const { return m_batteryStatus; } + void setBatteryStatus(const QString &status) + { + if (m_batteryStatus != status) + { + m_batteryStatus = status; + emit batteryStatusChanged(status); + } + } + + QString earDetectionStatus() const { return m_earDetectionStatus; } + void setEarDetectionStatus(const QString &status) + { + if (m_earDetectionStatus != status) + { + m_earDetectionStatus = status; + emit earDetectionStatusChanged(status); + } + } + + NoiseControlMode noiseControlMode() const { return m_noiseControlMode; } + void setNoiseControlMode(NoiseControlMode mode) + { + if (m_noiseControlMode != mode) + { + m_noiseControlMode = mode; + emit noiseControlModeChanged(mode); + emit noiseControlModeChangedInt(static_cast(mode)); + } + } + int noiseControlModeInt() const { return static_cast(noiseControlMode()); } + void setNoiseControlModeInt(int mode) { setNoiseControlMode(static_cast(mode)); } + + bool conversationalAwareness() const { return m_conversationalAwareness; } + void setConversationalAwareness(bool enabled) + { + if (m_conversationalAwareness != enabled) + { + m_conversationalAwareness = enabled; + emit conversationalAwarenessChanged(enabled); + } + } + + int adaptiveNoiseLevel() const { return m_adaptiveNoiseLevel; } + void setAdaptiveNoiseLevel(int level) + { + if (m_adaptiveNoiseLevel != level) + { + m_adaptiveNoiseLevel = level; + emit adaptiveNoiseLevelChanged(level); + } + } + + QString deviceName() const { return m_deviceName; } + void setDeviceName(const QString &name) + { + if (m_deviceName != name) + { + m_deviceName = name; + emit deviceNameChanged(name); + } + } + + Battery *getBattery() const { return m_battery; } + + bool isPrimaryInEar() const { return m_primaryInEar; } + void setPrimaryInEar(bool inEar) + { + if (m_primaryInEar != inEar) + { + m_primaryInEar = inEar; + emit primaryChanged(); + } + } + + bool isSecondaryInEar() const { return m_secoundaryInEar; } + void setSecondaryInEar(bool inEar) + { + if (m_secoundaryInEar != inEar) + { + m_secoundaryInEar = inEar; + emit primaryChanged(); + } + } + + bool oneBudANCMode() const { return m_oneBudANCMode; } + void setOneBudANCMode(bool enabled) + { + if (m_oneBudANCMode != enabled) + { + m_oneBudANCMode = enabled; + emit oneBudANCModeChanged(enabled); + } + } + + AirPodsModel model() const { return m_model; } + void setModel(AirPodsModel model) + { + if (m_model != model) + { + m_model = model; + emit modelChanged(); + } + } + + QByteArray magicAccIRK() const { return m_magicAccIRK; } + void setMagicAccIRK(const QByteArray &irk) { m_magicAccIRK = irk; } + + QByteArray magicAccEncKey() const { return m_magicAccEncKey; } + void setMagicAccEncKey(const QByteArray &key) { m_magicAccEncKey = key; } + + QString modelNumber() const { return m_modelNumber; } + void setModelNumber(const QString &modelNumber) { m_modelNumber = modelNumber; } + + QString manufacturer() const { return m_manufacturer; } + void setManufacturer(const QString &manufacturer) { m_manufacturer = manufacturer; } + + QString podIcon() const { return getModelIcon(model()).first; } + QString caseIcon() const { return getModelIcon(model()).second; } + bool isLeftPodInEar() const + { + if (getBattery()->getPrimaryPod() == Battery::Component::Left) return isPrimaryInEar(); + else return isSecondaryInEar(); + } + bool isRightPodInEar() const + { + if (getBattery()->getPrimaryPod() == Battery::Component::Right) return isPrimaryInEar(); + else return isSecondaryInEar(); + } + + bool adaptiveModeActive() const { return noiseControlMode() == NoiseControlMode::Adaptive; } + bool oneOrMorePodsInCase() const { return earDetectionStatus().contains("In case"); } + bool oneOrMorePodsInEar() const { return isPrimaryInEar() || isSecondaryInEar(); } + + void reset() + { + setDeviceName(""); + setModel(AirPodsModel::Unknown); + m_battery->reset(); + setBatteryStatus(""); + setEarDetectionStatus(""); + setPrimaryInEar(false); + setSecondaryInEar(false); + setNoiseControlMode(NoiseControlMode::Off); + } + +signals: + void batteryStatusChanged(const QString &status); + void earDetectionStatusChanged(const QString &status); + void noiseControlModeChanged(NoiseControlMode mode); + void noiseControlModeChangedInt(int mode); + void conversationalAwarenessChanged(bool enabled); + void adaptiveNoiseLevelChanged(int level); + void deviceNameChanged(const QString &name); + void primaryChanged(); + void oneBudANCModeChanged(bool enabled); + void modelChanged(); + +private: + QString m_batteryStatus; + QString m_earDetectionStatus; + NoiseControlMode m_noiseControlMode = NoiseControlMode::Off; + bool m_conversationalAwareness = false; + int m_adaptiveNoiseLevel = 50; + QString m_deviceName; + Battery *m_battery; + bool m_primaryInEar = false; + bool m_secoundaryInEar = false; + QByteArray m_magicAccIRK; + QByteArray m_magicAccEncKey; + bool m_oneBudANCMode = false; + AirPodsModel m_model = AirPodsModel::Unknown; + + // Additional metadata fields + QString m_modelNumber; + QString m_manufacturer; +}; \ No newline at end of file diff --git a/linux/main.cpp b/linux/main.cpp index 925757f..b507253 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -10,6 +10,7 @@ #include "battery.hpp" #include "BluetoothMonitor.h" #include "autostartmanager.hpp" +#include "deviceinfo.hpp" using namespace AirpodsTrayApp::Enums; @@ -17,19 +18,6 @@ Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp") class AirPodsTrayApp : public QObject { Q_OBJECT - Q_PROPERTY(QString batteryStatus READ batteryStatus NOTIFY batteryStatusChanged) - Q_PROPERTY(QString earDetectionStatus READ earDetectionStatus NOTIFY earDetectionStatusChanged) - Q_PROPERTY(int noiseControlMode READ noiseControlMode WRITE setNoiseControlMode NOTIFY noiseControlModeChanged) - Q_PROPERTY(bool conversationalAwareness READ conversationalAwareness WRITE setConversationalAwareness NOTIFY conversationalAwarenessChanged) - Q_PROPERTY(int adaptiveNoiseLevel READ adaptiveNoiseLevel WRITE setAdaptiveNoiseLevel NOTIFY adaptiveNoiseLevelChanged) - Q_PROPERTY(bool adaptiveModeActive READ adaptiveModeActive NOTIFY noiseControlModeChanged) - Q_PROPERTY(QString deviceName READ deviceName NOTIFY deviceNameChanged) - Q_PROPERTY(Battery* battery READ getBattery NOTIFY batteryStatusChanged) - Q_PROPERTY(bool oneOrMorePodsInCase READ oneOrMorePodsInCase NOTIFY earDetectionStatusChanged) - Q_PROPERTY(QString podIcon READ podIcon NOTIFY modelChanged) - Q_PROPERTY(QString caseIcon READ caseIcon NOTIFY modelChanged) - Q_PROPERTY(bool leftPodInEar READ isLeftPodInEar NOTIFY primaryChanged) - Q_PROPERTY(bool rightPodInEar READ isRightPodInEar NOTIFY primaryChanged) Q_PROPERTY(bool airpodsConnected READ areAirpodsConnected NOTIFY airPodsStatusChanged) Q_PROPERTY(int earDetectionBehavior READ earDetectionBehavior WRITE setEarDetectionBehavior NOTIFY earDetectionBehaviorChanged) Q_PROPERTY(bool crossDeviceEnabled READ crossDeviceEnabled WRITE setCrossDeviceEnabled NOTIFY crossDeviceEnabledChanged) @@ -37,24 +25,13 @@ class AirPodsTrayApp : public QObject { Q_PROPERTY(bool notificationsEnabled READ notificationsEnabled WRITE setNotificationsEnabled NOTIFY notificationsEnabledChanged) Q_PROPERTY(int retryAttempts READ retryAttempts WRITE setRetryAttempts NOTIFY retryAttemptsChanged) Q_PROPERTY(bool hideOnStart READ hideOnStart CONSTANT) - Q_PROPERTY(bool oneBudANCMode READ oneBudANCMode WRITE setOneBudANCMode NOTIFY oneBudANCModeChanged) + Q_PROPERTY(DeviceInfo *deviceInfo READ deviceInfo CONSTANT) public: AirPodsTrayApp(bool debugMode, bool hideOnStart, QQmlApplicationEngine *parent = nullptr) - : QObject(parent) - , debugMode(debugMode) - , m_battery(new Battery(this)) - , monitor(new BluetoothMonitor(this)) - , m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp")) - , m_autoStartManager(new AutoStartManager(this)) - , m_hideOnStart(hideOnStart) - , parent(parent) - { - if (debugMode) { - QLoggingCategory::setFilterRules("airpodsApp.debug=true"); - } else { - QLoggingCategory::setFilterRules("airpodsApp.debug=false"); - } + : QObject(parent), debugMode(debugMode), m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp")), m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), parent(parent), m_deviceInfo(new DeviceInfo(this)) + { + QLoggingCategory::setFilterRules(QString("airpodsApp.debug=%1").arg(debugMode ? "true" : "false")); LOG_INFO("Initializing AirPodsTrayApp"); // Initialize tray icon and connect signals @@ -63,25 +40,26 @@ public: connect(trayManager, &TrayIconManager::trayClicked, this, &AirPodsTrayApp::onTrayIconActivated); connect(trayManager, &TrayIconManager::openApp, this, &AirPodsTrayApp::onOpenApp); connect(trayManager, &TrayIconManager::openSettings, this, &AirPodsTrayApp::onOpenSettings); - connect(trayManager, &TrayIconManager::noiseControlChanged, this, qOverload(&AirPodsTrayApp::setNoiseControlMode)); + connect(trayManager, &TrayIconManager::noiseControlChanged, this, &AirPodsTrayApp::setNoiseControlMode); connect(trayManager, &TrayIconManager::conversationalAwarenessToggled, this, &AirPodsTrayApp::setConversationalAwareness); - connect(this, &AirPodsTrayApp::batteryStatusChanged, trayManager, &TrayIconManager::updateBatteryStatus); - connect(this, &AirPodsTrayApp::noiseControlModeChanged, trayManager, &TrayIconManager::updateNoiseControlState); - connect(this, &AirPodsTrayApp::conversationalAwarenessChanged, trayManager, &TrayIconManager::updateConversationalAwareness); + connect(m_deviceInfo, &DeviceInfo::batteryStatusChanged, trayManager, &TrayIconManager::updateBatteryStatus); + connect(m_deviceInfo, &DeviceInfo::noiseControlModeChanged, trayManager, &TrayIconManager::updateNoiseControlState); + connect(m_deviceInfo, &DeviceInfo::conversationalAwarenessChanged, trayManager, &TrayIconManager::updateConversationalAwareness); connect(trayManager, &TrayIconManager::notificationsEnabledChanged, this, &AirPodsTrayApp::saveNotificationsEnabled); connect(trayManager, &TrayIconManager::notificationsEnabledChanged, this, &AirPodsTrayApp::notificationsEnabledChanged); // Initialize MediaController and connect signals mediaController = new MediaController(this); - connect(this, &AirPodsTrayApp::earDetectionStatusChanged, mediaController, &MediaController::handleEarDetection); + connect(m_deviceInfo, &DeviceInfo::earDetectionStatusChanged, mediaController, &MediaController::handleEarDetection); connect(mediaController, &MediaController::mediaStateChanged, this, &AirPodsTrayApp::handleMediaStateChange); mediaController->initializeMprisInterface(); mediaController->followMediaChanges(); + monitor = new BluetoothMonitor(this); connect(monitor, &BluetoothMonitor::deviceConnected, this, &AirPodsTrayApp::bluezDeviceConnected); connect(monitor, &BluetoothMonitor::deviceDisconnected, this, &AirPodsTrayApp::bluezDeviceDisconnected); - connect(m_battery, &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged); + connect(m_deviceInfo->getBattery(), &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged); // Load settings CrossDevice.isEnabled = loadCrossDeviceEnabled(); @@ -114,31 +92,6 @@ public: delete phoneSocket; } - QString batteryStatus() const { return m_batteryStatus; } - QString earDetectionStatus() const { return m_earDetectionStatus; } - int noiseControlMode() const { return static_cast(m_noiseControlMode); } - bool conversationalAwareness() const { return m_conversationalAwareness; } - bool adaptiveModeActive() const { return m_noiseControlMode == NoiseControlMode::Adaptive; } - int adaptiveNoiseLevel() const { return m_adaptiveNoiseLevel; } - QString deviceName() const { return m_deviceName; } - Battery *getBattery() const { return m_battery; } - bool oneOrMorePodsInCase() const { return m_earDetectionStatus.contains("In case"); } - QString podIcon() const { return getModelIcon(m_model).first; } - QString caseIcon() const { return getModelIcon(m_model).second; } - bool isLeftPodInEar() const { - if (m_battery->getPrimaryPod() == Battery::Component::Left) { - return m_primaryInEar; - } else { - return m_secoundaryInEar; - } - } - bool isRightPodInEar() const { - if (m_battery->getPrimaryPod() == Battery::Component::Right) { - return m_primaryInEar; - } else { - return m_secoundaryInEar; - } - } bool areAirpodsConnected() const { return socket && socket->isOpen() && socket->state() == QBluetoothSocket::SocketState::ConnectedState; } int earDetectionBehavior() const { return mediaController->getEarDetectionBehavior(); } bool crossDeviceEnabled() const { return CrossDevice.isEnabled; } @@ -147,7 +100,7 @@ public: void setNotificationsEnabled(bool enabled) { trayManager->setNotificationsEnabled(enabled); } int retryAttempts() const { return m_retryAttempts; } bool hideOnStart() const { return m_hideOnStart; } - bool oneBudANCMode() const { return m_oneBudANCMode; } + DeviceInfo *deviceInfo() const { return m_deviceInfo; } private: bool debugMode; @@ -199,39 +152,32 @@ public slots: void setNoiseControlMode(NoiseControlMode mode) { LOG_INFO("Setting noise control mode to: " << mode); - if (m_noiseControlMode == mode) - { - LOG_INFO("Noise control mode is already " << mode); - return; - } QByteArray packet = AirPodsPackets::NoiseControl::getPacketForMode(mode); writePacketToSocket(packet, "Noise control mode packet written: "); } - void setNoiseControlMode(int mode) + void setNoiseControlModeInt(int mode) { + if (mode < 0 || mode > static_cast(NoiseControlMode::Adaptive)) + { + LOG_ERROR("Invalid noise control mode: " << mode); + return; + } setNoiseControlMode(static_cast(mode)); } void setConversationalAwareness(bool enabled) { - if (m_conversationalAwareness == enabled) - { - LOG_INFO("Conversational awareness is already " << (enabled ? "enabled" : "disabled")); - return; - } - LOG_INFO("Setting conversational awareness to: " << (enabled ? "enabled" : "disabled")); QByteArray packet = enabled ? AirPodsPackets::ConversationalAwareness::ENABLED : AirPodsPackets::ConversationalAwareness::DISABLED; writePacketToSocket(packet, "Conversational awareness packet written: "); - m_conversationalAwareness = enabled; - emit conversationalAwarenessChanged(enabled); + m_deviceInfo->setConversationalAwareness(enabled); } void setOneBudANCMode(bool enabled) { - if (m_oneBudANCMode == enabled) + if (m_deviceInfo->oneBudANCMode() == enabled) { LOG_INFO("One Bud ANC mode is already " << (enabled ? "enabled" : "disabled")); return; @@ -243,8 +189,7 @@ public slots: if (writePacketToSocket(packet, "One Bud ANC mode packet written: ")) { - m_oneBudANCMode = enabled; - emit oneBudANCModeChanged(enabled); + m_deviceInfo->setOneBudANCMode(enabled); } else { @@ -277,12 +222,11 @@ public slots: void setAdaptiveNoiseLevel(int level) { level = qBound(0, level, 100); - if (m_adaptiveNoiseLevel != level && adaptiveModeActive()) + if (m_deviceInfo->adaptiveNoiseLevel() != level && m_deviceInfo->adaptiveModeActive()) { - m_adaptiveNoiseLevel = level; QByteArray packet = AirPodsPackets::AdaptiveNoise::getPacket(level); writePacketToSocket(packet, "Adaptive noise level packet written: "); - emit adaptiveNoiseLevelChanged(level); + m_deviceInfo->setAdaptiveNoiseLevel(level); } } @@ -298,7 +242,7 @@ public slots: LOG_WARN("Name is too long, must be 32 characters or less"); return; } - if (newName == m_deviceName) + if (newName == m_deviceInfo->deviceName()) { LOG_INFO("Name is already set to: " << newName); return; @@ -308,8 +252,7 @@ public slots: if (writePacketToSocket(packet, "Rename packet written: ")) { LOG_INFO("Sent rename command for new name: " << newName); - m_deviceName = newName; - emit deviceNameChanged(newName); + m_deviceInfo->setDeviceName(newName); } else { @@ -413,7 +356,7 @@ private slots: writePacketToSocket(AirPodsPackets::Connection::HANDSHAKE, "Handshake packet written: "); } - void bluezDeviceConnected(const QString &address, const QString &name) + void bluezDeviceConnected(const QString &address, const QString &name) { QBluetoothDeviceInfo device(QBluetoothAddress(address), name, 0); connectToDevice(device); @@ -435,31 +378,7 @@ private slots: } // Clear the device name and model - m_deviceName.clear(); - connectedDeviceMacAddress.clear(); - mediaController->setConnectedDeviceMacAddress(connectedDeviceMacAddress); - m_model = AirPodsModel::Unknown; - emit deviceNameChanged(m_deviceName); - emit modelChanged(); - - // Reset battery status - m_battery->reset(); - m_batteryStatus.clear(); - emit batteryStatusChanged(m_batteryStatus); - - // Reset ear detection - m_earDetectionStatus.clear(); - m_primaryInEar = false; - m_secoundaryInEar = false; - emit earDetectionStatusChanged(m_earDetectionStatus); - emit primaryChanged(); - - // Reset noise control mode - m_noiseControlMode = NoiseControlMode::Off; - emit noiseControlModeChanged(m_noiseControlMode); - - mediaController->pause(); // Since the device is deconnected, we don't know if it was the active output device. Pause to be safe - emit airPodsStatusChanged(); + m_deviceInfo->reset(); // Show system notification trayManager->showNotification( @@ -516,43 +435,18 @@ private slots: return str; }; - m_deviceName = extractString(); - QString modelNumber = extractString(); - QString manufacturer = extractString(); - QString hardwareVersion = extractString(); - QString firmwareVersion = extractString(); - QString firmwareVersion2 = extractString(); - QString softwareVersion = extractString(); - QString appIdentifier = extractString(); - QString serialNumber1 = extractString(); - QString serialNumber2 = extractString(); - QString unknownNumeric = extractString(); - QString unknownHash = extractString(); - QString trailingByte = extractString(); - - m_model = parseModelNumber(modelNumber); + m_deviceInfo->setDeviceName(extractString()); + m_deviceInfo->setModelNumber(extractString()); + m_deviceInfo->setManufacturer(extractString()); + m_deviceInfo->setModel(parseModelNumber(m_deviceInfo->modelNumber())); emit modelChanged(); - m_model = parseModelNumber(modelNumber); - - emit modelChanged(); - emit deviceNameChanged(m_deviceName); // Log extracted metadata LOG_INFO("Parsed AirPods metadata:"); - LOG_INFO("Device Name: " << m_deviceName); - LOG_INFO("Model Number: " << modelNumber); - LOG_INFO("Manufacturer: " << manufacturer); - LOG_INFO("Hardware Version: " << hardwareVersion); - LOG_INFO("Firmware Version: " << firmwareVersion); - LOG_INFO("Firmware Version2: " << firmwareVersion2); - LOG_INFO("Software Version: " << softwareVersion); - LOG_INFO("App Identifier: " << appIdentifier); - LOG_INFO("Serial Number 1: " << serialNumber1); - LOG_INFO("Serial Number 2: " << serialNumber2); - LOG_INFO("Unknown Numeric: " << unknownNumeric); - LOG_INFO("Unknown Hash: " << unknownHash); - LOG_INFO("Trailing Byte: " << trailingByte); + LOG_INFO("Device Name: " << m_deviceInfo->deviceName()); + LOG_INFO("Model Number: " << m_deviceInfo->modelNumber()); + LOG_INFO("Manufacturer: " << m_deviceInfo->manufacturer()); } QString getEarStatus(char value) @@ -606,7 +500,7 @@ private slots: QTimer::singleShot(1500, this, [this, device]() { connectToDevice(device); }); } - else + else { LOG_ERROR("Failed to connect after 3 attempts"); retryCount = 0; @@ -635,7 +529,7 @@ private slots: writePacketToSocket(AirPodsPackets::Connection::REQUEST_NOTIFICATIONS, "Request notifications packet written: "); QTimer::singleShot(2000, this, [this]() { - if (m_batteryStatus.isEmpty()) { + if (m_deviceInfo->batteryStatus().isEmpty()) { writePacketToSocket(AirPodsPackets::Connection::REQUEST_NOTIFICATIONS, "Request notifications packet written: "); } }); @@ -648,34 +542,26 @@ private slots: LOG_INFO("MagicAccIRK: " << keys.magicAccIRK.toHex()); LOG_INFO("MagicAccEncKey: " << keys.magicAccEncKey.toHex()); - // Store the keys for later use if needed - m_magicAccIRK = keys.magicAccIRK; - m_magicAccEncKey = keys.magicAccEncKey; + // Store the keys + m_deviceInfo->setMagicAccIRK(keys.magicAccIRK); + m_deviceInfo->setMagicAccEncKey(keys.magicAccEncKey); } // Get CA state else if (data.startsWith(AirPodsPackets::ConversationalAwareness::HEADER)) { - auto result = AirPodsPackets::ConversationalAwareness::parseState(data); - if (result.has_value()) { - m_conversationalAwareness = result.value(); - LOG_INFO("Conversational awareness state received: " << m_conversationalAwareness); - emit conversationalAwarenessChanged(m_conversationalAwareness); - } else { - LOG_ERROR("Failed to parse conversational awareness state"); + if (auto result = AirPodsPackets::ConversationalAwareness::parseState(data)) + { + m_deviceInfo->setConversationalAwareness(result.value()); + LOG_INFO("Conversational awareness state received: " << m_deviceInfo->conversationalAwareness()); } } // Noise Control Mode else if (data.size() == 11 && data.startsWith(AirPodsPackets::NoiseControl::HEADER)) { - quint8 rawMode = data[7] - 1; // Offset still needed due to protocol - if (rawMode >= (int)NoiseControlMode::MinValue && rawMode <= (int)NoiseControlMode::MaxValue) + if (auto value = AirPodsPackets::NoiseControl::parseMode(data)) { - m_noiseControlMode = static_cast(rawMode); - LOG_INFO("Noise control mode: " << rawMode); - emit noiseControlModeChanged(m_noiseControlMode); - } - else - { - LOG_ERROR("Invalid noise control mode value received: " << rawMode); + LOG_INFO("Received noise control mode: " << value.value()); + m_deviceInfo->setNoiseControlMode(value.value()); + LOG_INFO("Noise control mode received: " << m_deviceInfo->noiseControlMode()); } } // Ear Detection @@ -683,28 +569,25 @@ private slots: { char primary = data[6]; char secondary = data[7]; - m_primaryInEar = data[6] == 0x00; - m_secoundaryInEar = data[7] == 0x00; - m_earDetectionStatus = QString("Primary: %1, Secondary: %2") - .arg(getEarStatus(primary), getEarStatus(secondary)); - LOG_INFO("Ear detection status: " << m_earDetectionStatus); - emit earDetectionStatusChanged(m_earDetectionStatus); - emit primaryChanged(); + m_deviceInfo->setPrimaryInEar(data[6] == 0x00); + m_deviceInfo->setSecondaryInEar(data[7] == 0x00); + m_deviceInfo->setEarDetectionStatus(QString("Primary: %1, Secondary: %2") + .arg(getEarStatus(primary), getEarStatus(secondary))); + LOG_INFO("Ear detection status: " << m_deviceInfo->earDetectionStatus()); } // Battery Status else if (data.size() == 22 && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS)) { - m_battery->parsePacket(data); + m_deviceInfo->getBattery()->parsePacket(data); - int leftLevel = m_battery->getState(Battery::Component::Left).level; - int rightLevel = m_battery->getState(Battery::Component::Right).level; - int caseLevel = m_battery->getState(Battery::Component::Case).level; - m_batteryStatus = QString("Left: %1%, Right: %2%, Case: %3%") - .arg(leftLevel) - .arg(rightLevel) - .arg(caseLevel); - LOG_INFO("Battery status: " << m_batteryStatus); - emit batteryStatusChanged(m_batteryStatus); + int leftLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Left).level; + int rightLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Right).level; + int caseLevel = m_deviceInfo->getBattery()->getState(Battery::Component::Case).level; + m_deviceInfo->setBatteryStatus(QString("Left: %1%, Right: %2%, Case: %3%") + .arg(leftLevel) + .arg(rightLevel) + .arg(caseLevel)); + LOG_INFO("Battery status: " << m_deviceInfo->batteryStatus()); } // Conversational Awareness Data else if (data.size() == 10 && data.startsWith(AirPodsPackets::ConversationalAwareness::DATA_HEADER)) @@ -717,23 +600,17 @@ private slots: parseMetadata(data); initiateMagicPairing(); mediaController->setConnectedDeviceMacAddress(connectedDeviceMacAddress); - if (isLeftPodInEar() || isRightPodInEar()) // AirPods get added as output device only after this + if (m_deviceInfo->oneOrMorePodsInEar()) // AirPods get added as output device only after this { mediaController->activateA2dpProfile(); } emit airPodsStatusChanged(); } else if (data.startsWith(AirPodsPackets::OneBudANCMode::HEADER)) { - auto result = AirPodsPackets::OneBudANCMode::parseState(data); - if (result.has_value()) + if (auto value = AirPodsPackets::OneBudANCMode::parseState(data)) { - m_oneBudANCMode = result.value(); - LOG_INFO("One Bud ANC mode received: " << m_conversationalAwareness); - emit oneBudANCModeChanged(m_conversationalAwareness); - } - else - { - LOG_ERROR("Failed to parse One Bud ANC mode"); + m_deviceInfo->setOneBudANCMode(value.value()); + LOG_INFO("One Bud ANC mode received: " << m_deviceInfo->oneBudANCMode()); } } else @@ -856,14 +733,14 @@ private slots: QMetaObject::invokeMethod(this, "handlePhonePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data)); } - public: - void handleMediaStateChange(MediaController::MediaState state) { - if (state == MediaController::MediaState::Playing) { - LOG_INFO("Media started playing, sending disconnect request to Android and taking over audio"); - sendDisconnectRequestToAndroid(); - connectToAirPods(true); - } +public: + void handleMediaStateChange(MediaController::MediaState state) { + if (state == MediaController::MediaState::Playing) { + LOG_INFO("Media started playing, sending disconnect request to Android and taking over audio"); + sendDisconnectRequestToAndroid(); + connectToAirPods(true); } + } void sendDisconnectRequestToAndroid() { @@ -980,20 +857,7 @@ private: AutoStartManager *m_autoStartManager; int m_retryAttempts = 3; bool m_hideOnStart = false; - - QString m_batteryStatus; - QString m_earDetectionStatus; - NoiseControlMode m_noiseControlMode = NoiseControlMode::Off; - bool m_conversationalAwareness = false; - int m_adaptiveNoiseLevel = 50; - QString m_deviceName; - Battery *m_battery; - AirPodsModel m_model = AirPodsModel::Unknown; - bool m_primaryInEar = false; - bool m_secoundaryInEar = false; - QByteArray m_magicAccIRK; - QByteArray m_magicAccEncKey; - bool m_oneBudANCMode = false; + DeviceInfo *m_deviceInfo; }; int main(int argc, char *argv[]) { @@ -1038,6 +902,7 @@ int main(int argc, char *argv[]) { QQmlApplicationEngine engine; qmlRegisterType("me.kavishdevar.Battery", 1, 0, "Battery"); + qmlRegisterType("me.kavishdevar.DeviceInfo", 1, 0, "DeviceInfo"); AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine); engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp); trayApp->loadMainModule(); From c05a37bcca35a9375c2a01e2e815b0fdf77e58b9 Mon Sep 17 00:00:00 2001 From: Tim Gromeyer <58736434+tim-gromeyer@users.noreply.github.com> Date: Tue, 3 Jun 2025 10:31:19 +0200 Subject: [PATCH 2/3] [Linux] Fix UI not working (#137) * Move mac adress to deviceinfo * Missing changes --- linux/Main.qml | 48 ++++++++++++++++++++++---------------------- linux/deviceinfo.hpp | 39 +++++++++++++++++++++++++++++++++-- linux/main.cpp | 19 +++++++++--------- 3 files changed, 70 insertions(+), 36 deletions(-) diff --git a/linux/Main.qml b/linux/Main.qml index 28e9eee..c225cd0 100644 --- a/linux/Main.qml +++ b/linux/Main.qml @@ -94,46 +94,46 @@ ApplicationWindow { spacing: 8 PodColumn { - isVisible: airPodsTrayApp.battery.leftPodAvailable - inEar: airPodsTrayApp.leftPodInEar - iconSource: "qrc:/icons/assets/" + airPodsTrayApp.podIcon - batteryLevel: airPodsTrayApp.battery.leftPodLevel - isCharging: airPodsTrayApp.battery.leftPodCharging + isVisible: airPodsTrayApp.deviceInfo.battery.leftPodAvailable + inEar: airPodsTrayApp.deviceInfo.leftPodInEar + iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon + batteryLevel: airPodsTrayApp.deviceInfo.battery.leftPodLevel + isCharging: airPodsTrayApp.deviceInfo.battery.leftPodCharging indicator: "L" } PodColumn { - isVisible: airPodsTrayApp.battery.rightPodAvailable - inEar: airPodsTrayApp.rightPodInEar - iconSource: "qrc:/icons/assets/" + airPodsTrayApp.podIcon - batteryLevel: airPodsTrayApp.battery.rightPodLevel - isCharging: airPodsTrayApp.battery.rightPodCharging + isVisible: airPodsTrayApp.deviceInfo.battery.rightPodAvailable + inEar: airPodsTrayApp.deviceInfo.rightPodInEar + iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon + batteryLevel: airPodsTrayApp.deviceInfo.battery.rightPodLevel + isCharging: airPodsTrayApp.deviceInfo.battery.rightPodCharging indicator: "R" } PodColumn { - isVisible: airPodsTrayApp.battery.caseAvailable + isVisible: airPodsTrayApp.deviceInfo.battery.caseAvailable inEar: true - iconSource: "qrc:/icons/assets/" + airPodsTrayApp.caseIcon - batteryLevel: airPodsTrayApp.battery.caseLevel - isCharging: airPodsTrayApp.battery.caseCharging + iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.caseIcon + batteryLevel: airPodsTrayApp.deviceInfo.battery.caseLevel + isCharging: airPodsTrayApp.deviceInfo.battery.caseCharging } } SegmentedControl { anchors.horizontalCenter: parent.horizontalCenter model: ["Off", "Noise Cancellation", "Transparency", "Adaptive"] - currentIndex: airPodsTrayApp.noiseControlMode - onCurrentIndexChanged: airPodsTrayApp.noiseControlMode = currentIndex + currentIndex: airPodsTrayApp.deviceInfo.noiseControlMode + onCurrentIndexChanged: airPodsTrayApp.setNoiseControlModeInt(currentIndex) visible: airPodsTrayApp.airpodsConnected } Slider { - visible: airPodsTrayApp.adaptiveModeActive + visible: airPodsTrayApp.deviceInfo.adaptiveModeActive from: 0 to: 100 stepSize: 1 - value: airPodsTrayApp.adaptiveNoiseLevel + value: airPodsTrayApp.deviceInfo.adaptiveNoiseLevel Timer { id: debounceTimer @@ -153,8 +153,8 @@ ApplicationWindow { Switch { visible: airPodsTrayApp.airpodsConnected text: "Conversational Awareness" - checked: airPodsTrayApp.conversationalAwareness - onCheckedChanged: airPodsTrayApp.conversationalAwareness = checked + checked: airPodsTrayApp.deviceInfo.conversationalAwareness + onCheckedChanged: airPodsTrayApp.setConversationalAwareness(checked) } } @@ -229,8 +229,8 @@ ApplicationWindow { Switch { visible: airPodsTrayApp.airpodsConnected text: "One Bud ANC Mode" - checked: airPodsTrayApp.oneBudANCMode - onCheckedChanged: airPodsTrayApp.oneBudANCMode = checked + checked: airPodsTrayApp.deviceInfo.oneBudANCMode + onCheckedChanged: airPodsTrayApp.deviceInfo.oneBudANCMode = checked ToolTip { visible: parent.hovered @@ -259,13 +259,13 @@ ApplicationWindow { TextField { id: newNameField - placeholderText: airPodsTrayApp.deviceName + placeholderText: airPodsTrayApp.deviceInfo.deviceName maximumLength: 32 } Button { text: "Rename" - onClicked: airPodsTrayApp.renameAirPods(newNameField.text) + onClicked: airPodsTrayApp.deviceInfo.renameAirPods(newNameField.text) } } } diff --git a/linux/deviceinfo.hpp b/linux/deviceinfo.hpp index 4bbabde..ceca235 100644 --- a/linux/deviceinfo.hpp +++ b/linux/deviceinfo.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "battery.hpp" #include "enums.h" @@ -26,6 +27,7 @@ class DeviceInfo : public QObject Q_PROPERTY(QString caseIcon READ caseIcon NOTIFY modelChanged) Q_PROPERTY(bool leftPodInEar READ isLeftPodInEar NOTIFY primaryChanged) Q_PROPERTY(bool rightPodInEar READ isRightPodInEar NOTIFY primaryChanged) + Q_PROPERTY(QString bluetoothAddress READ bluetoothAddress WRITE setBluetoothAddress NOTIFY bluetoothAddressChanged) public: explicit DeviceInfo(QObject *parent = nullptr) : QObject(parent), m_battery(new Battery(this)) {} @@ -147,6 +149,16 @@ public: QString manufacturer() const { return m_manufacturer; } void setManufacturer(const QString &manufacturer) { m_manufacturer = manufacturer; } + QString bluetoothAddress() const { return m_bluetoothAddress; } + void setBluetoothAddress(const QString &address) + { + if (m_bluetoothAddress != address) + { + m_bluetoothAddress = address; + emit bluetoothAddressChanged(address); + } + } + QString podIcon() const { return getModelIcon(model()).first; } QString caseIcon() const { return getModelIcon(model()).second; } bool isLeftPodInEar() const @@ -174,6 +186,29 @@ public: setPrimaryInEar(false); setSecondaryInEar(false); setNoiseControlMode(NoiseControlMode::Off); + setBluetoothAddress(""); + } + + void save() const + { + QSettings settings("AirpodsTrayApp", "DeviceInfo"); + settings.beginGroup("DeviceInfo"); + settings.setValue("deviceName", m_deviceName); + settings.setValue("bluetoothAddress", m_bluetoothAddress); + settings.setValue("magicAccIRK", m_magicAccIRK.toBase64()); + settings.setValue("magicAccEncKey", m_magicAccEncKey.toBase64()); + settings.endGroup(); + } + + void load() + { + QSettings settings("AirpodsTrayApp", "DeviceInfo"); + settings.beginGroup("DeviceInfo"); + setDeviceName(settings.value("deviceName", "").toString()); + setBluetoothAddress(settings.value("bluetoothAddress", "").toString()); + setMagicAccIRK(QByteArray::fromBase64(settings.value("magicAccIRK", "").toByteArray())); + setMagicAccEncKey(QByteArray::fromBase64(settings.value("magicAccEncKey", "").toByteArray())); + settings.endGroup(); } signals: @@ -187,6 +222,7 @@ signals: void primaryChanged(); void oneBudANCModeChanged(bool enabled); void modelChanged(); + void bluetoothAddressChanged(const QString &address); private: QString m_batteryStatus; @@ -202,8 +238,7 @@ private: QByteArray m_magicAccEncKey; bool m_oneBudANCMode = false; AirPodsModel m_model = AirPodsModel::Unknown; - - // Additional metadata fields QString m_modelNumber; QString m_manufacturer; + QString m_bluetoothAddress; }; \ No newline at end of file diff --git a/linux/main.cpp b/linux/main.cpp index b507253..7cb09a9 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -389,11 +389,11 @@ private slots: void bluezDeviceDisconnected(const QString &address, const QString &name) { - if (address == connectedDeviceMacAddress.replace("_", ":")) + if (address == m_deviceInfo->bluetoothAddress()) { - onDeviceDisconnected(QBluetoothAddress(address)); } - else { - LOG_WARN("Disconnected device does not match connected device: " << address << " != " << connectedDeviceMacAddress); + onDeviceDisconnected(QBluetoothAddress(address)); + } else { + LOG_WARN("Disconnected device does not match connected device: " << address << " != " << m_deviceInfo->bluetoothAddress()); } } @@ -512,7 +512,7 @@ private slots: this, handleError); localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a")); - connectedDeviceMacAddress = device.address().toString().replace(":", "_"); + m_deviceInfo->setBluetoothAddress(device.address().toString()); notifyAndroidDevice(); } @@ -599,7 +599,7 @@ private slots: { parseMetadata(data); initiateMagicPairing(); - mediaController->setConnectedDeviceMacAddress(connectedDeviceMacAddress); + mediaController->setConnectedDeviceMacAddress(m_deviceInfo->bluetoothAddress().replace(":", "_")); if (m_deviceInfo->oneOrMorePodsInEar()) // AirPods get added as output device only after this { mediaController->activateA2dpProfile(); @@ -708,7 +708,7 @@ private slots: socket->close(); LOG_INFO("Disconnected from AirPods"); QProcess process; - process.start("bluetoothctl", QStringList() << "disconnect" << connectedDeviceMacAddress.replace("_", ":")); + process.start("bluetoothctl", QStringList() << "disconnect" << m_deviceInfo->bluetoothAddress()); process.waitForFinished(); QString output = process.readAllStandardOutput().trimmed(); LOG_INFO("Bluetoothctl output: " << output); @@ -770,13 +770,13 @@ public: if (force) { LOG_INFO("Forcing connection to AirPods"); QProcess process; - process.start("bluetoothctl", QStringList() << "connect" << connectedDeviceMacAddress.replace("_", ":")); + process.start("bluetoothctl", QStringList() << "connect" << m_deviceInfo->bluetoothAddress()); process.waitForFinished(); QString output = process.readAllStandardOutput().trimmed(); LOG_INFO("Bluetoothctl output: " << output); if (output.contains("Connection successful")) { LOG_INFO("Connection successful, proceeding with L2CAP connection"); - QBluetoothAddress btAddress(connectedDeviceMacAddress.replace("_", ":")); + QBluetoothAddress btAddress(m_deviceInfo->bluetoothAddress()); forceL2capConnection(btAddress); } else { LOG_ERROR("Connection failed, cannot proceed with L2CAP connection"); @@ -847,7 +847,6 @@ signals: private: QBluetoothSocket *socket = nullptr; QBluetoothSocket *phoneSocket = nullptr; - QString connectedDeviceMacAddress; QByteArray lastBatteryStatus; QByteArray lastEarDetectionStatus; MediaController* mediaController; From 96baebee28c299195202fc615df9397b0e9387de Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Tue, 3 Jun 2025 14:22:57 +0530 Subject: [PATCH 3/3] Update name in linux README --- linux/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/README.md b/linux/README.md index f0e30db..552dd25 100644 --- a/linux/README.md +++ b/linux/README.md @@ -1,4 +1,4 @@ -# ALN Linux app +# LibrePods Linux A native Linux application to control your AirPods, with support for: