Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions linux/airpods_packets.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Event> parseEvent(const QByteArray &data)
{
if (data.size() < 8 || !data.startsWith(HEADER)) return std::nullopt;
const quint8 t = static_cast<quint8>(data.at(6));
if (t < 0x05 || t > 0x08) return std::nullopt;
return Event{ static_cast<Type>(t), static_cast<Bud>(data.at(7)) };
}
}

// Phone Communication Packets
Expand Down
20 changes: 20 additions & 0 deletions linux/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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<int>(ev.type), 16)
<< " bud=0x" << QString::number(static_cast<int>(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;
Expand Down
20 changes: 20 additions & 0 deletions linux/media/mediacontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> 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() {
}

Expand Down
3 changes: 3 additions & 0 deletions linux/media/mediacontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class MediaController : public QObject

void play();
void pause();
void togglePlayPause();
void nextTrack();
void previousTrack();
MediaState getCurrentMediaState() const;

Q_SIGNALS:
Expand Down