/* * Hci.cpp * Bluetooth HCI transport over USB * Copyright (c) 2026 Daniel Hammer */ #include "Hci.hpp" #include "L2cap.hpp" #include #include #include #include #include #include #include #include #include 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 = ¶ms[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 = ¶ms[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, ¶ms[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, ¶ms[0], 6); } break; } case EVT_LINK_KEY_REQUEST: { if (evtParamLen >= 6) { int idx = FindBondIndex(¶ms[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, ¶ms[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, ¶ms[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(¶ms[0], ¶ms[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 = ¶ms[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 = ¶ms[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, ¶ms[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 = ¶ms[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, ¶ms[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, ¶m, 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, ¶m, 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); } }