diff --git a/linux/README.md b/linux.old/README.md similarity index 100% rename from linux/README.md rename to linux.old/README.md diff --git a/linux/aln/AirPods/Pro2.py b/linux.old/aln/AirPods/Pro2.py similarity index 100% rename from linux/aln/AirPods/Pro2.py rename to linux.old/aln/AirPods/Pro2.py diff --git a/linux/aln/AirPods/__init__.py b/linux.old/aln/AirPods/__init__.py similarity index 100% rename from linux/aln/AirPods/__init__.py rename to linux.old/aln/AirPods/__init__.py diff --git a/linux/aln/Capabilites/__init__.py b/linux.old/aln/Capabilites/__init__.py similarity index 100% rename from linux/aln/Capabilites/__init__.py rename to linux.old/aln/Capabilites/__init__.py diff --git a/linux/aln/Notifications/ANC.py b/linux.old/aln/Notifications/ANC.py similarity index 100% rename from linux/aln/Notifications/ANC.py rename to linux.old/aln/Notifications/ANC.py diff --git a/linux/aln/Notifications/Battery.py b/linux.old/aln/Notifications/Battery.py similarity index 100% rename from linux/aln/Notifications/Battery.py rename to linux.old/aln/Notifications/Battery.py diff --git a/linux/aln/Notifications/ConversationalAwareness.py b/linux.old/aln/Notifications/ConversationalAwareness.py similarity index 100% rename from linux/aln/Notifications/ConversationalAwareness.py rename to linux.old/aln/Notifications/ConversationalAwareness.py diff --git a/linux/aln/Notifications/EarDetection.py b/linux.old/aln/Notifications/EarDetection.py similarity index 100% rename from linux/aln/Notifications/EarDetection.py rename to linux.old/aln/Notifications/EarDetection.py diff --git a/linux/aln/Notifications/Listener.py b/linux.old/aln/Notifications/Listener.py similarity index 100% rename from linux/aln/Notifications/Listener.py rename to linux.old/aln/Notifications/Listener.py diff --git a/linux/aln/Notifications/__init__.py b/linux.old/aln/Notifications/__init__.py similarity index 100% rename from linux/aln/Notifications/__init__.py rename to linux.old/aln/Notifications/__init__.py diff --git a/linux/aln/__init__.py b/linux.old/aln/__init__.py similarity index 100% rename from linux/aln/__init__.py rename to linux.old/aln/__init__.py diff --git a/linux/aln/__main__.py b/linux.old/aln/__main__.py similarity index 100% rename from linux/aln/__main__.py rename to linux.old/aln/__main__.py diff --git a/linux/aln/enums.py b/linux.old/aln/enums.py similarity index 100% rename from linux/aln/enums.py rename to linux.old/aln/enums.py diff --git a/linux/aln/listener.py b/linux.old/aln/listener.py similarity index 100% rename from linux/aln/listener.py rename to linux.old/aln/listener.py diff --git a/linux/examples/daemon/ear-detection.py b/linux.old/examples/daemon/ear-detection.py similarity index 100% rename from linux/examples/daemon/ear-detection.py rename to linux.old/examples/daemon/ear-detection.py diff --git a/linux/examples/daemon/read-data.py b/linux.old/examples/daemon/read-data.py similarity index 100% rename from linux/examples/daemon/read-data.py rename to linux.old/examples/daemon/read-data.py diff --git a/linux/examples/daemon/set-anc.py b/linux.old/examples/daemon/set-anc.py similarity index 100% rename from linux/examples/daemon/set-anc.py rename to linux.old/examples/daemon/set-anc.py diff --git a/linux/examples/daemon/tray.py b/linux.old/examples/daemon/tray.py similarity index 100% rename from linux/examples/daemon/tray.py rename to linux.old/examples/daemon/tray.py diff --git a/linux/examples/daemon/write-data.py b/linux.old/examples/daemon/write-data.py similarity index 100% rename from linux/examples/daemon/write-data.py rename to linux.old/examples/daemon/write-data.py diff --git a/linux/examples/logger-and-anc.py b/linux.old/examples/logger-and-anc.py similarity index 100% rename from linux/examples/logger-and-anc.py rename to linux.old/examples/logger-and-anc.py diff --git a/linux/examples/standalone.py b/linux.old/examples/standalone.py similarity index 100% rename from linux/examples/standalone.py rename to linux.old/examples/standalone.py diff --git a/linux/icon.png b/linux.old/icon.png similarity index 100% rename from linux/icon.png rename to linux.old/icon.png diff --git a/linux/imgs/daemon-log.png b/linux.old/imgs/daemon-log.png similarity index 100% rename from linux/imgs/daemon-log.png rename to linux.old/imgs/daemon-log.png diff --git a/linux/imgs/ear-detection.png b/linux.old/imgs/ear-detection.png similarity index 100% rename from linux/imgs/ear-detection.png rename to linux.old/imgs/ear-detection.png diff --git a/linux/imgs/read-data.png b/linux.old/imgs/read-data.png similarity index 100% rename from linux/imgs/read-data.png rename to linux.old/imgs/read-data.png diff --git a/linux/imgs/set-anc.png b/linux.old/imgs/set-anc.png similarity index 100% rename from linux/imgs/set-anc.png rename to linux.old/imgs/set-anc.png diff --git a/linux/imgs/tray-icon-hover.png b/linux.old/imgs/tray-icon-hover.png similarity index 100% rename from linux/imgs/tray-icon-hover.png rename to linux.old/imgs/tray-icon-hover.png diff --git a/linux/imgs/tray-icon-menu.png b/linux.old/imgs/tray-icon-menu.png similarity index 100% rename from linux/imgs/tray-icon-menu.png rename to linux.old/imgs/tray-icon-menu.png diff --git a/linux/pyproject.toml b/linux.old/pyproject.toml similarity index 100% rename from linux/pyproject.toml rename to linux.old/pyproject.toml diff --git a/linux/start-daemon.py b/linux.old/start-daemon.py similarity index 100% rename from linux/start-daemon.py rename to linux.old/start-daemon.py diff --git a/linux/test_l2.py b/linux.old/test_l2.py similarity index 100% rename from linux/test_l2.py rename to linux.old/test_l2.py diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..544ad89 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.16) + +project(linux VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Widgets Bluetooth) + +qt_standard_project_setup(REQUIRES 6.5) + +qt_add_executable(applinux + main.cpp +) + +qt_add_qml_module(applinux + URI linux + VERSION 1.0 + QML_FILES + Main.qml +) + +# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. +# If you are developing for iOS or macOS you should consider setting an +# explicit, fixed bundle identifier manually though. +set_target_properties(applinux PROPERTIES +# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.applinux + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +target_link_libraries(applinux + PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth +) + +include(GNUInstallDirs) +install(TARGETS applinux + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/linux/CMakeLists.txt.user b/linux/CMakeLists.txt.user new file mode 100644 index 0000000..4f171ef --- /dev/null +++ b/linux/CMakeLists.txt.user @@ -0,0 +1,423 @@ + + + + + + EnvironmentId + {92a8debe-2d62-4047-9556-203fff6fa8af} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + 0 + false + true + false + 2 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + false + + + 0 + true + + true + true + Builtin.DefaultTidyAndClazy + 6 + true + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Desktop + Desktop + Desktop + {3a52acb1-4f55-495e-b8ee-ee552a51c3d7} + 0 + 0 + 0 + + Debug + 2 + false + + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} +-DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_BUILD_TYPE:STRING=Debug + 0 + /home/kavish/AirPodsLikeNormal/linux/build/Desktop-Debug + + + + + all + + false + + true + Build + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + Build + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Debug + CMakeProjectManager.CMakeBuildConfiguration + + + Release + 2 + false + + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} +-DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_BUILD_TYPE:STRING=Release + /home/kavish/AirPodsLikeNormal/linux/build/Desktop-Release + + + + + all + + false + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release + CMakeProjectManager.CMakeBuildConfiguration + + + RelWithDebInfo + 2 + false + + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} +-DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo + /home/kavish/AirPodsLikeNormal/linux/build/Desktop-RelWithDebInfo + + + + + all + + false + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Release with Debug Information + CMakeProjectManager.CMakeBuildConfiguration + + + RelWithDebInfo + 2 + false + + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} +-DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo + 0 + /home/kavish/AirPodsLikeNormal/linux/build/Desktop-Profile + + + + + all + + false + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Profile + CMakeProjectManager.CMakeBuildConfiguration + + + MinSizeRel + 2 + false + + -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} +-DCMAKE_PREFIX_PATH:STRING=%{Qt:QT_INSTALL_PREFIX} +-DCMAKE_C_COMPILER:STRING=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} +-DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx} +-DCMAKE_GENERATOR:STRING=Ninja +-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake +-DCMAKE_BUILD_TYPE:STRING=MinSizeRel + /home/kavish/AirPodsLikeNormal/linux/build/Desktop-MinSizeRel + + + + + all + + false + + true + CMakeProjectManager.MakeStep + + 1 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + + + clean + + false + + true + CMakeProjectManager.MakeStep + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + false + + Minimum Size Release + CMakeProjectManager.CMakeBuildConfiguration + + 5 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + ProjectExplorer.DefaultDeployConfiguration + + 1 + + true + true + 0 + true + + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + applinux + CMakeProjectManager.CMakeRunConfiguration.applinux + applinux + false + true + true + true + /home/kavish/AirPodsLikeNormal/linux/build/Desktop-Debug + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/linux/Main.qml b/linux/Main.qml new file mode 100644 index 0000000..3ace210 --- /dev/null +++ b/linux/Main.qml @@ -0,0 +1,52 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +ApplicationWindow { + visible: true + width: 400 + height: 300 + title: "AirPods Settings" + property bool ignoreNoiseControlChange: false + + Column { + spacing: 20 + padding: 20 + + Text { + text: "Ear Detection Status: " + id: earDetectionStatus + } + + Text { + text: "Battery Status: " + id: batteryStatus + } + + ComboBox { + id: noiseControlMode + model: ["Off", "Noise Cancellation", "Transparency", "Adaptive"] + currentIndex: 0 + onCurrentIndexChanged: { + if (!ignoreNoiseControlChange) { + airPodsTrayApp.setNoiseControlMode(currentIndex) + } + } + Connections { + target: airPodsTrayApp + function onNoiseControlModeChanged(mode) { + ignoreNoiseControlChange = true + noiseControlMode.currentIndex = mode; + ignoreNoiseControlChange = false + } + } + } + + Switch { + id: caToggle + text: "Conversational Awareness" + onCheckedChanged: { + airPodsTrayApp.setConversationalAwareness(checked) + } + } + } +} diff --git a/linux/main.cpp b/linux/main.cpp new file mode 100644 index 0000000..91eda0e --- /dev/null +++ b/linux/main.cpp @@ -0,0 +1,403 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp") + +#define LOG_INFO(msg) qCInfo(airpodsApp) << "\033[32m" << msg << "\033[0m" +#define LOG_WARN(msg) qCWarning(airpodsApp) << "\033[33m" << msg << "\033[0m" +#define LOG_ERROR(msg) qCCritical(airpodsApp) << "\033[31m" << msg << "\033[0m" +#define LOG_DEBUG(msg) qCDebug(airpodsApp) << "\033[34m" << msg << "\033[0m" + +class AirPodsTrayApp : public QObject { + Q_OBJECT + +public: + AirPodsTrayApp() { + LOG_INFO("Initializing AirPodsTrayApp"); + trayIcon = new QSystemTrayIcon(QIcon(":/icons/airpods.png")); + trayMenu = new QMenu(); + + QAction *caToggleAction = new QAction("Toggle Conversational Awareness", trayMenu); + trayMenu->addAction(caToggleAction); + + QAction *offAction = new QAction("Off", trayMenu); + QAction *transparencyAction = new QAction("Transparency", trayMenu); + QAction *adaptiveAction = new QAction("Adaptive", trayMenu); + QAction *noiseCancellationAction = new QAction("Noise Cancellation", trayMenu); + + offAction->setCheckable(true); + transparencyAction->setCheckable(true); + adaptiveAction->setCheckable(true); + noiseCancellationAction->setCheckable(true); + + trayMenu->addAction(offAction); + trayMenu->addAction(transparencyAction); + trayMenu->addAction(adaptiveAction); + trayMenu->addAction(noiseCancellationAction); + + QActionGroup *noiseControlGroup = new QActionGroup(trayMenu); + noiseControlGroup->addAction(offAction); + noiseControlGroup->addAction(transparencyAction); + noiseControlGroup->addAction(adaptiveAction); + noiseControlGroup->addAction(noiseCancellationAction); + + connect(offAction, &QAction::triggered, this, [this]() { setNoiseControlMode(0); }); + connect(transparencyAction, &QAction::triggered, this, [this]() { setNoiseControlMode(2); }); + connect(adaptiveAction, &QAction::triggered, this, [this]() { setNoiseControlMode(3); }); + connect(noiseCancellationAction, &QAction::triggered, this, [this]() { setNoiseControlMode(1); }); + + connect(this, &AirPodsTrayApp::noiseControlModeChanged, this, &AirPodsTrayApp::updateNoiseControlMenu); + connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateBatteryTooltip); + connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateTrayIcon); + + trayIcon->setContextMenu(trayMenu); + trayIcon->show(); + + connect(trayIcon, &QSystemTrayIcon::activated, this, &AirPodsTrayApp::onTrayIconActivated); + + discoveryAgent = new QBluetoothDeviceDiscoveryAgent(); + connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &AirPodsTrayApp::onDeviceDiscovered); + connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &AirPodsTrayApp::onDiscoveryFinished); + discoveryAgent->start(); + LOG_INFO("AirPodsTrayApp initialized and started device discovery"); + + // Check for already connected devices + QBluetoothLocalDevice localDevice; + connect(&localDevice, &QBluetoothLocalDevice::deviceConnected, this, &AirPodsTrayApp::onDeviceConnected); + connect(&localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, &AirPodsTrayApp::onDeviceDisconnected); + + const QList connectedDevices = localDevice.connectedDevices(); + for (const QBluetoothAddress &address : connectedDevices) { + QBluetoothDeviceInfo device(address, "", 0); + if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) { + connectToDevice(device); + return; + } + } + } + +public slots: + void connectToDevice(const QString &address) { + LOG_INFO("Connecting to device with address: " << address); + QBluetoothAddress btAddress(address); + QBluetoothDeviceInfo device(btAddress, "", 0); + 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(int mode) { + LOG_INFO("Setting noise control mode to: " << mode); + QByteArray packet; + switch (mode) { + case 0: // Off + packet = QByteArray::fromHex("0400040009000D01000000"); + break; + case 1: // Noise Cancellation + packet = QByteArray::fromHex("0400040009000D02000000"); + break; + case 2: // Transparency + packet = QByteArray::fromHex("0400040009000D03000000"); + break; + case 3: // Adaptive + packet = QByteArray::fromHex("0400040009000D04000000"); + break; + } + if (socket && socket->isOpen()) { + socket->write(packet); + LOG_DEBUG("Noise control mode packet written: " << packet.toHex()); + } else { + LOG_ERROR("Socket is not open, cannot write noise control mode packet"); + } + } + + void setConversationalAwareness(bool enabled) { + LOG_INFO("Setting conversational awareness to: " << (enabled ? "enabled" : "disabled")); + QByteArray packet = enabled ? QByteArray::fromHex("0400040009002801000000") : QByteArray::fromHex("0400040009002802000000"); + if (socket && socket->isOpen()) { + socket->write(packet); + LOG_DEBUG("Conversational awareness packet written: " << packet.toHex()); + } else { + LOG_ERROR("Socket is not open, cannot write conversational awareness packet"); + } + } + + void updateNoiseControlMenu(int mode) { + QList actions = trayMenu->actions(); + for (QAction *action : actions) { + action->setChecked(false); + } + if (mode >= 0 && mode < actions.size()) { + actions[mode]->setChecked(true); + } + } + + void updateBatteryTooltip(const QString &status) { + trayIcon->setToolTip(status); + } + + void updateTrayIcon(const QString &status) { + QStringList parts = status.split(", "); + int leftLevel = parts[0].split(": ")[1].replace("%", "").toInt(); + int rightLevel = parts[1].split(": ")[1].replace("%", "").toInt(); + int minLevel = qMin(leftLevel, rightLevel); + + QPixmap pixmap(32, 32); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + QColor textColor = QApplication::palette().color(QPalette::WindowText); + painter.setPen(textColor); + painter.setFont(QFont("Arial", 12, QFont::Bold)); + painter.drawText(pixmap.rect(), Qt::AlignCenter, QString::number(minLevel) + "%"); + painter.end(); + + trayIcon->setIcon(QIcon(pixmap)); + } + +private slots: + void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) { + if (reason == QSystemTrayIcon::Trigger) { + LOG_INFO("Tray icon activated"); + // Show settings window + QQuickWindow *window = qobject_cast(QGuiApplication::topLevelWindows().first()); + if (window) { + window->show(); + window->raise(); + window->requestActivate(); + } + } + } + + void onDeviceDiscovered(const QBluetoothDeviceInfo &device) { + LOG_INFO("Device discovered: " << device.name() << " (" << device.address().toString() << ")"); + if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) { + LOG_DEBUG("Found AirPods device" + device.name()); + connectToDevice(device); + } + } + + void onDiscoveryFinished() { + LOG_INFO("Device discovery finished"); + const QList discoveredDevices = discoveryAgent->discoveredDevices(); + for (const QBluetoothDeviceInfo &device : discoveredDevices) { + if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) { + 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 (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) { + connectToDevice(device); + } + } + + void onDeviceDisconnected(const QBluetoothAddress &address) { + LOG_INFO("Device disconnected: " << address.toString()); + if (socket) { + LOG_WARN("Socket is still open, closing it"); + socket->close(); + socket = nullptr; + } + } + + 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() << " (" << device.address().toString() << ")"); + QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); + connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() { + LOG_INFO("Connected to device, sending initial packets"); + discoveryAgent->stop(); // Stop discovering once connected + + QByteArray handshakePacket = QByteArray::fromHex("00000400010002000000000000000000"); + QByteArray setSpecificFeaturesPacket = QByteArray::fromHex("040004004d00ff00000000000000"); + QByteArray requestNotificationsPacket = QByteArray::fromHex("040004000f00ffffffffff"); + + qint64 bytesWritten = localSocket->write(handshakePacket); + LOG_DEBUG("Handshake packet written: " << handshakePacket.toHex() << ", bytes written: " << bytesWritten); + + connect(localSocket, &QBluetoothSocket::bytesWritten, this, [this, localSocket, setSpecificFeaturesPacket, requestNotificationsPacket](qint64 bytes) { + LOG_INFO("Bytes written: " << bytes); + if (bytes > 0) { + static int step = 0; + switch (step) { + case 0: + localSocket->write(setSpecificFeaturesPacket); + LOG_DEBUG("Set specific features packet written: " << setSpecificFeaturesPacket.toHex()); + step++; + break; + case 1: + localSocket->write(requestNotificationsPacket); + LOG_DEBUG("Request notifications packet written: " << requestNotificationsPacket.toHex()); + step++; + break; + } + } + }); + + connect(localSocket, &QBluetoothSocket::readyRead, this, [this, localSocket]() { + QByteArray data = localSocket->readAll(); + LOG_DEBUG("Data received: " << data.toHex()); + parseData(data); + }); + }); + + connect(localSocket, QOverload::of(&QBluetoothSocket::errorOccurred), this, [this, localSocket](QBluetoothSocket::SocketError error) { + LOG_ERROR("Socket error: " << error << ", " << localSocket->errorString()); + }); + + localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a")); + socket = localSocket; + } + + void parseData(const QByteArray &data) { + LOG_DEBUG("Parsing data: " << data.toHex() << "Size: " << data.size()); + if (data.size() == 11 && data.startsWith(QByteArray::fromHex("0400040009000D"))) { + int mode = data[7] - 1; + LOG_INFO("Noise control mode: " << mode); + if (mode >= 0 && mode <= 3) { + emit noiseControlModeChanged(mode); + } else { + LOG_ERROR("Invalid noise control mode value received: " << mode); + } + } else if (data.size() == 8 && data.startsWith(QByteArray::fromHex("040004000600"))) { + bool primaryInEar = data[6] == 0x00; + bool secondaryInEar = data[7] == 0x00; + QString earDetectionStatus = QString("Primary: %1, Secondary: %2") + .arg(primaryInEar ? "In Ear" : "Out of Ear") + .arg(secondaryInEar ? "In Ear" : "Out of Ear"); + LOG_INFO("Ear detection status: " << earDetectionStatus); + emit earDetectionStatusChanged(earDetectionStatus); + } else if (data.size() == 22 && data.startsWith(QByteArray::fromHex("040004000400"))) { + int leftLevel = data[9]; + int rightLevel = data[14]; + int caseLevel = data[19]; + QString batteryStatus = QString("Left: %1%, Right: %2%, Case: %3%") + .arg(leftLevel) + .arg(rightLevel) + .arg(caseLevel); + LOG_INFO("Battery status: " << batteryStatus); + emit batteryStatusChanged(batteryStatus); + } + } + +signals: + void noiseControlModeChanged(int mode); + void earDetectionStatusChanged(const QString &status); + void batteryStatusChanged(const QString &status); + +private: + QSystemTrayIcon *trayIcon; + QMenu *trayMenu; + QBluetoothDeviceDiscoveryAgent *discoveryAgent; + QBluetoothSocket *socket = nullptr; +}; + +int main(int argc, char *argv[]) { + QApplication app(argc, argv); + + QQmlApplicationEngine engine; + engine.loadFromModule("linux", "Main"); + + AirPodsTrayApp trayApp; + + engine.rootContext()->setContextProperty("airPodsTrayApp", &trayApp); + + QObject::connect(&trayApp, &AirPodsTrayApp::noiseControlModeChanged, [&engine](int mode) { + LOG_DEBUG("Received noiseControlModeChanged signal with mode: " << mode); + QObject *rootObject = engine.rootObjects().first(); + + if (rootObject) { + LOG_DEBUG("Root object found"); + QObject *noiseControlMode = rootObject->findChild("noiseControlMode"); + if (noiseControlMode) { + LOG_DEBUG("noiseControlMode object found"); + if (mode >= 0 && mode <= 3) { + QMetaObject::invokeMethod(noiseControlMode, "setCurrentIndex", Q_ARG(int, mode)); + } else { + LOG_ERROR("Invalid mode value: " << mode); + } + } else { + LOG_ERROR("noiseControlMode object not found"); + } + } else { + LOG_ERROR("Root object not found"); + } + }); + + QObject::connect(&trayApp, &AirPodsTrayApp::earDetectionStatusChanged, [&engine](const QString &status) { + LOG_DEBUG("Received earDetectionStatusChanged signal with status: " << status); + QObject *rootObject = engine.rootObjects().first(); + if (rootObject) { + LOG_DEBUG("Root object found"); + QObject *earDetectionStatus = rootObject->findChild("earDetectionStatus"); + if (earDetectionStatus) { + LOG_DEBUG("earDetectionStatus object found"); + earDetectionStatus->setProperty("text", "Ear Detection Status: " + status); + } else { + LOG_ERROR("earDetectionStatus object not found"); + } + } else { + LOG_ERROR("Root object not found"); + } + }); + + QObject::connect(&trayApp, &AirPodsTrayApp::batteryStatusChanged, [&engine](const QString &status) { + LOG_DEBUG("Received batteryStatusChanged signal with status: " << status); + QObject *rootObject = engine.rootObjects().first(); + if (rootObject) { + LOG_DEBUG("Root object found"); + QObject *batteryStatus = rootObject->findChild("batteryStatus"); + if (batteryStatus) { + LOG_DEBUG("batteryStatus object found"); + batteryStatus->setProperty("text", "Battery Status: " + status); + } else { + LOG_ERROR("batteryStatus object not found"); + } + } else { + LOG_ERROR("Root object not found"); + } + }); + + return app.exec(); +} + +#include "main.moc"