From 9161f8b294071178967fbd27e86049f05554e439 Mon Sep 17 00:00:00 2001 From: Tim Gromeyer Date: Thu, 15 May 2025 12:00:01 +0200 Subject: [PATCH 1/3] [Linux] Add more control commands (4c0381968fe20d26ffece8d33764523609afc36f) --- linux/BasicControlCommand.hpp | 69 ++++++++++++++ linux/CMakeLists.txt | 1 + linux/airpods_packets.h | 164 +++++++++++++++++++++------------- linux/main.cpp | 2 +- 4 files changed, 171 insertions(+), 65 deletions(-) create mode 100644 linux/BasicControlCommand.hpp diff --git a/linux/BasicControlCommand.hpp b/linux/BasicControlCommand.hpp new file mode 100644 index 0000000..747068e --- /dev/null +++ b/linux/BasicControlCommand.hpp @@ -0,0 +1,69 @@ +#include + +// Control Command Header +namespace ControlCommand +{ + static const QByteArray HEADER = QByteArray::fromHex("040004000900"); + + // Helper function to create control command packets + static QByteArray createCommand(quint8 identifier, quint8 data1 = 0x00, quint8 data2 = 0x00, + quint8 data3 = 0x00, quint8 data4 = 0x00) + { + QByteArray packet = HEADER; + packet.append(static_cast(identifier)); + packet.append(static_cast(data1)); + packet.append(static_cast(data2)); + packet.append(static_cast(data3)); + packet.append(static_cast(data4)); + return packet; + } + + // Parse activated/not activated + inline std::optional parseActive(const QByteArray &data) + { + if (!data.startsWith(ControlCommand::HEADER)) + return std::nullopt; + + quint8 statusByte = static_cast(data.at(7)); + switch (statusByte) + { + case 0x01: // Enabled + return true; + case 0x02: // Disabled + return false; + default: + return std::nullopt; + } + } +} + +template +struct BasicControlCommand +{ + static constexpr quint8 ID = CommandId; + static const QByteArray HEADER; + + static const QByteArray ENABLED; + static const QByteArray DISABLED; + + static QByteArray create(quint8 data1 = 0x00, quint8 data2 = 0x00, + quint8 data3 = 0x00, quint8 data4 = 0x00) + { + return ControlCommand::createCommand(ID, data1, data2, data3, data4); + } + + // Basically returns the byte at the index 7 + static std::optional parseState(const QByteArray &data) + { + return ControlCommand::parseActive(data); + } +}; + +template +const QByteArray BasicControlCommand::HEADER = ControlCommand::HEADER + static_cast(CommandId); + +template +const QByteArray BasicControlCommand::ENABLED = create(0x01); + +template +const QByteArray BasicControlCommand::DISABLED = create(0x02); \ No newline at end of file diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 50363a6..186b108 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -22,6 +22,7 @@ qt_add_executable(applinux BluetoothMonitor.cpp BluetoothMonitor.h autostartmanager.hpp + BasicControlCommand.hpp ) qt_add_qml_module(applinux diff --git a/linux/airpods_packets.h b/linux/airpods_packets.h index 8148e92..938a88a 100644 --- a/linux/airpods_packets.h +++ b/linux/airpods_packets.h @@ -3,18 +3,21 @@ #define AIRPODS_PACKETS_H #include +#include + #include "enums.h" +#include "BasicControlCommand.hpp" namespace AirPodsPackets { // Noise Control Mode Packets namespace NoiseControl { - static const QByteArray HEADER = QByteArray::fromHex("0400040009000D"); // Added for parsing - static const QByteArray OFF = HEADER + QByteArray::fromHex("01000000"); - static const QByteArray NOISE_CANCELLATION = HEADER + QByteArray::fromHex("02000000"); - static const QByteArray TRANSPARENCY = HEADER + QByteArray::fromHex("03000000"); - static const QByteArray ADAPTIVE = HEADER + QByteArray::fromHex("04000000"); + static const QByteArray HEADER = ControlCommand::HEADER + 0x0D; + static const QByteArray OFF = ControlCommand::createCommand(0x0D, 0x01); + static const QByteArray NOISE_CANCELLATION = ControlCommand::createCommand(0x0D, 0x02); + static const QByteArray TRANSPARENCY = ControlCommand::createCommand(0x0D, 0x03); + static const QByteArray ADAPTIVE = ControlCommand::createCommand(0x0D, 0x04); static const QByteArray getPacketForMode(AirpodsTrayApp::Enums::NoiseControlMode mode) { @@ -35,30 +38,91 @@ namespace AirPodsPackets } } - // Conversational Awareness Packets + // VoiceTrigger for Siri + namespace VoiceTrigger + { + using Type = BasicControlCommand<0x12>; + static const QByteArray ENABLED = Type::ENABLED; + static const QByteArray DISABLED = Type::DISABLED; + static const QByteArray HEADER = Type::HEADER; + inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } + } + + // One Bud ANC Mode + namespace OneBudANCMode + { + using Type = BasicControlCommand<0x1B>; + static const QByteArray ENABLED = Type::ENABLED; + static const QByteArray DISABLED = Type::DISABLED; + static const QByteArray HEADER = Type::HEADER; + inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } + } + + // Volume Swipe (partial - still needs custom interval function) + namespace VolumeSwipe + { + using Type = BasicControlCommand<0x25>; + static const QByteArray ENABLED = Type::ENABLED; + static const QByteArray DISABLED = Type::DISABLED; + static const QByteArray HEADER = Type::HEADER; + inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } + + // Keep custom interval function + static QByteArray getIntervalPacket(quint8 interval) + { + return ControlCommand::createCommand(0x23, interval); + } + } + + // Adaptive Volume Config + namespace AdaptiveVolume + { + using Type = BasicControlCommand<0x26>; + static const QByteArray ENABLED = Type::ENABLED; + static const QByteArray DISABLED = Type::DISABLED; + static const QByteArray HEADER = Type::HEADER; + inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } + } + + // Conversational Awareness namespace ConversationalAwareness { - 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 + using Type = BasicControlCommand<0x28>; + static const QByteArray ENABLED = Type::ENABLED; + static const QByteArray DISABLED = Type::DISABLED; + static const QByteArray HEADER = Type::HEADER; + static const QByteArray DATA_HEADER = QByteArray::fromHex("040004004B00020001"); + inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } + } - static std::optional parseCAState(const QByteArray &data) - { - // Extract the status byte (index 7) - quint8 statusByte = static_cast(data.at(HEADER.size())); // HEADER.size() is 7 + // In Case Tone + namespace InCaseTone + { + using Type = BasicControlCommand<0x31>; + static const QByteArray ENABLED = Type::ENABLED; + static const QByteArray DISABLED = Type::DISABLED; + static const QByteArray HEADER = Type::HEADER; + inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } + } - // Interpret the status byte - switch (statusByte) - { - case 0x01: // Enabled - return true; - case 0x02: // Disabled - return false; - default: - return std::nullopt; - } - } + // Hearing Assist + namespace HearingAssist + { + using Type = BasicControlCommand<0x33>; + static const QByteArray ENABLED = Type::ENABLED; + static const QByteArray DISABLED = Type::DISABLED; + static const QByteArray HEADER = Type::HEADER; + inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } + } + + // Allow Off Option + namespace AllowOffOption + { + using Type = BasicControlCommand<0x34>; + static const QByteArray ENABLED = Type::ENABLED; + static const QByteArray DISABLED = Type::DISABLED; + static const QByteArray HEADER = Type::HEADER; + inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } } // Connection Packets @@ -118,65 +182,37 @@ namespace AirPodsPackets { MagicCloudKeys keys; - // Expected size: header (7 bytes) + (1 (tag) + 2 (length) + 1 (reserved) + 16 (value)) * 2 = 47 bytes. - if (data.size() < 47) + if (data.size() < 47 || !data.startsWith(MAGIC_CLOUD_KEYS_HEADER)) { - return keys; // or handle error as needed + return keys; } - // Check header - if (!data.startsWith(MAGIC_CLOUD_KEYS_HEADER)) - { - return keys; // header mismatch - } + int index = MAGIC_CLOUD_KEYS_HEADER.size(); - int index = MAGIC_CLOUD_KEYS_HEADER.size(); // Start after header (index 7) - - // --- TLV Block 1 (MagicAccIRK) --- - // Tag should be 0x01 + // First TLV block (MagicAccIRK) if (static_cast(data.at(index)) != 0x01) - { - return keys; // unexpected tag - } + return keys; index += 1; - // Read length (2 bytes, big-endian) quint16 len1 = (static_cast(data.at(index)) << 8) | static_cast(data.at(index + 1)); if (len1 != 16) - { - return keys; // invalid length - } - index += 2; + return keys; + index += 3; // Skip length and reserved byte - // 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 + // Second TLV block (MagicAccEncKey) if (static_cast(data.at(index)) != 0x04) - { - return keys; // unexpected tag - } + return keys; index += 1; - // Read length (2 bytes, big-endian) quint16 len2 = (static_cast(data.at(index)) << 8) | static_cast(data.at(index + 1)); if (len2 != 16) - { - return keys; // invalid length - } - index += 2; + return keys; + index += 3; // Skip length and reserved byte - // Skip reserved byte - index += 1; - - // Extract MagicAccEncKey (16 bytes) keys.magicAccEncKey = data.mid(index, 16); - index += 16; return keys; } diff --git a/linux/main.cpp b/linux/main.cpp index 92fb39f..05e099a 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -628,7 +628,7 @@ private slots: } // Get CA state else if (data.startsWith(AirPodsPackets::ConversationalAwareness::HEADER)) { - auto result = AirPodsPackets::ConversationalAwareness::parseCAState(data); + auto result = AirPodsPackets::ConversationalAwareness::parseState(data); if (result.has_value()) { m_conversationalAwareness = result.value(); LOG_INFO("Conversational awareness state received: " << m_conversationalAwareness); From 4b3cc92e566d4eb6b2c28082a350185e0f2e46fd Mon Sep 17 00:00:00 2001 From: Tim Gromeyer Date: Fri, 16 May 2025 08:41:29 +0200 Subject: [PATCH 2/3] Make the copilot reviewer happy --- linux/airpods_packets.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linux/airpods_packets.h b/linux/airpods_packets.h index 938a88a..9c33968 100644 --- a/linux/airpods_packets.h +++ b/linux/airpods_packets.h @@ -197,7 +197,7 @@ namespace AirPodsPackets quint16 len1 = (static_cast(data.at(index)) << 8) | static_cast(data.at(index + 1)); if (len1 != 16) return keys; - index += 3; // Skip length and reserved byte + index += 3; // Skip length (2 bytes) and reserved byte (1 byte) keys.magicAccIRK = data.mid(index, 16); index += 16; @@ -210,7 +210,7 @@ namespace AirPodsPackets quint16 len2 = (static_cast(data.at(index)) << 8) | static_cast(data.at(index + 1)); if (len2 != 16) return keys; - index += 3; // Skip length and reserved byte + index += 3; // Skip length (2 bytes) and reserved byte (1 byte) keys.magicAccEncKey = data.mid(index, 16); From 84891a0bdf526905343fa7fc841d14efd8c40639 Mon Sep 17 00:00:00 2001 From: Tim Gromeyer Date: Fri, 16 May 2025 12:10:40 +0200 Subject: [PATCH 3/3] Remove VoiceTrigger and InCaseTone --- linux/airpods_packets.h | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/linux/airpods_packets.h b/linux/airpods_packets.h index 9c33968..e7a4db1 100644 --- a/linux/airpods_packets.h +++ b/linux/airpods_packets.h @@ -38,16 +38,6 @@ namespace AirPodsPackets } } - // VoiceTrigger for Siri - namespace VoiceTrigger - { - using Type = BasicControlCommand<0x12>; - static const QByteArray ENABLED = Type::ENABLED; - static const QByteArray DISABLED = Type::DISABLED; - static const QByteArray HEADER = Type::HEADER; - inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } - } - // One Bud ANC Mode namespace OneBudANCMode { @@ -95,16 +85,6 @@ namespace AirPodsPackets inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } } - // In Case Tone - namespace InCaseTone - { - using Type = BasicControlCommand<0x31>; - static const QByteArray ENABLED = Type::ENABLED; - static const QByteArray DISABLED = Type::DISABLED; - static const QByteArray HEADER = Type::HEADER; - inline std::optional parseState(const QByteArray &data) { return Type::parseState(data); } - } - // Hearing Assist namespace HearingAssist {