Merge pull request #80 from tim-gromeyer/linux-ble-monitor-app

[Linux] BLE monitor app
This commit is contained in:
Kavish Devar
2025-03-24 20:26:56 +05:30
committed by GitHub
6 changed files with 758 additions and 0 deletions

29
linux/ble/CMakeLists.txt Normal file
View File

@@ -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}
)

172
linux/ble/blemanager.cpp Normal file
View File

@@ -0,0 +1,172 @@
#include "blemanager.h"
#include <QDebug>
#include <QTimer>
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<QString, DeviceInfo> &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<quint16>(data[4]) | (static_cast<quint8>(data[3]) << 8);
// Status byte for primary pod and other flags
quint8 status = static_cast<quint8>(data[5]);
deviceInfo.status = status;
// Pods battery byte (upper nibble: one pod, lower nibble: other pod)
quint8 podsBatteryByte = static_cast<quint8>(data[6]);
// Flags and case battery byte (upper nibble: case battery, lower nibble: flags)
quint8 flagsAndCaseBattery = static_cast<quint8>(data[7]);
// Lid open counter and device color
quint8 lidIndicator = static_cast<quint8>(data[8]);
deviceInfo.deviceColor = static_cast<quint8>(data[9]);
deviceInfo.connectionState = static_cast<DeviceInfo::ConnectionState>(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<quint8>((lidIndicator >> 3) & 0x01); // Extract bit 3 (lid state)
if (deviceInfo.isThisPodInTheCase) {
deviceInfo.lidState = static_cast<DeviceInfo::LidState>(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;
}
}
}

89
linux/ble/blemanager.h Normal file
View File

@@ -0,0 +1,89 @@
#ifndef BLEMANAGER_H
#define BLEMANAGER_H
#include <QObject>
#include <QBluetoothDeviceDiscoveryAgent>
#include <QMap>
#include <QString>
#include <QDateTime>
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<QString, DeviceInfo> &getDevices() const;
private slots:
void onDeviceDiscovered(const QBluetoothDeviceInfo &info);
void onScanFinished();
void onErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error);
void pruneOldDevices();
private:
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
QMap<QString, DeviceInfo> 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

397
linux/ble/blescanner.cpp Normal file
View File

@@ -0,0 +1,397 @@
#include "blescanner.h"
#include <QApplication>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QTableWidget>
#include <QHeaderView>
#include <QProgressBar>
#include <QGroupBox>
#include <QMenu>
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<QString, DeviceInfo> &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<QString, DeviceInfo> &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<quint8>(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");
}
}

61
linux/ble/blescanner.h Normal file
View File

@@ -0,0 +1,61 @@
#ifndef BLESCANNER_H
#define BLESCANNER_H
#include <QMainWindow>
#include "blemanager.h"
#include <QTimer>
#include <QSystemTrayIcon>
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

10
linux/ble/main.cpp Normal file
View File

@@ -0,0 +1,10 @@
#include "blescanner.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
BleScanner scanner;
scanner.show();
return app.exec();
}