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"