Skip to content

Commit fd917d3

Browse files
authored
[Linux] Add more control commands (#127)
2 parents b89d6d9 + 84891a0 commit fd917d3

File tree

4 files changed

+151
-65
lines changed

4 files changed

+151
-65
lines changed

linux/BasicControlCommand.hpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#include <QByteArray>
2+
3+
// Control Command Header
4+
namespace ControlCommand
5+
{
6+
static const QByteArray HEADER = QByteArray::fromHex("040004000900");
7+
8+
// Helper function to create control command packets
9+
static QByteArray createCommand(quint8 identifier, quint8 data1 = 0x00, quint8 data2 = 0x00,
10+
quint8 data3 = 0x00, quint8 data4 = 0x00)
11+
{
12+
QByteArray packet = HEADER;
13+
packet.append(static_cast<char>(identifier));
14+
packet.append(static_cast<char>(data1));
15+
packet.append(static_cast<char>(data2));
16+
packet.append(static_cast<char>(data3));
17+
packet.append(static_cast<char>(data4));
18+
return packet;
19+
}
20+
21+
// Parse activated/not activated
22+
inline std::optional<bool> parseActive(const QByteArray &data)
23+
{
24+
if (!data.startsWith(ControlCommand::HEADER))
25+
return std::nullopt;
26+
27+
quint8 statusByte = static_cast<quint8>(data.at(7));
28+
switch (statusByte)
29+
{
30+
case 0x01: // Enabled
31+
return true;
32+
case 0x02: // Disabled
33+
return false;
34+
default:
35+
return std::nullopt;
36+
}
37+
}
38+
}
39+
40+
template <quint8 CommandId>
41+
struct BasicControlCommand
42+
{
43+
static constexpr quint8 ID = CommandId;
44+
static const QByteArray HEADER;
45+
46+
static const QByteArray ENABLED;
47+
static const QByteArray DISABLED;
48+
49+
static QByteArray create(quint8 data1 = 0x00, quint8 data2 = 0x00,
50+
quint8 data3 = 0x00, quint8 data4 = 0x00)
51+
{
52+
return ControlCommand::createCommand(ID, data1, data2, data3, data4);
53+
}
54+
55+
// Basically returns the byte at the index 7
56+
static std::optional<bool> parseState(const QByteArray &data)
57+
{
58+
return ControlCommand::parseActive(data);
59+
}
60+
};
61+
62+
template <quint8 CommandId>
63+
const QByteArray BasicControlCommand<CommandId>::HEADER = ControlCommand::HEADER + static_cast<char>(CommandId);
64+
65+
template <quint8 CommandId>
66+
const QByteArray BasicControlCommand<CommandId>::ENABLED = create(0x01);
67+
68+
template <quint8 CommandId>
69+
const QByteArray BasicControlCommand<CommandId>::DISABLED = create(0x02);

linux/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ qt_add_executable(applinux
2222
BluetoothMonitor.cpp
2323
BluetoothMonitor.h
2424
autostartmanager.hpp
25+
BasicControlCommand.hpp
2526
)
2627

2728
qt_add_qml_module(applinux

linux/airpods_packets.h

Lines changed: 80 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
#define AIRPODS_PACKETS_H
44

55
#include <QByteArray>
6+
#include <optional>
7+
68
#include "enums.h"
9+
#include "BasicControlCommand.hpp"
710

811
namespace AirPodsPackets
912
{
1013
// Noise Control Mode Packets
1114
namespace NoiseControl
1215
{
13-
static const QByteArray HEADER = QByteArray::fromHex("0400040009000D"); // Added for parsing
14-
static const QByteArray OFF = HEADER + QByteArray::fromHex("01000000");
15-
static const QByteArray NOISE_CANCELLATION = HEADER + QByteArray::fromHex("02000000");
16-
static const QByteArray TRANSPARENCY = HEADER + QByteArray::fromHex("03000000");
17-
static const QByteArray ADAPTIVE = HEADER + QByteArray::fromHex("04000000");
16+
static const QByteArray HEADER = ControlCommand::HEADER + 0x0D;
17+
static const QByteArray OFF = ControlCommand::createCommand(0x0D, 0x01);
18+
static const QByteArray NOISE_CANCELLATION = ControlCommand::createCommand(0x0D, 0x02);
19+
static const QByteArray TRANSPARENCY = ControlCommand::createCommand(0x0D, 0x03);
20+
static const QByteArray ADAPTIVE = ControlCommand::createCommand(0x0D, 0x04);
1821

1922
static const QByteArray getPacketForMode(AirpodsTrayApp::Enums::NoiseControlMode mode)
2023
{
@@ -35,32 +38,73 @@ namespace AirPodsPackets
3538
}
3639
}
3740

38-
// Conversational Awareness Packets
39-
namespace ConversationalAwareness
41+
// One Bud ANC Mode
42+
namespace OneBudANCMode
4043
{
41-
static const QByteArray HEADER = QByteArray::fromHex("04000400090028"); // For command/status
42-
static const QByteArray ENABLED = HEADER + QByteArray::fromHex("01000000"); // Command to enable
43-
static const QByteArray DISABLED = HEADER + QByteArray::fromHex("02000000"); // Command to disable
44-
static const QByteArray DATA_HEADER = QByteArray::fromHex("040004004B00020001"); // For received speech level data
44+
using Type = BasicControlCommand<0x1B>;
45+
static const QByteArray ENABLED = Type::ENABLED;
46+
static const QByteArray DISABLED = Type::DISABLED;
47+
static const QByteArray HEADER = Type::HEADER;
48+
inline std::optional<bool> parseState(const QByteArray &data) { return Type::parseState(data); }
49+
}
4550

46-
static std::optional<bool> parseCAState(const QByteArray &data)
51+
// Volume Swipe (partial - still needs custom interval function)
52+
namespace VolumeSwipe
53+
{
54+
using Type = BasicControlCommand<0x25>;
55+
static const QByteArray ENABLED = Type::ENABLED;
56+
static const QByteArray DISABLED = Type::DISABLED;
57+
static const QByteArray HEADER = Type::HEADER;
58+
inline std::optional<bool> parseState(const QByteArray &data) { return Type::parseState(data); }
59+
60+
// Keep custom interval function
61+
static QByteArray getIntervalPacket(quint8 interval)
4762
{
48-
// Extract the status byte (index 7)
49-
quint8 statusByte = static_cast<quint8>(data.at(HEADER.size())); // HEADER.size() is 7
50-
51-
// Interpret the status byte
52-
switch (statusByte)
53-
{
54-
case 0x01: // Enabled
55-
return true;
56-
case 0x02: // Disabled
57-
return false;
58-
default:
59-
return std::nullopt;
60-
}
63+
return ControlCommand::createCommand(0x23, interval);
6164
}
6265
}
6366

67+
// Adaptive Volume Config
68+
namespace AdaptiveVolume
69+
{
70+
using Type = BasicControlCommand<0x26>;
71+
static const QByteArray ENABLED = Type::ENABLED;
72+
static const QByteArray DISABLED = Type::DISABLED;
73+
static const QByteArray HEADER = Type::HEADER;
74+
inline std::optional<bool> parseState(const QByteArray &data) { return Type::parseState(data); }
75+
}
76+
77+
// Conversational Awareness
78+
namespace ConversationalAwareness
79+
{
80+
using Type = BasicControlCommand<0x28>;
81+
static const QByteArray ENABLED = Type::ENABLED;
82+
static const QByteArray DISABLED = Type::DISABLED;
83+
static const QByteArray HEADER = Type::HEADER;
84+
static const QByteArray DATA_HEADER = QByteArray::fromHex("040004004B00020001");
85+
inline std::optional<bool> parseState(const QByteArray &data) { return Type::parseState(data); }
86+
}
87+
88+
// Hearing Assist
89+
namespace HearingAssist
90+
{
91+
using Type = BasicControlCommand<0x33>;
92+
static const QByteArray ENABLED = Type::ENABLED;
93+
static const QByteArray DISABLED = Type::DISABLED;
94+
static const QByteArray HEADER = Type::HEADER;
95+
inline std::optional<bool> parseState(const QByteArray &data) { return Type::parseState(data); }
96+
}
97+
98+
// Allow Off Option
99+
namespace AllowOffOption
100+
{
101+
using Type = BasicControlCommand<0x34>;
102+
static const QByteArray ENABLED = Type::ENABLED;
103+
static const QByteArray DISABLED = Type::DISABLED;
104+
static const QByteArray HEADER = Type::HEADER;
105+
inline std::optional<bool> parseState(const QByteArray &data) { return Type::parseState(data); }
106+
}
107+
64108
// Connection Packets
65109
namespace Connection
66110
{
@@ -118,65 +162,37 @@ namespace AirPodsPackets
118162
{
119163
MagicCloudKeys keys;
120164

121-
// Expected size: header (7 bytes) + (1 (tag) + 2 (length) + 1 (reserved) + 16 (value)) * 2 = 47 bytes.
122-
if (data.size() < 47)
123-
{
124-
return keys; // or handle error as needed
125-
}
126-
127-
// Check header
128-
if (!data.startsWith(MAGIC_CLOUD_KEYS_HEADER))
165+
if (data.size() < 47 || !data.startsWith(MAGIC_CLOUD_KEYS_HEADER))
129166
{
130-
return keys; // header mismatch
167+
return keys;
131168
}
132169

133-
int index = MAGIC_CLOUD_KEYS_HEADER.size(); // Start after header (index 7)
170+
int index = MAGIC_CLOUD_KEYS_HEADER.size();
134171

135-
// --- TLV Block 1 (MagicAccIRK) ---
136-
// Tag should be 0x01
172+
// First TLV block (MagicAccIRK)
137173
if (static_cast<quint8>(data.at(index)) != 0x01)
138-
{
139-
return keys; // unexpected tag
140-
}
174+
return keys;
141175
index += 1;
142176

143-
// Read length (2 bytes, big-endian)
144177
quint16 len1 = (static_cast<quint8>(data.at(index)) << 8) | static_cast<quint8>(data.at(index + 1));
145178
if (len1 != 16)
146-
{
147-
return keys; // invalid length
148-
}
149-
index += 2;
179+
return keys;
180+
index += 3; // Skip length (2 bytes) and reserved byte (1 byte)
150181

151-
// Skip reserved byte
152-
index += 1;
153-
154-
// Extract MagicAccIRK (16 bytes)
155182
keys.magicAccIRK = data.mid(index, 16);
156183
index += 16;
157184

158-
// --- TLV Block 2 (MagicAccEncKey) ---
159-
// Tag should be 0x04
185+
// Second TLV block (MagicAccEncKey)
160186
if (static_cast<quint8>(data.at(index)) != 0x04)
161-
{
162-
return keys; // unexpected tag
163-
}
187+
return keys;
164188
index += 1;
165189

166-
// Read length (2 bytes, big-endian)
167190
quint16 len2 = (static_cast<quint8>(data.at(index)) << 8) | static_cast<quint8>(data.at(index + 1));
168191
if (len2 != 16)
169-
{
170-
return keys; // invalid length
171-
}
172-
index += 2;
173-
174-
// Skip reserved byte
175-
index += 1;
192+
return keys;
193+
index += 3; // Skip length (2 bytes) and reserved byte (1 byte)
176194

177-
// Extract MagicAccEncKey (16 bytes)
178195
keys.magicAccEncKey = data.mid(index, 16);
179-
index += 16;
180196

181197
return keys;
182198
}

linux/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ private slots:
628628
}
629629
// Get CA state
630630
else if (data.startsWith(AirPodsPackets::ConversationalAwareness::HEADER)) {
631-
auto result = AirPodsPackets::ConversationalAwareness::parseCAState(data);
631+
auto result = AirPodsPackets::ConversationalAwareness::parseState(data);
632632
if (result.has_value()) {
633633
m_conversationalAwareness = result.value();
634634
LOG_INFO("Conversational awareness state received: " << m_conversationalAwareness);

0 commit comments

Comments
 (0)