fix: improve Bluetooth reliability
This commit is contained in:
@@ -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()) {
|
||||
KernelLogStream(WARNING, "BT")
|
||||
<< "Intel BT firmware not loaded; limited functionality";
|
||||
// Standard init already issued an HCI Reset above.
|
||||
return true;
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user