Skip to content

Commit 543362d

Browse files
tim-gromeyerTim Gromeyer
andauthored
[Linux] Implement renaming airpods (#88)
* [Linux] Implement rename airpods * [Linux] Get airpods name from airpods metadata * [Linux] Rename AirPods: Ui improvements --------- Co-authored-by: Tim Gromeyer <[email protected]>
1 parent 5a71d96 commit 543362d

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

linux/Main.qml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,23 @@ ApplicationWindow {
7272
anchors.top: parent.bottom
7373
}
7474
}
75+
76+
Row {
77+
spacing: 10
78+
79+
TextField {
80+
id: newNameField
81+
placeholderText: airPodsTrayApp.deviceName
82+
maximumLength: 32
83+
}
84+
85+
Button {
86+
text: "Rename"
87+
onClicked: {
88+
airPodsTrayApp.renameAirPods(newNameField.text)
89+
// Optional: newNameField.text = "" // Clear field after rename
90+
}
91+
}
92+
}
7593
}
7694
}

linux/airpods_packets.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,26 @@ namespace AirPodsPackets
7474
}
7575
}
7676

77+
namespace Rename
78+
{
79+
static QByteArray getPacket(const QString &newName)
80+
{
81+
QByteArray nameBytes = newName.toUtf8(); // Convert name to UTF-8
82+
quint8 size = static_cast<char>(nameBytes.size()); // Name length (1 byte)
83+
QByteArray packet = QByteArray::fromHex("040004001A0001"); // Header
84+
packet.append(size); // Append size byte
85+
packet.append('\0'); // Append null byte
86+
packet.append(nameBytes); // Append name bytes
87+
return packet;
88+
}
89+
}
90+
7791
// Parsing Headers
7892
namespace Parse
7993
{
8094
static const QByteArray EAR_DETECTION = QByteArray::fromHex("040004000600");
8195
static const QByteArray BATTERY_STATUS = QByteArray::fromHex("040004000400");
96+
static const QByteArray METADATA = QByteArray::fromHex("040004001d");
8297
}
8398
}
8499

linux/main.cpp

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class AirPodsTrayApp : public QObject {
2020
Q_PROPERTY(bool conversationalAwareness READ conversationalAwareness WRITE setConversationalAwareness NOTIFY conversationalAwarenessChanged)
2121
Q_PROPERTY(int adaptiveNoiseLevel READ adaptiveNoiseLevel WRITE setAdaptiveNoiseLevel NOTIFY adaptiveNoiseLevelChanged)
2222
Q_PROPERTY(bool adaptiveModeActive READ adaptiveModeActive NOTIFY noiseControlModeChanged)
23+
Q_PROPERTY(QString deviceName READ deviceName NOTIFY deviceNameChanged)
2324

2425
public:
2526
AirPodsTrayApp(bool debugMode) : debugMode(debugMode) {
@@ -100,6 +101,7 @@ class AirPodsTrayApp : public QObject {
100101
bool conversationalAwareness() const { return m_conversationalAwareness; }
101102
bool adaptiveModeActive() const { return m_noiseControlMode == NoiseControlMode::Adaptive; }
102103
int adaptiveNoiseLevel() const { return m_adaptiveNoiseLevel; }
104+
QString deviceName() const { return m_deviceName; }
103105

104106
private:
105107
bool debugMode;
@@ -272,6 +274,37 @@ public slots:
272274
}
273275
}
274276

277+
void renameAirPods(const QString &newName)
278+
{
279+
if (newName.isEmpty())
280+
{
281+
LOG_WARN("Cannot set empty name");
282+
return;
283+
}
284+
if (newName.size() > 32)
285+
{
286+
LOG_WARN("Name is too long, must be 32 characters or less");
287+
return;
288+
}
289+
if (newName == m_deviceName)
290+
{
291+
LOG_INFO("Name is already set to: " << newName);
292+
return;
293+
}
294+
295+
QByteArray packet = AirPodsPackets::Rename::getPacket(newName);
296+
if (writePacketToSocket(packet, "Rename packet written: "))
297+
{
298+
LOG_INFO("Sent rename command for new name: " << newName);
299+
m_deviceName = newName;
300+
emit deviceNameChanged(newName);
301+
}
302+
else
303+
{
304+
LOG_ERROR("Failed to send rename command: socket not open");
305+
}
306+
}
307+
275308
bool writePacketToSocket(const QByteArray &packet, const QString &logMessage)
276309
{
277310
if (socket && socket->isOpen())
@@ -363,6 +396,77 @@ private slots:
363396
}
364397
}
365398

