/* * Avrcp.cpp * Bluetooth AVRCP -- minimal Target role over AVCTP (PSM 0x0017) * Copyright (c) 2026 Daniel Hammer */ #include "Avrcp.hpp" #include "A2dp.hpp" #include "L2cap.hpp" #include #include #include using namespace Kt; namespace Drivers::USB::Bluetooth::Avrcp { // ========================================================================= // Constants // ========================================================================= // AV/C command types (commands) / response codes (responses) constexpr uint8_t AVC_CTYPE_CONTROL = 0x0; constexpr uint8_t AVC_CTYPE_STATUS = 0x1; constexpr uint8_t AVC_CTYPE_NOTIFY = 0x3; constexpr uint8_t AVC_RSP_NOT_IMPL = 0x8; constexpr uint8_t AVC_RSP_ACCEPTED = 0x9; constexpr uint8_t AVC_RSP_REJECTED = 0xA; constexpr uint8_t AVC_RSP_STABLE = 0xC; constexpr uint8_t AVC_RSP_INTERIM = 0xF; // AV/C opcodes constexpr uint8_t AVC_OP_VENDOR = 0x00; constexpr uint8_t AVC_OP_UNIT_INFO = 0x30; constexpr uint8_t AVC_OP_SUBUNIT_INFO = 0x31; constexpr uint8_t AVC_OP_PASSTHROUGH = 0x7C; // AVRCP vendor-dependent PDUs (BT SIG company id 00 19 58) constexpr uint8_t PDU_GET_CAPABILITIES = 0x10; constexpr uint8_t PDU_GET_PLAY_STATUS = 0x30; constexpr uint8_t PDU_REGISTER_NOTIFY = 0x31; constexpr uint8_t PDU_SET_ABS_VOLUME = 0x50; // AVRCP notification events constexpr uint8_t EVT_PLAYBACK_STATUS = 0x01; constexpr uint8_t EVT_TRACK_CHANGED = 0x02; constexpr uint8_t EVT_VOLUME_CHANGED = 0x0D; // ========================================================================= // Send helpers // ========================================================================= // AVCTP single-packet response: byte0 = transaction<<4 | pktType<<2 | // C/R(1=response)<<1 | IPID; bytes 1-2 = PID big-endian. static void SendAvctp(uint16_t cid, uint8_t transaction, uint16_t pid, bool invalidPid, const uint8_t* avc, uint16_t avcLen) { uint8_t buf[96] = {}; if (3u + avcLen > sizeof(buf)) return; buf[0] = (uint8_t)((transaction << 4) | 0x02 | (invalidPid ? 0x01 : 0)); buf[1] = (uint8_t)(pid >> 8); buf[2] = (uint8_t)(pid & 0xFF); if (avc && avcLen) memcpy(&buf[3], avc, avcLen); L2cap::SendData(cid, buf, (uint16_t)(3 + avcLen)); } // AVRCP vendor-dependent response frame: // [rsp][subunit 0x48 panel][opcode 0x00][company 00 19 58][pdu][pkt 0][len BE][params] static void SendVendorRsp(uint16_t cid, uint8_t transaction, uint8_t rspCode, uint8_t pdu, const uint8_t* p, uint16_t n) { uint8_t avc[64] = {}; if (10u + n > sizeof(avc)) return; avc[0] = rspCode; avc[1] = 0x48; // PANEL subunit avc[2] = AVC_OP_VENDOR; avc[3] = 0x00; avc[4] = 0x19; avc[5] = 0x58; // BT SIG company id avc[6] = pdu; avc[7] = 0x00; // packet type: single avc[8] = (uint8_t)(n >> 8); avc[9] = (uint8_t)(n & 0xFF); if (p && n) memcpy(&avc[10], p, n); SendAvctp(cid, transaction, 0x110E, false, avc, (uint16_t)(10 + n)); } // ========================================================================= // ProcessAvctp // ========================================================================= void ProcessAvctp(uint16_t localCid, const uint8_t* d, uint16_t len) { if (len < 3) return; uint8_t transaction = (d[0] >> 4) & 0x0F; uint8_t pktType = (d[0] >> 2) & 0x03; bool isCommand = ((d[0] >> 1) & 0x01) == 0; uint16_t pid = ((uint16_t)d[1] << 8) | d[2]; if (!isCommand) return; // a response to something we sent if (pktType != 0) return; // only single-packet AVCTP supported if (pid != 0x110E) { // Unsupported PID: respond with the IPID bit set, no message body. SendAvctp(localCid, transaction, pid, true, nullptr, 0); return; } const uint8_t* avc = d + 3; uint16_t alen = (uint16_t)(len - 3); if (alen < 3) return; uint8_t ctype = avc[0] & 0x0F; uint8_t opcode = avc[2]; switch (opcode) { case AVC_OP_UNIT_INFO: { // Fixed-format reply: PANEL subunit, BT SIG company id. uint8_t rsp[8] = {AVC_RSP_STABLE, 0xFF, AVC_OP_UNIT_INFO, 0x07, 0x48, 0x00, 0x19, 0x58}; SendAvctp(localCid, transaction, pid, false, rsp, sizeof(rsp)); break; } case AVC_OP_SUBUNIT_INFO: { uint8_t rsp[8] = {AVC_RSP_STABLE, 0xFF, AVC_OP_SUBUNIT_INFO, 0x07, 0x48, 0xFF, 0xFF, 0xFF}; SendAvctp(localCid, transaction, pid, false, rsp, sizeof(rsp)); break; } case AVC_OP_PASSTHROUGH: { // Accept every pass-through (play/pause/etc.). Echo the frame // with the response code swapped in. Wiring the operation ids // to the Music app is a later feature; ACCEPTED keeps the // controller-side state machine happy. uint8_t rsp[16] = {}; uint16_t cp = (alen > sizeof(rsp)) ? (uint16_t)sizeof(rsp) : alen; memcpy(rsp, avc, cp); rsp[0] = AVC_RSP_ACCEPTED; SendAvctp(localCid, transaction, pid, false, rsp, cp); if (alen >= 4) { KernelLogStream(INFO, "BT-AVRCP") << "pass-through op=" << base::hex << (uint64_t)(avc[3] & 0x7F) << ((avc[3] & 0x80) ? " release" : " press") << base::dec; } break; } case AVC_OP_VENDOR: { // [ctype][subunit][00][company 3][pdu][pkt][len BE][params] if (alen < 10) break; if (avc[3] != 0x00 || avc[4] != 0x19 || avc[5] != 0x58) { uint8_t rsp[8] = {}; memcpy(rsp, avc, 8); rsp[0] = AVC_RSP_NOT_IMPL; SendAvctp(localCid, transaction, pid, false, rsp, 8); break; } uint8_t pdu = avc[6]; uint16_t plen = ((uint16_t)avc[8] << 8) | avc[9]; const uint8_t* p = &avc[10]; if (10u + plen > alen) plen = (uint16_t)(alen - 10); if (pdu == PDU_GET_CAPABILITIES && ctype == AVC_CTYPE_STATUS && plen >= 1) { if (p[0] == 0x02) { // CompanyID list uint8_t rp[5] = {0x02, 0x01, 0x00, 0x19, 0x58}; SendVendorRsp(localCid, transaction, AVC_RSP_STABLE, pdu, rp, sizeof(rp)); } else if (p[0] == 0x03) { // EventsSupported uint8_t rp[5] = {0x03, 0x03, EVT_PLAYBACK_STATUS, EVT_TRACK_CHANGED, EVT_VOLUME_CHANGED}; SendVendorRsp(localCid, transaction, AVC_RSP_STABLE, pdu, rp, sizeof(rp)); } else { uint8_t err = 0x01; // invalid parameter SendVendorRsp(localCid, transaction, AVC_RSP_REJECTED, pdu, &err, 1); } } else if (pdu == PDU_GET_PLAY_STATUS && ctype == AVC_CTYPE_STATUS) { // song length / position unknown (0xFFFFFFFF), status by state uint8_t rp[9] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, (uint8_t)(A2dp::IsStreaming() ? 0x01 : 0x00)}; SendVendorRsp(localCid, transaction, AVC_RSP_STABLE, pdu, rp, sizeof(rp)); } else if (pdu == PDU_REGISTER_NOTIFY && ctype == AVC_CTYPE_NOTIFY && plen >= 1) { // INTERIM response with the current value. (A CHANGED // follow-up on actual change is a later feature.) if (p[0] == EVT_VOLUME_CHANGED) { uint8_t rp[2] = {EVT_VOLUME_CHANGED, (uint8_t)((A2dp::GetVolume() * 127) / 100)}; SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM, pdu, rp, sizeof(rp)); } else if (p[0] == EVT_PLAYBACK_STATUS) { uint8_t rp[2] = {EVT_PLAYBACK_STATUS, (uint8_t)(A2dp::IsStreaming() ? 0x01 : 0x00)}; SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM, pdu, rp, sizeof(rp)); } else if (p[0] == EVT_TRACK_CHANGED) { uint8_t rp[9] = {EVT_TRACK_CHANGED, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM, pdu, rp, sizeof(rp)); } else { uint8_t err = 0x01; SendVendorRsp(localCid, transaction, AVC_RSP_REJECTED, pdu, &err, 1); } } else if (pdu == PDU_SET_ABS_VOLUME && ctype == AVC_CTYPE_CONTROL && plen >= 1) { uint8_t vol = p[0] & 0x7F; A2dp::SetVolume(((int)vol * 100) / 127); SendVendorRsp(localCid, transaction, AVC_RSP_ACCEPTED, pdu, &vol, 1); KernelLogStream(INFO, "BT-AVRCP") << "absolute volume -> " << (uint64_t)(((int)vol * 100) / 127) << "%"; } else { uint8_t err = 0x00; // invalid command SendVendorRsp(localCid, transaction, AVC_RSP_REJECTED, pdu, &err, 1); } break; } default: { uint8_t rsp[8] = {}; uint16_t cp = (alen > sizeof(rsp)) ? (uint16_t)sizeof(rsp) : alen; memcpy(rsp, avc, cp); rsp[0] = AVC_RSP_NOT_IMPL; SendAvctp(localCid, transaction, pid, false, rsp, cp); break; } } } }