diff --git a/linux/ble/CMakeLists.txt b/linux/ble/CMakeLists.txt new file mode 100644 index 0000000..71f1998 --- /dev/null +++ b/linux/ble/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.16) + +project(ble_monitor VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt6 6.5 REQUIRED COMPONENTS Core Bluetooth Widgets) + +qt_add_executable(ble_monitor + main.cpp + blemanager.h + blemanager.cpp + blescanner.h + blescanner.cpp +) + +target_link_libraries(ble_monitor + PRIVATE Qt6::Core Qt6::Bluetooth Qt6::Widgets +) + +install(TARGETS ble_monitor + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) \ No newline at end of file diff --git a/linux/ble/blemanager.cpp b/linux/ble/blemanager.cpp new file mode 100644 index 0000000..1b5d0bd --- /dev/null +++ b/linux/ble/blemanager.cpp @@ -0,0 +1,172 @@ +#include "blemanager.h" +#include +#include + +BleManager::BleManager(QObject *parent) : QObject(parent) +{ + discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); + discoveryAgent->setLowEnergyDiscoveryTimeout(0); // Continuous scanning + + connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + this, &BleManager::onDeviceDiscovered); + connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, + this, &BleManager::onScanFinished); + connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, + this, &BleManager::onErrorOccurred); + + // Set up pruning timer + pruneTimer = new QTimer(this); + connect(pruneTimer, &QTimer::timeout, this, &BleManager::pruneOldDevices); + pruneTimer->start(PRUNE_INTERVAL_MS); // Start timer (runs every 5 seconds) +} + +BleManager::~BleManager() +{ + delete discoveryAgent; + delete pruneTimer; +} + +void BleManager::startScan() +{ + qDebug() << "Starting BLE scan..."; + devices.clear(); + discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + pruneTimer->start(PRUNE_INTERVAL_MS); // Ensure timer is running +} + +void BleManager::stopScan() +{ + qDebug() << "Stopping BLE scan..."; + discoveryAgent->stop(); +} + +const QMap &BleManager::getDevices() const +{ + return devices; +} + +void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info) +{ + // Check for Apple's manufacturer ID (0x004C) + if (info.manufacturerData().contains(0x004C)) + { + QByteArray data = info.manufacturerData().value(0x004C); + // Ensure data is long enough and starts with prefix 0x07 + if (data.size() >= 10 && data[0] == 0x07) + { + QString address = info.address().toString(); + DeviceInfo deviceInfo; + deviceInfo.name = info.name().isEmpty() ? "AirPods" : info.name(); + deviceInfo.address = address; + deviceInfo.rawData = data; + + // Check if pairing mode is paired (0x01) or pairing (0x00) + if (data[2] == 0x00) + { + return; // Skip pairing mode devices (the values are differently structured) + } + + // Parse device model (big-endian: high byte at data[3], low byte at data[4]) + deviceInfo.deviceModel = static_cast(data[4]) | (static_cast(data[3]) << 8); + + // Status byte for primary pod and other flags + quint8 status = static_cast(data[5]); + deviceInfo.status = status; + + // Pods battery byte (upper nibble: one pod, lower nibble: other pod) + quint8 podsBatteryByte = static_cast(data[6]); + + // Flags and case battery byte (upper nibble: case battery, lower nibble: flags) + quint8 flagsAndCaseBattery = static_cast(data[7]); + + // Lid open counter and device color + quint8 lidIndicator = static_cast(data[8]); + deviceInfo.deviceColor = static_cast(data[9]); + + deviceInfo.connectionState = static_cast(data[10]); + + // Determine primary pod (bit 5 of status) and value flipping + bool primaryLeft = (status & 0x20) != 0; // Bit 5: 1 = left primary, 0 = right primary + bool areValuesFlipped = !primaryLeft; // Flipped when right pod is primary + + // Parse battery levels + int leftNibble = areValuesFlipped ? (podsBatteryByte >> 4) & 0x0F : podsBatteryByte & 0x0F; + int rightNibble = areValuesFlipped ? podsBatteryByte & 0x0F : (podsBatteryByte >> 4) & 0x0F; + deviceInfo.leftPodBattery = (leftNibble == 15) ? -1 : leftNibble * 10; + deviceInfo.rightPodBattery = (rightNibble == 15) ? -1 : rightNibble * 10; + int caseNibble = flagsAndCaseBattery & 0x0F; // Extracts lower nibble + deviceInfo.caseBattery = (caseNibble == 15) ? -1 : caseNibble * 10; + + // Parse charging statuses from flags (uper 4 bits of data[7]) + quint8 flags = (flagsAndCaseBattery >> 4) & 0x0F; // Extracts lower nibble + deviceInfo.rightCharging = areValuesFlipped ? (flags & 0x01) != 0 : (flags & 0x02) != 0; // Depending on primary, bit 0 or 1 + deviceInfo.leftCharging = areValuesFlipped ? (flags & 0x02) != 0 : (flags & 0x01) != 0; // Depending on primary, bit 1 or 0 + deviceInfo.caseCharging = (flags & 0x04) != 0; // bit 2 + + // Additional status flags from status byte (data[5]) + deviceInfo.isThisPodInTheCase = (status & 0x40) != 0; // Bit 6 + deviceInfo.isOnePodInCase = (status & 0x10) != 0; // Bit 4 + deviceInfo.areBothPodsInCase = (status & 0x04) != 0; // Bit 2 + + // In-ear detection with XOR logic + bool xorFactor = areValuesFlipped ^ deviceInfo.isThisPodInTheCase; + deviceInfo.isLeftPodInEar = xorFactor ? (status & 0x08) != 0 : (status & 0x02) != 0; // Bit 3 or 1 + deviceInfo.isRightPodInEar = xorFactor ? (status & 0x02) != 0 : (status & 0x08) != 0; // Bit 1 or 3 + + // Microphone status + deviceInfo.isLeftPodMicrophone = primaryLeft ^ deviceInfo.isThisPodInTheCase; + deviceInfo.isRightPodMicrophone = !primaryLeft ^ deviceInfo.isThisPodInTheCase; + + deviceInfo.lidOpenCounter = lidIndicator & 0x07; // Extract bits 0-2 (count) + quint8 lidState = static_cast((lidIndicator >> 3) & 0x01); // Extract bit 3 (lid state) + if (deviceInfo.isThisPodInTheCase) { + deviceInfo.lidState = static_cast(lidState); + } + + // Update timestamp + deviceInfo.lastSeen = QDateTime::currentDateTime(); + + // Store device info in the map + devices[address] = deviceInfo; + + // Debug output + qDebug() << "Found device:" << deviceInfo.name + << "Left:" << (deviceInfo.leftPodBattery >= 0 ? QString("%1%").arg(deviceInfo.leftPodBattery) : "N/A") + << "Right:" << (deviceInfo.rightPodBattery >= 0 ? QString("%1%").arg(deviceInfo.rightPodBattery) : "N/A") + << "Case:" << (deviceInfo.caseBattery >= 0 ? QString("%1%").arg(deviceInfo.caseBattery) : "N/A"); + } + } +} + +void BleManager::onScanFinished() +{ + qDebug() << "Scan finished."; + if (discoveryAgent->isActive()) + { + discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + } +} + +void BleManager::onErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error) +{ + qDebug() << "Error occurred:" << error; + stopScan(); +} + +void BleManager::pruneOldDevices() +{ + QDateTime now = QDateTime::currentDateTime(); + auto it = devices.begin(); + while (it != devices.end()) + { + if (it.value().lastSeen.msecsTo(now) > DEVICE_TIMEOUT_MS) + { + qDebug() << "Removing old device:" << it.value().name << "at" << it.key(); + it = devices.erase(it); // Remove device if not seen recently + } + else + { + ++it; + } + } +} \ No newline at end of file diff --git a/linux/ble/blemanager.h b/linux/ble/blemanager.h new file mode 100644 index 0000000..380d814 --- /dev/null +++ b/linux/ble/blemanager.h @@ -0,0 +1,89 @@ +#ifndef BLEMANAGER_H +#define BLEMANAGER_H + +#include +#include +#include +#include +#include + +class QTimer; + +class DeviceInfo +{ +public: + QString name; + QString address; + int leftPodBattery = -1; // -1 indicates not available + int rightPodBattery = -1; + int caseBattery = -1; + bool leftCharging = false; + bool rightCharging = false; + bool caseCharging = false; + quint16 deviceModel = 0; + quint8 lidOpenCounter = 0; + quint8 deviceColor = 0; + quint8 status = 0; + QByteArray rawData; + + // Additional status flags from Kotlin version + bool isLeftPodInEar = false; + bool isRightPodInEar = false; + bool isLeftPodMicrophone = false; + bool isRightPodMicrophone = false; + bool isThisPodInTheCase = false; + bool isOnePodInCase = false; + bool areBothPodsInCase = false; + + // Lid state enumeration + enum class LidState + { + OPEN = 0x0, + CLOSED = 0x1, + UNKNOWN, + }; + LidState lidState = LidState::UNKNOWN; + + // Connection state enumeration + enum class ConnectionState : uint8_t + { + DISCONNECTED = 0x00, + IDLE = 0x04, + MUSIC = 0x05, + CALL = 0x06, + RINGING = 0x07, + HANGING_UP = 0x09, + UNKNOWN = 0xFF // Using 0xFF for representing null in the original + }; + ConnectionState connectionState = ConnectionState::UNKNOWN; + + QDateTime lastSeen; // Timestamp of last detection +}; + +class BleManager : public QObject +{ + Q_OBJECT +public: + explicit BleManager(QObject *parent = nullptr); + ~BleManager(); + + void startScan(); + void stopScan(); + const QMap &getDevices() const; + +private slots: + void onDeviceDiscovered(const QBluetoothDeviceInfo &info); + void onScanFinished(); + void onErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error); + void pruneOldDevices(); + +private: + QBluetoothDeviceDiscoveryAgent *discoveryAgent; + QMap devices; + + QTimer *pruneTimer; // Timer for periodic pruning + static const int PRUNE_INTERVAL_MS = 5000; // Check every 5 seconds + static const int DEVICE_TIMEOUT_MS = 10000; // Remove after 10 seconds +}; + +#endif // BLEMANAGER_H \ No newline at end of file diff --git a/linux/ble/blescanner.cpp b/linux/ble/blescanner.cpp new file mode 100644 index 0000000..2ce149d --- /dev/null +++ b/linux/ble/blescanner.cpp @@ -0,0 +1,397 @@ +#include "blescanner.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +BleScanner::BleScanner(QWidget *parent) : QMainWindow(parent) +{ + setWindowTitle("AirPods Battery Monitor"); + resize(600, 400); + + QWidget *centralWidget = new QWidget(this); + QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget); + setCentralWidget(centralWidget); + + QHBoxLayout *buttonLayout = new QHBoxLayout(); + scanButton = new QPushButton("Start Scan", this); + stopButton = new QPushButton("Stop Scan", this); + stopButton->setEnabled(false); + buttonLayout->addWidget(scanButton); + buttonLayout->addWidget(stopButton); + buttonLayout->addStretch(); + mainLayout->addLayout(buttonLayout); + + deviceTable = new QTableWidget(0, 5, this); + deviceTable->setHorizontalHeaderLabels({"Device", "Left Pod", "Right Pod", "Case", "Address"}); + deviceTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + deviceTable->setSelectionBehavior(QTableWidget::SelectRows); + deviceTable->setEditTriggers(QTableWidget::NoEditTriggers); + mainLayout->addWidget(deviceTable); + + detailsGroup = new QGroupBox("Device Details", this); + QGridLayout *detailsLayout = new QGridLayout(detailsGroup); + + // Row 0: Left Pod + detailsLayout->addWidget(new QLabel("Left Pod:"), 0, 0); + leftBatteryBar = new QProgressBar(this); + leftBatteryBar->setRange(0, 100); + leftBatteryBar->setTextVisible(true); + detailsLayout->addWidget(leftBatteryBar, 0, 1); + leftChargingLabel = new QLabel(this); + detailsLayout->addWidget(leftChargingLabel, 0, 2); + + // Row 1: Right Pod + detailsLayout->addWidget(new QLabel("Right Pod:"), 1, 0); + rightBatteryBar = new QProgressBar(this); + rightBatteryBar->setRange(0, 100); + rightBatteryBar->setTextVisible(true); + detailsLayout->addWidget(rightBatteryBar, 1, 1); + rightChargingLabel = new QLabel(this); + detailsLayout->addWidget(rightChargingLabel, 1, 2); + + // Row 2: Case + detailsLayout->addWidget(new QLabel("Case:"), 2, 0); + caseBatteryBar = new QProgressBar(this); + caseBatteryBar->setRange(0, 100); + caseBatteryBar->setTextVisible(true); + detailsLayout->addWidget(caseBatteryBar, 2, 1); + caseChargingLabel = new QLabel(this); + detailsLayout->addWidget(caseChargingLabel, 2, 2); + + // Row 3: Model + detailsLayout->addWidget(new QLabel("Model:"), 3, 0); + modelLabel = new QLabel(this); + detailsLayout->addWidget(modelLabel, 3, 1); + + // Row 4: Status + detailsLayout->addWidget(new QLabel("Status:"), 4, 0); + statusLabel = new QLabel(this); + detailsLayout->addWidget(statusLabel, 4, 1); + + // Row 5: Lid State (replaces Lid Opens) + detailsLayout->addWidget(new QLabel("Lid State:"), 5, 0); + lidStateLabel = new QLabel(this); + detailsLayout->addWidget(lidStateLabel, 5, 1); + + // Row 6: Color + detailsLayout->addWidget(new QLabel("Color:"), 6, 0); + colorLabel = new QLabel(this); + detailsLayout->addWidget(colorLabel, 6, 1); + + // Row 7: Raw Data + detailsLayout->addWidget(new QLabel("Raw Data:"), 7, 0); + rawDataLabel = new QLabel(this); + rawDataLabel->setWordWrap(true); + detailsLayout->addWidget(rawDataLabel, 7, 1, 1, 2); + + // New Rows for Additional Info + // Row 8: Left Pod In Ear + detailsLayout->addWidget(new QLabel("Left Pod In Ear:"), 8, 0); + leftInEarLabel = new QLabel(this); + detailsLayout->addWidget(leftInEarLabel, 8, 1); + + // Row 9: Right Pod In Ear + detailsLayout->addWidget(new QLabel("Right Pod In Ear:"), 9, 0); + rightInEarLabel = new QLabel(this); + detailsLayout->addWidget(rightInEarLabel, 9, 1); + + // Row 10: Left Pod Microphone + detailsLayout->addWidget(new QLabel("Left Pod Microphone:"), 10, 0); + leftMicLabel = new QLabel(this); + detailsLayout->addWidget(leftMicLabel, 10, 1); + + // Row 11: Right Pod Microphone + detailsLayout->addWidget(new QLabel("Right Pod Microphone:"), 11, 0); + rightMicLabel = new QLabel(this); + detailsLayout->addWidget(rightMicLabel, 11, 1); + + // Row 12: This Pod In Case + detailsLayout->addWidget(new QLabel("This Pod In Case:"), 12, 0); + thisPodInCaseLabel = new QLabel(this); + detailsLayout->addWidget(thisPodInCaseLabel, 12, 1); + + // Row 13: One Pod In Case + detailsLayout->addWidget(new QLabel("One Pod In Case:"), 13, 0); + onePodInCaseLabel = new QLabel(this); + detailsLayout->addWidget(onePodInCaseLabel, 13, 1); + + // Row 14: Both Pods In Case + detailsLayout->addWidget(new QLabel("Both Pods In Case:"), 14, 0); + bothPodsInCaseLabel = new QLabel(this); + detailsLayout->addWidget(bothPodsInCaseLabel, 14, 1); + + // Row 15: Connection State + detailsLayout->addWidget(new QLabel("Connection State:"), 15, 0); + connectionStateLabel = new QLabel(this); + detailsLayout->addWidget(connectionStateLabel, 15, 1); + + mainLayout->addWidget(detailsGroup); + detailsGroup->setVisible(false); + + bleManager = new BleManager(this); + refreshTimer = new QTimer(this); + + connect(scanButton, &QPushButton::clicked, this, &BleScanner::startScan); + connect(stopButton, &QPushButton::clicked, this, &BleScanner::stopScan); + connect(deviceTable, &QTableWidget::itemSelectionChanged, this, &BleScanner::onDeviceSelected); + connect(refreshTimer, &QTimer::timeout, this, &BleScanner::updateDeviceList); +} + +void BleScanner::startScan() +{ + scanButton->setEnabled(false); + stopButton->setEnabled(true); + deviceTable->setRowCount(0); + detailsGroup->setVisible(false); + bleManager->startScan(); + refreshTimer->start(500); +} + +void BleScanner::stopScan() +{ + bleManager->stopScan(); + refreshTimer->stop(); + scanButton->setEnabled(true); + stopButton->setEnabled(false); +} + +void BleScanner::updateDeviceList() +{ + const QMap &devices = bleManager->getDevices(); + QString selectedAddress; + if (deviceTable->selectionModel()->hasSelection()) + { + int row = deviceTable->selectionModel()->selectedRows().first().row(); + selectedAddress = deviceTable->item(row, 4)->text(); + } + + deviceTable->setRowCount(0); + deviceTable->setRowCount(devices.size()); + int row = 0; + for (auto it = devices.begin(); it != devices.end(); ++it, ++row) + { + const DeviceInfo &device = it.value(); + deviceTable->setItem(row, 0, new QTableWidgetItem(device.name)); + QString leftStatus = (device.leftPodBattery >= 0 ? QString::number(device.leftPodBattery) + "%" : "N/A") + + (device.leftCharging ? " ⚡" : ""); + deviceTable->setItem(row, 1, new QTableWidgetItem(leftStatus)); + QString rightStatus = (device.rightPodBattery >= 0 ? QString::number(device.rightPodBattery) + "%" : "N/A") + + (device.rightCharging ? " ⚡" : ""); + deviceTable->setItem(row, 2, new QTableWidgetItem(rightStatus)); + QString caseStatus = (device.caseBattery >= 0 ? QString::number(device.caseBattery) + "%" : "N/A") + + (device.caseCharging ? " ⚡" : ""); + deviceTable->setItem(row, 3, new QTableWidgetItem(caseStatus)); + deviceTable->setItem(row, 4, new QTableWidgetItem(device.address)); + if (device.address == selectedAddress) + { + deviceTable->selectRow(row); + } + } + + if (deviceTable->selectedItems().isEmpty()) { + deviceTable->selectRow(0); + } +} + +void BleScanner::onDeviceSelected() +{ + if (!deviceTable->selectionModel()->hasSelection()) + { + detailsGroup->setVisible(false); + return; + } + + int row = deviceTable->selectionModel()->selectedRows().first().row(); + QString address = deviceTable->item(row, 4)->text(); + const QMap &devices = bleManager->getDevices(); + if (!devices.contains(address)) + { + detailsGroup->setVisible(false); + return; + } + + const DeviceInfo &device = devices[address]; + + // Battery bars with N/A handling + if (device.leftPodBattery >= 0) + { + leftBatteryBar->setValue(device.leftPodBattery); + leftBatteryBar->setFormat("%p%"); + } + else + { + leftBatteryBar->setValue(0); + leftBatteryBar->setFormat("N/A"); + } + + if (device.rightPodBattery >= 0) + { + rightBatteryBar->setValue(device.rightPodBattery); + rightBatteryBar->setFormat("%p%"); + } + else + { + rightBatteryBar->setValue(0); + rightBatteryBar->setFormat("N/A"); + } + + if (device.caseBattery >= 0) + { + caseBatteryBar->setValue(device.caseBattery); + caseBatteryBar->setFormat("%p%"); + } + else + { + caseBatteryBar->setValue(0); + caseBatteryBar->setFormat("N/A"); + } + + leftChargingLabel->setText(device.leftCharging ? "Charging" : "Not Charging"); + rightChargingLabel->setText(device.rightCharging ? "Charging" : "Not Charging"); + caseChargingLabel->setText(device.caseCharging ? "Charging" : "Not Charging"); + + QString modelName = getModelName(device.deviceModel); + modelLabel->setText(modelName + " (0x" + QString::number(device.deviceModel, 16).toUpper() + ")"); + + QString statusBinary = QString("%1").arg(device.status, 8, 2, QChar('0')); + statusLabel->setText(QString("0x%1 (%2) - Binary: %3") + .arg(device.status, 2, 16, QChar('0')) + .toUpper() + .arg(device.status) + .arg(statusBinary)); + + // Lid State enum handling + QString lidStateStr; + + switch (device.lidState) + { + case DeviceInfo::LidState::OPEN: + lidStateStr.append("Open"); + break; + case DeviceInfo::LidState::CLOSED: + lidStateStr.append("Closed"); + break; + case DeviceInfo::LidState::UNKNOWN: + lidStateStr.append("Unknown"); + break; + } + lidStateStr.append(" (0x" + QString::number(device.lidOpenCounter, 16).toUpper() + " = " + QString::number(device.lidOpenCounter) + ")"); + lidStateLabel->setText(lidStateStr); + + QString colorName = getColorName(device.deviceColor); + colorLabel->setText(colorName + " (" + QString::number(device.deviceColor) + ")"); + + QString rawDataStr = "Bytes: "; + for (int i = 0; i < device.rawData.size(); ++i) + { + rawDataStr += QString("0x%1 ").arg(static_cast(device.rawData[i]), 2, 16, QChar('0')).toUpper(); + } + rawDataLabel->setText(rawDataStr); + + // Set new status labels + leftInEarLabel->setText(device.isLeftPodInEar ? "Yes" : "No"); + rightInEarLabel->setText(device.isRightPodInEar ? "Yes" : "No"); + leftMicLabel->setText(device.isLeftPodMicrophone ? "Yes" : "No"); + rightMicLabel->setText(device.isRightPodMicrophone ? "Yes" : "No"); + thisPodInCaseLabel->setText(device.isThisPodInTheCase ? "Yes" : "No"); + onePodInCaseLabel->setText(device.isOnePodInCase ? "Yes" : "No"); + bothPodsInCaseLabel->setText(device.areBothPodsInCase ? "Yes" : "No"); + connectionStateLabel->setText(getConnectionStateName(device.connectionState)); + + detailsGroup->setVisible(true); +} + +QString BleScanner::getModelName(quint16 modelId) +{ + switch (modelId) + { + case 0x0220: + return "AirPods 1st Gen"; + case 0x0F20: + return "AirPods 2nd Gen"; + case 0x1320: + return "AirPods 3rd Gen"; + case 0x1920: + return "AirPods 4th Gen"; + case 0x1B20: + return "AirPods 4th Gen (ANC)"; + case 0x0A20: + return "AirPods Max"; + case 0x1F20: + return "AirPods Max (USB-C)"; + case 0x0E20: + return "AirPods Pro"; + case 0x1420: + return "AirPods Pro 2nd Gen"; + case 0x2420: + return "AirPods Pro 2nd Gen (USB-C)"; + default: + return "Unknown Apple Device"; + } +} + +QString BleScanner::getColorName(quint8 colorId) +{ + switch (colorId) + { + case 0x00: + return "White"; + case 0x01: + return "Black"; + case 0x02: + return "Red"; + case 0x03: + return "Blue"; + case 0x04: + return "Pink"; + case 0x05: + return "Gray"; + case 0x06: + return "Silver"; + case 0x07: + return "Gold"; + case 0x08: + return "Rose Gold"; + case 0x09: + return "Space Gray"; + case 0x0A: + return "Dark Blue"; + case 0x0B: + return "Light Blue"; + case 0x0C: + return "Yellow"; + default: + return "Unknown"; + } +} + +QString BleScanner::getConnectionStateName(DeviceInfo::ConnectionState state) +{ + using ConnectionState = DeviceInfo::ConnectionState; + switch (state) + { + case ConnectionState::DISCONNECTED: + return QString("Disconnected"); + case ConnectionState::IDLE: + return QString("Idle"); + case ConnectionState::MUSIC: + return QString("Playing Music"); + case ConnectionState::CALL: + return QString("On Call"); + case ConnectionState::RINGING: + return QString("Ringing"); + case ConnectionState::HANGING_UP: + return QString("Hanging Up"); + case ConnectionState::UNKNOWN: + default: + return QString("Unknown"); + } +} \ No newline at end of file diff --git a/linux/ble/blescanner.h b/linux/ble/blescanner.h new file mode 100644 index 0000000..a3958f6 --- /dev/null +++ b/linux/ble/blescanner.h @@ -0,0 +1,61 @@ +#ifndef BLESCANNER_H +#define BLESCANNER_H + +#include +#include "blemanager.h" +#include +#include + +class QTableWidget; +class QGroupBox; +class QProgressBar; +class QLabel; +class QPushButton; + +class BleScanner : public QMainWindow +{ + Q_OBJECT +public: + explicit BleScanner(QWidget *parent = nullptr); + +private slots: + void startScan(); + void stopScan(); + void updateDeviceList(); + void onDeviceSelected(); + +private: + QString getModelName(quint16 modelId); + QString getColorName(quint8 colorId); + QString getConnectionStateName(DeviceInfo::ConnectionState state); + + BleManager *bleManager; + QTimer *refreshTimer; + QPushButton *scanButton; + QPushButton *stopButton; + QTableWidget *deviceTable; + QGroupBox *detailsGroup; + QProgressBar *leftBatteryBar; + QProgressBar *rightBatteryBar; + QProgressBar *caseBatteryBar; + QLabel *leftChargingLabel; + QLabel *rightChargingLabel; + QLabel *caseChargingLabel; + QLabel *modelLabel; + QLabel *statusLabel; + QLabel *lidStateLabel; // Renamed from lidOpenLabel + QLabel *colorLabel; + QLabel *rawDataLabel; + + // New labels for additional DeviceInfo fields + QLabel *leftInEarLabel; + QLabel *rightInEarLabel; + QLabel *leftMicLabel; + QLabel *rightMicLabel; + QLabel *thisPodInCaseLabel; + QLabel *onePodInCaseLabel; + QLabel *bothPodsInCaseLabel; + QLabel *connectionStateLabel; +}; + +#endif // BLESCANNER_H \ No newline at end of file diff --git a/linux/ble/main.cpp b/linux/ble/main.cpp new file mode 100644 index 0000000..02dd425 --- /dev/null +++ b/linux/ble/main.cpp @@ -0,0 +1,10 @@ +#include "blescanner.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + BleScanner scanner; + scanner.show(); + return app.exec(); +} \ No newline at end of file