From 9161f8b294071178967fbd27e86049f05554e439 Mon Sep 17 00:00:00 2001 From: Tim Gromeyer Date: Thu, 15 May 2025 12:00:01 +0200 Subject: [PATCH] [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);