mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-29 06:10:52 +00:00
[Linux] Read AirPods state from BLE broadcast when not connected (#138)
* Fix possible build error * [Linux] Read AirPods state from BLE broadcast when not connected * SImplify * Remove old code * Remove old code * Maintain charging state when state is unknown * Simplify * Remove unused var
This commit is contained in:
@@ -5,12 +5,12 @@ project(linux VERSION 0.1 LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick Widgets Bluetooth DBus)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
qt_standard_project_setup(REQUIRES 6.4)
|
||||
|
||||
qt_add_executable(applinux
|
||||
main.cpp
|
||||
main.h
|
||||
logger.h
|
||||
mediacontroller.cpp
|
||||
mediacontroller.h
|
||||
@@ -24,6 +24,10 @@ qt_add_executable(applinux
|
||||
autostartmanager.hpp
|
||||
BasicControlCommand.hpp
|
||||
deviceinfo.hpp
|
||||
ble/bleutils.cpp
|
||||
ble/bleutils.h
|
||||
ble/blemanager.cpp
|
||||
ble/blemanager.h
|
||||
)
|
||||
|
||||
qt_add_qml_module(applinux
|
||||
@@ -54,7 +58,7 @@ qt_add_resources(applinux "resources"
|
||||
)
|
||||
|
||||
target_link_libraries(applinux
|
||||
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus
|
||||
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::DBus OpenSSL::SSL OpenSSL::Crypto
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <QByteArray>
|
||||
#include <optional>
|
||||
#include <climits>
|
||||
|
||||
#include "enums.h"
|
||||
#include "BasicControlCommand.hpp"
|
||||
|
||||
@@ -130,6 +130,58 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseEncryptedPacket(const QByteArray &packet, bool isLeftPodPrimary)
|
||||
{
|
||||
// Validate packet size (expect 16 bytes based on provided payloads)
|
||||
if (packet.size() != 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine byte indices based on isFlipped
|
||||
int leftByteIndex = isLeftPodPrimary ? 1 : 2;
|
||||
int rightByteIndex = isLeftPodPrimary ? 2 : 1;
|
||||
|
||||
// Extract raw battery bytes
|
||||
unsigned char rawLeftBatteryByte = static_cast<unsigned char>(packet.at(leftByteIndex));
|
||||
unsigned char rawRightBatteryByte = static_cast<unsigned char>(packet.at(rightByteIndex));
|
||||
unsigned char rawCaseBatteryByte = static_cast<unsigned char>(packet.at(3));
|
||||
|
||||
// Extract battery data (charging status and raw level 0-127)
|
||||
auto [isLeftCharging, rawLeftBattery] = formatBattery(rawLeftBatteryByte);
|
||||
auto [isRightCharging, rawRightBattery] = formatBattery(rawRightBatteryByte);
|
||||
auto [isCaseCharging, rawCaseBattery] = formatBattery(rawCaseBatteryByte);
|
||||
|
||||
// If raw byte is 0xFF or (0x7F and charging), use the last known level
|
||||
if (rawLeftBatteryByte == 0xFF || (rawLeftBatteryByte == 0x7F && isLeftCharging)) {
|
||||
rawLeftBatteryByte = states.value(Component::Left).level; // Use last valid level
|
||||
isLeftCharging = states.value(Component::Left).status == BatteryStatus::Charging;
|
||||
}
|
||||
|
||||
// If raw byte is 0xFF or (0x7F and charging), use the last known level
|
||||
if (rawRightBatteryByte == 0xFF || (rawRightBatteryByte == 0x7F && isRightCharging)) {
|
||||
rawRightBattery = states.value(Component::Right).level; // Use last valid level
|
||||
isRightCharging = states.value(Component::Right).status == BatteryStatus::Charging;
|
||||
}
|
||||
|
||||
// If raw byte is 0xFF or (0x7F and charging), use the last known level
|
||||
if (rawCaseBatteryByte == 0xFF || (rawCaseBatteryByte == 0x7F && isCaseCharging)) {
|
||||
rawCaseBattery = states.value(Component::Case).level; // Use last valid level
|
||||
isCaseCharging = states.value(Component::Case).status == BatteryStatus::Charging;
|
||||
}
|
||||
|
||||
// Update states
|
||||
states[Component::Left] = {static_cast<quint8>(rawLeftBattery), isLeftCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
states[Component::Right] = {static_cast<quint8>(rawRightBattery), isRightCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
states[Component::Case] = {static_cast<quint8>(rawCaseBattery), isCaseCharging ? BatteryStatus::Charging : BatteryStatus::Discharging};
|
||||
primaryPod = isLeftPodPrimary ? Component::Left : Component::Right;
|
||||
secondaryPod = isLeftPodPrimary ? Component::Right : Component::Left;
|
||||
emit batteryStatusChanged();
|
||||
emit primaryChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the raw state for a component
|
||||
BatteryState getState(Component comp) const
|
||||
{
|
||||
@@ -187,7 +239,14 @@ private:
|
||||
return states.value(component).status == status;
|
||||
}
|
||||
|
||||
std::pair<bool, int> formatBattery(unsigned char byteVal)
|
||||
{
|
||||
bool charging = (byteVal & 0x80) != 0;
|
||||
int level = byteVal & 0x7F;
|
||||
return std::make_pair(charging, level);
|
||||
}
|
||||
|
||||
QMap<Component, BatteryState> states;
|
||||
Component primaryPod;
|
||||
Component secondaryPod;
|
||||
};
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
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.4 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}
|
||||
)
|
||||
@@ -1,6 +1,86 @@
|
||||
#include "blemanager.h"
|
||||
#include "enums.h"
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include "logger.h"
|
||||
#include <QMap>
|
||||
|
||||
AirpodsTrayApp::Enums::AirPodsModel getModelName(quint16 modelId)
|
||||
{
|
||||
using namespace AirpodsTrayApp::Enums;
|
||||
static const QMap<quint16, AirPodsModel> modelMap = {
|
||||
{0x0220, AirPodsModel::AirPods1},
|
||||
{0x0F20, AirPodsModel::AirPods2},
|
||||
{0x1320, AirPodsModel::AirPods3},
|
||||
{0x1920, AirPodsModel::AirPods4},
|
||||
{0x1B20, AirPodsModel::AirPods4ANC},
|
||||
{0x0A20, AirPodsModel::AirPodsMaxLightning},
|
||||
{0x1F20, AirPodsModel::AirPodsMaxUSBC},
|
||||
{0x0E20, AirPodsModel::AirPodsPro},
|
||||
{0x1420, AirPodsModel::AirPodsPro2Lightning},
|
||||
{0x2420, AirPodsModel::AirPodsPro2USBC}
|
||||
};
|
||||
|
||||
return modelMap.value(modelId, AirPodsModel::Unknown);
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
QString getConnectionStateName(BleInfo::ConnectionState state)
|
||||
{
|
||||
using ConnectionState = BleInfo::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");
|
||||
}
|
||||
}
|
||||
|
||||
BleManager::BleManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
@@ -13,38 +93,25 @@ BleManager::BleManager(QObject *parent) : QObject(parent)
|
||||
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();
|
||||
LOG_DEBUG("Starting BLE scan...");
|
||||
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
|
||||
pruneTimer->start(PRUNE_INTERVAL_MS); // Ensure timer is running
|
||||
}
|
||||
|
||||
void BleManager::stopScan()
|
||||
{
|
||||
qDebug() << "Stopping BLE scan...";
|
||||
LOG_DEBUG("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)
|
||||
@@ -55,10 +122,11 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
if (data.size() >= 10 && data[0] == 0x07)
|
||||
{
|
||||
QString address = info.address().toString();
|
||||
DeviceInfo deviceInfo;
|
||||
BleInfo deviceInfo;
|
||||
deviceInfo.name = info.name().isEmpty() ? "AirPods" : info.name();
|
||||
deviceInfo.address = address;
|
||||
deviceInfo.rawData = data;
|
||||
deviceInfo.rawData = data.left(data.size() - 16);
|
||||
deviceInfo.encryptedPayload = data.mid(data.size() - 16);
|
||||
|
||||
// data[1] is the length of the data, so we can skip it
|
||||
|
||||
@@ -68,8 +136,9 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
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);
|
||||
deviceInfo.modelName = getModelName(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]);
|
||||
@@ -83,9 +152,9 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
|
||||
// Lid open counter and device color
|
||||
quint8 lidIndicator = static_cast<quint8>(data[8]);
|
||||
deviceInfo.deviceColor = static_cast<quint8>(data[9]);
|
||||
deviceInfo.color = getColorName((quint8)(data[9]));
|
||||
|
||||
deviceInfo.connectionState = static_cast<DeviceInfo::ConnectionState>(data[10]);
|
||||
deviceInfo.connectionState = static_cast<BleInfo::ConnectionState>(data[10]);
|
||||
|
||||
// Next: Encrypted Payload: 16 bytes
|
||||
|
||||
@@ -93,6 +162,8 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
bool primaryLeft = (status & 0x20) != 0; // Bit 5: 1 = left primary, 0 = right primary
|
||||
bool areValuesFlipped = !primaryLeft; // Flipped when right pod is primary
|
||||
|
||||
deviceInfo.primaryLeft = primaryLeft; // Store primary pod information
|
||||
|
||||
// Parse battery levels
|
||||
int leftNibble = areValuesFlipped ? (podsBatteryByte >> 4) & 0x0F : podsBatteryByte & 0x0F;
|
||||
int rightNibble = areValuesFlipped ? podsBatteryByte & 0x0F : (podsBatteryByte >> 4) & 0x0F;
|
||||
@@ -117,6 +188,10 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
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
|
||||
|
||||
// Determine primary and secondary in-ear status
|
||||
deviceInfo.isPrimaryInEar = primaryLeft ? deviceInfo.isLeftPodInEar : deviceInfo.isRightPodInEar;
|
||||
deviceInfo.isSecondaryInEar = primaryLeft ? deviceInfo.isRightPodInEar : deviceInfo.isLeftPodInEar;
|
||||
|
||||
// Microphone status
|
||||
deviceInfo.isLeftPodMicrophone = primaryLeft ^ deviceInfo.isThisPodInTheCase;
|
||||
deviceInfo.isRightPodMicrophone = !primaryLeft ^ deviceInfo.isThisPodInTheCase;
|
||||
@@ -124,27 +199,19 @@ void BleManager::onDeviceDiscovered(const QBluetoothDeviceInfo &info)
|
||||
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);
|
||||
deviceInfo.lidState = static_cast<BleInfo::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");
|
||||
emit deviceFound(deviceInfo); // Emit signal for device found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BleManager::onScanFinished()
|
||||
{
|
||||
qDebug() << "Scan finished.";
|
||||
if (discoveryAgent->isActive())
|
||||
{
|
||||
discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod);
|
||||
@@ -153,24 +220,6 @@ void BleManager::onScanFinished()
|
||||
|
||||
void BleManager::onErrorOccurred(QBluetoothDeviceDiscoveryAgent::Error error)
|
||||
{
|
||||
qDebug() << "Error occurred:" << error;
|
||||
LOG_ERROR("BLE scan 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include "enums.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
class DeviceInfo
|
||||
class BleInfo
|
||||
{
|
||||
public:
|
||||
QString name;
|
||||
@@ -20,20 +21,24 @@ public:
|
||||
bool leftCharging = false;
|
||||
bool rightCharging = false;
|
||||
bool caseCharging = false;
|
||||
quint16 deviceModel = 0;
|
||||
AirpodsTrayApp::Enums::AirPodsModel modelName = AirpodsTrayApp::Enums::AirPodsModel::Unknown;
|
||||
quint8 lidOpenCounter = 0;
|
||||
quint8 deviceColor = 0;
|
||||
QString color = "Unknown"; // Default color
|
||||
quint8 status = 0;
|
||||
QByteArray rawData;
|
||||
QByteArray encryptedPayload; // 16 bytes of encrypted payload
|
||||
|
||||
// Additional status flags from Kotlin version
|
||||
bool isLeftPodInEar = false;
|
||||
bool isRightPodInEar = false;
|
||||
bool isPrimaryInEar = false;
|
||||
bool isSecondaryInEar = false;
|
||||
bool isLeftPodMicrophone = false;
|
||||
bool isRightPodMicrophone = false;
|
||||
bool isThisPodInTheCase = false;
|
||||
bool isOnePodInCase = false;
|
||||
bool areBothPodsInCase = false;
|
||||
bool primaryLeft = true; // True if left pod is primary, false if right pod is primary
|
||||
|
||||
// Lid state enumeration
|
||||
enum class LidState
|
||||
@@ -41,8 +46,7 @@ public:
|
||||
OPEN = 0x0,
|
||||
CLOSED = 0x1,
|
||||
UNKNOWN,
|
||||
};
|
||||
LidState lidState = LidState::UNKNOWN;
|
||||
} lidState = LidState::UNKNOWN;
|
||||
|
||||
// Connection state enumeration
|
||||
enum class ConnectionState : uint8_t
|
||||
@@ -54,8 +58,7 @@ public:
|
||||
RINGING = 0x07,
|
||||
HANGING_UP = 0x09,
|
||||
UNKNOWN = 0xFF // Using 0xFF for representing null in the original
|
||||
};
|
||||
ConnectionState connectionState = ConnectionState::UNKNOWN;
|
||||
} connectionState = ConnectionState::UNKNOWN;
|
||||
|
||||
QDateTime lastSeen; // Timestamp of last detection
|
||||
};
|
||||
@@ -69,21 +72,17 @@ public:
|
||||
|
||||
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();
|
||||
|
||||
signals:
|
||||
void deviceFound(const BleInfo &device);
|
||||
|
||||
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
|
||||
@@ -1,398 +0,0 @@
|
||||
#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);
|
||||
rawDataLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#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
|
||||
138
linux/ble/bleutils.cpp
Normal file
138
linux/ble/bleutils.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include <openssl/aes.h>
|
||||
#include "deviceinfo.hpp"
|
||||
#include "bleutils.h"
|
||||
#include <QDebug>
|
||||
#include <QByteArray>
|
||||
#include <QtEndian>
|
||||
#include <QCryptographicHash>
|
||||
#include <cstring> // For memset
|
||||
|
||||
BLEUtils::BLEUtils(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool BLEUtils::verifyRPA(const QString &address, const QByteArray &irk)
|
||||
{
|
||||
if (address.isEmpty() || irk.isEmpty() || irk.size() != 16)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split address into bytes and reverse order
|
||||
QStringList parts = address.split(':');
|
||||
if (parts.size() != 6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray rpa;
|
||||
bool ok;
|
||||
for (int i = parts.size() - 1; i >= 0; --i)
|
||||
{
|
||||
rpa.append(static_cast<char>(parts[i].toInt(&ok, 16)));
|
||||
if (!ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (rpa.size() != 6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray prand = rpa.mid(3, 3);
|
||||
QByteArray hash = rpa.left(3);
|
||||
QByteArray computedHash = ah(irk, prand);
|
||||
|
||||
return hash == computedHash;
|
||||
}
|
||||
|
||||
bool BLEUtils::isValidIrkRpa(const QByteArray &irk, const QString &rpa)
|
||||
{
|
||||
return verifyRPA(rpa, irk);
|
||||
}
|
||||
|
||||
QByteArray BLEUtils::e(const QByteArray &key, const QByteArray &data)
|
||||
{
|
||||
if (key.size() != 16 || data.size() != 16)
|
||||
{
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// Prepare key and data (needs to be reversed)
|
||||
QByteArray reversedKey(key);
|
||||
std::reverse(reversedKey.begin(), reversedKey.end());
|
||||
|
||||
QByteArray reversedData(data);
|
||||
std::reverse(reversedData.begin(), reversedData.end());
|
||||
|
||||
// Set up AES encryption
|
||||
AES_KEY aesKey;
|
||||
if (AES_set_encrypt_key(reinterpret_cast<const unsigned char *>(reversedKey.constData()), 128, &aesKey) != 0)
|
||||
{
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
unsigned char out[16];
|
||||
AES_encrypt(reinterpret_cast<const unsigned char *>(reversedData.constData()), out, &aesKey);
|
||||
|
||||
// Convert output to QByteArray and reverse it
|
||||
QByteArray result(reinterpret_cast<char *>(out), 16);
|
||||
std::reverse(result.begin(), result.end());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray BLEUtils::ah(const QByteArray &k, const QByteArray &r)
|
||||
{
|
||||
if (r.size() < 3)
|
||||
{
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// Pad the random part to 16 bytes
|
||||
QByteArray rPadded(16, 0);
|
||||
rPadded.replace(0, 3, r.left(3));
|
||||
|
||||
QByteArray encrypted = e(k, rPadded);
|
||||
if (encrypted.isEmpty())
|
||||
{
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return encrypted.left(3);
|
||||
}
|
||||
|
||||
QByteArray BLEUtils::decryptLastBytes(const QByteArray &data, const QByteArray &key)
|
||||
{
|
||||
if (data.size() < 16 || key.size() != 16)
|
||||
{
|
||||
qDebug() << "Invalid input: data size < 16 or key size != 16";
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// Extract the last 16 bytes
|
||||
QByteArray block = data.right(16);
|
||||
|
||||
// Set up AES decryption key (use key directly, no reversal)
|
||||
AES_KEY aesKey;
|
||||
if (AES_set_decrypt_key(reinterpret_cast<const unsigned char *>(key.constData()), 128, &aesKey) != 0)
|
||||
{
|
||||
qDebug() << "Failed to set AES decryption key";
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
unsigned char out[16];
|
||||
unsigned char iv[16];
|
||||
memset(iv, 0, 16); // Zero IV for CBC mode
|
||||
|
||||
// Perform AES decryption using CBC mode with zero IV
|
||||
// AES_cbc_encrypt is used for both encryption and decryption depending on the key schedule
|
||||
AES_cbc_encrypt(reinterpret_cast<const unsigned char *>(block.constData()), out, 16, &aesKey, iv, AES_DECRYPT);
|
||||
|
||||
// Convert output to QByteArray (no reversal)
|
||||
QByteArray result(reinterpret_cast<char *>(out), 16);
|
||||
|
||||
return result;
|
||||
}
|
||||
52
linux/ble/bleutils.h
Normal file
52
linux/ble/bleutils.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
|
||||
class BLEUtils : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BLEUtils(QObject *parent = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Verifies if the provided Bluetooth address is an RPA that matches the given Identity Resolving Key (IRK)
|
||||
* @param address The Bluetooth address to verify
|
||||
* @param irk The Identity Resolving Key to use for verification
|
||||
* @return true if the address is verified as an RPA matching the IRK
|
||||
*/
|
||||
static bool verifyRPA(const QString &address, const QByteArray &irk);
|
||||
|
||||
/**
|
||||
* @brief Checks if the given IRK and RPA are valid
|
||||
* @param irk The Identity Resolving Key
|
||||
* @param rpa The Resolvable Private Address
|
||||
* @return true if the RPA is valid for the given IRK
|
||||
*/
|
||||
Q_INVOKABLE static bool isValidIrkRpa(const QByteArray &irk, const QString &rpa);
|
||||
|
||||
/**
|
||||
* @brief Decrypts the last 16 bytes of the input data using the provided key with AES-128 ECB
|
||||
* @param data The input data containing at least 16 bytes
|
||||
* @param key The 16-byte key for decryption
|
||||
* @return The decrypted 16 bytes, or an empty QByteArray on failure
|
||||
*/
|
||||
static QByteArray decryptLastBytes(const QByteArray &data, const QByteArray &key);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Performs E function (AES-128) as specified in Bluetooth Core Specification
|
||||
* @param key The key for encryption
|
||||
* @param data The data to encrypt
|
||||
* @return The encrypted data
|
||||
*/
|
||||
static QByteArray e(const QByteArray &key, const QByteArray &data);
|
||||
|
||||
/**
|
||||
* @brief Performs the ah function as specified in Bluetooth Core Specification
|
||||
* @param k The IRK key
|
||||
* @param r The random part of the address
|
||||
* @return The hash part of the address
|
||||
*/
|
||||
static QByteArray ah(const QByteArray &k, const QByteArray &r);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "blescanner.h"
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
BleScanner scanner;
|
||||
scanner.show();
|
||||
return app.exec();
|
||||
}
|
||||
@@ -189,26 +189,21 @@ public:
|
||||
setBluetoothAddress("");
|
||||
}
|
||||
|
||||
void save() const
|
||||
void saveToSettings(QSettings &settings)
|
||||
{
|
||||
QSettings settings("AirpodsTrayApp", "DeviceInfo");
|
||||
settings.beginGroup("DeviceInfo");
|
||||
settings.setValue("deviceName", m_deviceName);
|
||||
settings.setValue("bluetoothAddress", m_bluetoothAddress);
|
||||
settings.setValue("magicAccIRK", m_magicAccIRK.toBase64());
|
||||
settings.setValue("magicAccEncKey", m_magicAccEncKey.toBase64());
|
||||
settings.setValue("deviceName", deviceName());
|
||||
settings.setValue("model", static_cast<int>(model()));
|
||||
settings.setValue("magicAccIRK", magicAccIRK());
|
||||
settings.setValue("magicAccEncKey", magicAccEncKey());
|
||||
settings.endGroup();
|
||||
}
|
||||
|
||||
void load()
|
||||
void loadFromSettings(const QSettings &settings)
|
||||
{
|
||||
QSettings settings("AirpodsTrayApp", "DeviceInfo");
|
||||
settings.beginGroup("DeviceInfo");
|
||||
setDeviceName(settings.value("deviceName", "").toString());
|
||||
setBluetoothAddress(settings.value("bluetoothAddress", "").toString());
|
||||
setMagicAccIRK(QByteArray::fromBase64(settings.value("magicAccIRK", "").toByteArray()));
|
||||
setMagicAccEncKey(QByteArray::fromBase64(settings.value("magicAccEncKey", "").toByteArray()));
|
||||
settings.endGroup();
|
||||
setDeviceName(settings.value("DeviceInfo/deviceName", "").toString());
|
||||
setModel(static_cast<AirPodsModel>(settings.value("DeviceInfo/model", (int)(AirPodsModel::Unknown)).toInt()));
|
||||
setMagicAccIRK(settings.value("DeviceInfo/magicAccIRK", QByteArray()).toByteArray());
|
||||
setMagicAccEncKey(settings.value("DeviceInfo/magicAccEncKey", QByteArray()).toByteArray());
|
||||
}
|
||||
|
||||
signals:
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
#include <QSettings>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include "main.h"
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QBluetoothSocket>
|
||||
#include <QQuickWindow>
|
||||
#include <QLoggingCategory>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QProcess>
|
||||
|
||||
#include "airpods_packets.h"
|
||||
#include "logger.h"
|
||||
#include "mediacontroller.h"
|
||||
@@ -11,6 +21,8 @@
|
||||
#include "BluetoothMonitor.h"
|
||||
#include "autostartmanager.hpp"
|
||||
#include "deviceinfo.hpp"
|
||||
#include "ble/blemanager.h"
|
||||
#include "ble/bleutils.h"
|
||||
|
||||
using namespace AirpodsTrayApp::Enums;
|
||||
|
||||
@@ -29,7 +41,9 @@ class AirPodsTrayApp : public QObject {
|
||||
|
||||
public:
|
||||
AirPodsTrayApp(bool debugMode, bool hideOnStart, QQmlApplicationEngine *parent = nullptr)
|
||||
: QObject(parent), debugMode(debugMode), m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp")), m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), parent(parent), m_deviceInfo(new DeviceInfo(this))
|
||||
: QObject(parent), debugMode(debugMode), m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp"))
|
||||
, m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), parent(parent)
|
||||
, m_deviceInfo(new DeviceInfo(this)), m_bleManager(new BleManager(this))
|
||||
{
|
||||
QLoggingCategory::setFilterRules(QString("airpodsApp.debug=%1").arg(debugMode ? "true" : "false"));
|
||||
LOG_INFO("Initializing AirPodsTrayApp");
|
||||
@@ -59,6 +73,7 @@ public:
|
||||
connect(monitor, &BluetoothMonitor::deviceConnected, this, &AirPodsTrayApp::bluezDeviceConnected);
|
||||
connect(monitor, &BluetoothMonitor::deviceDisconnected, this, &AirPodsTrayApp::bluezDeviceDisconnected);
|
||||
|
||||
connect(m_bleManager, &BleManager::deviceFound, this, &AirPodsTrayApp::bleDeviceFound);
|
||||
connect(m_deviceInfo->getBattery(), &Battery::primaryChanged, this, &AirPodsTrayApp::primaryChanged);
|
||||
|
||||
// Load settings
|
||||
@@ -379,6 +394,8 @@ private slots:
|
||||
|
||||
// Clear the device name and model
|
||||
m_deviceInfo->reset();
|
||||
m_bleManager->startScan();
|
||||
emit airPodsStatusChanged();
|
||||
|
||||
// Show system notification
|
||||
trayManager->showNotification(
|
||||
@@ -545,6 +562,7 @@ private slots:
|
||||
// Store the keys
|
||||
m_deviceInfo->setMagicAccIRK(keys.magicAccIRK);
|
||||
m_deviceInfo->setMagicAccEncKey(keys.magicAccEncKey);
|
||||
m_deviceInfo->saveToSettings(*m_settings);
|
||||
}
|
||||
// Get CA state
|
||||
else if (data.startsWith(AirPodsPackets::ConversationalAwareness::HEADER)) {
|
||||
@@ -604,6 +622,7 @@ private slots:
|
||||
{
|
||||
mediaController->activateA2dpProfile();
|
||||
}
|
||||
m_bleManager->stopScan();
|
||||
emit airPodsStatusChanged();
|
||||
}
|
||||
else if (data.startsWith(AirPodsPackets::OneBudANCMode::HEADER)) {
|
||||
@@ -733,6 +752,17 @@ private slots:
|
||||
QMetaObject::invokeMethod(this, "handlePhonePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data));
|
||||
}
|
||||
|
||||
void bleDeviceFound(const BleInfo &device)
|
||||
{
|
||||
if (BLEUtils::isValidIrkRpa(m_deviceInfo->magicAccIRK(), device.address)) {
|
||||
m_deviceInfo->setModel(device.modelName);
|
||||
auto decryptet = BLEUtils::decryptLastBytes(device.encryptedPayload, m_deviceInfo->magicAccEncKey());
|
||||
m_deviceInfo->getBattery()->parseEncryptedPacket(decryptet, device.primaryLeft);
|
||||
m_deviceInfo->setPrimaryInEar(device.isPrimaryInEar);
|
||||
m_deviceInfo->setSecondaryInEar(device.isSecondaryInEar);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void handleMediaStateChange(MediaController::MediaState state) {
|
||||
if (state == MediaController::MediaState::Playing) {
|
||||
@@ -822,6 +852,11 @@ public:
|
||||
|
||||
void initializeBluetooth() {
|
||||
connectToPhone();
|
||||
|
||||
m_deviceInfo->loadFromSettings(*m_settings);
|
||||
if (!areAirpodsConnected()) {
|
||||
m_bleManager->startScan();
|
||||
}
|
||||
}
|
||||
|
||||
void loadMainModule() {
|
||||
@@ -857,6 +892,7 @@ private:
|
||||
int m_retryAttempts = 3;
|
||||
bool m_hideOnStart = false;
|
||||
DeviceInfo *m_deviceInfo;
|
||||
BleManager *m_bleManager;
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
|
||||
36
linux/main.h
36
linux/main.h
@@ -1,36 +0,0 @@
|
||||
#ifndef MAIN_H
|
||||
#define MAIN_H
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QActionGroup>
|
||||
#include <QBluetoothDeviceDiscoveryAgent>
|
||||
#include <QBluetoothLocalDevice>
|
||||
#include <QBluetoothSocket>
|
||||
#include <QQuickWindow>
|
||||
#include <QDebug>
|
||||
#include <QInputDialog>
|
||||
#include <QQmlContext>
|
||||
#include <QLoggingCategory>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#define PHONE_MAC_ADDRESS "22:22:F5:BB:1C:A0"
|
||||
|
||||
#define MANUFACTURER_ID 0x1234
|
||||
#define MANUFACTURER_DATA "ALN_AirPods"
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user