mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-13 06:46:47 +00:00
Update
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -657,3 +657,4 @@ obj/
|
|||||||
!/gradle/wrapper/gradle-wrapper.jar
|
!/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/qt,c++,clion,kotlin,python,android,pycharm,androidstudio,visualstudiocode,linux
|
# End of https://www.toptal.com/developers/gitignore/api/qt,c++,clion,kotlin,python,android,pycharm,androidstudio,visualstudiocode,linux
|
||||||
|
linux/.qmlls.ini
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ find_package(Qt6 6.5 REQUIRED COMPONENTS Core Bluetooth Widgets)
|
|||||||
|
|
||||||
qt_add_executable(ble_monitor
|
qt_add_executable(ble_monitor
|
||||||
main.cpp
|
main.cpp
|
||||||
|
blemanager.h
|
||||||
|
blemanager.cpp
|
||||||
|
blescanner.h
|
||||||
|
blescanner.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(ble_monitor
|
target_link_libraries(ble_monitor
|
||||||
|
|||||||
139
linux/ble/blemanager.cpp
Normal file
139
linux/ble/blemanager.cpp
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
#include "blemanager.h"
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
BleManager::~BleManager()
|
||||||
|
{
|
||||||
|
delete discoveryAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BleManager::startScan()
|
||||||
|
{
|
||||||
|
qDebug() << "Starting BLE scan...";
|
||||||
|
devices.clear();
|
||||||
|
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
deviceInfo.lidOpenCounter = static_cast<quint8>(data[8]);
|
||||||
|
deviceInfo.deviceColor = static_cast<quint8>(data[9]);
|
||||||
|
|
||||||
|
// 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 >> 4) & 0x0F;
|
||||||
|
deviceInfo.caseBattery = (caseNibble == 15) ? -1 : caseNibble * 10;
|
||||||
|
|
||||||
|
// Parse charging statuses from flags (lower 4 bits of data[7])
|
||||||
|
quint8 flags = flagsAndCaseBattery & 0x0F;
|
||||||
|
deviceInfo.leftCharging = areValuesFlipped ? (flags & 0x02) != 0 : (flags & 0x01) != 0; // Bit 1 or 0
|
||||||
|
deviceInfo.rightCharging = areValuesFlipped ? (flags & 0x01) != 0 : (flags & 0x02) != 0; // Bit 0 or 1
|
||||||
|
deviceInfo.caseCharging = (flags & 0x04) != 0; // Keeping original bit 1 for consistency
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Determine lid state based on lid open counter
|
||||||
|
if (deviceInfo.lidOpenCounter >= 0x30 && deviceInfo.lidOpenCounter <= 0x37)
|
||||||
|
deviceInfo.lidState = DeviceInfo::LidState::OPEN;
|
||||||
|
else if (deviceInfo.lidOpenCounter >= 0x38 && deviceInfo.lidOpenCounter <= 0x3F)
|
||||||
|
deviceInfo.lidState = DeviceInfo::LidState::CLOSED;
|
||||||
|
else if (deviceInfo.lidOpenCounter <= 0x03)
|
||||||
|
deviceInfo.lidState = DeviceInfo::LidState::NOT_IN_CASE;
|
||||||
|
else
|
||||||
|
deviceInfo.lidState = DeviceInfo::LidState::UNKNOWN;
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
67
linux/ble/blemanager.h
Normal file
67
linux/ble/blemanager.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#ifndef BLEMANAGER_H
|
||||||
|
#define BLEMANAGER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QBluetoothDeviceDiscoveryAgent>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
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,
|
||||||
|
CLOSED,
|
||||||
|
NOT_IN_CASE,
|
||||||
|
UNKNOWN
|
||||||
|
};
|
||||||
|
LidState lidState = LidState::UNKNOWN;
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
||||||
|
QMap<QString, DeviceInfo> devices;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLEMANAGER_H
|
||||||
388
linux/ble/blescanner.cpp
Normal file
388
linux/ble/blescanner.cpp
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
mainLayout->addWidget(detailsGroup);
|
||||||
|
detailsGroup->setVisible(false);
|
||||||
|
|
||||||
|
trayIcon = new QSystemTrayIcon(QIcon::fromTheme("bluetooth"), this);
|
||||||
|
QMenu *trayMenu = new QMenu(this);
|
||||||
|
QAction *showAction = trayMenu->addAction("Show");
|
||||||
|
trayMenu->addSeparator();
|
||||||
|
QAction *exitAction = trayMenu->addAction("Exit");
|
||||||
|
trayIcon->setContextMenu(trayMenu);
|
||||||
|
trayIcon->setToolTip("AirPods Battery Monitor");
|
||||||
|
trayIcon->show();
|
||||||
|
|
||||||
|
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);
|
||||||
|
connect(showAction, &QAction::triggered, this, &BleScanner::show);
|
||||||
|
connect(exitAction, &QAction::triggered, qApp, &QApplication::quit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BleScanner::startScan()
|
||||||
|
{
|
||||||
|
scanButton->setEnabled(false);
|
||||||
|
stopButton->setEnabled(true);
|
||||||
|
deviceTable->setRowCount(0);
|
||||||
|
detailsGroup->setVisible(false);
|
||||||
|
bleManager->startScan();
|
||||||
|
refreshTimer->start(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!devices.isEmpty())
|
||||||
|
{
|
||||||
|
auto it = devices.begin();
|
||||||
|
QString leftBat = (it.value().leftPodBattery >= 0 ? QString::number(it.value().leftPodBattery) + "%" : "N/A");
|
||||||
|
QString rightBat = (it.value().rightPodBattery >= 0 ? QString::number(it.value().rightPodBattery) + "%" : "N/A");
|
||||||
|
QString caseBat = (it.value().caseBattery >= 0 ? QString::number(it.value().caseBattery) + "%" : "N/A");
|
||||||
|
QString tooltip = QString("%1\nLeft: %2 | Right: %3 | Case: %4")
|
||||||
|
.arg(it.value().name)
|
||||||
|
.arg(leftBat)
|
||||||
|
.arg(rightBat)
|
||||||
|
.arg(caseBat);
|
||||||
|
trayIcon->setToolTip(tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
switch (device.lidState)
|
||||||
|
{
|
||||||
|
case DeviceInfo::LidState::OPEN:
|
||||||
|
lidStateLabel->setText("Open");
|
||||||
|
break;
|
||||||
|
case DeviceInfo::LidState::CLOSED:
|
||||||
|
lidStateLabel->setText("Closed");
|
||||||
|
break;
|
||||||
|
case DeviceInfo::LidState::NOT_IN_CASE:
|
||||||
|
lidStateLabel->setText("Not in Case");
|
||||||
|
break;
|
||||||
|
case DeviceInfo::LidState::UNKNOWN:
|
||||||
|
lidStateLabel->setText("Unknown");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
60
linux/ble/blescanner.h
Normal file
60
linux/ble/blescanner.h
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
QSystemTrayIcon *trayIcon;
|
||||||
|
|
||||||
|
// New labels for additional DeviceInfo fields
|
||||||
|
QLabel *leftInEarLabel;
|
||||||
|
QLabel *rightInEarLabel;
|
||||||
|
QLabel *leftMicLabel;
|
||||||
|
QLabel *rightMicLabel;
|
||||||
|
QLabel *thisPodInCaseLabel;
|
||||||
|
QLabel *onePodInCaseLabel;
|
||||||
|
QLabel *bothPodsInCaseLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLESCANNER_H
|
||||||
@@ -1,490 +1,10 @@
|
|||||||
|
#include "blescanner.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QMainWindow>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QGridLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QPushButton>
|
|
||||||
#include <QTableWidget>
|
|
||||||
#include <QHeaderView>
|
|
||||||
#include <QProgressBar>
|
|
||||||
#include <QTimer>
|
|
||||||
#include <QGroupBox>
|
|
||||||
#include <QBluetoothDeviceDiscoveryAgent>
|
|
||||||
#include <QBluetoothDeviceInfo>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QMap>
|
|
||||||
#include <QSystemTrayIcon>
|
|
||||||
#include <QMenu>
|
|
||||||
|
|
||||||
class DeviceInfo
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
QString name;
|
|
||||||
QString address;
|
|
||||||
int leftPodBattery = 0;
|
|
||||||
int rightPodBattery = 0;
|
|
||||||
int caseBattery = 0;
|
|
||||||
bool leftCharging = false;
|
|
||||||
bool rightCharging = false;
|
|
||||||
bool caseCharging = false;
|
|
||||||
quint16 deviceModel = 0;
|
|
||||||
quint8 lidOpenCounter = 0;
|
|
||||||
quint8 deviceColor = 0;
|
|
||||||
quint8 status = 0;
|
|
||||||
QByteArray rawData;
|
|
||||||
};
|
|
||||||
|
|
||||||
class BleScanner : public QMainWindow
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
BleScanner(QWidget *parent = nullptr) : QMainWindow(parent)
|
|
||||||
{
|
|
||||||
setWindowTitle("AirPods Battery Monitor");
|
|
||||||
resize(600, 400);
|
|
||||||
|
|
||||||
// Create central widget and layout
|
|
||||||
QWidget *centralWidget = new QWidget(this);
|
|
||||||
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
|
|
||||||
setCentralWidget(centralWidget);
|
|
||||||
|
|
||||||
// Create scan control buttons
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Create device table
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Create detail view for selected device
|
|
||||||
detailsGroup = new QGroupBox("Device Details", this);
|
|
||||||
QGridLayout *detailsLayout = new QGridLayout(detailsGroup);
|
|
||||||
|
|
||||||
// Left pod details
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Right pod details
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Case details
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Additional info
|
|
||||||
detailsLayout->addWidget(new QLabel("Model:"), 3, 0);
|
|
||||||
modelLabel = new QLabel(this);
|
|
||||||
detailsLayout->addWidget(modelLabel, 3, 1);
|
|
||||||
|
|
||||||
detailsLayout->addWidget(new QLabel("Status:"), 4, 0);
|
|
||||||
statusLabel = new QLabel(this);
|
|
||||||
detailsLayout->addWidget(statusLabel, 4, 1);
|
|
||||||
|
|
||||||
detailsLayout->addWidget(new QLabel("Lid Opens:"), 5, 0);
|
|
||||||
lidOpenLabel = new QLabel(this);
|
|
||||||
detailsLayout->addWidget(lidOpenLabel, 5, 1);
|
|
||||||
|
|
||||||
detailsLayout->addWidget(new QLabel("Color:"), 6, 0);
|
|
||||||
colorLabel = new QLabel(this);
|
|
||||||
detailsLayout->addWidget(colorLabel, 6, 1);
|
|
||||||
|
|
||||||
// Raw data display
|
|
||||||
detailsLayout->addWidget(new QLabel("Raw Data:"), 7, 0);
|
|
||||||
rawDataLabel = new QLabel(this);
|
|
||||||
rawDataLabel->setWordWrap(true);
|
|
||||||
detailsLayout->addWidget(rawDataLabel, 7, 1, 1, 2);
|
|
||||||
|
|
||||||
mainLayout->addWidget(detailsGroup);
|
|
||||||
detailsGroup->setVisible(false);
|
|
||||||
|
|
||||||
// Create system tray icon
|
|
||||||
trayIcon = new QSystemTrayIcon(QIcon::fromTheme("bluetooth"), this);
|
|
||||||
QMenu *trayMenu = new QMenu(this);
|
|
||||||
QAction *showAction = trayMenu->addAction("Show");
|
|
||||||
trayMenu->addSeparator();
|
|
||||||
QAction *exitAction = trayMenu->addAction("Exit");
|
|
||||||
|
|
||||||
trayIcon->setContextMenu(trayMenu);
|
|
||||||
trayIcon->setToolTip("AirPods Battery Monitor");
|
|
||||||
trayIcon->show();
|
|
||||||
|
|
||||||
// Create BLE discovery agent
|
|
||||||
discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
|
|
||||||
discoveryAgent->setLowEnergyDiscoveryTimeout(0); // Infinite scanning
|
|
||||||
|
|
||||||
// Setup auto-refresh timer
|
|
||||||
refreshTimer = new QTimer(this);
|
|
||||||
|
|
||||||
// Connect signals and slots
|
|
||||||
connect(scanButton, &QPushButton::clicked, this, &BleScanner::startScan);
|
|
||||||
connect(stopButton, &QPushButton::clicked, this, &BleScanner::stopScan);
|
|
||||||
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered,
|
|
||||||
this, &BleScanner::onDeviceDiscovered);
|
|
||||||
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished,
|
|
||||||
this, &BleScanner::onScanFinished);
|
|
||||||
connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred,
|
|
||||||
this, &BleScanner::onErrorOccurred);
|
|
||||||
connect(deviceTable, &QTableWidget::itemSelectionChanged,
|
|
||||||
this, &BleScanner::onDeviceSelected);
|
|
||||||
connect(refreshTimer, &QTimer::timeout, this, &BleScanner::updateDeviceList);
|
|
||||||
connect(showAction, &QAction::triggered, this, &BleScanner::show);
|
|
||||||
connect(exitAction, &QAction::triggered, qApp, &QApplication::quit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void startScan()
|
|
||||||
{
|
|
||||||
qDebug() << "Starting BLE scan...";
|
|
||||||
scanButton->setEnabled(false);
|
|
||||||
stopButton->setEnabled(true);
|
|
||||||
|
|
||||||
// Clear previous devices
|
|
||||||
devices.clear();
|
|
||||||
deviceTable->setRowCount(0);
|
|
||||||
detailsGroup->setVisible(false);
|
|
||||||
|
|
||||||
// Start discovery
|
|
||||||
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
|
|
||||||
|
|
||||||
// Start refresh timer (update UI every 2 seconds)
|
|
||||||
refreshTimer->start(2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
void stopScan()
|
|
||||||
{
|
|
||||||
qDebug() << "Stopping BLE scan...";
|
|
||||||
discoveryAgent->stop();
|
|
||||||
refreshTimer->stop();
|
|
||||||
scanButton->setEnabled(true);
|
|
||||||
stopButton->setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
|
||||||
{
|
|
||||||
// Check if this is an Apple device with the manufacturer data we're interested in
|
|
||||||
if (info.manufacturerData().contains(0x004C))
|
|
||||||
{
|
|
||||||
QByteArray data = info.manufacturerData().value(0x004C);
|
|
||||||
|
|
||||||
// Check for proximity pairing format (byte 0 = 0x07)
|
|
||||||
if (data.size() >= 10 && data[0] == 0x07)
|
|
||||||
{
|
|
||||||
QString address = info.address().toString();
|
|
||||||
|
|
||||||
// Create or up date device info
|
|
||||||
DeviceInfo deviceInfo;
|
|
||||||
deviceInfo.name = info.name().isEmpty() ? "AirPods" : info.name();
|
|
||||||
deviceInfo.address = address;
|
|
||||||
deviceInfo.rawData = data; // Store the raw data
|
|
||||||
|
|
||||||
// Parse device model
|
|
||||||
deviceInfo.deviceModel = static_cast<quint16>(data[4]) | (static_cast<quint8>(data[3]) << 8);
|
|
||||||
|
|
||||||
// Store status byte (byte 5)
|
|
||||||
deviceInfo.status = static_cast<quint8>(data[5]);
|
|
||||||
|
|
||||||
// Parse pods battery levels (byte 6)
|
|
||||||
quint8 podsBatteryByte = static_cast<quint8>(data[6]);
|
|
||||||
deviceInfo.leftPodBattery = ((podsBatteryByte >> 4) & 0x0F) * 10; // Scale to 0-100
|
|
||||||
deviceInfo.rightPodBattery = (podsBatteryByte & 0x0F) * 10; // Scale to 0-100
|
|
||||||
|
|
||||||
// Parse charging status and case battery level (byte 7)
|
|
||||||
quint8 statusByte = static_cast<quint8>(data[7]);
|
|
||||||
deviceInfo.caseCharging = (statusByte & 0x02) != 0;
|
|
||||||
deviceInfo.rightCharging = (statusByte & 0x04) != 0;
|
|
||||||
deviceInfo.leftCharging = (statusByte & 0x08) != 0;
|
|
||||||
deviceInfo.caseBattery = ((statusByte >> 4) & 0x0F) * 10; // Scale to 0-100
|
|
||||||
|
|
||||||
// Byte 8 is the lid open counter
|
|
||||||
deviceInfo.lidOpenCounter = static_cast<quint8>(data[8]);
|
|
||||||
|
|
||||||
// Byte 9 is the device color
|
|
||||||
deviceInfo.deviceColor = static_cast<quint8>(data[9]);
|
|
||||||
|
|
||||||
// Update device list
|
|
||||||
devices[address] = deviceInfo;
|
|
||||||
|
|
||||||
qDebug() << "Found device:" << deviceInfo.name
|
|
||||||
<< "Left:" << deviceInfo.leftPodBattery << "%"
|
|
||||||
<< "Right:" << deviceInfo.rightPodBattery << "%"
|
|
||||||
<< "Case:" << deviceInfo.caseBattery << "%";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onScanFinished()
|
|
||||||
{
|
|
||||||
qDebug() << "Scan finished.";
|
|
||||||
// In case of a manual stop, we don't restart
|
|
||||||
if (stopButton->isEnabled())
|
|
||||||
{
|
|
||||||
// Restart scanning to keep it continuous
|
|
||||||
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error)
|
|
||||||
{
|
|
||||||
qDebug() << "Error occurred:" << error;
|
|
||||||
stopScan();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateDeviceList()
|
|
||||||
{
|
|
||||||
// Store currently selected device address
|
|
||||||
QString selectedAddress;
|
|
||||||
if (deviceTable->selectionModel()->hasSelection())
|
|
||||||
{
|
|
||||||
int row = deviceTable->selectionModel()->selectedRows().first().row();
|
|
||||||
selectedAddress = deviceTable->item(row, 4)->text();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear and repopulate the table
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Device name
|
|
||||||
QTableWidgetItem *nameItem = new QTableWidgetItem(device.name);
|
|
||||||
deviceTable->setItem(row, 0, nameItem);
|
|
||||||
|
|
||||||
// Left pod battery
|
|
||||||
QString leftStatus = QString::number(device.leftPodBattery) + "%";
|
|
||||||
if (device.leftCharging)
|
|
||||||
leftStatus += " ⚡";
|
|
||||||
QTableWidgetItem *leftItem = new QTableWidgetItem(leftStatus);
|
|
||||||
deviceTable->setItem(row, 1, leftItem);
|
|
||||||
|
|
||||||
// Right pod battery
|
|
||||||
QString rightStatus = QString::number(device.rightPodBattery) + "%";
|
|
||||||
if (device.rightCharging)
|
|
||||||
rightStatus += " ⚡";
|
|
||||||
QTableWidgetItem *rightItem = new QTableWidgetItem(rightStatus);
|
|
||||||
deviceTable->setItem(row, 2, rightItem);
|
|
||||||
|
|
||||||
// Case battery
|
|
||||||
QString caseStatus = QString::number(device.caseBattery) + "%";
|
|
||||||
if (device.caseCharging)
|
|
||||||
caseStatus += " ⚡";
|
|
||||||
QTableWidgetItem *caseItem = new QTableWidgetItem(caseStatus);
|
|
||||||
deviceTable->setItem(row, 3, caseItem);
|
|
||||||
|
|
||||||
// Address
|
|
||||||
QTableWidgetItem *addressItem = new QTableWidgetItem(device.address);
|
|
||||||
deviceTable->setItem(row, 4, addressItem);
|
|
||||||
|
|
||||||
// Reselect the previously selected device
|
|
||||||
if (device.address == selectedAddress)
|
|
||||||
{
|
|
||||||
deviceTable->selectRow(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update system tray tooltip with device info if available
|
|
||||||
if (!devices.isEmpty())
|
|
||||||
{
|
|
||||||
auto it = devices.begin();
|
|
||||||
QString tooltip = QString("%1\nLeft: %2% | Right: %3% | Case: %4%")
|
|
||||||
.arg(it.value().name)
|
|
||||||
.arg(it.value().leftPodBattery)
|
|
||||||
.arg(it.value().rightPodBattery)
|
|
||||||
.arg(it.value().caseBattery);
|
|
||||||
trayIcon->setToolTip(tooltip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDeviceSelected()
|
|
||||||
{
|
|
||||||
if (!deviceTable->selectionModel()->hasSelection())
|
|
||||||
{
|
|
||||||
detailsGroup->setVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int row = deviceTable->selectionModel()->selectedRows().first().row();
|
|
||||||
QString address = deviceTable->item(row, 4)->text();
|
|
||||||
|
|
||||||
if (!devices.contains(address))
|
|
||||||
{
|
|
||||||
detailsGroup->setVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeviceInfo &device = devices[address];
|
|
||||||
|
|
||||||
// Update details view
|
|
||||||
leftBatteryBar->setValue(device.leftPodBattery);
|
|
||||||
rightBatteryBar->setValue(device.rightPodBattery);
|
|
||||||
caseBatteryBar->setValue(device.caseBattery);
|
|
||||||
|
|
||||||
leftChargingLabel->setText(device.leftCharging ? "Charging" : "Not Charging");
|
|
||||||
rightChargingLabel->setText(device.rightCharging ? "Charging" : "Not Charging");
|
|
||||||
caseChargingLabel->setText(device.caseCharging ? "Charging" : "Not Charging");
|
|
||||||
|
|
||||||
// Set model name based on model ID
|
|
||||||
QString modelName = getModelName(device.deviceModel);
|
|
||||||
modelLabel->setText(modelName + " (0x" + QString::number(device.deviceModel, 16).toUpper() + ")");
|
|
||||||
|
|
||||||
// Display status byte with binary representation
|
|
||||||
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));
|
|
||||||
|
|
||||||
lidOpenLabel->setText(QString::number(device.lidOpenCounter));
|
|
||||||
|
|
||||||
// Set color name based on color ID
|
|
||||||
QString colorName = getColorName(device.deviceColor);
|
|
||||||
colorLabel->setText(colorName + " (" + QString::number(device.deviceColor) + ")");
|
|
||||||
|
|
||||||
// Display raw data bytes
|
|
||||||
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);
|
|
||||||
|
|
||||||
detailsGroup->setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
QString getModelName(quint16 modelId)
|
|
||||||
{
|
|
||||||
switch (modelId)
|
|
||||||
{
|
|
||||||
// AirPods
|
|
||||||
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)";
|
|
||||||
|
|
||||||
// AirPods Max
|
|
||||||
case 0x0A20:
|
|
||||||
return "AirPods Max";
|
|
||||||
case 0x1F20:
|
|
||||||
return "AirPods Max (USB-C)";
|
|
||||||
|
|
||||||
// Airpods Pro
|
|
||||||
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 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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
|
||||||
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 *lidOpenLabel;
|
|
||||||
QLabel *colorLabel;
|
|
||||||
QLabel *rawDataLabel;
|
|
||||||
QSystemTrayIcon *trayIcon;
|
|
||||||
|
|
||||||
// Map of discovered devices (address -> device info)
|
|
||||||
QMap<QString, DeviceInfo> devices;
|
|
||||||
};
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
BleScanner scanner;
|
BleScanner scanner;
|
||||||
scanner.show();
|
scanner.show();
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "main.moc"
|
|
||||||
Reference in New Issue
Block a user