diff --git a/linux/airpods_packets.h b/linux/airpods_packets.h index 94153a484..cd7d1273e 100644 --- a/linux/airpods_packets.h +++ b/linux/airpods_packets.h @@ -152,6 +152,26 @@ namespace AirPodsPackets static const QByteArray SET_SPECIFIC_FEATURES = QByteArray::fromHex("040004004d00d700000000000000"); static const QByteArray REQUEST_NOTIFICATIONS = QByteArray::fromHex("040004000f00ffffffffff"); static const QByteArray AIRPODS_DISCONNECTED = QByteArray::fromHex("00010000"); + // STEM_CONFIG (0x39) bitmask 0x0F — customize single|double|triple|long. + static const QByteArray STEM_CONFIG_ENABLE = QByteArray::fromHex("040004000900390f000000"); + } + + // Stem press (opcode 0x19): 04 00 04 00 19 00 [type] [bud] + namespace StemPress + { + enum class Type : quint8 { Single = 0x05, Double = 0x06, Triple = 0x07, Long = 0x08 }; + enum class Bud : quint8 { Left = 0x01, Right = 0x02 }; + struct Event { Type type; Bud bud; }; + + static const QByteArray HEADER = QByteArray::fromHex("040004001900"); + + inline std::optional parseEvent(const QByteArray &data) + { + if (data.size() < 8 || !data.startsWith(HEADER)) return std::nullopt; + const quint8 t = static_cast(data.at(6)); + if (t < 0x05 || t > 0x08) return std::nullopt; + return Event{ static_cast(t), static_cast(data.at(7)) }; + } } // Phone Communication Packets diff --git a/linux/main.cpp b/linux/main.cpp index 7b1826b49..61b91e637 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -676,6 +676,7 @@ private slots: writePacketToSocket(AirPodsPackets::Connection::REQUEST_NOTIFICATIONS, "Request notifications packet written: "); } }); + writePacketToSocket(AirPodsPackets::Connection::STEM_CONFIG_ENABLE, "Stem config enable packet written: "); } // Magic Cloud Keys Response else if (data.startsWith(AirPodsPackets::MagicPairing::MAGIC_CLOUD_KEYS_HEADER)) @@ -753,12 +754,31 @@ private slots: LOG_INFO("One Bud ANC mode received: " << m_deviceInfo->oneBudANCMode()); } } + else if (data.startsWith(AirPodsPackets::StemPress::HEADER)) + { + if (auto ev = AirPodsPackets::StemPress::parseEvent(data)) + handleStemPress(*ev); + } else { LOG_DEBUG("Unrecognized packet format: " << data.toHex()); } } + void handleStemPress(const AirPodsPackets::StemPress::Event &ev) + { + using AirPodsPackets::StemPress::Type; + LOG_INFO("Stem press type=0x" << QString::number(static_cast(ev.type), 16) + << " bud=0x" << QString::number(static_cast(ev.bud), 16)); + switch (ev.type) + { + case Type::Double: mediaController->nextTrack(); break; + case Type::Triple: mediaController->previousTrack(); break; + case Type::Single: + case Type::Long: mediaController->togglePlayPause(); break; // long = cycle ANC on Android; PlayPause fallback + } + } + void connectToPhone() { if (!CrossDevice.isEnabled) { return; diff --git a/linux/media/mediacontroller.cpp b/linux/media/mediacontroller.cpp index 078129c5a..9d76d6179 100644 --- a/linux/media/mediacontroller.cpp +++ b/linux/media/mediacontroller.cpp @@ -404,6 +404,26 @@ void MediaController::pause() } } +// Invoke an MPRIS method on the first responsive org.mpris.MediaPlayer2.* service. +static void invokeMprisMethod(const char *method) +{ + QDBusConnection bus = QDBusConnection::sessionBus(); + for (const QString &service : bus.interface()->registeredServiceNames().value()) + { + if (!service.startsWith("org.mpris.MediaPlayer2.")) continue; + QDBusInterface player(service, "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player", bus); + if (!player.isValid()) continue; + QDBusReply reply = player.call(method); + if (reply.isValid()) { LOG_INFO("MPRIS " << method << " -> " << service); return; } + } + LOG_WARN("MPRIS " << method << ": no responsive player"); +} + +void MediaController::togglePlayPause() { invokeMprisMethod("PlayPause"); } +void MediaController::nextTrack() { invokeMprisMethod("Next"); } +void MediaController::previousTrack() { invokeMprisMethod("Previous"); } + MediaController::~MediaController() { } diff --git a/linux/media/mediacontroller.h b/linux/media/mediacontroller.h index 0a064400a..06beda92c 100644 --- a/linux/media/mediacontroller.h +++ b/linux/media/mediacontroller.h @@ -47,6 +47,9 @@ class MediaController : public QObject void play(); void pause(); + void togglePlayPause(); + void nextTrack(); + void previousTrack(); MediaState getCurrentMediaState() const; Q_SIGNALS: