diff --git a/linux/Main.qml b/linux/Main.qml index 5e86f2b..faeb9fa 100644 --- a/linux/Main.qml +++ b/linux/Main.qml @@ -72,5 +72,23 @@ ApplicationWindow { anchors.top: parent.bottom } } + + Row { + spacing: 10 + + TextField { + id: newNameField + placeholderText: airPodsTrayApp.deviceName + maximumLength: 32 + } + + Button { + text: "Rename" + onClicked: { + airPodsTrayApp.renameAirPods(newNameField.text) + // Optional: newNameField.text = "" // Clear field after rename + } + } + } } } \ No newline at end of file diff --git a/linux/airpods_packets.h b/linux/airpods_packets.h index 2f71555..e606e02 100644 --- a/linux/airpods_packets.h +++ b/linux/airpods_packets.h @@ -74,11 +74,26 @@ namespace AirPodsPackets } } + namespace Rename + { + static QByteArray getPacket(const QString &newName) + { + QByteArray nameBytes = newName.toUtf8(); // Convert name to UTF-8 + quint8 size = static_cast(nameBytes.size()); // Name length (1 byte) + QByteArray packet = QByteArray::fromHex("040004001A0001"); // Header + packet.append(size); // Append size byte + packet.append('\0'); // Append null byte + packet.append(nameBytes); // Append name bytes + return packet; + } + } + // 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"); } } diff --git a/linux/main.cpp b/linux/main.cpp index 530985f..bde2a51 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -20,6 +20,7 @@ class AirPodsTrayApp : public QObject { Q_PROPERTY(bool conversationalAwareness READ conversationalAwareness WRITE setConversationalAwareness NOTIFY conversationalAwarenessChanged) Q_PROPERTY(int adaptiveNoiseLevel READ adaptiveNoiseLevel WRITE setAdaptiveNoiseLevel NOTIFY adaptiveNoiseLevelChanged) Q_PROPERTY(bool adaptiveModeActive READ adaptiveModeActive NOTIFY noiseControlModeChanged) + Q_PROPERTY(QString deviceName READ deviceName NOTIFY deviceNameChanged) public: AirPodsTrayApp(bool debugMode) : debugMode(debugMode) { @@ -100,6 +101,7 @@ public: bool conversationalAwareness() const { return m_conversationalAwareness; } bool adaptiveModeActive() const { return m_noiseControlMode == NoiseControlMode::Adaptive; } int adaptiveNoiseLevel() const { return m_adaptiveNoiseLevel; } + QString deviceName() const { return m_deviceName; } private: bool debugMode; @@ -272,6 +274,37 @@ public slots: } } + void renameAirPods(const QString &newName) + { + if (newName.isEmpty()) + { + LOG_WARN("Cannot set empty name"); + return; + } + if (newName.size() > 32) + { + LOG_WARN("Name is too long, must be 32 characters or less"); + return; + } + if (newName == m_deviceName) + { + LOG_INFO("Name is already set to: " << newName); + return; + } + + QByteArray packet = AirPodsPackets::Rename::getPacket(newName); + if (writePacketToSocket(packet, "Rename packet written: ")) + { + LOG_INFO("Sent rename command for new name: " << newName); + m_deviceName = newName; + emit deviceNameChanged(newName); + } + else + { + LOG_ERROR("Failed to send rename command: socket not open"); + } + } + bool writePacketToSocket(const QByteArray &packet, const QString &logMessage) { if (socket && socket->isOpen()) @@ -363,6 +396,77 @@ private slots: } } + void parseMetadata(const QByteArray &data) + { + // Verify the data starts with the METADATA header + if (!data.startsWith(AirPodsPackets::Parse::METADATA)) + { + LOG_ERROR("Invalid metadata packet: Incorrect header"); + return; + } + + int pos = AirPodsPackets::Parse::METADATA.size(); // Start after the header + + // Check if there is enough data to skip the initial bytes (based on example structure) + if (data.size() < pos + 6) + { + LOG_ERROR("Metadata packet too short to parse initial bytes"); + return; + } + pos += 6; // Skip 6 bytes after the header as per example structure + + auto extractString = [&data, &pos]() -> QString + { + if (pos >= data.size()) + { + return QString(); + } + int start = pos; + while (pos < data.size() && data.at(pos) != '\0') + { + ++pos; + } + QString str = QString::fromUtf8(data.mid(start, pos - start)); + if (pos < data.size()) + { + ++pos; // Move past the null terminator + } + return str; + }; + + m_deviceName = extractString(); + QString modelNumber = extractString(); + QString manufacturer = extractString(); + QString hardwareVersion = extractString(); + QString firmwareVersion = extractString(); + QString firmwareVersion2 = extractString(); + QString softwareVersion = extractString(); + QString appIdentifier = extractString(); + QString serialNumber1 = extractString(); + QString serialNumber2 = extractString(); + QString unknownNumeric = extractString(); + QString unknownHash = extractString(); + QString trailingByte = extractString(); + + emit deviceNameChanged(m_deviceName); + + // Log extracted metadata + LOG_INFO("Parsed AirPods metadata:"); + LOG_INFO("Device Name: " << m_deviceName); + LOG_INFO("Model Number: " << modelNumber); + LOG_INFO("Manufacturer: " << manufacturer); + LOG_INFO("Hardware Version: " << hardwareVersion); + LOG_INFO("Firmware Version: " << firmwareVersion); + LOG_INFO("Firmware Version2: " << firmwareVersion2); + LOG_INFO("Software Version: " << softwareVersion); + LOG_INFO("App Identifier: " << appIdentifier); + LOG_INFO("Serial Number 1: " << serialNumber1); + LOG_INFO("Serial Number 2: " << serialNumber2); + LOG_INFO("Unknown Numeric: " << unknownNumeric); + LOG_INFO("Unknown Hash: " << unknownHash); + LOG_INFO("Trailing Byte: " << trailingByte); + } + void connectToDevice(const QBluetoothDeviceInfo &device) { if (socket && socket->isOpen() && socket->peerAddress() == device.address()) { LOG_INFO("Already connected to the device: " << device.name()); @@ -490,6 +594,10 @@ private slots: LOG_INFO("Received conversational awareness data"); mediaController->handleConversationalAwareness(data); } + else if (data.startsWith(AirPodsPackets::Parse::METADATA)) + { + parseMetadata(data); + } else { LOG_DEBUG("Unrecognized packet format: " << data.toHex()); @@ -714,6 +822,7 @@ signals: void batteryStatusChanged(const QString &status); void conversationalAwarenessChanged(bool enabled); void adaptiveNoiseLevelChanged(int level); + void deviceNameChanged(const QString &name); private: QSystemTrayIcon *trayIcon; @@ -733,6 +842,7 @@ private: NoiseControlMode m_noiseControlMode = NoiseControlMode::Off; bool m_conversationalAwareness = false; int m_adaptiveNoiseLevel = 50; + QString m_deviceName; }; int main(int argc, char *argv[]) {