mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-28 22:01:50 +00:00
[Linux] New ear detection implementation (#145)
* New ear detection implementation * [Linux] Improved case battery detection when not connected
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <climits>
|
||||
|
||||
#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<quint8>(rawLeftBattery), isLeftCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
states[Component::Right] = {static_cast<quint8>(rawRightBattery), isRightCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
states[Component::Case] = {static_cast<quint8>(rawCaseBattery), isCaseCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
if (podInCase) {
|
||||
states[Component::Case] = {static_cast<quint8>(rawCaseBattery), isCaseCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
}
|
||||
primaryPod = isLeftPodPrimary ? Component::Left : Component::Right;
|
||||
secondaryPod = isLeftPodPrimary ? Component::Right : Component::Left;
|
||||
emit batteryStatusChanged();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QSettings>
|
||||
#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;
|
||||
};
|
||||
94
linux/eardetection.hpp
Normal file
94
linux/eardetection.hpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QPair>
|
||||
#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<EarDetectionStatus, EarDetectionStatus> parseStatusBytes(const QByteArray &data) const
|
||||
{
|
||||
quint8 primaryByte = static_cast<quint8>(data[6]);
|
||||
quint8 secondaryByte = static_cast<quint8>(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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "mediacontroller.h"
|
||||
#include "logger.h"
|
||||
#include "eardetection.hpp"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QProcess>
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QObject>
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user