From 2fe9724da5379244aa8e5bd418d35b3e88c903c8 Mon Sep 17 00:00:00 2001 From: Tim Gromeyer <58736434+tim-gromeyer@users.noreply.github.com> Date: Thu, 17 Apr 2025 08:30:31 +0200 Subject: [PATCH] [Linux] Improve connection stability (#98) --- linux/BluetoothMonitor.cpp | 130 ++++++++++++++++------ linux/BluetoothMonitor.h | 12 +- linux/Main.qml | 1 + linux/main.cpp | 219 +++++++++++++++++-------------------- 4 files changed, 208 insertions(+), 154 deletions(-) diff --git a/linux/BluetoothMonitor.cpp b/linux/BluetoothMonitor.cpp index 66501f9..eae23e8 100644 --- a/linux/BluetoothMonitor.cpp +++ b/linux/BluetoothMonitor.cpp @@ -2,10 +2,16 @@ #include "logger.h" #include +#include +#include -BluetoothMonitor::BluetoothMonitor(QObject *parent) +BluetoothMonitor::BluetoothMonitor(QObject *parent) : QObject(parent), m_dbus(QDBusConnection::systemBus()) { + // Register meta-types for D-Bus interaction + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + if (!m_dbus.isConnected()) { LOG_WARN("Failed to connect to system D-Bus"); @@ -13,6 +19,7 @@ BluetoothMonitor::BluetoothMonitor(QObject *parent) } registerDBusService(); + checkAlreadyConnectedDevices(); // Check for already connected devices on startup } BluetoothMonitor::~BluetoothMonitor() @@ -23,18 +30,6 @@ BluetoothMonitor::~BluetoothMonitor() void BluetoothMonitor::registerDBusService() { // Match signals for PropertiesChanged on any BlueZ Device interface - QString matchRule = QStringLiteral("type='signal'," - "interface='org.freedesktop.DBus.Properties'," - "member='PropertiesChanged'," - "path_namespace='/org/bluez'"); - - m_dbus.connect("org.freedesktop.DBus", - "/org/freedesktop/DBus", - "org.freedesktop.DBus", - "AddMatch", - this, - SLOT(onPropertiesChanged(QString, QVariantMap, QStringList))); - if (!m_dbus.connect("", "", "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onPropertiesChanged(QString, QVariantMap, QStringList)))) { @@ -42,6 +37,86 @@ void BluetoothMonitor::registerDBusService() } } +bool BluetoothMonitor::isAirPodsDevice(const QString &devicePath) +{ + QDBusInterface deviceInterface("org.bluez", devicePath, "org.freedesktop.DBus.Properties", m_dbus); + + // Get UUIDs to check if it's an AirPods device + QDBusReply uuidsReply = deviceInterface.call("Get", "org.bluez.Device1", "UUIDs"); + if (!uuidsReply.isValid()) + { + return false; + } + + QStringList uuids = uuidsReply.value().toStringList(); + return uuids.contains("74ec2172-0bad-4d01-8f77-997b2be0722a"); +} + +QString BluetoothMonitor::getDeviceName(const QString &devicePath) +{ + QDBusInterface deviceInterface("org.bluez", devicePath, "org.freedesktop.DBus.Properties", m_dbus); + QDBusReply nameReply = deviceInterface.call("Get", "org.bluez.Device1", "Name"); + if (nameReply.isValid()) + { + return nameReply.value().toString(); + } + return "Unknown"; +} + +bool BluetoothMonitor::checkAlreadyConnectedDevices() +{ + QDBusInterface objectManager("org.bluez", "/", "org.freedesktop.DBus.ObjectManager", m_dbus); + QDBusMessage reply = objectManager.call("GetManagedObjects"); + + if (reply.type() == QDBusMessage::ErrorMessage) + { + LOG_WARN("Failed to get managed objects: " << reply.errorMessage()); + return false; + } + + QVariant firstArg = reply.arguments().constFirst(); + QDBusArgument arg = firstArg.value(); + ManagedObjectList managedObjects; + arg >> managedObjects; + + bool deviceFound = false; + + for (auto it = managedObjects.constBegin(); it != managedObjects.constEnd(); ++it) + { + const QDBusObjectPath &objPath = it.key(); + const QMap &interfaces = it.value(); + + if (interfaces.contains("org.bluez.Device1")) + { + const QVariantMap &deviceProps = interfaces.value("org.bluez.Device1"); + + // Check if the device has the necessary properties + if (!deviceProps.contains("UUIDs") || !deviceProps.contains("Connected") || + !deviceProps.contains("Address") || !deviceProps.contains("Name")) + { + continue; + } + + QStringList uuids = deviceProps["UUIDs"].toStringList(); + bool isAirPods = uuids.contains("74ec2172-0bad-4d01-8f77-997b2be0722a"); + + if (isAirPods) + { + bool connected = deviceProps["Connected"].toBool(); + if (connected) + { + QString macAddress = deviceProps["Address"].toString(); + QString deviceName = deviceProps["Name"].toString(); + emit deviceConnected(macAddress, deviceName); + LOG_DEBUG("Found already connected AirPods: " << macAddress << " Name: " << deviceName); + deviceFound = true; + } + } + } + } + return deviceFound; +} + void BluetoothMonitor::onPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps) { Q_UNUSED(invalidatedProps); @@ -56,8 +131,13 @@ void BluetoothMonitor::onPropertiesChanged(const QString &interface, const QVari bool connected = changedProps["Connected"].toBool(); QString path = QDBusContext::message().path(); + if (!isAirPodsDevice(path)) + { + return; + } + QDBusInterface deviceInterface("org.bluez", path, "org.freedesktop.DBus.Properties", m_dbus); - + // Get the device address QDBusReply addrReply = deviceInterface.call("Get", "org.bluez.Device1", "Address"); if (!addrReply.isValid()) @@ -65,29 +145,17 @@ void BluetoothMonitor::onPropertiesChanged(const QString &interface, const QVari return; } QString macAddress = addrReply.value().toString(); - - // Get UUIDs to check if it's an AirPods device - QDBusReply uuidsReply = deviceInterface.call("Get", "org.bluez.Device1", "UUIDs"); - if (!uuidsReply.isValid()) - { - return; - } - - QStringList uuids = uuidsReply.value().toStringList(); - if (!uuids.contains("74ec2172-0bad-4d01-8f77-997b2be0722a")) - { - return; // Not an AirPods device - } + QString deviceName = getDeviceName(path); if (connected) { - emit deviceConnected(macAddress); - LOG_DEBUG("AirPods device connected:" << macAddress); + emit deviceConnected(macAddress, deviceName); + LOG_DEBUG("AirPods device connected:" << macAddress << " Name:" << deviceName); } else { - emit deviceDisconnected(macAddress); - LOG_DEBUG("AirPods device disconnected:" << macAddress); + emit deviceDisconnected(macAddress, deviceName); + LOG_DEBUG("AirPods device disconnected:" << macAddress << " Name:" << deviceName); } } } \ No newline at end of file diff --git a/linux/BluetoothMonitor.h b/linux/BluetoothMonitor.h index c91a32c..48758af 100644 --- a/linux/BluetoothMonitor.h +++ b/linux/BluetoothMonitor.h @@ -4,6 +4,10 @@ #include #include +// Forward declarations for D-Bus types +typedef QMap> ManagedObjectList; +Q_DECLARE_METATYPE(ManagedObjectList) + class BluetoothMonitor : public QObject, protected QDBusContext { Q_OBJECT @@ -11,9 +15,11 @@ public: explicit BluetoothMonitor(QObject *parent = nullptr); ~BluetoothMonitor(); + bool checkAlreadyConnectedDevices(); + signals: - void deviceConnected(const QString &macAddress); - void deviceDisconnected(const QString &macAddress); + void deviceConnected(const QString &macAddress, const QString &deviceName); + void deviceDisconnected(const QString &macAddress, const QString &deviceName); private slots: void onPropertiesChanged(const QString &interface, const QVariantMap &changedProps, const QStringList &invalidatedProps); @@ -21,6 +27,8 @@ private slots: private: QDBusConnection m_dbus; void registerDBusService(); + bool isAirPodsDevice(const QString &devicePath); + QString getDeviceName(const QString &devicePath); }; #endif // BLUETOOTHMONITOR_H \ No newline at end of file diff --git a/linux/Main.qml b/linux/Main.qml index 08b62cb..98a4ab1 100644 --- a/linux/Main.qml +++ b/linux/Main.qml @@ -104,6 +104,7 @@ ApplicationWindow { model: ["Off", "Noise Cancellation", "Transparency", "Adaptive"] currentIndex: airPodsTrayApp.noiseControlMode onCurrentIndexChanged: airPodsTrayApp.noiseControlMode = currentIndex + visible: airPodsTrayApp.airpodsConnected } Text { diff --git a/linux/main.cpp b/linux/main.cpp index f04db4c..01ee3a2 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -28,6 +28,7 @@ class AirPodsTrayApp : 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(bool airpodsConnected READ areAirpodsConnected NOTIFY airPodsStatusChanged) public: AirPodsTrayApp(bool debugMode) @@ -64,13 +65,8 @@ public: CrossDevice.isEnabled = loadCrossDeviceEnabled(); - discoveryAgent = new QBluetoothDeviceDiscoveryAgent(); - discoveryAgent->setLowEnergyDiscoveryTimeout(15000); - - connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &AirPodsTrayApp::onDeviceDiscovered); - connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &AirPodsTrayApp::onDiscoveryFinished); - discoveryAgent->start(); - LOG_INFO("AirPodsTrayApp initialized and started device discovery"); + monitor->checkAlreadyConnectedDevices(); + LOG_INFO("AirPodsTrayApp initialized"); QBluetoothLocalDevice localDevice; @@ -92,7 +88,6 @@ public: delete trayIcon; delete trayMenu; - delete discoveryAgent; delete socket; delete phoneSocket; } @@ -122,6 +117,7 @@ public: return m_secoundaryInEar; } } + bool areAirpodsConnected() const { return socket && socket->isOpen() && socket->state() == QBluetoothSocket::SocketState::ConnectedState; } private: bool debugMode; @@ -140,6 +136,10 @@ private: void notifyAndroidDevice() { + if (!CrossDevice.isEnabled) { + return; + } + if (phoneSocket && phoneSocket->isOpen()) { phoneSocket->write(AirPodsPackets::Phone::NOTIFICATION); @@ -163,21 +163,6 @@ public slots: connectToDevice(device); } - void showAvailableDevices() { - LOG_INFO("Showing available devices"); - QStringList devices; - const QList discoveredDevices = discoveryAgent->discoveredDevices(); - for (const QBluetoothDeviceInfo &device : discoveredDevices) { - devices << device.address().toString() + " - " + device.name(); - } - bool ok; - QString selectedDevice = QInputDialog::getItem(nullptr, "Select Device", "Devices:", devices, 0, false, &ok); - if (ok && !selectedDevice.isEmpty()) { - QString address = selectedDevice.split(" - ").first(); - connectToDevice(address); - } - } - void setNoiseControlMode(NoiseControlMode mode) { LOG_INFO("Setting noise control mode to: " << mode); @@ -308,46 +293,12 @@ private slots: void sendHandshake() { LOG_INFO("Connected to device, sending initial packets"); - discoveryAgent->stop(); writePacketToSocket(AirPodsPackets::Connection::HANDSHAKE, "Handshake packet written: "); } - void onDeviceDiscovered(const QBluetoothDeviceInfo &device) { - QByteArray manufacturerData = device.manufacturerData(MANUFACTURER_ID); - if (manufacturerData.startsWith(MANUFACTURER_DATA)) { - LOG_INFO("Detected AirPods via BLE manufacturer data"); - connectToDevice(device.address().toString()); - } - LOG_INFO("Device discovered: " << device.name() << " (" << device.address().toString() << ")"); - if (isAirPodsDevice(device)) { - LOG_DEBUG("Found AirPods device: " + device.name()); - connectToDevice(device); - } - } - - void onDiscoveryFinished() { - LOG_INFO("Device discovery finished"); - discoveryAgent->start(); - const QList discoveredDevices = discoveryAgent->discoveredDevices(); - for (const QBluetoothDeviceInfo &device : discoveredDevices) { - if (isAirPodsDevice(device)) { - connectToDevice(device); - return; - } - } - LOG_WARN("No device with the specified UUID found"); - } - - void onDeviceConnected(const QBluetoothAddress &address) { - LOG_INFO("Device connected: " << address.toString()); - QBluetoothDeviceInfo device(address, "", 0); - if (isAirPodsDevice(device)) { - connectToDevice(device); - } - } - void bluezDeviceConnected(const QString &address) + void bluezDeviceConnected(const QString &address, const QString &name) { - QBluetoothDeviceInfo device(QBluetoothAddress(address), "", 0); + QBluetoothDeviceInfo device(QBluetoothAddress(address), name, 0); connectToDevice(device); } @@ -366,42 +317,43 @@ private slots: LOG_DEBUG("AIRPODS_DISCONNECTED packet written: " << AirPodsPackets::Connection::AIRPODS_DISCONNECTED.toHex()); } + // Clear the device name and model + m_deviceName.clear(); + connectedDeviceMacAddress.clear(); + 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 - discoveryAgent->start(); + emit airPodsStatusChanged(); // Show system notification trayManager->showNotification( tr("AirPods Disconnected"), tr("Your AirPods have been disconnected")); } - void bluezDeviceDisconnected(const QString &address) + + void bluezDeviceDisconnected(const QString &address, const QString &name) { if (address == connectedDeviceMacAddress.replace("_", ":")) { - onDeviceDisconnected(QBluetoothAddress(address)); - - // Clear the device name and model - m_deviceName.clear(); - 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); - } + onDeviceDisconnected(QBluetoothAddress(address)); } else { LOG_WARN("Disconnected device does not match connected device: " << address << " != " << connectedDeviceMacAddress); } @@ -484,53 +436,74 @@ private slots: LOG_INFO("Trailing Byte: " << trailingByte); } - void connectToDevice(const QBluetoothDeviceInfo &device) { - if (socket && socket->isOpen() && socket->peerAddress() == device.address()) { + QString getEarStatus(char value) + { + return (value == 0x00) ? "In Ear" : (value == 0x01) ? "Out of Ear" + : "In case"; + } + + void connectToDevice(const QBluetoothDeviceInfo &device) + { + if (socket && socket->isOpen() && socket->peerAddress() == device.address()) + { LOG_INFO("Already connected to the device: " << device.name()); return; } LOG_INFO("Connecting to device: " << device.name()); + + // Clean up any existing socket + if (socket) + { + socket->close(); + socket->deleteLater(); + socket = nullptr; + } + QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); - connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() { - // Start periodic magic pairing attempts - QTimer *magicPairingTimer = new QTimer(this); - connect(magicPairingTimer, &QTimer::timeout, this, [this, magicPairingTimer]() { - if (m_magicAccIRK.isEmpty() || m_magicAccEncKey.isEmpty()) { - initiateMagicPairing(); - } else { - magicPairingTimer->stop(); - magicPairingTimer->deleteLater(); - } - }); - magicPairingTimer->start(500); - - connect(localSocket, &QBluetoothSocket::readyRead, this, [this, localSocket]() { - QByteArray data = localSocket->readAll(); - QMetaObject::invokeMethod(this, "parseData", Qt::QueuedConnection, Q_ARG(QByteArray, data)); - QMetaObject::invokeMethod(this, "relayPacketToPhone", Qt::QueuedConnection, Q_ARG(QByteArray, data)); - }); - - sendHandshake(); - }); - - connect(localSocket, QOverload::of(&QBluetoothSocket::errorOccurred), this, [this, localSocket](QBluetoothSocket::SocketError error) { - LOG_ERROR("Socket error: " << error << ", " << localSocket->errorString()); - }); - socket = localSocket; + + // Connection handler + auto handleConnection = [this, localSocket]() + { + connect(localSocket, &QBluetoothSocket::readyRead, this, [this, localSocket]() + { + QByteArray data = localSocket->readAll(); + QMetaObject::invokeMethod(this, "parseData", Qt::QueuedConnection, Q_ARG(QByteArray, data)); + QMetaObject::invokeMethod(this, "relayPacketToPhone", Qt::QueuedConnection, Q_ARG(QByteArray, data)); }); + sendHandshake(); + }; + + // Error handler with retry + auto handleError = [this, device, localSocket](QBluetoothSocket::SocketError error) + { + LOG_ERROR("Socket error: " << error << ", " << localSocket->errorString()); + + static int retryCount = 0; + if (retryCount < 3) + { + retryCount++; + LOG_INFO("Retrying connection (attempt " << retryCount << ")"); + QTimer::singleShot(1500, this, [this, device]() + { connectToDevice(device); }); + } + else + { + LOG_ERROR("Failed to connect after 3 attempts"); + retryCount = 0; + } + }; + + connect(localSocket, &QBluetoothSocket::connected, this, handleConnection); + connect(localSocket, QOverload::of(&QBluetoothSocket::errorOccurred), + this, handleError); + localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a")); connectedDeviceMacAddress = device.address().toString().replace(":", "_"); mediaController->setConnectedDeviceMacAddress(connectedDeviceMacAddress); notifyAndroidDevice(); } - QString getEarStatus(char value) - { - return (value == 0x00) ? "In Ear" : (value == 0x01) ? "Out of Ear" - : "In case"; - } - void parseData(const QByteArray &data) { LOG_DEBUG("Received: " << data.toHex()); @@ -539,7 +512,7 @@ private slots: { writePacketToSocket(AirPodsPackets::Connection::SET_SPECIFIC_FEATURES, "Set specific features packet written: "); } - if (data.startsWith(AirPodsPackets::Parse::FEATURES_ACK)) + else if (data.startsWith(AirPodsPackets::Parse::FEATURES_ACK)) { writePacketToSocket(AirPodsPackets::Connection::REQUEST_NOTIFICATIONS, "Request notifications packet written: "); @@ -626,6 +599,8 @@ private slots: else if (data.startsWith(AirPodsPackets::Parse::METADATA)) { parseMetadata(data); + initiateMagicPairing(); + emit airPodsStatusChanged(); } else { @@ -754,6 +729,8 @@ private slots: void sendDisconnectRequestToAndroid() { + if (!CrossDevice.isEnabled) return; + if (phoneSocket && phoneSocket->isOpen()) { phoneSocket->write(AirPodsPackets::Phone::DISCONNECT_REQUEST); @@ -841,11 +818,11 @@ signals: void deviceNameChanged(const QString &name); void modelChanged(); void primaryChanged(); + void airPodsStatusChanged(); private: QSystemTrayIcon *trayIcon; QMenu *trayMenu; - QBluetoothDeviceDiscoveryAgent *discoveryAgent; QBluetoothSocket *socket = nullptr; QBluetoothSocket *phoneSocket = nullptr; QString connectedDeviceMacAddress;