diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 85fdcff..5fe9f94 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -31,6 +31,7 @@ qt_add_executable(applinux thirdparty/QR-Code-generator/qrcodegen.cpp thirdparty/QR-Code-generator/qrcodegen.hpp QRCodeImageProvider.hpp + eardetection.hpp ) qt_add_qml_module(applinux diff --git a/linux/Main.qml b/linux/Main.qml index fd67ca9..9c4026b 100644 --- a/linux/Main.qml +++ b/linux/Main.qml @@ -94,7 +94,7 @@ ApplicationWindow { spacing: 8 PodColumn { - isVisible: airPodsTrayApp.deviceInfo.battery.leftPodAvailable + visible: airPodsTrayApp.deviceInfo.battery.leftPodAvailable inEar: airPodsTrayApp.deviceInfo.leftPodInEar iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon batteryLevel: airPodsTrayApp.deviceInfo.battery.leftPodLevel @@ -103,7 +103,7 @@ ApplicationWindow { } PodColumn { - isVisible: airPodsTrayApp.deviceInfo.battery.rightPodAvailable + visible: airPodsTrayApp.deviceInfo.battery.rightPodAvailable inEar: airPodsTrayApp.deviceInfo.rightPodInEar iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.podIcon batteryLevel: airPodsTrayApp.deviceInfo.battery.rightPodLevel @@ -112,7 +112,7 @@ ApplicationWindow { } PodColumn { - isVisible: airPodsTrayApp.deviceInfo.battery.caseAvailable + visible: airPodsTrayApp.deviceInfo.battery.caseAvailable inEar: true iconSource: "qrc:/icons/assets/" + airPodsTrayApp.deviceInfo.caseIcon batteryLevel: airPodsTrayApp.deviceInfo.battery.caseLevel diff --git a/linux/PodColumn.qml b/linux/PodColumn.qml index 66fe14b..c68497c 100644 --- a/linux/PodColumn.qml +++ b/linux/PodColumn.qml @@ -1,16 +1,25 @@ import QtQuick 2.15 Column { - property bool isVisible: true + id: root property bool inEar: true property string iconSource property int batteryLevel: 0 property bool isCharging: false property string indicator: "" + property real targetOpacity: inEar ? 1 : 0.5 + + Timer { + id: opacityTimer + interval: 50 + onTriggered: root.opacity = root.targetOpacity + } + + onInEarChanged: { + opacityTimer.restart() + } spacing: 5 - opacity: inEar ? 1 : 0.5 - visible: isVisible Image { source: parent.iconSource diff --git a/linux/battery.hpp b/linux/battery.hpp index ba57107..e0defca 100644 --- a/linux/battery.hpp +++ b/linux/battery.hpp @@ -7,6 +7,7 @@ #include #include "airpods_packets.h" +#include "logger.h" class Battery : public QObject { @@ -128,10 +129,14 @@ public: // Emit signal to notify about battery status change emit batteryStatusChanged(); + // Log which is left and right pod + LOG_INFO("Primary Pod:" << primaryPod); + LOG_INFO("Secondary Pod:" << secondaryPod); + return true; } - bool parseEncryptedPacket(const QByteArray &packet, bool isLeftPodPrimary) + bool parseEncryptedPacket(const QByteArray &packet, bool isLeftPodPrimary, bool podInCase) { // Validate packet size (expect 16 bytes based on provided payloads) if (packet.size() != 16) @@ -171,7 +176,9 @@ public: // Update states states[Component::Left] = {static_cast(rawLeftBattery), isLeftCharging ? BatteryStatus::Charging : BatteryStatus::Discharging}; states[Component::Right] = {static_cast(rawRightBattery), isRightCharging ? BatteryStatus::Charging : BatteryStatus::Discharging}; - states[Component::Case] = {static_cast(rawCaseBattery), isCaseCharging ? BatteryStatus::Charging : BatteryStatus::Discharging}; + if (podInCase) { + states[Component::Case] = {static_cast(rawCaseBattery), isCaseCharging ? BatteryStatus::Charging : BatteryStatus::Discharging}; + } primaryPod = isLeftPodPrimary ? Component::Left : Component::Right; secondaryPod = isLeftPodPrimary ? Component::Right : Component::Left; emit batteryStatusChanged(); diff --git a/linux/deviceinfo.hpp b/linux/deviceinfo.hpp index eb6a540..6e5e17f 100644 --- a/linux/deviceinfo.hpp +++ b/linux/deviceinfo.hpp @@ -5,6 +5,7 @@ #include #include "battery.hpp" #include "enums.h" +#include "eardetection.hpp" using namespace AirpodsTrayApp::Enums; @@ -12,14 +13,11 @@ 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) @@ -32,7 +30,9 @@ class DeviceInfo : public QObject Q_PROPERTY(QString magicAccEncKey READ magicAccEncKeyHex CONSTANT) public: - explicit DeviceInfo(QObject *parent = nullptr) : QObject(parent), m_battery(new Battery(this)) {} + explicit DeviceInfo(QObject *parent = nullptr) : QObject(parent), m_battery(new Battery(this)), m_earDetection(new EarDetection(this)) { + connect(getEarDetection(), &EarDetection::statusChanged, this, &DeviceInfo::primaryChanged); + } QString batteryStatus() const { return m_batteryStatus; } void setBatteryStatus(const QString &status) @@ -44,16 +44,6 @@ public: } } - 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) { @@ -99,26 +89,6 @@ public: 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) { @@ -167,18 +137,18 @@ public: QString caseIcon() const { return getModelIcon(model()).second; } bool isLeftPodInEar() const { - if (getBattery()->getPrimaryPod() == Battery::Component::Left) return isPrimaryInEar(); - else return isSecondaryInEar(); + if (getBattery()->getPrimaryPod() == Battery::Component::Left) return getEarDetection()->isPrimaryInEar(); + else return getEarDetection()->isSecondaryInEar(); } bool isRightPodInEar() const { - if (getBattery()->getPrimaryPod() == Battery::Component::Right) return isPrimaryInEar(); - else return isSecondaryInEar(); + if (getBattery()->getPrimaryPod() == Battery::Component::Right) return getEarDetection()->isPrimaryInEar(); + else return getEarDetection()->isSecondaryInEar(); } bool adaptiveModeActive() const { return noiseControlMode() == NoiseControlMode::Adaptive; } - bool oneOrMorePodsInCase() const { return earDetectionStatus().contains("In case"); } - bool oneOrMorePodsInEar() const { return isPrimaryInEar() || isSecondaryInEar(); } + + EarDetection *getEarDetection() const { return m_earDetection; } void reset() { @@ -186,11 +156,9 @@ public: setModel(AirPodsModel::Unknown); m_battery->reset(); setBatteryStatus(""); - setEarDetectionStatus(""); - setPrimaryInEar(false); - setSecondaryInEar(false); setNoiseControlMode(NoiseControlMode::Off); setBluetoothAddress(""); + getEarDetection()->reset(); } void saveToSettings(QSettings &settings) @@ -210,9 +178,16 @@ public: setMagicAccEncKey(settings.value("DeviceInfo/magicAccEncKey", QByteArray()).toByteArray()); } + void updateBatteryStatus() + { + int leftLevel = getBattery()->getState(Battery::Component::Left).level; + int rightLevel = getBattery()->getState(Battery::Component::Right).level; + int caseLevel = getBattery()->getState(Battery::Component::Case).level; + setBatteryStatus(QString("Left: %1%, Right: %2%, Case: %3%").arg(leftLevel).arg(rightLevel).arg(caseLevel)); + } + signals: void batteryStatusChanged(const QString &status); - void earDetectionStatusChanged(const QString &status); void noiseControlModeChanged(NoiseControlMode mode); void noiseControlModeChangedInt(int mode); void conversationalAwarenessChanged(bool enabled); @@ -225,14 +200,11 @@ signals: private: QString m_batteryStatus; - QString m_earDetectionStatus; NoiseControlMode m_noiseControlMode = NoiseControlMode::Transparency; 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; @@ -240,4 +212,5 @@ private: QString m_modelNumber; QString m_manufacturer; QString m_bluetoothAddress; + EarDetection *m_earDetection; }; \ No newline at end of file diff --git a/linux/eardetection.hpp b/linux/eardetection.hpp new file mode 100644 index 0000000..04ef2a9 --- /dev/null +++ b/linux/eardetection.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include "logger.h" + +class EarDetection : public QObject +{ + Q_OBJECT + +public: + enum class EarDetectionStatus + { + InEar, + NotInEar, + InCase, + Disconnected, + }; + Q_ENUM(EarDetectionStatus) + + explicit EarDetection(QObject *parent = nullptr) : QObject(parent) + { + reset(); + } + + void reset() + { + primaryStatus = EarDetectionStatus::Disconnected; + secondaryStatus = EarDetectionStatus::Disconnected; + emit statusChanged(); + } + + bool parseData(const QByteArray &data) + { + if (data.size() < 2) + { + return false; + } + + auto [newprimaryStatus, newsecondaryStatus] = parseStatusBytes(data); + + primaryStatus = newprimaryStatus; + secondaryStatus = newsecondaryStatus; + LOG_DEBUG("Parsed Ear Detection Status: Primary - " << primaryStatus + << ", Secondary - " << secondaryStatus); + emit statusChanged(); + + return true; + } + void overrideEarDetectionStatus(bool primaryInEar, bool secondaryInEar) + { + primaryStatus = primaryInEar ? EarDetectionStatus::InEar : EarDetectionStatus::NotInEar; + secondaryStatus = secondaryInEar ? EarDetectionStatus::InEar : EarDetectionStatus::NotInEar; + emit statusChanged(); + } + + bool isPrimaryInEar() const { return primaryStatus == EarDetectionStatus::InEar; } + bool isSecondaryInEar() const { return secondaryStatus == EarDetectionStatus::InEar; } + bool oneOrMorePodsInCase() const { return primaryStatus == EarDetectionStatus::InCase || secondaryStatus == EarDetectionStatus::InCase; } + bool oneOrMorePodsInEar() const { return isPrimaryInEar() || isSecondaryInEar(); } + + EarDetectionStatus getprimaryStatus() const { return primaryStatus; } + EarDetectionStatus getsecondaryStatus() const { return secondaryStatus; } + +signals: + void statusChanged(); + +private: + QPair parseStatusBytes(const QByteArray &data) const + { + quint8 primaryByte = static_cast(data[6]); + quint8 secondaryByte = static_cast(data[7]); + + auto primaryStatus = parseStatusByte(primaryByte); + auto secondaryStatus = parseStatusByte(secondaryByte); + + return qMakePair(primaryStatus, secondaryStatus); + } + + EarDetectionStatus parseStatusByte(quint8 byte) const + { + if (byte == 0x00) + return EarDetectionStatus::InEar; + if (byte == 0x01) + return EarDetectionStatus::NotInEar; + if (byte == 0x02) + return EarDetectionStatus::InCase; + return EarDetectionStatus::Disconnected; + } + + EarDetectionStatus primaryStatus = EarDetectionStatus::Disconnected; + EarDetectionStatus secondaryStatus = EarDetectionStatus::Disconnected; +}; \ No newline at end of file diff --git a/linux/main.cpp b/linux/main.cpp index 28219da..4c8702e 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -65,7 +65,6 @@ public: // Initialize MediaController and connect signals mediaController = new MediaController(this); - connect(m_deviceInfo, &DeviceInfo::earDetectionStatusChanged, mediaController, &MediaController::handleEarDetection); connect(mediaController, &MediaController::mediaStateChanged, this, &AirPodsTrayApp::handleMediaStateChange); mediaController->initializeMprisInterface(); mediaController->followMediaChanges(); @@ -590,26 +589,14 @@ private slots: // Ear Detection else if (data.size() == 8 && data.startsWith(AirPodsPackets::Parse::EAR_DETECTION)) { - char primary = data[6]; - char secondary = data[7]; - 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()); + m_deviceInfo->getEarDetection()->parseData(data); + mediaController->handleEarDetection(m_deviceInfo->getEarDetection()); } // Battery Status else if (data.size() == 22 && data.startsWith(AirPodsPackets::Parse::BATTERY_STATUS)) { m_deviceInfo->getBattery()->parsePacket(data); - - 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)); + m_deviceInfo->updateBatteryStatus(); LOG_INFO("Battery status: " << m_deviceInfo->batteryStatus()); } // Conversational Awareness Data @@ -623,7 +610,7 @@ private slots: parseMetadata(data); initiateMagicPairing(); mediaController->setConnectedDeviceMacAddress(m_deviceInfo->bluetoothAddress().replace(":", "_")); - if (m_deviceInfo->oneOrMorePodsInEar()) // AirPods get added as output device only after this + if (m_deviceInfo->getEarDetection()->oneOrMorePodsInEar()) // AirPods get added as output device only after this { mediaController->activateA2dpProfile(); } @@ -762,9 +749,8 @@ private slots: if (BLEUtils::isValidIrkRpa(m_deviceInfo->magicAccIRK(), device.address)) { m_deviceInfo->setModel(device.modelName); auto decryptet = BLEUtils::decryptLastBytes(device.encryptedPayload, m_deviceInfo->magicAccEncKey()); - m_deviceInfo->getBattery()->parseEncryptedPacket(decryptet, device.primaryLeft); - m_deviceInfo->setPrimaryInEar(device.isPrimaryInEar); - m_deviceInfo->setSecondaryInEar(device.isSecondaryInEar); + m_deviceInfo->getBattery()->parseEncryptedPacket(decryptet, device.primaryLeft, device.isThisPodInTheCase); + m_deviceInfo->getEarDetection()->overrideEarDetectionStatus(device.isPrimaryInEar, device.isSecondaryInEar); } } diff --git a/linux/mediacontroller.cpp b/linux/mediacontroller.cpp index 65438dd..c9db7b5 100644 --- a/linux/mediacontroller.cpp +++ b/linux/mediacontroller.cpp @@ -1,5 +1,6 @@ #include "mediacontroller.h" #include "logger.h" +#include "eardetection.hpp" #include #include @@ -38,7 +39,7 @@ void MediaController::initializeMprisInterface() { } } -void MediaController::handleEarDetection(const QString &status) +void MediaController::handleEarDetection(EarDetection *earDetection) { if (earDetectionBehavior == Disabled) { @@ -46,15 +47,8 @@ void MediaController::handleEarDetection(const QString &status) return; } - bool primaryInEar = false; - bool secondaryInEar = false; - - QStringList parts = status.split(", "); - if (parts.size() == 2) - { - primaryInEar = parts[0].contains("In Ear"); - secondaryInEar = parts[1].contains("In Ear"); - } + bool primaryInEar = earDetection->isPrimaryInEar(); + bool secondaryInEar = earDetection->isSecondaryInEar(); LOG_DEBUG("Ear detection status: primaryInEar=" << primaryInEar << ", secondaryInEar=" << secondaryInEar diff --git a/linux/mediacontroller.h b/linux/mediacontroller.h index 2df071f..9fbf9ee 100644 --- a/linux/mediacontroller.h +++ b/linux/mediacontroller.h @@ -5,6 +5,7 @@ #include class QProcess; +class EarDetection; class MediaController : public QObject { @@ -29,7 +30,7 @@ public: ~MediaController(); void initializeMprisInterface(); - void handleEarDetection(const QString &status); + void handleEarDetection(EarDetection*); void followMediaChanges(); bool isActiveOutputDeviceAirPods(); void handleConversationalAwareness(const QByteArray &data);