Files
MontaukOS/kernel/src/Drivers/USB/Bluetooth/Hci.cpp
T

1375 lines
58 KiB
C++

/*
* Hci.cpp
* Bluetooth HCI transport over USB
* Copyright (c) 2026 Daniel Hammer
*/
#include "Hci.hpp"
#include "L2cap.hpp"
#include <Fs/Vfs.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Libraries/Memory.hpp>
#include <Timekeeping/ApicTimer.hpp>
using namespace Kt;
namespace Drivers::USB::Bluetooth::Hci {
// =========================================================================
// State
// =========================================================================
static uint8_t g_slotId = 0;
static bool g_initialized = false;
// Event receive buffer (filled by xHCI interrupt IN callback)
static uint8_t g_eventBuf[256] = {};
static volatile uint32_t g_eventLen = 0;
static volatile bool g_eventReady = false;
// ACL receive ring buffer. The bulk-IN callback (nested under PollEvents)
// only copies an incoming packet into a slot; DrainEvents() processes them
// at top level. A ring (not a single buffer) is required because the headset
// bursts many ACL packets at once -- the single buffer was overwriting and
// dropping the L2CAP Config Response, leaving our channel half-configured.
static constexpr int ACL_RX_SLOTS = 32;
static constexpr int ACL_RX_SLOT_SIZE = 1024;
static uint8_t g_aclRxRing[ACL_RX_SLOTS][ACL_RX_SLOT_SIZE] = {};
static volatile uint16_t g_aclRxLens[ACL_RX_SLOTS] = {};
static volatile uint8_t g_aclRxHead = 0;
static volatile uint8_t g_aclRxTail = 0;
// ACL transmit DMA buffers (a ring, not one): SendAcl queues an async bulk
// OUT transfer, so two sends in quick succession (e.g. our Config Request
// then a Config Response, both fired while DrainEvents processes a burst)
// would have the second overwrite the first buffer before it is DMA'd to the
// wire -- corrupting the first packet. Rotate buffers to avoid that.
static constexpr int ACL_TX_SLOTS = 8;
static uint8_t* g_aclTxRing[ACL_TX_SLOTS] = {};
static uint64_t g_aclTxRingPhys[ACL_TX_SLOTS] = {};
static uint8_t g_aclTxSlot = 0;
// HCI command DMA buffer (separate from ACL to avoid conflicts)
static uint8_t* g_cmdDmaBuf = nullptr;
static uint64_t g_cmdDmaBufPhys = 0;
// Connection table
static ConnectionInfo g_connections[MAX_CONNECTIONS] = {};
// ACL buffer size (from controller)
static uint16_t g_aclMaxLen = 0;
static uint16_t g_aclMaxNum = 0;
static volatile uint16_t g_aclPendingCount = 0;
// Diagnostic ACL data-path counters: TX submitted, TX completed (bulk OUT
// completion), RX received (bulk IN). Used to tell whether L2CAP signaling
// actually flows over ACL after encryption is enabled.
static volatile uint32_t g_aclTxCount = 0;
static volatile uint32_t g_aclTxDoneCount = 0;
static volatile uint32_t g_aclRxCount = 0;
// Incremented when an incoming ACL packet is dropped because the RX ring was
// full -- a nonzero value means a burst overran the ring (and could have
// dropped an L2CAP Config Response). Surfaced by DumpAclStats().
static volatile uint32_t g_aclRxDropCount = 0;
// Inquiry results
static InquiryDevice g_inquiryResults[MAX_INQUIRY_RESULTS] = {};
static volatile int g_inquiryResultCount = 0;
static volatile bool g_inquiryActive = false;
// Set when the Intel "bootup" vendor event arrives after a firmware boot.
// Written from the USB transfer callback (ProcessEvent), polled by
// IntelBootFirmware, hence volatile.
static volatile bool g_intelBootup = false;
// Latest Intel "secure send result" vendor event (0xFF sub-opcode 0x06).
// The bootloader uses this, not Command Complete, to report the outcome of
// secure-send (0xFC09) firmware download. Written from ProcessEvent.
static volatile bool g_secureResultValid = false;
static volatile uint8_t g_secureResult = 0; // 0 = success
static volatile uint8_t g_secureStatus = 0; // 0 = success
// Firmware-download diagnostics. g_ssBytesSent / g_ssFragsSent accumulate
// the bytes / 0xFC09 fragments handed to the controller since the last
// ClearSecureSendResult(), so an async 0xFF/0x06 result or a fragment error
// can be pinned to an exact upload position. g_lastControlCC is the xHCI
// completion code of the most recent SendCommand() control transfer.
static volatile uint64_t g_ssBytesSent = 0;
static volatile uint32_t g_ssFragsSent = 0;
static volatile uint32_t g_lastControlCC = 0;
// Lockless HCI-event trace for diagnosing the pairing/SSP sequence.
// ProcessEvent (which may run from the xHCI IRQ) only does an array write
// here -- no lock, no terminal I/O, so it cannot deadlock against g_termLock.
// DumpEventTrace() prints it from top-level (process) context.
static constexpr int EVT_TRACE_MAX = 48;
static volatile uint8_t g_evtTrace[EVT_TRACE_MAX] = {};
static volatile uint8_t g_evtTraceCount = 0;
// Status byte of the last Simple Pairing Complete (0x36) / Auth Complete
// (0x06) / Disconnection (0x05) -- captured to find WHY pairing fails.
static volatile uint8_t g_lastSppStatus = 0xEE;
static volatile uint8_t g_lastAuthStatus = 0xEE;
static volatile uint8_t g_lastDiscReason = 0xEE;
// Pending-command queue. Pairing replies (IO-cap / user-confirm / link-key)
// are triggered from event handlers running NESTED under PollEvents, where a
// command can only be fire-and-forget (the reentrancy guard makes a nested
// wait a no-op). Fire-and-forget proved unreliable for SSP -- the IO-cap
// value feeds the authentication confirmation, so a late/garbled reply makes
// the DHKey check fail (Simple Pairing Complete status 0x05). Instead the
// handlers ENQUEUE the reply here; ProcessPendingCommands() sends it later
// from top-level (process) context with a real, confirmed transfer.
struct PendingHciCmd { uint16_t opcode; uint8_t len; uint8_t params[16]; };
static PendingHciCmd g_pending[16] = {};
static volatile uint8_t g_pendingHead = 0;
static volatile uint8_t g_pendingTail = 0;
static void EnqueueHciCmd(uint16_t opcode, const uint8_t* params, uint8_t len) {
uint8_t next = (uint8_t)((g_pendingHead + 1) & 15);
if (next == g_pendingTail) return; // full -> drop (should never happen)
if (len > 16) len = 16;
g_pending[g_pendingHead].opcode = opcode;
g_pending[g_pendingHead].len = len;
for (uint8_t i = 0; i < len; i++) g_pending[g_pendingHead].params[i] = params[i];
g_pendingHead = next;
}
// =========================================================================
// Bonded-device link key store
// =========================================================================
// Persisted to disk so a pairing survives reboots. Without it a once-paired
// device challenges us for the link key on reconnect, we have nothing to
// answer with, and authentication fails -> the remote drops the link with
// disconnect reason 0x05. On EVT_LINK_KEY_NOTIFICATION we cache the new key
// (RAM, marked dirty); FlushLinkKeys() writes it from process context so the
// disk I/O never blocks mid-pairing while nested under PollEvents.
static constexpr int MAX_BONDS = 8;
static constexpr uint32_t LINK_KEY_MAGIC = 0x314B5442; // 'BTK1'
struct StoredLinkKey {
uint8_t addr[6];
uint8_t key[16];
bool valid;
};
static StoredLinkKey g_bonds[MAX_BONDS] = {};
static bool g_bondsDirty = false;
static bool AddrEq(const uint8_t* a, const uint8_t* b) {
for (int i = 0; i < 6; i++) if (a[i] != b[i]) return false;
return true;
}
static int FindBondIndex(const uint8_t* addr) {
for (int i = 0; i < MAX_BONDS; i++) {
if (g_bonds[i].valid && AddrEq(g_bonds[i].addr, addr)) return i;
}
return -1;
}
static void StoreLinkKey(const uint8_t* addr, const uint8_t* key) {
int idx = FindBondIndex(addr);
if (idx < 0) {
for (int i = 0; i < MAX_BONDS; i++) {
if (!g_bonds[i].valid) { idx = i; break; }
}
if (idx < 0) idx = 0; // table full: recycle the first slot
}
memcpy(g_bonds[idx].addr, addr, 6);
memcpy(g_bonds[idx].key, key, 16);
g_bonds[idx].valid = true;
g_bondsDirty = true; // FlushLinkKeys() persists from safe context
}
// On-disk layout: [magic u32][MAX_BONDS x { addr[6], key[16], valid[1] }].
static constexpr uint64_t LINK_KEY_BLOB_SIZE = 4 + MAX_BONDS * 23;
void LoadLinkKeys() {
Fs::Vfs::BackendFile f;
if (Fs::Vfs::OpenBackendFile("0:/os/btkeys.bin", f) < 0) return; // none stored yet
uint64_t size = Fs::Vfs::GetBackendFileSize(f);
if (size < LINK_KEY_BLOB_SIZE) { Fs::Vfs::CloseBackendFile(f); return; }
uint8_t blob[LINK_KEY_BLOB_SIZE];
Fs::Vfs::ReadBackendFile(f, blob, 0, LINK_KEY_BLOB_SIZE);
Fs::Vfs::CloseBackendFile(f);
uint32_t magic = (uint32_t)blob[0] | ((uint32_t)blob[1] << 8)
| ((uint32_t)blob[2] << 16) | ((uint32_t)blob[3] << 24);
if (magic != LINK_KEY_MAGIC) return;
int off = 4, n = 0;
for (int i = 0; i < MAX_BONDS; i++) {
memcpy(g_bonds[i].addr, &blob[off], 6); off += 6;
memcpy(g_bonds[i].key, &blob[off], 16); off += 16;
g_bonds[i].valid = blob[off++] != 0;
if (g_bonds[i].valid) n++;
}
g_bondsDirty = false;
KernelLogStream(INFO, "BT-HCI") << "Loaded " << (uint64_t)n << " bonded device key(s)";
}
void FlushLinkKeys() {
if (!g_bondsDirty) return;
uint8_t blob[LINK_KEY_BLOB_SIZE] = {};
blob[0] = (uint8_t)(LINK_KEY_MAGIC);
blob[1] = (uint8_t)(LINK_KEY_MAGIC >> 8);
blob[2] = (uint8_t)(LINK_KEY_MAGIC >> 16);
blob[3] = (uint8_t)(LINK_KEY_MAGIC >> 24);
int off = 4;
for (int i = 0; i < MAX_BONDS; i++) {
memcpy(&blob[off], g_bonds[i].addr, 6); off += 6;
memcpy(&blob[off], g_bonds[i].key, 16); off += 16;
blob[off++] = g_bonds[i].valid ? 1 : 0;
}
Fs::Vfs::BackendFile f;
if (Fs::Vfs::CreateBackendFile("0:/os/btkeys.bin", f) < 0) {
KernelLogStream(WARNING, "BT-HCI") << "Could not open link key store for writing";
return;
}
Fs::Vfs::WriteBackendFile(f, blob, 0, LINK_KEY_BLOB_SIZE);
Fs::Vfs::CloseBackendFile(f);
g_bondsDirty = false;
KernelLogStream(OK, "BT-HCI") << "Link key store persisted";
}
// =========================================================================
// USB transfer callback
// =========================================================================
static void TransferCallback(uint8_t slotId, uint8_t epDci,
const uint8_t* data, uint32_t length,
uint32_t completionCode) {
if (slotId != g_slotId) return;
auto* dev = Xhci::GetDevice(slotId);
if (!dev) return;
uint8_t intDci = dev->InterruptEpNum ? (dev->InterruptEpNum * 2 + 1) : 0;
uint8_t bulkInDci = dev->BulkInEpNum ? (dev->BulkInEpNum * 2 + 1) : 0;
if (epDci == intDci && data && length > 0) {
// HCI Event received on interrupt IN.
// Dispatch asynchronous events (inquiry results, connection events,
// etc.) immediately so they are never lost. Only buffer
// Command Complete / Command Status events — those are consumed
// by WaitCommandComplete / WaitCommandStatus.
uint8_t evtCode = (length >= 1) ? data[0] : 0;
if (evtCode == EVT_COMMAND_COMPLETE || evtCode == EVT_COMMAND_STATUS) {
uint32_t copyLen = length;
if (copyLen > sizeof(g_eventBuf)) copyLen = sizeof(g_eventBuf);
memcpy(g_eventBuf, data, copyLen);
g_eventLen = copyLen;
g_eventReady = true;
} else {
// Process immediately (inquiry results, connection events, etc.)
ProcessEvent(data, length);
}
// Re-queue interrupt transfer for next event
Xhci::QueueInterruptTransfer(slotId);
} else if (epDci == bulkInDci) {
// ACL data received on bulk IN -> copy into the ring; processed by
// DrainEvents() at top level (do NOT process here, nested).
// Only enqueue a packet big enough to carry an L2CAP header (ACL
// header + L2CAP header = 8 bytes). Smaller completions are ZLP /
// re-arm artifacts and the firmware-phase bulk-IN runts (4-7 bytes,
// the old "rx flood") -- ProcessPacket would reject them anyway, and
// dropping them here keeps the ring + rx stats clean. We STILL
// re-arm on every completion below (the bulk IN must keep cycling to
// absorb the device's ~635 KB cc=4 glitch during firmware download).
if (data && length >= sizeof(AclHeader) + 4) {
g_aclRxCount++;
uint8_t next = (uint8_t)((g_aclRxHead + 1) % ACL_RX_SLOTS);
if (next != g_aclRxTail) { // ring not full
uint32_t copyLen = length;
if (copyLen > ACL_RX_SLOT_SIZE) copyLen = ACL_RX_SLOT_SIZE;
memcpy(g_aclRxRing[g_aclRxHead], data, copyLen);
g_aclRxLens[g_aclRxHead] = (uint16_t)copyLen;
g_aclRxHead = next;
} else {
g_aclRxDropCount++; // ring overran -> a packet was lost
}
}
// Re-queue bulk IN transfer (only on a real success/short completion;
// the error path passes data==nullptr and is handled elsewhere).
if (data) {
Xhci::QueueBulkInTransfer(slotId, nullptr, 0, dev->BulkInMaxPacket);
}
} else if (epDci == (dev->BulkOutEpNum ? (uint8_t)(dev->BulkOutEpNum * 2) : (uint8_t)0)) {
// Bulk OUT completion — decrement pending count
g_aclTxDoneCount++;
if (g_aclPendingCount > 0) g_aclPendingCount--;
}
}
// =========================================================================
// Busy wait with event polling
// =========================================================================
static void BusyWaitMs(uint64_t ms) {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < ms) {
asm volatile("pause" ::: "memory");
}
}
// Poll for events while waiting
static void PollWait(uint32_t ms) {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < ms) {
Xhci::PollEvents();
for (int j = 0; j < 100; j++) {
asm volatile("" ::: "memory");
}
}
}
// =========================================================================
// Initialize
// =========================================================================
void Initialize(uint8_t slotId) {
g_slotId = slotId;
// Register our transfer callback
Xhci::RegisterTransferCallback(slotId, TransferCallback);
// Allocate DMA buffers for HCI commands and ACL data
g_cmdDmaBuf = (uint8_t*)Memory::g_pfa->AllocateZeroed();
g_cmdDmaBufPhys = Memory::SubHHDM(g_cmdDmaBuf);
for (int i = 0; i < ACL_TX_SLOTS; i++) {
g_aclTxRing[i] = (uint8_t*)Memory::g_pfa->AllocateZeroed();
g_aclTxRingPhys[i] = Memory::SubHHDM(g_aclTxRing[i]);
}
// NOTE: Do NOT queue interrupt IN or bulk IN transfers here.
// The BT controller is not yet HCI-initialized and may misbehave.
// Call StartEventPipe() after HCI Reset and initial setup.
g_initialized = true;
KernelLogStream(OK, "BT-HCI") << "HCI transport initialized on slot " << (uint64_t)slotId;
}
// Start receiving HCI events and ACL data — call after HCI init sequence.
// Arms BOTH the interrupt IN (events) and the bulk IN (ACL). The bulk IN
// MUST stay armed across the firmware download: on this controller the device
// glitches a USB transaction error (cc=4) near ~635 KB of the upload, and an
// armed bulk IN ABSORBS it (a benign cc=4 on the bulk endpoint) so the
// interrupt-IN event pipe survives and the download completes. Deferring the
// bulk-IN arm (build 40) moved that cc=4 onto the interrupt IN and wedged the
// download at 635 KB -- reverted. The harmless firmware-phase bulk-IN runts
// (4-7 byte completions) are filtered at enqueue in TransferCallback so they
// never pollute the RX ring.
void StartEventPipe() {
if (!g_initialized) return;
// Queue initial interrupt IN transfer for HCI events
Xhci::QueueInterruptTransfer(g_slotId);
// Queue initial bulk IN transfer for ACL data
auto* dev = Xhci::GetDevice(g_slotId);
if (dev && dev->BulkInEpNum) {
Xhci::QueueBulkInTransfer(g_slotId, nullptr, 0, dev->BulkInMaxPacket);
}
KernelLogStream(INFO, "BT-HCI") << "Event pipe started (interrupt IN + bulk IN)";
}
// =========================================================================
// SendCommand — via USB control transfer on EP0
// =========================================================================
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen) {
if (!g_initialized || !g_cmdDmaBuf) return false;
// HCI command packet: opcode (2) + paramLen (1) + params
// USB-BT spec: HCI commands are sent via control transfer
// bmRequestType = 0x20 (Host-to-device, Class, Device)
// bRequest = 0x00
// wValue = 0, wIndex = 0
// wLength = sizeof(CommandHeader) + paramLen
// Use DMA-allocated buffer (not stack) for the command data.
// xHCI reads from this buffer via DMA for OUT transfers.
memset(g_cmdDmaBuf, 0, 512);
g_cmdDmaBuf[0] = (uint8_t)(opcode & 0xFF);
g_cmdDmaBuf[1] = (uint8_t)(opcode >> 8);
g_cmdDmaBuf[2] = paramLen;
if (params && paramLen > 0) {
memcpy(&g_cmdDmaBuf[3], params, paramLen);
}
uint16_t totalLen = 3 + paramLen;
uint32_t cc = Xhci::ControlTransfer(g_slotId,
0x20, // bmRequestType: Host-to-device, Class, Device
0x00, // bRequest: 0
0x0000, // wValue
0x0000, // wIndex
totalLen,
g_cmdDmaBuf,
false); // dirIn = false (host to device)
g_lastControlCC = cc;
if (cc != Xhci::CC_SUCCESS) {
KernelLogStream(WARNING, "BT-HCI") << "SendCommand failed, opcode="
<< base::hex << (uint64_t)opcode << " cc=" << base::dec << (uint64_t)cc;
return false;
}
return true;
}
// =========================================================================
// WaitCommandComplete
// =========================================================================
bool WaitCommandComplete(uint16_t opcode, uint8_t* outParams,
uint8_t maxLen, uint32_t timeoutMs) {
// Nested inside PollEvents (an event handler issued this command): we
// cannot wait -- a nested PollEvents is a no-op, so the Command Complete
// is reaped by the active PollEvents after we return. The command was
// already submitted (fire-and-forget); report success.
if (Xhci::InPollContext()) return true;
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
if (g_eventReady) {
g_eventReady = false;
if (g_eventLen >= 2) {
uint8_t evtCode = g_eventBuf[0];
uint8_t evtParamLen = g_eventBuf[1];
if (evtCode == EVT_COMMAND_COMPLETE && evtParamLen >= 3) {
// Command Complete: NumPkts(1) + Opcode(2) + Status(1) + Params
uint16_t evtOpcode = (uint16_t)g_eventBuf[3] | ((uint16_t)g_eventBuf[4] << 8);
if (evtOpcode == opcode) {
if (outParams && maxLen > 0) {
// Copy params starting after the status byte
uint8_t availLen = (evtParamLen > 4) ? (evtParamLen - 4) : 0;
uint8_t copyLen = (availLen < maxLen) ? availLen : maxLen;
// Include status byte + return params
copyLen = (evtParamLen > 3) ? (evtParamLen - 3) : 0;
if (copyLen > maxLen) copyLen = maxLen;
memcpy(outParams, &g_eventBuf[5], copyLen);
}
// Check status
uint8_t status = g_eventBuf[5];
if (status != 0) {
KernelLogStream(WARNING, "BT-HCI") << "Command Complete status="
<< (uint64_t)status << " opcode=" << base::hex << (uint64_t)opcode;
}
return true;
}
}
}
}
for (int j = 0; j < 100; j++) {
asm volatile("" ::: "memory");
}
}
KernelLogStream(WARNING, "BT-HCI") << "WaitCommandComplete timeout, opcode="
<< base::hex << (uint64_t)opcode;
return false;
}
// =========================================================================
// WaitCommandStatus
// =========================================================================
bool WaitCommandStatus(uint16_t opcode, uint32_t timeoutMs) {
// See WaitCommandComplete: cannot wait when nested under PollEvents.
if (Xhci::InPollContext()) return true;
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
if (g_eventReady) {
g_eventReady = false;
if (g_eventLen >= 2) {
uint8_t evtCode = g_eventBuf[0];
uint8_t evtParamLen = g_eventBuf[1];
if (evtCode == EVT_COMMAND_STATUS && evtParamLen >= 4) {
uint8_t status = g_eventBuf[2];
uint16_t evtOpcode = (uint16_t)g_eventBuf[4] | ((uint16_t)g_eventBuf[5] << 8);
if (evtOpcode == opcode) {
return (status == 0);
}
}
}
}
for (int j = 0; j < 100; j++) {
asm volatile("" ::: "memory");
}
}
return false;
}
// =========================================================================
// SendAcl — via USB bulk OUT
// =========================================================================
bool SendAcl(uint16_t handle, uint16_t pbFlag, const uint8_t* data, uint16_t len) {
if (!g_initialized || !g_aclTxRing[0]) return false;
if (len + sizeof(AclHeader) > 4096) return false; // Single page DMA buffer
// Use the next TX ring slot so a rapid second send can't overwrite this
// packet before its bulk OUT transfer DMAs it to the wire.
uint8_t* txBuf = g_aclTxRing[g_aclTxSlot];
uint64_t txPhys = g_aclTxRingPhys[g_aclTxSlot];
g_aclTxSlot = (uint8_t)((g_aclTxSlot + 1) % ACL_TX_SLOTS);
// Build ACL packet in DMA buffer
auto* hdr = (AclHeader*)txBuf;
hdr->HandleFlags = (handle & 0x0FFF) | pbFlag;
hdr->DataLength = len;
if (data && len > 0) {
memcpy(txBuf + sizeof(AclHeader), data, len);
}
uint32_t totalLen = sizeof(AclHeader) + len;
g_aclPendingCount++;
g_aclTxCount++;
Xhci::QueueBulkOutTransfer(g_slotId, txBuf, txPhys, totalLen);
return true;
}
uint16_t AclPendingCount() { return g_aclPendingCount; }
uint16_t AclMaxPackets() { return g_aclMaxNum; }
void DumpAclStats() {
KernelLogStream(INFO, "BT-HCI") << "ACL stats: tx=" << (uint64_t)g_aclTxCount
<< " txDone=" << (uint64_t)g_aclTxDoneCount
<< " rx=" << (uint64_t)g_aclRxCount
<< " rxDrop=" << (uint64_t)g_aclRxDropCount
<< " pending=" << (uint64_t)g_aclPendingCount
<< " bufNum=" << (uint64_t)g_aclMaxNum;
}
// =========================================================================
// ProcessEvent — handle HCI events
// =========================================================================
void ProcessEvent(const uint8_t* data, uint32_t len) {
if (len < 2) return;
uint8_t evtCode = data[0];
uint8_t evtParamLen = data[1];
const uint8_t* params = data + 2;
// Record into the lockless trace (safe from the IRQ path: array write
// only, no lock / no terminal I/O). DumpEventTrace() prints it later
// from top-level context.
if (evtCode != EVT_NUM_COMPLETED_PACKETS && g_evtTraceCount < EVT_TRACE_MAX) {
g_evtTrace[g_evtTraceCount++] = evtCode;
}
// status byte = params[0] for these (Disconnection: status,handle,reason)
if (evtCode == EVT_SIMPLE_PAIRING_COMPLETE && evtParamLen >= 1) g_lastSppStatus = params[0];
if (evtCode == EVT_AUTH_COMPLETE && evtParamLen >= 1) g_lastAuthStatus = params[0];
if (evtCode == EVT_DISCONNECTION_COMPLETE && evtParamLen >= 4) g_lastDiscReason = params[3];
switch (evtCode) {
case EVT_CONNECTION_COMPLETE: {
if (evtParamLen >= 11) {
uint8_t status = params[0];
uint16_t handle = (uint16_t)params[1] | ((uint16_t)params[2] << 8);
const uint8_t* bdAddr = &params[3];
uint8_t linkType = params[9];
KernelLogStream(INFO, "BT-HCI") << "Connection Complete: status="
<< (uint64_t)status << " handle=" << (uint64_t)handle
<< " link=" << (uint64_t)linkType;
if (status == 0) {
// Find empty connection slot
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (!g_connections[i].Active) {
g_connections[i].Active = true;
g_connections[i].Handle = handle;
memcpy(g_connections[i].BdAddr, bdAddr, 6);
g_connections[i].LinkType = linkType;
g_connections[i].Encrypted = false;
break;
}
}
// Initialize L2CAP for this connection
L2cap::Initialize(handle);
}
}
break;
}
case EVT_DISCONNECTION_COMPLETE: {
if (evtParamLen >= 4) {
uint16_t handle = (uint16_t)params[1] | ((uint16_t)params[2] << 8);
uint8_t reason = params[3];
KernelLogStream(INFO, "BT-HCI") << "Disconnection: handle="
<< (uint64_t)handle << " reason=" << (uint64_t)reason;
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (g_connections[i].Active && g_connections[i].Handle == handle) {
g_connections[i].Active = false;
break;
}
}
}
break;
}
case EVT_CONNECTION_REQUEST: {
if (evtParamLen >= 10) {
const uint8_t* bdAddr = &params[0];
uint8_t linkType = params[9];
KernelLogStream(INFO, "BT-HCI") << "Connection Request: link="
<< (uint64_t)linkType;
// Auto-accept ACL connections
if (linkType == 0x01) {
AcceptConnection(bdAddr, 0x01); // Role = slave
}
}
break;
}
case EVT_NUM_COMPLETED_PACKETS: {
if (evtParamLen >= 1) {
uint8_t numHandles = params[0];
for (int i = 0; i < numHandles && (3 + i * 4) < evtParamLen; i++) {
uint16_t completed = (uint16_t)params[3 + i * 4]
| ((uint16_t)params[4 + i * 4] << 8);
if (g_aclPendingCount >= completed) {
g_aclPendingCount -= completed;
} else {
g_aclPendingCount = 0;
}
}
}
break;
}
case EVT_IO_CAPABILITY_REQUEST: {
if (evtParamLen >= 6) {
// Reply with NoInputNoOutput for simple pairing
uint8_t reply[9] = {};
memcpy(reply, &params[0], 6); // BD_ADDR
reply[6] = 0x03; // IO Capability: NoInputNoOutput
reply[7] = 0x00; // OOB data not present
reply[8] = 0x04; // Auth req: MITM not required, General Bonding
// (0x04, not 0x00 "No Bonding") so a
// persistent link key is created -> the bond
// survives reboots via the link-key store.
// Queue for reliable top-level delivery (see EnqueueHciCmd):
// the IO-cap value feeds the SSP confirmation, so it must be
// sent intact, not fire-and-forget.
EnqueueHciCmd(OP_IO_CAPABILITY_REPLY, reply, 9);
}
break;
}
case EVT_USER_CONFIRM_REQUEST: {
if (evtParamLen >= 6) {
// Auto-confirm (Just Works). Queue for reliable top-level
// delivery -- a late confirm makes pairing fail (spp=0x05).
EnqueueHciCmd(OP_USER_CONFIRM_REPLY, &params[0], 6);
}
break;
}
case EVT_LINK_KEY_REQUEST: {
if (evtParamLen >= 6) {
int idx = FindBondIndex(&params[0]);
if (idx >= 0) {
// We remember this device: hand back the stored key so
// authentication succeeds without re-pairing.
uint8_t reply[22];
memcpy(reply, &params[0], 6);
memcpy(&reply[6], g_bonds[idx].key, 16);
EnqueueHciCmd(OP_LINK_KEY_REQ_REPLY, reply, 22);
} else {
// Unknown device: tell the controller we have no key so
// the remote falls back to fresh Secure Simple Pairing
// (Just Works) instead of failing auth (reason 0x05).
EnqueueHciCmd(OP_LINK_KEY_REQ_NEG_REPLY, &params[0], 6);
}
}
break;
}
case EVT_LINK_KEY_NOTIFICATION: {
// Pairing produced a new link key: BD_ADDR[6] + key[16] + type[1].
// Cache it now (fast); FlushLinkKeys() persists it to disk from
// process context so the disk write never stalls this nested
// event handler mid-pairing.
if (evtParamLen >= 22) {
StoreLinkKey(&params[0], &params[6]);
KernelLogStream(INFO, "BT-HCI") << "Link key notification (new bond cached)";
}
break;
}
case EVT_AUTH_COMPLETE: {
// Authentication succeeded -> turn on encryption. The link must
// be encrypted before A2DP; without this the headset finishes
// pairing, waits for encryption that never comes, and drops us
// (reason 0x05). Set Connection Encryption = handle(2) + 0x01.
// Queue for reliable top-level delivery.
if (evtParamLen >= 3 && params[0] == 0) {
uint8_t enc[3] = { params[1], params[2], 0x01 };
EnqueueHciCmd(OP_SET_CONN_ENCRYPT, enc, 3);
}
break;
}
case EVT_INQUIRY_COMPLETE: {
g_inquiryActive = false;
KernelLogStream(INFO, "BT-HCI") << "Inquiry complete, "
<< (uint64_t)g_inquiryResultCount << " device(s) found";
break;
}
case EVT_INQUIRY_RESULT: {
// Standard inquiry result: NumResp(1) + per-device(14 bytes each)
if (evtParamLen >= 1) {
uint8_t numResp = params[0];
for (int i = 0; i < numResp && g_inquiryResultCount < MAX_INQUIRY_RESULTS; i++) {
const uint8_t* entry = &params[1 + i * 14];
auto& dev = g_inquiryResults[g_inquiryResultCount];
memset(&dev, 0, sizeof(dev));
memcpy(dev.BdAddr, entry, 6);
dev.ClassOfDevice = (uint32_t)entry[9]
| ((uint32_t)entry[10] << 8)
| ((uint32_t)entry[11] << 16);
dev.Rssi = -128; // Unknown for standard inquiry
g_inquiryResultCount++;
}
}
break;
}
case EVT_INQUIRY_RESULT_RSSI: {
// Inquiry Result with RSSI: NumResp(1) + per-device(15 bytes each)
if (evtParamLen >= 1) {
uint8_t numResp = params[0];
for (int i = 0; i < numResp && g_inquiryResultCount < MAX_INQUIRY_RESULTS; i++) {
const uint8_t* entry = &params[1 + i * 15];
auto& dev = g_inquiryResults[g_inquiryResultCount];
memset(&dev, 0, sizeof(dev));
memcpy(dev.BdAddr, entry, 6);
dev.ClassOfDevice = (uint32_t)entry[9]
| ((uint32_t)entry[10] << 8)
| ((uint32_t)entry[11] << 16);
dev.Rssi = (int8_t)entry[14];
g_inquiryResultCount++;
}
}
break;
}
case EVT_EXTENDED_INQUIRY_RESULT: {
// Extended Inquiry Result: NumResp(1) + BD_ADDR(6) + PSRM(1) + reserved(1)
// + CoD(3) + ClockOff(2) + RSSI(1) + EIR(240)
if (evtParamLen >= 15 && g_inquiryResultCount < MAX_INQUIRY_RESULTS) {
auto& dev = g_inquiryResults[g_inquiryResultCount];
memset(&dev, 0, sizeof(dev));
memcpy(dev.BdAddr, &params[1], 6);
dev.ClassOfDevice = (uint32_t)params[9]
| ((uint32_t)params[10] << 8)
| ((uint32_t)params[11] << 16);
dev.Rssi = (int8_t)params[14];
// Parse EIR data for device name
const uint8_t* eir = &params[15];
int eirLen = evtParamLen - 15;
int pos = 0;
while (pos < eirLen && pos < 240) {
uint8_t len = eir[pos];
if (len == 0) break;
if (pos + 1 + len > eirLen) break;
uint8_t type = eir[pos + 1];
// Type 0x08 = Shortened Local Name, 0x09 = Complete Local Name
if (type == 0x08 || type == 0x09) {
int nameLen = len - 1;
if (nameLen > 63) nameLen = 63;
memcpy(dev.Name, &eir[pos + 2], nameLen);
dev.Name[nameLen] = '\0';
}
pos += 1 + len;
}
g_inquiryResultCount++;
}
break;
}
case EVT_ENCRYPT_CHANGE: {
if (evtParamLen >= 4) {
uint16_t handle = (uint16_t)params[1] | ((uint16_t)params[2] << 8);
uint8_t encryption = params[3];
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (g_connections[i].Active && g_connections[i].Handle == handle) {
g_connections[i].Encrypted = (encryption != 0);
break;
}
}
}
break;
}
case EVT_VENDOR_SPECIFIC: {
// Intel vendor events carry a sub-opcode in the first byte.
// 0x02 = "bootup" (operational firmware booted after 0xFC01)
// 0x06 = "secure send result": result(1) opcode(2) status(1)
uint8_t sub = (evtParamLen >= 1) ? params[0] : 0xFF;
if (sub == 0x02) {
g_intelBootup = true;
} else if (sub == 0x06 && evtParamLen >= 5) {
g_secureResult = params[1];
g_secureStatus = params[4];
g_secureResultValid = true;
// A healthy bootloader stays silent until the final
// fragment, so the byte/frag position here pins exactly
// where it reacted -- and a non-zero result/status mid
// upload is the signature of an active rejection.
bool err = (params[1] != 0) || (params[4] != 0);
KernelLogStream(err ? ERROR : INFO, "BT-HCI") << "secure-send result="
<< base::hex << (uint64_t)params[1] << " status="
<< (uint64_t)params[4] << base::dec
<< " @ byte " << g_ssBytesSent
<< " frag " << (uint64_t)g_ssFragsSent;
}
break;
}
default:
break;
}
}
// =========================================================================
// ProcessAcl — handle incoming ACL data
// =========================================================================
void ProcessAcl(const uint8_t* data, uint32_t len) {
if (len < sizeof(AclHeader)) return;
auto* hdr = (const AclHeader*)data;
uint16_t handle = hdr->HandleFlags & 0x0FFF;
uint16_t pbFlag = hdr->HandleFlags & 0x3000;
uint16_t dataLen = hdr->DataLength;
if (dataLen + sizeof(AclHeader) > len) return;
// Dispatch to L2CAP
L2cap::ProcessPacket(handle, data + sizeof(AclHeader), dataLen);
}
// =========================================================================
// Connection management
// =========================================================================
ConnectionInfo* GetConnection(uint16_t handle) {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (g_connections[i].Active && g_connections[i].Handle == handle) {
return &g_connections[i];
}
}
return nullptr;
}
ConnectionInfo* GetActiveConnection() {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (g_connections[i].Active) {
return &g_connections[i];
}
}
return nullptr;
}
ConnectionInfo* GetConnectionByIndex(int index) {
if (index < 0 || index >= MAX_CONNECTIONS) return nullptr;
return &g_connections[index];
}
// =========================================================================
// Convenience HCI commands
// =========================================================================
bool Reset() {
if (!SendCommand(OP_RESET, nullptr, 0)) return false;
BusyWaitMs(100);
return WaitCommandComplete(OP_RESET, nullptr, 0, 5000);
}
bool ReadBdAddr(uint8_t* addr) {
if (!SendCommand(OP_READ_BD_ADDR, nullptr, 0)) return false;
uint8_t params[7] = {};
if (!WaitCommandComplete(OP_READ_BD_ADDR, params, sizeof(params))) return false;
// params[0] = status, params[1..6] = BD_ADDR
if (params[0] != 0) return false;
memcpy(addr, &params[1], 6);
return true;
}
bool ReadLocalVersion(LocalVersion* ver) {
if (!SendCommand(OP_READ_LOCAL_VERSION, nullptr, 0)) return false;
uint8_t params[9] = {};
if (!WaitCommandComplete(OP_READ_LOCAL_VERSION, params, sizeof(params))) return false;
if (params[0] != 0) return false;
if (ver) {
ver->Status = params[0];
ver->HciVersion = params[1];
ver->HciRevision = (uint16_t)params[2] | ((uint16_t)params[3] << 8);
ver->LmpVersion = params[4];
ver->Manufacturer = (uint16_t)params[5] | ((uint16_t)params[6] << 8);
ver->LmpSubversion = (uint16_t)params[7] | ((uint16_t)params[8] << 8);
}
return true;
}
bool ReadIntelVersion(IntelVersion* ver) {
// Newer Intel BT controllers (AX200/AX201/AX211, THP+) require a
// parameter byte of 0xFF for 0xFC05 to return the full version in
// TLV format. Try with the parameter first; fall back to the
// legacy (no-param) format if the command fails.
uint8_t param = 0xFF;
bool sent = SendCommand(OP_INTEL_READ_VERSION, &param, 1);
if (!sent) {
// Fallback: legacy format (no parameter)
sent = SendCommand(OP_INTEL_READ_VERSION, nullptr, 0);
}
if (!sent) return false;
uint8_t params[32] = {};
if (!WaitCommandComplete(OP_INTEL_READ_VERSION, params, sizeof(params))) return false;
// Log raw response for diagnostics
KernelLogStream(INFO, "BT-HCI") << "Intel version raw: "
<< base::hex
<< (uint64_t)params[0] << " " << (uint64_t)params[1] << " "
<< (uint64_t)params[2] << " " << (uint64_t)params[3] << " "
<< (uint64_t)params[4] << " " << (uint64_t)params[5] << " "
<< (uint64_t)params[6] << " " << (uint64_t)params[7] << " "
<< (uint64_t)params[8] << " " << (uint64_t)params[9]
<< base::dec;
if (ver) memcpy(ver, params, sizeof(IntelVersion));
return true;
}
// =========================================================================
// Intel firmware download primitives
// =========================================================================
int ReadIntelVersionTlv(uint8_t* outBuf, int maxLen) {
if (!outBuf || maxLen <= 0) return -1;
uint8_t param = 0xFF;
if (!SendCommand(OP_INTEL_READ_VERSION, &param, 1)) return -1;
// Wait for the matching Command Complete and copy the *actually
// received* return parameters. The TLV version response can exceed a
// single interrupt packet; WaitCommandComplete trusts the event's
// declared length, so we read g_eventBuf/g_eventLen directly to avoid
// copying past what the controller delivered.
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < 2000) {
Xhci::PollEvents();
if (g_eventReady) {
g_eventReady = false;
if (g_eventLen >= 5 && g_eventBuf[0] == EVT_COMMAND_COMPLETE) {
uint16_t op = (uint16_t)g_eventBuf[3] | ((uint16_t)g_eventBuf[4] << 8);
if (op == OP_INTEL_READ_VERSION) {
// Return params begin at byte 5 (status, then TLVs).
int avail = (int)g_eventLen - 5;
if (avail < 0) avail = 0;
int n = (avail < maxLen) ? avail : maxLen;
memcpy(outBuf, &g_eventBuf[5], n);
return n;
}
}
}
for (int j = 0; j < 100; j++) asm volatile("" ::: "memory");
}
KernelLogStream(WARNING, "BT-HCI") << "ReadIntelVersionTlv timeout";
return -1;
}
void ClearSecureSendResult() {
g_secureResultValid = false;
g_ssBytesSent = 0;
g_ssFragsSent = 0;
}
bool PeekSecureSendResult(uint8_t* outResult, uint8_t* outStatus) {
if (!g_secureResultValid) return false;
if (outResult) *outResult = g_secureResult;
if (outStatus) *outStatus = g_secureStatus;
return true;
}
void ResetEventTrace() {
g_evtTraceCount = 0;
g_lastSppStatus = 0xEE;
g_lastAuthStatus = 0xEE;
g_lastDiscReason = 0xEE;
}
void DumpEventTrace() {
// Top-level only (acquires g_termLock). Shows the HCI-event sequence so
// a stalled pairing/SSP flow is visible, e.g. whether an IO Capability
// Request (0x31) / User Confirm (0x33) arrive after a link-key reply, or
// the link just goes 0x03 (connect) -> 0x17 (link-key req) -> 0x05
// (disconnect/auth-fail) with no pairing in between.
KernelLogStream s(INFO, "BT-HCI");
s << "event trace:";
uint8_t n = g_evtTraceCount;
if (n > EVT_TRACE_MAX) n = EVT_TRACE_MAX;
for (uint8_t i = 0; i < n; i++) {
s << " " << base::hex << (uint64_t)g_evtTrace[i] << base::dec;
}
s << " | spp=" << base::hex << (uint64_t)g_lastSppStatus
<< " auth=" << (uint64_t)g_lastAuthStatus
<< " disc=" << (uint64_t)g_lastDiscReason << base::dec;
}
void ProcessPendingCommands() {
// Top-level only: drains queued pairing replies with real, confirmed
// control transfers. Must NOT be called from inside PollEvents.
if (Xhci::InPollContext()) return;
while (g_pendingTail != g_pendingHead) {
PendingHciCmd c = g_pending[g_pendingTail];
g_pendingTail = (uint8_t)((g_pendingTail + 1) & 15);
SendCommand(c.opcode, c.params, c.len);
// Set Connection Encryption returns Command Status (not Complete),
// so wait on that instead of burning a 1s timeout that delays A2DP.
if (c.opcode == OP_SET_CONN_ENCRYPT) {
WaitCommandStatus(c.opcode, 1000);
} else {
WaitCommandComplete(c.opcode, nullptr, 0, 1000);
}
}
}
bool WaitSecureSendResult(uint32_t timeoutMs, uint8_t* outResult, uint8_t* outStatus) {
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
if (g_secureResultValid) {
if (outResult) *outResult = g_secureResult;
if (outStatus) *outStatus = g_secureStatus;
return true;
}
for (int j = 0; j < 100; j++) asm volatile("" ::: "memory");
}
return false;
}
bool IntelSecureSend(uint8_t fragmentType, const uint8_t* data, uint32_t len) {
uint32_t off = 0;
while (len > 0) {
uint8_t frag = (len > 252) ? 252 : (uint8_t)len;
// Fragment: [type][up to 252 data bytes]
uint8_t buf[253];
buf[0] = fragmentType;
memcpy(&buf[1], data + off, frag);
// The Intel bootloader does NOT return a Command Complete for
// 0xFC09 (Linux's btusb injects a fake one). Pacing comes from the
// synchronous USB transfer in SendCommand; the download outcome is
// reported asynchronously via the 0xFF/0x06 secure-send result
// event. So: send, drain events, and move on -- do not block per
// fragment waiting for a reply that never arrives.
if (!SendCommand(OP_INTEL_SECURE_SEND, buf, (uint8_t)(frag + 1))) {
KernelLogStream(ERROR, "BT-HCI") << "Secure send transport error: type="
<< base::hex << (uint64_t)fragmentType << " cc=" << (uint64_t)g_lastControlCC
<< base::dec << " at frag #" << (uint64_t)g_ssFragsSent
<< " byte " << g_ssBytesSent;
return false;
}
Xhci::PollEvents();
g_ssBytesSent += frag;
g_ssFragsSent++;
len -= frag;
off += frag;
}
return true;
}
bool IntelBootFirmware(uint32_t bootAddr, uint32_t timeoutMs) {
g_intelBootup = false;
// intel_reset: reset_type=0x00, patch_enable=0x01, ddc_reload=0x00,
// boot_option=0x01 (boot at specified address), boot_param (LE32).
uint8_t params[8] = {
0x00, 0x01, 0x00, 0x01,
(uint8_t)(bootAddr & 0xFF),
(uint8_t)((bootAddr >> 8) & 0xFF),
(uint8_t)((bootAddr >> 16) & 0xFF),
(uint8_t)((bootAddr >> 24) & 0xFF),
};
// Fire and forget: the controller reboots into operational firmware
// and signals readiness via the Intel bootup vendor event rather than
// a Command Complete for 0xFC01.
if (!SendCommand(OP_INTEL_RESET, params, sizeof(params))) return false;
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
if (g_intelBootup) return true;
for (int j = 0; j < 100; j++) asm volatile("" ::: "memory");
}
KernelLogStream(ERROR, "BT-HCI") << "Timed out waiting for Intel bootup event";
return false;
}
bool IntelWriteDdcRecord(const uint8_t* record, uint8_t recordLen) {
if (!record || recordLen == 0) return false;
if (!SendCommand(OP_INTEL_DDC_CONFIG_WRITE, record, recordLen)) return false;
uint8_t st[4] = {};
if (!WaitCommandComplete(OP_INTEL_DDC_CONFIG_WRITE, st, sizeof(st), 2000)) return false;
return st[0] == 0;
}
bool IntelSetEventMask() {
// Enables the Intel vendor events used during/after firmware load.
uint8_t mask[8] = { 0x87, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
if (!SendCommand(OP_INTEL_SET_EVENT_MASK, mask, sizeof(mask))) return false;
return WaitCommandComplete(OP_INTEL_SET_EVENT_MASK, nullptr, 0, 2000);
}
bool WriteLocalName(const char* name) {
uint8_t params[248] = {};
int i = 0;
for (; i < 247 && name[i]; i++) params[i] = name[i];
params[i] = '\0';
if (!SendCommand(OP_WRITE_LOCAL_NAME, params, 248)) return false;
return WaitCommandComplete(OP_WRITE_LOCAL_NAME);
}
bool WriteClassOfDevice(uint32_t cod) {
uint8_t params[3] = {
(uint8_t)(cod & 0xFF),
(uint8_t)((cod >> 8) & 0xFF),
(uint8_t)((cod >> 16) & 0xFF)
};
if (!SendCommand(OP_WRITE_CLASS_OF_DEVICE, params, 3)) return false;
return WaitCommandComplete(OP_WRITE_CLASS_OF_DEVICE);
}
bool WriteScanEnable(uint8_t mode) {
if (!SendCommand(OP_WRITE_SCAN_ENABLE, &mode, 1)) return false;
return WaitCommandComplete(OP_WRITE_SCAN_ENABLE);
}
bool WriteSSPMode(uint8_t mode) {
if (!SendCommand(OP_WRITE_SSP_MODE, &mode, 1)) return false;
return WaitCommandComplete(OP_WRITE_SSP_MODE);
}
bool AcceptConnection(const uint8_t* bdAddr, uint8_t role) {
uint8_t params[7];
memcpy(params, bdAddr, 6);
params[6] = role;
if (!SendCommand(OP_ACCEPT_CONN_REQ, params, 7)) return false;
return WaitCommandStatus(OP_ACCEPT_CONN_REQ);
}
bool AuthenticateLink(uint16_t handle) {
// Request authentication on an established ACL link. As the connection
// initiator we must drive this: a bonded headset will not start pairing
// on its own for a device it believes it already knows, so the link just
// sits unauthenticated until it drops (reason 0x05). This kicks off the
// flow -- the controller raises a Link Key Request (we answer with a
// stored key, or negatively to force fresh Secure Simple Pairing).
uint8_t params[2] = { (uint8_t)(handle & 0xFF), (uint8_t)(handle >> 8) };
if (!SendCommand(OP_AUTH_REQUESTED, params, 2)) return false;
return WaitCommandStatus(OP_AUTH_REQUESTED, 2000);
}
bool SetBdAddr(const uint8_t* addr) {
// Intel 0xFC31: 6-byte BD_ADDR, little-endian (same order as ReadBdAddr).
if (!SendCommand(OP_INTEL_WRITE_BD_ADDR, addr, 6)) return false;
return WaitCommandComplete(OP_INTEL_WRITE_BD_ADDR, nullptr, 0, 2000);
}
bool Disconnect(uint16_t handle, uint8_t reason) {
uint8_t params[3] = {
(uint8_t)(handle & 0xFF),
(uint8_t)((handle >> 8) & 0xFF),
reason
};
if (!SendCommand(OP_DISCONNECT, params, 3)) return false;
return WaitCommandStatus(OP_DISCONNECT);
}
bool ReadBufferSize(uint16_t* aclLen, uint8_t* scoLen,
uint16_t* aclNum, uint16_t* scoNum) {
if (!SendCommand(OP_READ_BUFFER_SIZE, nullptr, 0)) return false;
uint8_t params[8] = {};
if (!WaitCommandComplete(OP_READ_BUFFER_SIZE, params, sizeof(params))) return false;
if (params[0] != 0) return false;
if (aclLen) *aclLen = (uint16_t)params[1] | ((uint16_t)params[2] << 8);
if (scoLen) *scoLen = params[3];
if (aclNum) *aclNum = (uint16_t)params[4] | ((uint16_t)params[5] << 8);
if (scoNum) *scoNum = (uint16_t)params[6] | ((uint16_t)params[7] << 8);
g_aclMaxLen = (uint16_t)params[1] | ((uint16_t)params[2] << 8);
g_aclMaxNum = (uint16_t)params[4] | ((uint16_t)params[5] << 8);
return true;
}
// =========================================================================
// Inquiry (device discovery)
// =========================================================================
bool StartInquiry(uint8_t durationUnits) {
g_inquiryResultCount = 0;
g_inquiryActive = true;
// HCI Inquiry: LAP(3) + InquiryLength(1) + NumResponses(1)
// GIAC LAP = 0x9E8B33
uint8_t params[5] = {
0x33, 0x8B, 0x9E, // LAP (General Inquiry Access Code)
durationUnits, // Duration in 1.28s units
0x00 // Unlimited responses
};
if (!SendCommand(OP_INQUIRY, params, 5)) {
g_inquiryActive = false;
return false;
}
// Inquiry uses Command Status (not Command Complete)
if (!WaitCommandStatus(OP_INQUIRY)) {
g_inquiryActive = false;
return false;
}
return true;
}
bool CancelInquiry() {
if (!g_inquiryActive) return true;
if (!SendCommand(OP_INQUIRY_CANCEL, nullptr, 0)) return false;
WaitCommandComplete(OP_INQUIRY_CANCEL, nullptr, 0, 2000);
g_inquiryActive = false;
return true;
}
int GetInquiryResults(InquiryDevice* buf, int maxCount) {
int count = g_inquiryResultCount;
if (count > maxCount) count = maxCount;
if (buf && count > 0) {
memcpy(buf, g_inquiryResults, count * sizeof(InquiryDevice));
}
return count;
}
void ClearInquiryResults() {
g_inquiryResultCount = 0;
}
bool IsInquiryActive() {
return g_inquiryActive;
}
// =========================================================================
// Create ACL connection
// =========================================================================
void DrainEvents() {
// Discard any unconsumed Command Complete/Status events that weren't
// picked up by WaitCommandComplete/WaitCommandStatus.
if (g_eventReady) {
g_eventReady = false;
}
// Drain all queued ACL packets (process the whole ring, not just one,
// so a burst is never left unhandled).
while (g_aclRxTail != g_aclRxHead) {
uint8_t slot = g_aclRxTail;
uint16_t pl = g_aclRxLens[slot];
// Diagnostic (top-level, safe to log -- not the IRQ path): trace the
// first few ACL packets so the rx flood / missing Config Response is
// identifiable in ONE boot. Layout: ACL header(4) + L2CAP header(4),
// so the L2CAP CID is at bytes 6-7; on the signaling channel (CID 1)
// byte 8 is the command code (0x03 CONN_RSP, 0x04 CONFIG_REQ,
// 0x05 CONFIG_RSP). A flood of len==maxpacket junk CIDs vs real
// cid=0001 code=05 packets tells the two failure modes apart.
static uint32_t s_rxTraced = 0;
if (s_rxTraced < 24 && pl >= 8) {
const uint8_t* d = g_aclRxRing[slot];
uint16_t cid = (uint16_t)d[6] | ((uint16_t)d[7] << 8);
uint8_t code = (pl >= 9) ? d[8] : 0;
KernelLogStream(INFO, "BT-HCI") << "rx[" << (uint64_t)s_rxTraced
<< "] len=" << (uint64_t)pl << " cid=" << base::hex << (uint64_t)cid
<< " code=" << (uint64_t)code << base::dec;
s_rxTraced++;
}
ProcessAcl(g_aclRxRing[slot], pl);
g_aclRxTail = (uint8_t)((g_aclRxTail + 1) % ACL_RX_SLOTS);
}
}
bool CreateConnection(const uint8_t* bdAddr) {
// HCI Create Connection:
// BD_ADDR(6) + PacketType(2) + PSRM(1) + reserved(1) + ClockOffset(2) + AllowRoleSwitch(1)
uint8_t params[13] = {};
memcpy(params, bdAddr, 6);
// Packet types: DM1, DH1, DM3, DH3, DM5, DH5
params[6] = 0x18; // CC18 = allow DM1, DH1, DM3, DH3, DM5, DH5
params[7] = 0xCC;
params[8] = 0x02; // Page Scan Repetition Mode R2
params[9] = 0x00; // Reserved
params[10] = 0x00; // Clock offset
params[11] = 0x00;
params[12] = 0x01; // Allow role switch
if (!SendCommand(OP_CREATE_CONNECTION, params, 13)) return false;
// Create Connection uses Command Status, then Connection Complete event
return WaitCommandStatus(OP_CREATE_CONNECTION, 5000);
}
}