399+
void parseMetadata(const QByteArray &data)
400+
{
401+
// Verify the data starts with the METADATA header
402+
if (!data.startsWith(AirPodsPackets::Parse::METADATA))
403+
{
404+
LOG_ERROR("Invalid metadata packet: Incorrect header");
405+
return;
406+
}
407+
408+
int pos = AirPodsPackets::Parse::METADATA.size(); // Start after the header
409+
410+
// Check if there is enough data to skip the initial bytes (based on example structure)
411+
if (data.size() < pos + 6)
412+
{
413+
LOG_ERROR("Metadata packet too short to parse initial bytes");
414+
return;
415+
}
416+
pos += 6; // Skip 6 bytes after the header as per example structure
417+
418+
auto extractString = [&data, &pos]() -> QString
419+
{
420+
if (pos >= data.size())
421+
{
422+
return QString();
423+
}
424+
int start = pos;
425+
while (pos < data.size() && data.at(pos) != '\0')
426+
{
427+
++pos;
428+
}
429+
QString str = QString::fromUtf8(data.mid(start, pos - start));
430+
if (pos < data.size())
431+
{
432+
++pos; // Move past the null terminator
433+
}
434+
return str;
435+
};
436+
437+
m_deviceName = extractString();
438+
QString modelNumber = extractString();
439+
QString manufacturer = extractString();
440+
QString hardwareVersion = extractString();
441+
QString firmwareVersion = extractString();
442+
QString firmwareVersion2 = extractString();
443+
QString softwareVersion = extractString();
444+
QString appIdentifier = extractString();
445+
QString serialNumber1 = extractString();
446+
QString serialNumber2 = extractString();
447+
QString unknownNumeric = extractString();
448+
QString unknownHash = extractString();
449+
QString trailingByte = extractString();
450+
451+
emit deviceNameChanged(m_deviceName);
452+
453+
// Log extracted metadata
454+
LOG_INFO("Parsed AirPods metadata:");
455+
LOG_INFO("Device Name: " << m_deviceName);
456+
LOG_INFO("Model Number: " << modelNumber);
457+
LOG_INFO("Manufacturer: " << manufacturer);
458+
LOG_INFO("Hardware Version: " << hardwareVersion);
459+
LOG_INFO("Firmware Version: " << firmwareVersion);
460+
LOG_INFO("Firmware Version2: " << firmwareVersion2);
461+
LOG_INFO("Software Version: " << softwareVersion);
462+
LOG_INFO("App Identifier: " << appIdentifier);
463+
LOG_INFO("Serial Number 1: " << serialNumber1);
464+
LOG_INFO("Serial Number 2: " << serialNumber2);
465+
LOG_INFO("Unknown Numeric: " << unknownNumeric);
466+
LOG_INFO("Unknown Hash: " << unknownHash);
467+
LOG_INFO("Trailing Byte: " << trailingByte);
468+
}
469+
366470
void connectToDevice(const QBluetoothDeviceInfo &device) {
367471
if (socket && socket->isOpen() && socket->peerAddress() == device.address()) {
368472
LOG_INFO("Already connected to the device: " << device.name());
@@ -490,6 +594,10 @@ private slots:
490594
LOG_INFO("Received conversational awareness data");
491595
mediaController->handleConversationalAwareness(data);
492596
}
597+
else if (data.startsWith(AirPodsPackets::Parse::METADATA))
598+
{
599+
parseMetadata(data);
600+
}
493601
else
494602
{
495603
LOG_DEBUG("Unrecognized packet format: " << data.toHex());
@@ -714,6 +822,7 @@ private slots:
714822
void batteryStatusChanged(const QString &status);
715823
void conversationalAwarenessChanged(bool enabled);
716824
void adaptiveNoiseLevelChanged(int level);
825+
void deviceNameChanged(const QString &name);
717826

718827
private:
719828
QSystemTrayIcon *trayIcon;
@@ -733,6 +842,7 @@ private slots:
733842
NoiseControlMode m_noiseControlMode = NoiseControlMode::Off;
734843
bool m_conversationalAwareness = false;
735844
int m_adaptiveNoiseLevel = 50;
845+
QString m_deviceName;
736846
};
737847

738848
int main(int argc, char *argv[]) {

0 commit comments

Comments
 (0)