fix: improve Bluetooth reliability
This commit is contained in:
@@ -56,7 +56,10 @@ namespace montauk::abi {
|
||||
(Drivers::Audio::Mixer::Output)value);
|
||||
if (cmd == AUDIO_CTL_BT_STATUS) {
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||
return (int64_t)Drivers::USB::Bluetooth::A2dp::GetState();
|
||||
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2;
|
||||
return Drivers::USB::Bluetooth::A2dp::GetState()
|
||||
== Drivers::USB::Bluetooth::A2dp::State::Idle
|
||||
? 0 : 1;
|
||||
}
|
||||
|
||||
return (int64_t)Drivers::Audio::Mixer::Control(handle, cmd, value);
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 30
|
||||
#define MONTAUK_BUILD_NUMBER 33
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace montauk::abi {
|
||||
static constexpr int AUDIO_CTL_PAUSE = 3;
|
||||
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
||||
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth status
|
||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // 0=unavailable, 1=setup, 2=ready
|
||||
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
|
||||
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
|
||||
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "IntelHda.hpp"
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||
#include <Drivers/USB/Bluetooth/Avrcp.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
@@ -142,10 +143,7 @@ namespace Drivers::Audio::Mixer {
|
||||
|
||||
static bool BluetoothReady() {
|
||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return false;
|
||||
auto state = Drivers::USB::Bluetooth::A2dp::GetState();
|
||||
return state == Drivers::USB::Bluetooth::A2dp::State::Configured ||
|
||||
state == Drivers::USB::Bluetooth::A2dp::State::Open ||
|
||||
state == Drivers::USB::Bluetooth::A2dp::State::Streaming;
|
||||
return Drivers::USB::Bluetooth::A2dp::IsReady();
|
||||
}
|
||||
|
||||
static void SyncHdaMasterMute() {
|
||||
@@ -664,6 +662,8 @@ namespace Drivers::Audio::Mixer {
|
||||
if (changed) BumpSerialLocked();
|
||||
g_lock.Release();
|
||||
|
||||
if (changed)
|
||||
Drivers::USB::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
|
||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,11 +32,22 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// handshakes), NOT from an event/interrupt path. Leaves the stream Open.
|
||||
bool StartSource(uint32_t timeoutMs = 5000);
|
||||
|
||||
// Reset all per-link AVDTP state for a newly established ACL connection.
|
||||
// This is deliberately separate from StartSource(): the peer may open its
|
||||
// AVDTP channels during authentication/the post-encryption settle window,
|
||||
// and StartSource must preserve those already-live inbound channels.
|
||||
void OnConnected(uint16_t aclHandle);
|
||||
|
||||
// Called by L2CAP when an AVDTP channel becomes ready
|
||||
void OnChannelReady(uint16_t l2capCid);
|
||||
|
||||
// Process an AVDTP signaling packet
|
||||
void ProcessAvdtp(const uint8_t* data, uint16_t len);
|
||||
void ProcessAvdtp(uint16_t localCid, const uint8_t* data, uint16_t len);
|
||||
|
||||
// L2CAP channel teardown can happen while the ACL link remains alive.
|
||||
// Clear stale signaling/media CIDs so subsequent writes do not target a
|
||||
// dead channel and a manual reconnect can rebuild the A2DP path.
|
||||
void OnChannelClosed(uint16_t localCid);
|
||||
|
||||
// Process an SDP packet (on any SDP L2CAP channel). Dispatches by PDU:
|
||||
// requests (the headset querying OUR services) are answered by the minimal
|
||||
@@ -80,6 +91,10 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
||||
// Get current state
|
||||
State GetState();
|
||||
|
||||
// True only when signaling and media transports are both live and the SEP
|
||||
// has reached a state from which audio can be started.
|
||||
bool IsReady();
|
||||
|
||||
// Check if currently streaming
|
||||
bool IsStreaming();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Drivers/Audio/Mixer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
@@ -28,6 +29,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
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_CHANGED = 0xD;
|
||||
constexpr uint8_t AVC_RSP_INTERIM = 0xF;
|
||||
|
||||
// AV/C opcodes
|
||||
@@ -43,33 +45,35 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
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;
|
||||
|
||||
static kcp::Spinlock g_notifyLock;
|
||||
static uint16_t g_volumeNotifyCid = 0;
|
||||
static uint8_t g_volumeNotifyTransaction = 0;
|
||||
|
||||
// =========================================================================
|
||||
// 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,
|
||||
static bool 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;
|
||||
if (3u + avcLen > sizeof(buf)) return false;
|
||||
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));
|
||||
return 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,
|
||||
static bool 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;
|
||||
if (10u + n > sizeof(avc)) return false;
|
||||
avc[0] = rspCode;
|
||||
avc[1] = 0x48; // PANEL subunit
|
||||
avc[2] = AVC_OP_VENDOR;
|
||||
@@ -79,7 +83,8 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
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));
|
||||
return SendAvctp(cid, transaction, 0x110E, false,
|
||||
avc, (uint16_t)(10 + n));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -126,14 +131,13 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
}
|
||||
|
||||
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.
|
||||
// No media-session control bridge exists yet. Claiming these
|
||||
// commands were ACCEPTED made headset buttons appear broken and
|
||||
// left the controller believing an action had happened.
|
||||
uint8_t rsp[16] = {};
|
||||
uint16_t cp = (alen > sizeof(rsp)) ? (uint16_t)sizeof(rsp) : alen;
|
||||
memcpy(rsp, avc, cp);
|
||||
rsp[0] = AVC_RSP_ACCEPTED;
|
||||
rsp[0] = AVC_RSP_NOT_IMPL;
|
||||
SendAvctp(localCid, transaction, pid, false, rsp, cp);
|
||||
if (alen >= 4) {
|
||||
KernelLogStream(INFO, "BT-AVRCP") << "pass-through op="
|
||||
@@ -156,7 +160,12 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
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 (10u + plen > alen) {
|
||||
uint8_t err = 0x01; // invalid parameter
|
||||
SendVendorRsp(localCid, transaction, AVC_RSP_REJECTED,
|
||||
pdu, &err, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
if (pdu == PDU_GET_CAPABILITIES && ctype == AVC_CTYPE_STATUS && plen >= 1) {
|
||||
if (p[0] == 0x02) { // CompanyID list
|
||||
@@ -164,8 +173,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
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};
|
||||
uint8_t rp[3] = {0x03, 0x01, EVT_VOLUME_CHANGED};
|
||||
SendVendorRsp(localCid, transaction, AVC_RSP_STABLE,
|
||||
pdu, rp, sizeof(rp));
|
||||
} else {
|
||||
@@ -181,22 +189,18 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
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.)
|
||||
// response is sent by NotifyVolumeChanged; registrations
|
||||
// are one-shot as required by AVRCP.)
|
||||
if (p[0] == EVT_VOLUME_CHANGED) {
|
||||
uint8_t rp[2] = {EVT_VOLUME_CHANGED,
|
||||
(uint8_t)((Drivers::Audio::Mixer::GetMasterVolume() * 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));
|
||||
g_notifyLock.Acquire();
|
||||
if (SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM,
|
||||
pdu, rp, sizeof(rp))) {
|
||||
g_volumeNotifyCid = localCid;
|
||||
g_volumeNotifyTransaction = transaction;
|
||||
}
|
||||
g_notifyLock.Release();
|
||||
} else {
|
||||
uint8_t err = 0x01;
|
||||
SendVendorRsp(localCid, transaction, AVC_RSP_REJECTED,
|
||||
@@ -228,4 +232,28 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
}
|
||||
}
|
||||
|
||||
void NotifyVolumeChanged(int percent) {
|
||||
if (percent < 0) percent = 0;
|
||||
if (percent > 100) percent = 100;
|
||||
|
||||
g_notifyLock.Acquire();
|
||||
uint16_t cid = g_volumeNotifyCid;
|
||||
uint8_t transaction = g_volumeNotifyTransaction;
|
||||
g_volumeNotifyCid = 0; // one-shot, regardless of transport outcome
|
||||
if (cid != 0) {
|
||||
uint8_t rp[2] = {
|
||||
EVT_VOLUME_CHANGED, (uint8_t)((percent * 127) / 100)
|
||||
};
|
||||
SendVendorRsp(cid, transaction, AVC_RSP_CHANGED,
|
||||
PDU_REGISTER_NOTIFY, rp, sizeof(rp));
|
||||
}
|
||||
g_notifyLock.Release();
|
||||
}
|
||||
|
||||
void OnChannelClosed(uint16_t localCid) {
|
||||
g_notifyLock.Acquire();
|
||||
if (g_volumeNotifyCid == localCid) g_volumeNotifyCid = 0;
|
||||
g_notifyLock.Release();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,10 +10,18 @@
|
||||
namespace Drivers::USB::Bluetooth::Avrcp {
|
||||
|
||||
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
|
||||
// Implements a minimal AVRCP Target: unit/subunit info, pass-through accept,
|
||||
// GetCapabilities, RegisterNotification (interim), absolute volume. Runs
|
||||
// Implements a minimal AVRCP Target: unit/subunit info, honest rejection of
|
||||
// unimplemented pass-through controls, capabilities, one-shot absolute-
|
||||
// volume notification, and absolute-volume control. Runs
|
||||
// from the ACL receive path (DrainEvents, top-level) -- sending is safe
|
||||
// there, blocking waits are not (none are used).
|
||||
void ProcessAvctp(uint16_t localCid, const uint8_t* data, uint16_t len);
|
||||
|
||||
// Complete an outstanding EVENT_VOLUME_CHANGED registration. AVRCP
|
||||
// notifications are one-shot: after CHANGED the controller registers again.
|
||||
void NotifyVolumeChanged(int percent);
|
||||
|
||||
// Drop registrations tied to a channel that L2CAP has torn down.
|
||||
void OnChannelClosed(uint16_t localCid);
|
||||
|
||||
}
|
||||
|
||||
@@ -157,58 +157,45 @@ namespace Drivers::USB::Bluetooth {
|
||||
// Intel Bluetooth firmware detection
|
||||
// =========================================================================
|
||||
|
||||
static bool InitIntelBluetooth(uint8_t slotId) {
|
||||
static bool InitIntelBluetooth() {
|
||||
KernelLogStream(INFO, "BT") << "Intel Bluetooth adapter detected";
|
||||
|
||||
// Intel BT controllers require HCI Reset before they respond to
|
||||
// vendor-specific commands. This mirrors the Linux btintel driver
|
||||
// sequence: Reset → Read Version → (firmware load) → Reset.
|
||||
if (!Hci::Reset()) {
|
||||
KernelLogStream(ERROR, "BT") << "Initial HCI Reset failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read standard HCI version -- if this fails, the controller is likely
|
||||
// in bootloader mode where only vendor commands are accepted.
|
||||
Hci::LocalVersion lver = {};
|
||||
bool hciVersionOk = Hci::ReadLocalVersion(&lver);
|
||||
if (hciVersionOk) {
|
||||
KernelLogStream(INFO, "BT") << "HCI version=" << (uint64_t)lver.HciVersion
|
||||
<< " rev=" << base::hex << (uint64_t)lver.HciRevision
|
||||
<< " LMP=" << (uint64_t)lver.LmpVersion
|
||||
<< " manufacturer=" << (uint64_t)lver.Manufacturer
|
||||
<< " subver=" << (uint64_t)lver.LmpSubversion << base::dec;
|
||||
}
|
||||
|
||||
// Read legacy Intel version for diagnostics (TLV parts return this in
|
||||
// a different layout; the authoritative state check happens inside the
|
||||
// firmware download path below via the TLV version).
|
||||
Hci::IntelVersion ver = {};
|
||||
if (!Hci::ReadIntelVersion(&ver)) {
|
||||
KernelLogStream(WARNING, "BT") << "Failed to read Intel BT version";
|
||||
} else {
|
||||
KernelLogStream(INFO, "BT") << "Intel BT: HW variant=" << (uint64_t)ver.HwVariant
|
||||
<< " FW variant=" << base::hex << (uint64_t)ver.FwVariant
|
||||
<< " FW rev=" << (uint64_t)ver.FwRevision << "."
|
||||
<< (uint64_t)ver.FwBuildNum << base::dec;
|
||||
}
|
||||
|
||||
// Run the firmware download path. This reads the TLV version, and if
|
||||
// the controller is in bootloader mode, loads the matching .sfi image
|
||||
// from the ramdisk, secure-sends it, boots the operational firmware
|
||||
// Read the Intel TLV version first. It is the mode probe accepted by
|
||||
// both Intel's bootloader and operational firmware. A standard HCI
|
||||
// Reset must not precede it: the bootloader answers Reset with status
|
||||
// 0x01 (Unknown HCI Command), which is normal rather than a fatal
|
||||
// transport failure.
|
||||
//
|
||||
// If the controller is in bootloader mode, this loads the matching SFI
|
||||
// image from the ramdisk, secure-sends it, boots operational firmware,
|
||||
// and applies DDC parameters. Returns true if the controller ends up
|
||||
// operational (either already loaded, or freshly downloaded).
|
||||
if (!DownloadIntelFirmware()) {
|
||||
// Older Intel parts may lack the TLV command while already running
|
||||
// usable operational firmware. Preserve that compatibility only
|
||||
// when standard HCI proves it is genuinely operational.
|
||||
if (Hci::Reset()) {
|
||||
KernelLogStream(WARNING, "BT")
|
||||
<< "Intel BT firmware not loaded; limited functionality";
|
||||
// Standard init already issued an HCI Reset above.
|
||||
<< "Intel firmware query failed; continuing with operational HCI";
|
||||
return true;
|
||||
}
|
||||
KernelLogStream(ERROR, "BT")
|
||||
<< "Intel controller remains in bootloader mode";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The operational firmware just (re)booted. Give it a clean reset and
|
||||
// re-enable the Intel vendor event mask before the generic HCI setup.
|
||||
Hci::Reset();
|
||||
Hci::IntelSetEventMask();
|
||||
// Whether it was already present or was just booted, operational
|
||||
// firmware must now accept standard HCI. Do not mark the adapter ready
|
||||
// if this transition did not actually happen.
|
||||
if (!Hci::Reset()) {
|
||||
KernelLogStream(ERROR, "BT")
|
||||
<< "Operational firmware did not accept HCI Reset";
|
||||
return false;
|
||||
}
|
||||
if (!Hci::IntelSetEventMask()) {
|
||||
KernelLogStream(WARNING, "BT")
|
||||
<< "Intel vendor event mask was not accepted";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -272,10 +259,15 @@ namespace Drivers::USB::Bluetooth {
|
||||
// Intel-specific initialization (firmware download + HCI Reset)
|
||||
bool didReset = false;
|
||||
if (IsIntelBt(dev->VendorId, dev->ProductId)) {
|
||||
if (InitIntelBluetooth(g_slotId)) {
|
||||
if (InitIntelBluetooth()) {
|
||||
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
||||
} else {
|
||||
KernelLogStream(WARNING, "BT") << "Intel BT init failed, continuing with basic HCI";
|
||||
// A recognized Intel part that failed its vendor initialization
|
||||
// is normally still a bootloader, where a second standard Reset
|
||||
// only repeats status 0x01. Stop with an accurate failure
|
||||
// instead of pretending a basic-HCI fallback exists.
|
||||
KernelLogStream(ERROR, "BT") << "Intel BT initialization failed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +307,10 @@ namespace Drivers::USB::Bluetooth {
|
||||
KernelLogStream(INFO, "BT") << "ACL buffer: " << (uint64_t)aclLen
|
||||
<< " bytes x " << (uint64_t)aclNum;
|
||||
}
|
||||
// Bulk IN was armed before Intel firmware loading, but bootloader runts
|
||||
// are not HCI ACL traffic and an absorbed firmware-phase USB error may
|
||||
// have stopped the endpoint. Start framed ACL reception only now.
|
||||
Hci::EnableAclDataReception();
|
||||
|
||||
// Set local name
|
||||
Hci::WriteLocalName("MontaukOS");
|
||||
@@ -363,10 +359,15 @@ namespace Drivers::USB::Bluetooth {
|
||||
// Request (0x33), Simple Pairing Complete (0x36) all live there. It was
|
||||
// 0x00 -> the controller started SSP but the IO-Capability Request event
|
||||
// never reached us, so pairing always timed out with auth failure 0x05.
|
||||
uint8_t eventMask[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0x20};
|
||||
uint8_t eventMask[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20};
|
||||
Hci::SendCommand(Hci::OP_SET_EVENT_MASK, eventMask, 8);
|
||||
Hci::WaitCommandComplete(Hci::OP_SET_EVENT_MASK);
|
||||
|
||||
// Request Extended Inquiry Results so scan entries carry EIR names and
|
||||
// RSSI. Older controllers may support only mode 1 (RSSI); the parser
|
||||
// handles both result layouts and the fallback preserves discovery.
|
||||
if (!Hci::WriteInquiryMode(2)) Hci::WriteInquiryMode(1);
|
||||
|
||||
// Enable inquiry + page scan (discoverable and connectable)
|
||||
Hci::WriteScanEnable(0x03);
|
||||
|
||||
@@ -420,8 +421,11 @@ namespace Drivers::USB::Bluetooth {
|
||||
void ServiceEvents() {
|
||||
if (!g_initialized) return;
|
||||
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
||||
Hci::ProcessPendingCommands();
|
||||
A2dp::ServiceMedia(); // reap events and feed queued media
|
||||
// DrainEvents may just have queued an accept/auth/encryption reply.
|
||||
// Send it in this same service pass instead of adding a scheduler-turn
|
||||
// delay to the controller's security timeout.
|
||||
Hci::ProcessPendingCommands();
|
||||
Drivers::Audio::Mixer::OnBluetoothWritable();
|
||||
int requestedVolume;
|
||||
if (A2dp::ConsumeVolumeRequest(&requestedVolume))
|
||||
@@ -472,13 +476,22 @@ namespace Drivers::USB::Bluetooth {
|
||||
}
|
||||
}
|
||||
// Let the disconnection(s) complete before reprogramming the address.
|
||||
// A fixed 300 ms pause was merely hopeful and could change the identity
|
||||
// under a still-live encrypted link.
|
||||
if (droppedLink) {
|
||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 300) {
|
||||
bool anyActive = true;
|
||||
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
anyActive = false;
|
||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||
auto* conn = Hci::GetConnectionByIndex(i);
|
||||
if (conn && conn->Active) { anyActive = true; break; }
|
||||
}
|
||||
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
||||
}
|
||||
if (anyActive) return false;
|
||||
}
|
||||
|
||||
// Program the new address. Do NOT issue an HCI Reset afterwards: the
|
||||
@@ -515,9 +528,10 @@ namespace Drivers::USB::Bluetooth {
|
||||
Hci::ClearInquiryResults();
|
||||
|
||||
// Convert timeout to 1.28s units (min 1, max 30)
|
||||
uint8_t duration = (uint8_t)(timeoutMs / 1280);
|
||||
if (duration < 1) duration = 1;
|
||||
if (duration > 30) duration = 30;
|
||||
uint32_t durationUnits = timeoutMs / 1280;
|
||||
if (durationUnits < 1) durationUnits = 1;
|
||||
if (durationUnits > 30) durationUnits = 30;
|
||||
uint8_t duration = (uint8_t)durationUnits;
|
||||
|
||||
if (!Hci::StartInquiry(duration)) return -1;
|
||||
|
||||
@@ -534,7 +548,21 @@ namespace Drivers::USB::Bluetooth {
|
||||
|
||||
// Cancel if still running
|
||||
if (Hci::IsInquiryActive()) {
|
||||
Hci::CancelInquiry();
|
||||
// A failed cancel must not be hidden: Create Connection while the
|
||||
// controller is still in Inquiry is commonly rejected as Command
|
||||
// Disallowed. Give a command that briefly lost HCI ownership one
|
||||
// retry, continuing to service the completion event in between.
|
||||
if (!Hci::CancelInquiry()) {
|
||||
uint64_t cancelStart = Timekeeping::GetMilliseconds();
|
||||
while (Hci::IsInquiryActive()
|
||||
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
for (int j = 0; j < 100; j++)
|
||||
asm volatile("pause" ::: "memory");
|
||||
}
|
||||
if (Hci::IsInquiryActive() && !Hci::CancelInquiry()) return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return Hci::GetInquiryResults(buf, maxCount);
|
||||
@@ -544,89 +572,82 @@ namespace Drivers::USB::Bluetooth {
|
||||
// Connect — initiate ACL connection
|
||||
// =========================================================================
|
||||
|
||||
static bool SameAddress(const uint8_t* a, const uint8_t* b) {
|
||||
if (!a || !b) return false;
|
||||
for (int i = 0; i < 6; i++) if (a[i] != b[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static Hci::ConnectionInfo* FindAclConnection(const uint8_t* bdAddr) {
|
||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||
auto* conn = Hci::GetConnectionByIndex(i);
|
||||
if (conn && conn->Active && conn->LinkType == 0x01
|
||||
&& SameAddress(conn->BdAddr, bdAddr)) return conn;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int Connect(const uint8_t* bdAddr, uint32_t timeoutMs) {
|
||||
if (!g_initialized || !bdAddr) return -1;
|
||||
|
||||
if (!Hci::CreateConnection(bdAddr)) return -1;
|
||||
// A previous attempt can leave a healthy encrypted ACL link with A2DP
|
||||
// incomplete. Treat another click as an A2DP repair attempt on that
|
||||
// link; issuing HCI Create Connection again just returns "connection
|
||||
// already exists" and made manual recovery impossible.
|
||||
Hci::ConnectionInfo* target = FindAclConnection(bdAddr);
|
||||
bool reusedAcl = target != nullptr;
|
||||
|
||||
if (!target && !Hci::CreateConnection(bdAddr)) return -1;
|
||||
|
||||
// Wait for Connection Complete event
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
|
||||
// Check connection table for matching BD_ADDR
|
||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||
auto* conn = Hci::GetConnectionByIndex(i);
|
||||
if (conn && conn->Active) {
|
||||
bool match = true;
|
||||
for (int j = 0; j < 6; j++) {
|
||||
if (conn->BdAddr[j] != bdAddr[j]) { match = false; break; }
|
||||
}
|
||||
if (match) {
|
||||
// The headset (in pairing mode) drives Secure Simple
|
||||
// Pairing itself right after the ACL link comes up. Let
|
||||
// authentication + encryption finish BEFORE opening any
|
||||
// L2CAP/AVDTP channels: doing A2DP on a not-yet-
|
||||
// authenticated link races with the pairing handshake and
|
||||
// the headset drops us (reason 0x05). Drain events here
|
||||
// so the IO-capability / user-confirm / link-key / encrypt
|
||||
// events all get serviced.
|
||||
//
|
||||
// We are the initiator: request authentication so the
|
||||
// controller starts Secure Simple Pairing (Link Key
|
||||
// Request -> our negative reply -> IO Capability Request
|
||||
// -> ... ). The headset does not start this on its own.
|
||||
// NB this only works now that octet 6 of the event mask
|
||||
// is enabled so the IO-Capability Request event actually
|
||||
// reaches us; before that this produced 03 17 06 05.
|
||||
Hci::AuthenticateLink(conn->Handle);
|
||||
|
||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
// Send queued pairing replies reliably (top-level,
|
||||
// not nested under PollEvents).
|
||||
Hci::ProcessPendingCommands();
|
||||
if (!conn->Active) break; // link dropped during pairing
|
||||
if (conn->Encrypted) break; // authenticated + encrypted -> ready
|
||||
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
||||
}
|
||||
|
||||
// Bring up the A2DP source stream (signaling + media
|
||||
// channels, SBC negotiation) only once the link is
|
||||
// secured. Without a media stream the headset also drops
|
||||
// the link (reason 0x13), so this keeps it engaged too.
|
||||
if (conn->Active) {
|
||||
// Let the link settle after Encryption Change before
|
||||
// dialing L2CAP: some sinks ignore a CONN_REQ that
|
||||
// arrives the instant encryption completes. Drain
|
||||
// (don't blind-sleep) so the ACL RX ring stays live.
|
||||
uint64_t st = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - st < 300) {
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
||||
}
|
||||
A2dp::StartSource();
|
||||
Drivers::Audio::Mixer::OnBluetoothStateChanged();
|
||||
}
|
||||
// Persist any new link key now (process context), even if
|
||||
// the link later dropped, so the disk write never stalls
|
||||
// the nested pairing event handler.
|
||||
Hci::FlushLinkKeys();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target = FindAclConnection(bdAddr);
|
||||
for (int j = 0; j < 200; j++) {
|
||||
asm volatile("pause" ::: "memory");
|
||||
}
|
||||
}
|
||||
if (!target) return -1;
|
||||
|
||||
return -1; // Timeout
|
||||
// Connection Complete queues authentication for both incoming and
|
||||
// outgoing ACLs. Deliver that common request and wait for encryption;
|
||||
// a manual A2DP repair on an encrypted ACL skips this entire wait.
|
||||
if (!target->Encrypted) {
|
||||
// A link that predates this syscall may have exhausted or missed its
|
||||
// earlier security attempt; explicitly restart it. A freshly-created
|
||||
// link already has the request queued by Connection Complete.
|
||||
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
|
||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
||||
Xhci::PollEvents();
|
||||
Hci::DrainEvents();
|
||||
Hci::ProcessPendingCommands();
|
||||
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
|
||||
if (target->Encrypted) break;
|
||||
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
||||
}
|
||||
}
|
||||
|
||||
bool targetAlive = target->Active
|
||||
&& SameAddress(target->BdAddr, bdAddr);
|
||||
bool a2dpReady = false;
|
||||
if (targetAlive) {
|
||||
a2dpReady = A2dp::StartSource();
|
||||
Drivers::Audio::Mixer::OnBluetoothStateChanged();
|
||||
}
|
||||
|
||||
// Persist any new link key now (process context), even if the link later
|
||||
// dropped, so disk I/O never stalls the nested pairing event handler.
|
||||
Hci::FlushLinkKeys();
|
||||
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) return -1;
|
||||
if (!a2dpReady) {
|
||||
KernelLogStream(WARNING, "BT")
|
||||
<< "ACL connected but A2DP source setup failed";
|
||||
return -2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -640,11 +661,7 @@ namespace Drivers::USB::Bluetooth {
|
||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||
auto* conn = Hci::GetConnectionByIndex(i);
|
||||
if (conn && conn->Active) {
|
||||
bool match = true;
|
||||
for (int j = 0; j < 6; j++) {
|
||||
if (conn->BdAddr[j] != bdAddr[j]) { match = false; break; }
|
||||
}
|
||||
if (match) {
|
||||
if (SameAddress(conn->BdAddr, bdAddr)) {
|
||||
Hci::Disconnect(conn->Handle, 0x13); // 0x13 = Remote User Terminated
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ namespace Drivers::USB::Bluetooth {
|
||||
int Scan(Hci::InquiryDevice* buf, int maxCount, uint32_t timeoutMs);
|
||||
|
||||
// Initiate connection to a remote device by BD_ADDR
|
||||
// Returns 0 on success (connection established), -1 on failure
|
||||
// Returns 0 when ACL + A2DP are ready, -1 when the ACL connection failed,
|
||||
// or -2 when ACL connected but A2DP setup did not complete.
|
||||
int Connect(const uint8_t* bdAddr, uint32_t timeoutMs = 10000);
|
||||
|
||||
// Disconnect a device by BD_ADDR
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -181,6 +181,11 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// and keeps the event pipe alive -- see the definition).
|
||||
void StartEventPipe();
|
||||
|
||||
// Enable operational ACL receive framing after firmware/HCI setup. Bulk IN
|
||||
// is armed earlier to protect Intel firmware loading; pre-operational runts
|
||||
// are intentionally ignored and any endpoint error is recovered here.
|
||||
void EnableAclDataReception();
|
||||
|
||||
// Send an HCI command via USB control transfer (EP0)
|
||||
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen);
|
||||
|
||||
@@ -308,6 +313,9 @@ namespace Drivers::USB::Bluetooth::Hci {
|
||||
// Write Simple Secure Pairing mode
|
||||
bool WriteSSPMode(uint8_t mode);
|
||||
|
||||
// Select inquiry result format: 0=standard, 1=RSSI, 2=extended/EIR.
|
||||
bool WriteInquiryMode(uint8_t mode);
|
||||
|
||||
// Accept an incoming connection
|
||||
bool AcceptConnection(const uint8_t* bdAddr, uint8_t role);
|
||||
|
||||
|
||||
@@ -26,11 +26,6 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
static bool g_initialized = false;
|
||||
static uint8_t g_sigIdentifier = 1;
|
||||
|
||||
// Diagnostic: result of the most recent L2CAP Connection Response we got for
|
||||
// an outgoing connection (0=success, 1=pending, 2=PSM-unsupported,
|
||||
// 3=security-block, 4=no-resources). 0xFFFF = none received yet.
|
||||
static volatile uint16_t g_lastConnRspResult = 0xFFFF;
|
||||
|
||||
// Channel table
|
||||
static ChannelInfo g_channels[MAX_CHANNELS] = {};
|
||||
static uint16_t g_nextCid = CID_DYNAMIC_START;
|
||||
@@ -39,8 +34,29 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
static uint8_t NextIdentifier() {
|
||||
uint8_t id = g_sigIdentifier++;
|
||||
if (id == 0) id = g_sigIdentifier++;
|
||||
if (g_sigIdentifier == 0) g_sigIdentifier = 1;
|
||||
return id;
|
||||
}
|
||||
|
||||
static uint16_t AllocCid() {
|
||||
return g_nextCid++;
|
||||
// Dynamic CIDs are 0x0040..0xFFFF and must not collide with an active
|
||||
// channel after wraparound.
|
||||
for (uint32_t tries = 0; tries <= 0xFFC0; tries++) {
|
||||
if (g_nextCid < CID_DYNAMIC_START) g_nextCid = CID_DYNAMIC_START;
|
||||
uint16_t candidate = g_nextCid++;
|
||||
bool used = false;
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active && g_channels[i].LocalCid == candidate) {
|
||||
used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!used) return candidate;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ChannelInfo* AllocChannel(uint16_t psm) {
|
||||
@@ -48,12 +64,18 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
if (!g_channels[i].Active) {
|
||||
g_channels[i].Active = true;
|
||||
g_channels[i].LocalCid = AllocCid();
|
||||
if (g_channels[i].LocalCid == 0) {
|
||||
g_channels[i].Active = false;
|
||||
return nullptr;
|
||||
}
|
||||
g_channels[i].RemoteCid = 0;
|
||||
g_channels[i].Psm = psm;
|
||||
g_channels[i].RemoteMtu = 672; // Default L2CAP MTU
|
||||
g_channels[i].Configured = false;
|
||||
g_channels[i].LocalConfigDone = false;
|
||||
g_channels[i].RemoteConfigDone = false;
|
||||
g_channels[i].ConnRspResult = 0xFFFF;
|
||||
g_channels[i].ConnRspStatus = 0;
|
||||
return &g_channels[i];
|
||||
}
|
||||
}
|
||||
@@ -61,13 +83,14 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
}
|
||||
|
||||
// Send L2CAP signaling command
|
||||
static void SendSignal(uint8_t code, uint8_t identifier,
|
||||
static bool SendSignal(uint8_t code, uint8_t identifier,
|
||||
const uint8_t* payload, uint16_t payloadLen) {
|
||||
// L2CAP header + Signal header + payload
|
||||
uint16_t sigLen = sizeof(SignalHeader) + payloadLen;
|
||||
uint16_t totalPayload = sizeof(L2capHeader) + sigLen;
|
||||
|
||||
uint8_t buf[128] = {};
|
||||
if (totalPayload > sizeof(buf)) return false;
|
||||
auto* l2hdr = (L2capHeader*)buf;
|
||||
l2hdr->Length = sigLen;
|
||||
l2hdr->ChannelId = CID_SIGNALING;
|
||||
@@ -81,7 +104,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
memcpy(buf + sizeof(L2capHeader) + sizeof(SignalHeader), payload, payloadLen);
|
||||
}
|
||||
|
||||
Hci::SendAcl(g_aclHandle, Hci::ACL_PB_FIRST_FLUSH,
|
||||
return Hci::SendAcl(g_aclHandle, Hci::ACL_PB_FIRST_NON_FLUSH,
|
||||
buf, totalPayload);
|
||||
}
|
||||
|
||||
@@ -96,17 +119,31 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
g_nextCid = CID_DYNAMIC_START;
|
||||
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active)
|
||||
Avrcp::OnChannelClosed(g_channels[i].LocalCid);
|
||||
g_channels[i].Active = false;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "BT-L2CAP") << "Initialized for ACL handle " << (uint64_t)aclHandle;
|
||||
}
|
||||
|
||||
void OnDisconnected(uint16_t aclHandle) {
|
||||
if (!g_initialized || aclHandle != g_aclHandle) return;
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active)
|
||||
Avrcp::OnChannelClosed(g_channels[i].LocalCid);
|
||||
g_channels[i].Active = false;
|
||||
}
|
||||
g_aclHandle = 0;
|
||||
g_initialized = false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ProcessPacket
|
||||
// =========================================================================
|
||||
|
||||
void ProcessPacket(uint16_t aclHandle, const uint8_t* data, uint16_t len) {
|
||||
if (!g_initialized || aclHandle != g_aclHandle) return;
|
||||
if (len < sizeof(L2capHeader)) return;
|
||||
|
||||
auto* l2hdr = (const L2capHeader*)data;
|
||||
@@ -145,8 +182,41 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
|
||||
// Accept connections for AVDTP, SDP, and AVRCP control
|
||||
if (psm == PSM_AVDTP || psm == PSM_SDP || psm == PSM_AVCTP) {
|
||||
auto* ch = AllocChannel(psm);
|
||||
if (ch) {
|
||||
// Detect a duplicate source CID instead of creating
|
||||
// two local channels that address the same endpoint.
|
||||
// Same-PSM duplicates are normal retransmissions;
|
||||
// a different PSM reusing the CID is invalid.
|
||||
ChannelInfo* existing = nullptr;
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active
|
||||
&& g_channels[i].RemoteCid == srcCid) {
|
||||
existing = &g_channels[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto* ch = existing ? nullptr : AllocChannel(psm);
|
||||
if (existing && existing->Psm == psm) {
|
||||
// Retransmission of a request whose response was
|
||||
// lost: repeat SUCCESS for the original channel,
|
||||
// and repeat our config request as well.
|
||||
uint8_t rsp[8] = {
|
||||
(uint8_t)existing->LocalCid,
|
||||
(uint8_t)(existing->LocalCid >> 8),
|
||||
(uint8_t)srcCid, (uint8_t)(srcCid >> 8),
|
||||
0, 0, 0, 0
|
||||
};
|
||||
SendSignal(SIG_CONN_RSP, sig->Identifier, rsp, 8);
|
||||
uint8_t cfgReq[4] = {
|
||||
(uint8_t)srcCid, (uint8_t)(srcCid >> 8), 0, 0
|
||||
};
|
||||
SendSignal(SIG_CONFIG_REQ, NextIdentifier(), cfgReq, 4);
|
||||
} else if (existing) {
|
||||
uint8_t rsp[8] = {
|
||||
0, 0, (uint8_t)srcCid, (uint8_t)(srcCid >> 8),
|
||||
0x07, 0x00, 0, 0 // source CID already allocated
|
||||
};
|
||||
SendSignal(SIG_CONN_RSP, sig->Identifier, rsp, 8);
|
||||
} else if (ch) {
|
||||
ch->RemoteCid = srcCid;
|
||||
|
||||
// Send Connection Response (success)
|
||||
@@ -168,7 +238,13 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
cfgReq[0] = (uint8_t)(srcCid & 0xFF);
|
||||
cfgReq[1] = (uint8_t)(srcCid >> 8);
|
||||
cfgReq[2] = 0; cfgReq[3] = 0; // Flags
|
||||
SendSignal(SIG_CONFIG_REQ, g_sigIdentifier++, cfgReq, 4);
|
||||
SendSignal(SIG_CONFIG_REQ, NextIdentifier(), cfgReq, 4);
|
||||
} else {
|
||||
uint8_t rsp[8] = {
|
||||
0, 0, (uint8_t)srcCid, (uint8_t)(srcCid >> 8),
|
||||
0x04, 0x00, 0, 0 // no resources
|
||||
};
|
||||
SendSignal(SIG_CONN_RSP, sig->Identifier, rsp, 8);
|
||||
}
|
||||
} else {
|
||||
// Reject: PSM not supported
|
||||
@@ -193,8 +269,6 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
// info, 1=authentication pending, 2=authorization pending.
|
||||
uint16_t status = (sigPayloadLen >= 8)
|
||||
? ((uint16_t)sigPayload[6] | ((uint16_t)sigPayload[7] << 8)) : 0;
|
||||
g_lastConnRspResult = result;
|
||||
|
||||
KernelLogStream(INFO, "BT-L2CAP") << "Connection Response: dstCID="
|
||||
<< base::hex << (uint64_t)dstCid << " result=" << (uint64_t)result
|
||||
<< " status=" << (uint64_t)status << base::dec;
|
||||
@@ -203,6 +277,8 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
// Find our channel by srcCid (which is our local CID)
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active && g_channels[i].LocalCid == srcCid) {
|
||||
g_channels[i].ConnRspResult = result;
|
||||
g_channels[i].ConnRspStatus = status;
|
||||
g_channels[i].RemoteCid = dstCid;
|
||||
|
||||
// Send Configuration Request
|
||||
@@ -210,7 +286,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
cfgReq[0] = (uint8_t)(dstCid & 0xFF);
|
||||
cfgReq[1] = (uint8_t)(dstCid >> 8);
|
||||
cfgReq[2] = 0; cfgReq[3] = 0; // Flags
|
||||
SendSignal(SIG_CONFIG_REQ, g_sigIdentifier++, cfgReq, 4);
|
||||
SendSignal(SIG_CONFIG_REQ, NextIdentifier(), cfgReq, 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -223,10 +299,21 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
// a Disconnect for the half-open remote endpoint.
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active && g_channels[i].LocalCid == srcCid) {
|
||||
g_channels[i].ConnRspResult = result;
|
||||
g_channels[i].ConnRspStatus = status;
|
||||
g_channels[i].RemoteCid = dstCid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active
|
||||
&& g_channels[i].LocalCid == srcCid) {
|
||||
g_channels[i].ConnRspResult = result;
|
||||
g_channels[i].ConnRspStatus = status;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -333,7 +420,10 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active && g_channels[i].LocalCid == dstCid) {
|
||||
uint16_t closedCid = g_channels[i].LocalCid;
|
||||
g_channels[i].Active = false;
|
||||
A2dp::OnChannelClosed(closedCid);
|
||||
Avrcp::OnChannelClosed(closedCid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -378,6 +468,13 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
// SDP client channel after a query); nothing left to do.
|
||||
break;
|
||||
|
||||
case SIG_COMMAND_REJECT:
|
||||
case SIG_ECHO_RSP:
|
||||
case SIG_INFO_RSP:
|
||||
// Terminal responses to requests sent by us. No response
|
||||
// is required; in particular, never reject a response.
|
||||
break;
|
||||
|
||||
case SIG_ECHO_REQ: {
|
||||
// Echo Request must be answered (echo the payload back) or
|
||||
// a peer probing the link times out on us.
|
||||
@@ -392,6 +489,10 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
KernelLogStream(INFO, "BT-L2CAP") << "Unhandled signaling code="
|
||||
<< base::hex << (uint64_t)sig->Code << base::dec
|
||||
<< " len=" << (uint64_t)sigPayloadLen;
|
||||
// Command Reject, reason 0x0000 = command not understood.
|
||||
// Silence leaves the peer waiting for its signaling timer.
|
||||
uint8_t reject[2] = {0x00, 0x00};
|
||||
SendSignal(SIG_COMMAND_REJECT, sig->Identifier, reject, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -402,7 +503,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active && g_channels[i].LocalCid == cid) {
|
||||
if (g_channels[i].Psm == PSM_AVDTP) {
|
||||
A2dp::ProcessAvdtp(payload, l2len);
|
||||
A2dp::ProcessAvdtp(cid, payload, l2len);
|
||||
} else if (g_channels[i].Psm == PSM_SDP) {
|
||||
A2dp::ProcessSdp(cid, payload, l2len);
|
||||
} else if (g_channels[i].Psm == PSM_AVCTP) {
|
||||
@@ -421,8 +522,6 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
uint16_t Connect(uint16_t psm) {
|
||||
if (!g_initialized) return 0;
|
||||
|
||||
g_lastConnRspResult = 0xFFFF; // reset diagnostic for this attempt
|
||||
|
||||
auto* ch = AllocChannel(psm);
|
||||
if (!ch) return 0;
|
||||
|
||||
@@ -432,7 +531,10 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
req[1] = (uint8_t)(psm >> 8);
|
||||
req[2] = (uint8_t)(ch->LocalCid & 0xFF);
|
||||
req[3] = (uint8_t)(ch->LocalCid >> 8);
|
||||
SendSignal(SIG_CONN_REQ, g_sigIdentifier++, req, 4);
|
||||
if (!SendSignal(SIG_CONN_REQ, NextIdentifier(), req, 4)) {
|
||||
ch->Active = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ch->LocalCid;
|
||||
}
|
||||
@@ -518,9 +620,11 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
req[1] = (uint8_t)(g_channels[i].RemoteCid >> 8);
|
||||
req[2] = (uint8_t)(g_channels[i].LocalCid & 0xFF);
|
||||
req[3] = (uint8_t)(g_channels[i].LocalCid >> 8);
|
||||
SendSignal(SIG_DISCONN_REQ, g_sigIdentifier++, req, 4);
|
||||
SendSignal(SIG_DISCONN_REQ, NextIdentifier(), req, 4);
|
||||
}
|
||||
g_channels[i].Active = false;
|
||||
A2dp::OnChannelClosed(localCid);
|
||||
Avrcp::OnChannelClosed(localCid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -547,12 +651,28 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t FindAvdtpChannelExcept(uint16_t exceptCid) {
|
||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||
if (g_channels[i].Active && g_channels[i].Psm == PSM_AVDTP
|
||||
&& g_channels[i].LocalCid != exceptCid) {
|
||||
return g_channels[i].LocalCid;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t GetAclHandle() {
|
||||
return g_aclHandle;
|
||||
}
|
||||
|
||||
uint16_t LastConnRspResult() {
|
||||
return g_lastConnRspResult;
|
||||
uint16_t ConnRspResult(uint16_t localCid) {
|
||||
auto* ch = GetChannel(localCid);
|
||||
return ch ? ch->ConnRspResult : 0xFFFF;
|
||||
}
|
||||
|
||||
uint16_t ConnRspStatus(uint16_t localCid) {
|
||||
auto* ch = GetChannel(localCid);
|
||||
return ch ? ch->ConnRspStatus : 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
bool Configured; // Both sides configured
|
||||
bool LocalConfigDone;
|
||||
bool RemoteConfigDone;
|
||||
uint16_t ConnRspResult; // 0xFFFF until this outgoing dial is answered
|
||||
uint16_t ConnRspStatus; // meaningful while result == CONN_PENDING
|
||||
};
|
||||
|
||||
constexpr int MAX_CHANNELS = 8;
|
||||
@@ -89,6 +91,11 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
// Initialize L2CAP for a new HCI connection
|
||||
void Initialize(uint16_t aclHandle);
|
||||
|
||||
// Invalidate every dynamic channel when its ACL link disappears. Without
|
||||
// this, AVRCP or media code can send to stale CIDs until the next connection
|
||||
// happens to reinitialize the table.
|
||||
void OnDisconnected(uint16_t aclHandle);
|
||||
|
||||
// Process an L2CAP packet (called from HCI ACL processing)
|
||||
void ProcessPacket(uint16_t aclHandle, const uint8_t* data, uint16_t len);
|
||||
|
||||
@@ -114,6 +121,11 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
// it (we dial it after AVDTP_OPEN, but some sinks open it inbound instead).
|
||||
uint16_t FindConfiguredAvdtpChannelExcept(uint16_t exceptCid);
|
||||
|
||||
// Find any active AVDTP channel except the signaling channel, including a
|
||||
// still-pending outgoing media dial. Manual retries reuse that channel
|
||||
// instead of leaking another slot/CID while authorization is in progress.
|
||||
uint16_t FindAvdtpChannelExcept(uint16_t exceptCid);
|
||||
|
||||
// Reclaim a dialed channel during A2DP bring-up retries so a failed connect
|
||||
// doesn't leak the fixed-size channel table. If the peer had acknowledged
|
||||
// the channel (RemoteCid != 0) a Disconnect Request is sent first; an
|
||||
@@ -125,10 +137,9 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
||||
// Get the ACL handle
|
||||
uint16_t GetAclHandle();
|
||||
|
||||
// Result code of the most recent outgoing L2CAP Connection Response
|
||||
// (0xFFFF if none yet). 0=success, 1=pending, 2=PSM-unsupported,
|
||||
// 3=security-block, 4=no-resources. StartSource() retries only when the
|
||||
// remote ignored the dial entirely (0xFFFF).
|
||||
uint16_t LastConnRspResult();
|
||||
// Per-channel connection response state. A global "last result" is unsafe:
|
||||
// an unrelated SDP, AVDTP, or AVRCP response can arrive concurrently.
|
||||
uint16_t ConnRspResult(uint16_t localCid);
|
||||
uint16_t ConnRspStatus(uint16_t localCid);
|
||||
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ namespace Drivers::USB::Bluetooth::Sbc {
|
||||
headerBits += enc->Subbands; // join bits
|
||||
}
|
||||
uint32_t dataBits = enc->Blocks * enc->Bitpool;
|
||||
if (enc->ChannelMode == MODE_DUAL_CHANNEL) dataBits *= enc->Channels;
|
||||
enc->FrameSize = (headerBits + dataBits + 7) / 8;
|
||||
}
|
||||
|
||||
@@ -143,6 +144,7 @@ namespace Drivers::USB::Bluetooth::Sbc {
|
||||
uint32_t headerBits = 32 + (4 * enc->Subbands * enc->Channels);
|
||||
if (enc->ChannelMode == MODE_JOINT_STEREO) headerBits += enc->Subbands;
|
||||
uint32_t dataBits = enc->Blocks * enc->Bitpool;
|
||||
if (enc->ChannelMode == MODE_DUAL_CHANNEL) dataBits *= enc->Channels;
|
||||
enc->FrameSize = (headerBits + dataBits + 7) / 8;
|
||||
}
|
||||
|
||||
|
||||
@@ -988,6 +988,38 @@ namespace Drivers::USB::Xhci {
|
||||
SendCommand(deqTrb);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ResetBulkOutEndpoint - clear a halted bulk OUT endpoint
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
void ResetBulkOutEndpoint(uint8_t slotId) {
|
||||
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
|
||||
UsbDeviceInfo& dev = g_devices[slotId];
|
||||
if (dev.BulkOutEpNum == 0 || !dev.BulkOutRing) return;
|
||||
|
||||
uint8_t dci = dev.BulkOutEpNum * 2;
|
||||
|
||||
TRB resetTrb = {};
|
||||
resetTrb.Control = (TRB_RESET_ENDPOINT << TRB_TYPE_SHIFT)
|
||||
| ((uint32_t)slotId << 24)
|
||||
| ((uint32_t)dci << 16);
|
||||
SendCommand(resetTrb);
|
||||
|
||||
// Skip the errored TRB and everything queued behind it. The class
|
||||
// driver releases the corresponding DMA slots before it sends again.
|
||||
uint64_t newDeq = dev.BulkOutRingPhys
|
||||
+ (uint64_t)dev.BulkOutRingEnqueue * sizeof(TRB);
|
||||
if (dev.BulkOutRingCCS) newDeq |= 1; // DCS bit
|
||||
|
||||
TRB deqTrb = {};
|
||||
deqTrb.Parameter0 = (uint32_t)(newDeq & 0xFFFFFFFF);
|
||||
deqTrb.Parameter1 = (uint32_t)(newDeq >> 32);
|
||||
deqTrb.Control = (TRB_SET_TR_DEQUEUE << TRB_TYPE_SHIFT)
|
||||
| ((uint32_t)slotId << 24)
|
||||
| ((uint32_t)dci << 16);
|
||||
SendCommand(deqTrb);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// QueueBulkInTransfer
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -326,6 +326,11 @@ namespace Drivers::USB::Xhci {
|
||||
// Used for SDR stream stall recovery (RTL2832 bulk IN can STALL on start).
|
||||
void ResetBulkInEndpoint(uint8_t slotId);
|
||||
|
||||
// Clear a halted bulk OUT endpoint and discard the errored/queued TRBs.
|
||||
// Must be called from process context; the class driver reconciles its
|
||||
// outstanding-transfer accounting before submitting fresh work.
|
||||
void ResetBulkOutEndpoint(uint8_t slotId);
|
||||
|
||||
// True while PollEvents() is draining the event ring. A command submitter
|
||||
// reached from inside an event callback must fire-and-forget (not wait),
|
||||
// since a nested PollEvents() is a no-op.
|
||||
|
||||
@@ -258,7 +258,7 @@ namespace montauk::abi {
|
||||
static constexpr int AUDIO_CTL_PAUSE = 3;
|
||||
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
||||
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth status
|
||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // 0=unavailable, 1=setup, 2=ready
|
||||
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
|
||||
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
|
||||
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
|
||||
|
||||
@@ -550,7 +550,7 @@
|
||||
audio_get_output AUDIO_CTL_GET_OUTPUT (4): 0=HDA, 1=Bluetooth
|
||||
audio_set_output AUDIO_CTL_SET_OUTPUT (5): switch all streams
|
||||
(SET_OUTPUT, 5) switch a stream's output route
|
||||
audio_bt_status AUDIO_CTL_BT_STATUS (6)
|
||||
audio_bt_status AUDIO_CTL_BT_STATUS (6): 0=unavailable, 1=setup, 2=ready
|
||||
audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100
|
||||
audio_set_mute, audio_get_mute AUDIO_CTL_{SET,GET}_MUTE (9/10), per-stream
|
||||
audio_set_master_mute, _get_ AUDIO_CTL_{SET,GET}_MASTER_MUTE (11/12)
|
||||
|
||||
@@ -325,6 +325,7 @@ static void finish_scan() {
|
||||
static void do_connect(const uint8_t* addr) {
|
||||
int r = montauk::bt_connect(addr);
|
||||
if (r >= 0) set_status("Connected");
|
||||
else if (r == -2) set_status("Connected; audio setup failed - use Retry");
|
||||
else set_status("Connection failed");
|
||||
refresh_devices();
|
||||
clamp_scroll();
|
||||
@@ -354,6 +355,10 @@ static bool is_connected(const uint8_t* addr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_audio_ready() {
|
||||
return montauk::audio_bt_status(-1) >= 2;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Format helpers
|
||||
// ============================================================================
|
||||
@@ -565,8 +570,14 @@ static bool handle_click(int mx, int my) {
|
||||
if (!row.contains(mx, my)) continue;
|
||||
|
||||
Rect action = row_action_button_rect(row);
|
||||
Rect retry = row_forget_button_rect(row);
|
||||
bool connected = is_connected(g_scan[i].bdAddr);
|
||||
if (connected && !is_audio_ready() && retry.contains(mx, my)) {
|
||||
do_connect(g_scan[i].bdAddr);
|
||||
return true;
|
||||
}
|
||||
if (action.contains(mx, my)) {
|
||||
if (is_connected(g_scan[i].bdAddr))
|
||||
if (connected)
|
||||
do_disconnect(g_scan[i].bdAddr);
|
||||
else
|
||||
do_connect(g_scan[i].bdAddr);
|
||||
@@ -580,6 +591,11 @@ static bool handle_click(int mx, int my) {
|
||||
|
||||
Rect action = row_action_button_rect(row);
|
||||
if (g_devices[i].connected) {
|
||||
Rect retry = row_forget_button_rect(row);
|
||||
if (!is_audio_ready() && retry.contains(mx, my)) {
|
||||
do_connect(g_devices[i].bdAddr);
|
||||
return true;
|
||||
}
|
||||
if (action.contains(mx, my)) {
|
||||
do_disconnect(g_devices[i].bdAddr);
|
||||
return true;
|
||||
@@ -672,6 +688,7 @@ static bool handle_adapter_key(const montauk::abi::KeyEvent& key) {
|
||||
static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||
Rect list = {0, devices_list_top(), g_win_w, gui_max(devices_list_bottom() - devices_list_top(), 0)};
|
||||
int fh = system_font_height();
|
||||
bool bt_audio_ready = is_audio_ready();
|
||||
|
||||
if (g_device_count == 0) {
|
||||
draw_centered_text(canvas, list.y, list.h, "No paired devices", theme.text_subtle);
|
||||
@@ -688,15 +705,17 @@ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||
mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme);
|
||||
|
||||
bool connected = g_devices[i].connected;
|
||||
bool audio_ready = !connected || bt_audio_ready;
|
||||
Color dot_color = connected ? GREEN : theme.text_subtle;
|
||||
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, dot_color);
|
||||
|
||||
Rect action = row_action_button_rect(row);
|
||||
Rect forget = row_forget_button_rect(row);
|
||||
int text_x = PAD + 20;
|
||||
// Connected rows have one button (Disconnect); paired rows have two
|
||||
// (Connect + Forget), so the text must stop before the leftmost button.
|
||||
int text_w_max = (connected ? action.x : forget.x) - text_x - 10;
|
||||
// A connected row whose media path failed exposes Retry + Disconnect;
|
||||
// otherwise connected rows need only Disconnect.
|
||||
int text_w_max = (connected && audio_ready ? action.x : forget.x)
|
||||
- text_x - 10;
|
||||
draw_text_fit(canvas, text_x, row.y + 8, g_device_names[i], text_w_max, theme.text);
|
||||
|
||||
char addr_str[24];
|
||||
@@ -704,6 +723,10 @@ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||
draw_text_fit(canvas, text_x, row.y + 8 + fh + 2, addr_str, text_w_max, theme.text_subtle);
|
||||
|
||||
if (connected) {
|
||||
if (!audio_ready) {
|
||||
mtk::draw_button(canvas, forget, "Retry", mtk::BUTTON_PRIMARY,
|
||||
button_state(forget, true), theme);
|
||||
}
|
||||
mtk::draw_button(canvas, action, "Disconnect", mtk::BUTTON_DANGER,
|
||||
button_state(action, true), theme);
|
||||
} else {
|
||||
@@ -721,6 +744,7 @@ static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||
Rect scan_btn = scan_button_rect();
|
||||
Rect list = {0, scan_list_top(), g_win_w, gui_max(scan_list_bottom() - scan_list_top(), 0)};
|
||||
int fh = system_font_height();
|
||||
bool bt_audio_ready = is_audio_ready();
|
||||
|
||||
if (g_scanning) {
|
||||
mtk::draw_button(canvas, scan_btn, "Scanning...", mtk::BUTTON_SECONDARY,
|
||||
@@ -747,8 +771,12 @@ static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, theme.accent);
|
||||
|
||||
Rect action = row_action_button_rect(row);
|
||||
Rect retry = row_forget_button_rect(row);
|
||||
int text_x = PAD + 20;
|
||||
int text_w_max = action.x - text_x - 10;
|
||||
bool conn = is_connected(g_scan[i].bdAddr);
|
||||
bool audio_ready = !conn || bt_audio_ready;
|
||||
int text_w_max = (conn && !audio_ready ? retry.x : action.x)
|
||||
- text_x - 10;
|
||||
|
||||
const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device";
|
||||
draw_text_fit(canvas, text_x, row.y + 4, display_name, text_w_max, theme.text);
|
||||
@@ -760,7 +788,10 @@ static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||
device_class_str(g_scan[i].classOfDevice), addr_str, rssi_bar(g_scan[i].rssi));
|
||||
draw_text_fit(canvas, text_x, row.y + 4 + fh + 2, detail, text_w_max, theme.text_subtle);
|
||||
|
||||
bool conn = is_connected(g_scan[i].bdAddr);
|
||||
if (conn && !audio_ready) {
|
||||
mtk::draw_button(canvas, retry, "Retry", mtk::BUTTON_PRIMARY,
|
||||
button_state(retry, true), theme);
|
||||
}
|
||||
mtk::draw_button(canvas, action, conn ? "Disconnect" : "Connect",
|
||||
conn ? mtk::BUTTON_DANGER : mtk::BUTTON_PRIMARY,
|
||||
button_state(action, true), theme);
|
||||
|
||||
Reference in New Issue
Block a user