[Linux] CA state parsing, robuster handshake, persistent window (#94)

* [Linux] Don't quit app when closing window

* Add magic pairing functionality

* BLE: Allow selecting text

* Parse CA state from airpods

* Add ability to disable cross-device

* More robust handshake/notification request
This commit is contained in:
Tim Gromeyer
2025-04-14 12:58:55 +02:00
committed by GitHub
parent 42f91c4c46
commit b1811770a3
4 changed files with 194 additions and 55 deletions

View File

@@ -38,10 +38,27 @@ namespace AirPodsPackets
// Conversational Awareness Packets
namespace ConversationalAwareness
{
static const QByteArray HEADER = QByteArray::fromHex("04000400090028"); // Added for parsing
static const QByteArray ENABLED = HEADER + QByteArray::fromHex("01000000");
static const QByteArray DISABLED = HEADER + QByteArray::fromHex("02000000");
static const QByteArray DATA_HEADER = QByteArray::fromHex("040004004B00020001"); // For received data
static const QByteArray HEADER = QByteArray::fromHex("04000400090028"); // For command/status
static const QByteArray ENABLED = HEADER + QByteArray::fromHex("01000000"); // Command to enable
static const QByteArray DISABLED = HEADER + QByteArray::fromHex("02000000"); // Command to disable
static const QByteArray DATA_HEADER = QByteArray::fromHex("040004004B00020001"); // For received speech level data
static std::optional<bool> parseCAState(const QByteArray &data)
{
// Extract the status byte (index 7)
quint8 statusByte = static_cast<quint8>(data.at(HEADER.size())); // HEADER.size() is 7
// Interpret the status byte
switch (statusByte)
{
case 0x01: // Enabled
return true;
case 0x02: // Disabled
return false;
default:
return std::nullopt;
}
}
}
// Connection Packets
@@ -88,12 +105,91 @@ namespace AirPodsPackets
}
}
namespace MagicPairing {
static const QByteArray REQUEST_MAGIC_CLOUD_KEYS = QByteArray::fromHex("0400040030000500");
static const QByteArray MAGIC_CLOUD_KEYS_HEADER = QByteArray::fromHex("04000400310002");
struct MagicCloudKeys {
QByteArray magicAccIRK; // 16 bytes
QByteArray magicAccEncKey; // 16 bytes
};
inline MagicCloudKeys parseMagicCloudKeysPacket(const QByteArray &data)
{
MagicCloudKeys keys;
// Expected size: header (7 bytes) + (1 (tag) + 2 (length) + 1 (reserved) + 16 (value)) * 2 = 47 bytes.
if (data.size() < 47)
{
return keys; // or handle error as needed
}
// Check header
if (!data.startsWith(MAGIC_CLOUD_KEYS_HEADER))
{
return keys; // header mismatch
}
int index = MAGIC_CLOUD_KEYS_HEADER.size(); // Start after header (index 7)
// --- TLV Block 1 (MagicAccIRK) ---
// Tag should be 0x01
if (static_cast<quint8>(data.at(index)) != 0x01)
{
return keys; // unexpected tag
}
index += 1;
// Read length (2 bytes, big-endian)
quint16 len1 = (static_cast<quint8>(data.at(index)) << 8) | static_cast<quint8>(data.at(index + 1));
if (len1 != 16)
{
return keys; // invalid length
}
index += 2;
// Skip reserved byte
index += 1;
// Extract MagicAccIRK (16 bytes)
keys.magicAccIRK = data.mid(index, 16);
index += 16;
// --- TLV Block 2 (MagicAccEncKey) ---
// Tag should be 0x04
if (static_cast<quint8>(data.at(index)) != 0x04)
{
return keys; // unexpected tag
}
index += 1;
// Read length (2 bytes, big-endian)
quint16 len2 = (static_cast<quint8>(data.at(index)) << 8) | static_cast<quint8>(data.at(index + 1));
if (len2 != 16)
{
return keys; // invalid length
}
index += 2;
// Skip reserved byte
index += 1;
// Extract MagicAccEncKey (16 bytes)
keys.magicAccEncKey = data.mid(index, 16);
index += 16;
return keys;
}
}
// Parsing Headers
namespace Parse
{
static const QByteArray EAR_DETECTION = QByteArray::fromHex("040004000600");
static const QByteArray BATTERY_STATUS = QByteArray::fromHex("040004000400");
static const QByteArray METADATA = QByteArray::fromHex("040004001d");
static const QByteArray HANDSHAKE_ACK = QByteArray::fromHex("01000400");
static const QByteArray FEATURES_ACK = QByteArray::fromHex("040004002b00"); // Note: Only tested with airpods pro 2
}
}