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

650 lines
28 KiB
C++

/*
* Bluetooth.cpp
* Top-level Bluetooth subsystem — adapter registration and Intel BT initialization
* Copyright (c) 2026 Daniel Hammer
*/
#include "Bluetooth.hpp"
#include "Hci.hpp"
#include "A2dp.hpp"
#include "IntelFirmware.hpp"
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
#include <Fs/Vfs.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Libraries/Memory.hpp>
#include <Timekeeping/ApicTimer.hpp>
using namespace Kt;
namespace Drivers::USB::Bluetooth {
// =========================================================================
// State
// =========================================================================
static bool g_initialized = false;
static uint8_t g_slotId = 0;
static uint8_t g_bdAddr[6] = {};
// True when the USB transport is up but the firmware-dependent HCI init is
// still waiting for the ramdisk (drive 0) to be mounted. Set when an
// adapter enumerates during the boot port scan, which runs before the boot
// filesystems are mounted; cleared by ServiceDeferredInit() once VFS is up.
static bool g_initPending = false;
// Forward declaration: firmware-dependent HCI bring-up, run once VFS is up.
static void CompleteInit();
// Path to the Bluetooth config (BD_ADDR override). Written by the
// Bluetooth desktop app; read here on boot. See ApplyConfiguredAddress().
static constexpr const char* BT_CONFIG_PATH = "0:/config/bluetooth.toml";
// =========================================================================
// bluetooth.toml BD_ADDR override
// =========================================================================
static bool HexNibble(char c, uint8_t& out) {
if (c >= '0' && c <= '9') { out = (uint8_t)(c - '0'); return true; }
if (c >= 'a' && c <= 'f') { out = (uint8_t)(c - 'a' + 10); return true; }
if (c >= 'A' && c <= 'F') { out = (uint8_t)(c - 'A' + 10); return true; }
return false;
}
// Parse exactly six ':'/'-'-separated hex octets from a string fragment.
// Fills out[0..5] in written order (out[0] is the first printed octet,
// matching the desktop app's format_addr); returns false on any deviation.
static bool ParseMacStr(const char* s, int len, uint8_t out[6]) {
int byteIdx = 0, i = 0;
while (byteIdx < 6) {
while (i < len && (s[i] == ':' || s[i] == '-' || s[i] == ' ')) i++;
uint8_t hi = 0, lo = 0;
if (i + 1 >= len || !HexNibble(s[i], hi) || !HexNibble(s[i + 1], lo))
return false;
out[byteIdx++] = (uint8_t)((hi << 4) | lo);
i += 2;
}
return true;
}
// Read the optional `mac = "XX:XX:XX:XX:XX:XX"` key from bluetooth.toml.
// Minimal line scanner (the kernel has no TOML parser, and the file holds a
// single value); ignores section headers/comments. Returns false when the
// file is absent or the key is missing/malformed.
static bool ReadConfiguredMac(uint8_t out[6]) {
Fs::Vfs::BackendFile f;
if (Fs::Vfs::OpenBackendFile(BT_CONFIG_PATH, f) < 0) return false;
uint64_t size = Fs::Vfs::GetBackendFileSize(f);
if (size == 0) { Fs::Vfs::CloseBackendFile(f); return false; }
char buf[512];
uint64_t n = size < sizeof(buf) - 1 ? size : sizeof(buf) - 1;
Fs::Vfs::ReadBackendFile(f, (uint8_t*)buf, 0, n);
Fs::Vfs::CloseBackendFile(f);
buf[n] = '\0';
const char* p = buf;
while (*p) {
while (*p == ' ' || *p == '\t') p++;
// Match a bare "mac" key (next char must end the identifier).
if (p[0] == 'm' && p[1] == 'a' && p[2] == 'c' &&
(p[3] == ' ' || p[3] == '\t' || p[3] == '=')) {
const char* q = p + 3;
while (*q == ' ' || *q == '\t') q++;
if (*q == '=') {
const char* quote = q + 1;
while (*quote && *quote != '"' && *quote != '\n') quote++;
if (*quote == '"') {
const char* end = quote + 1;
while (*end && *end != '"' && *end != '\n') end++;
if (*end == '"' &&
ParseMacStr(quote + 1, (int)(end - quote - 1), out))
return true;
}
}
}
while (*p && *p != '\n') p++;
if (*p == '\n') p++;
}
return false;
}
// Apply a configured BD_ADDR override (if any) to the freshly-reset
// controller. Called from CompleteInit BEFORE ReadBdAddr so the rest of
// bring-up uses the overridden address; no HCI Reset must follow (the Intel
// 0xFC31 override is volatile and a reset reverts to the factory address).
static void ApplyConfiguredAddress() {
uint8_t mac[6];
if (!ReadConfiguredMac(mac)) return; // no override configured
if (Hci::SetBdAddr(mac)) {
KernelLogStream(OK, "BT") << "Applied BD_ADDR override from bluetooth.toml";
} else {
KernelLogStream(WARNING, "BT")
<< "BD_ADDR override from bluetooth.toml rejected by controller";
}
}
// Intel Bluetooth device IDs
static bool IsIntelBt(uint16_t vid, uint16_t pid) {
if (vid != 0x8087) return false;
// Known Intel Bluetooth USB product IDs
switch (pid) {
case 0x0032: // AX211 variant
case 0x0033: // AX211
case 0x0036: // AX211 variant
case 0x0038: // AX211 variant
case 0x0AAA: // AX200
case 0x0026: // AX201
case 0x0029: // AX201 variant
case 0x0025: // 9560
case 0x0A2B: // 8265
case 0x0A2A: // 8260
case 0x07DC: // 8265 variant
case 0x0AA7: // AX200 variant
return true;
default:
return false;
}
}
// =========================================================================
// Intel Bluetooth firmware detection
// =========================================================================
static bool InitIntelBluetooth(uint8_t slotId) {
KernelLogStream(INFO, "BT") << "Intel Bluetooth adapter detected";
// Intel BT controllers require HCI Reset before they respond to
// vendor-specific commands. This mirrors the Linux btintel driver
// sequence: Reset → Read Version → (firmware load) → Reset.
if (!Hci::Reset()) {
KernelLogStream(ERROR, "BT") << "Initial HCI Reset failed";
return false;
}
// Read standard HCI version -- if this fails, the controller is likely
// in bootloader mode where only vendor commands are accepted.
Hci::LocalVersion lver = {};
bool hciVersionOk = Hci::ReadLocalVersion(&lver);
if (hciVersionOk) {
KernelLogStream(INFO, "BT") << "HCI version=" << (uint64_t)lver.HciVersion
<< " rev=" << base::hex << (uint64_t)lver.HciRevision
<< " LMP=" << (uint64_t)lver.LmpVersion
<< " manufacturer=" << (uint64_t)lver.Manufacturer
<< " subver=" << (uint64_t)lver.LmpSubversion << base::dec;
}
// Read legacy Intel version for diagnostics (TLV parts return this in
// a different layout; the authoritative state check happens inside the
// firmware download path below via the TLV version).
Hci::IntelVersion ver = {};
if (!Hci::ReadIntelVersion(&ver)) {
KernelLogStream(WARNING, "BT") << "Failed to read Intel BT version";
} else {
KernelLogStream(INFO, "BT") << "Intel BT: HW variant=" << (uint64_t)ver.HwVariant
<< " FW variant=" << base::hex << (uint64_t)ver.FwVariant
<< " FW rev=" << (uint64_t)ver.FwRevision << "."
<< (uint64_t)ver.FwBuildNum << base::dec;
}
// Run the firmware download path. This reads the TLV version, and if
// the controller is in bootloader mode, loads the matching .sfi image
// from the ramdisk, secure-sends it, boots the operational firmware
// and applies DDC parameters. Returns true if the controller ends up
// operational (either already loaded, or freshly downloaded).
if (!DownloadIntelFirmware()) {
KernelLogStream(WARNING, "BT")
<< "Intel BT firmware not loaded; limited functionality";
// Standard init already issued an HCI Reset above.
return true;
}
// The operational firmware just (re)booted. Give it a clean reset and
// re-enable the Intel vendor event mask before the generic HCI setup.
Hci::Reset();
Hci::IntelSetEventMask();
return true;
}
// =========================================================================
// RegisterAdapter — entry point from USB enumeration
// =========================================================================
void RegisterAdapter(uint8_t slotId) {
if (g_initialized) {
KernelLogStream(WARNING, "BT") << "Bluetooth adapter already registered";
return;
}
g_slotId = slotId;
// Initialize HCI transport (allocates DMA buffers, registers callback)
// NOTE: Does NOT queue receive transfers yet — device isn't ready
Hci::Initialize(slotId);
auto* dev = Xhci::GetDevice(slotId);
if (!dev) return;
// Wait for the USB device to be ready after SET_CONFIGURATION
// Intel BT controllers need 200-500ms after config before accepting HCI
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < 200) {
Xhci::PollEvents();
asm volatile("pause" ::: "memory");
}
// Start the event pipe BEFORE sending any HCI commands.
// HCI command responses arrive as events on the interrupt IN endpoint,
// so it must be queued to receive them.
Hci::StartEventPipe();
// The firmware download path reads the .sfi/.ddc images from the
// ramdisk (drive 0). Adapters present at boot enumerate during the
// xHCI port scan, which runs before the boot filesystems are mounted,
// so defer the firmware-dependent bring-up until VFS is available.
if (!Fs::Vfs::IsDriveRegistered(0)) {
g_initPending = true;
KernelLogStream(INFO, "BT") << "Transport up; deferring init until ramdisk is mounted";
return;
}
CompleteInit();
}
// =========================================================================
// CompleteInit — firmware-dependent HCI bring-up (needs VFS/ramdisk)
// =========================================================================
static void CompleteInit() {
auto* dev = Xhci::GetDevice(g_slotId);
if (!dev) return;
// Intel-specific initialization (firmware download + HCI Reset)
bool didReset = false;
if (IsIntelBt(dev->VendorId, dev->ProductId)) {
if (InitIntelBluetooth(g_slotId)) {
didReset = true; // InitIntelBluetooth already sent HCI Reset
} else {
KernelLogStream(WARNING, "BT") << "Intel BT init failed, continuing with basic HCI";
}
}
// Standard HCI Reset (skip if Intel init already did one)
if (!didReset) {
if (!Hci::Reset()) {
KernelLogStream(ERROR, "BT") << "HCI Reset failed";
return;
}
}
// Apply a persisted BD_ADDR override (0:/config/bluetooth.toml) now,
// after the last reset and before the address is read back, so the
// whole bring-up below uses the overridden address.
ApplyConfiguredAddress();
// Read BD_ADDR
if (Hci::ReadBdAddr(g_bdAddr)) {
KernelLogStream(OK, "BT") << "BD_ADDR: "
<< base::hex
<< (uint64_t)g_bdAddr[5] << ":" << (uint64_t)g_bdAddr[4] << ":"
<< (uint64_t)g_bdAddr[3] << ":" << (uint64_t)g_bdAddr[2] << ":"
<< (uint64_t)g_bdAddr[1] << ":" << (uint64_t)g_bdAddr[0] << base::dec;
}
// NOTE: an earlier build overrode the BD_ADDR via 0xFC31 to dodge a
// (since disproven) stale-bond theory. Removed: the BD_ADDR is an input
// to the SSP authentication confirmation, and if the override only
// changes the advertised address but not the address the firmware uses
// in the crypto, the two sides compute different confirmations and
// pairing fails (Simple Pairing Complete = 0x05). Use the real address.
// Read buffer size
uint16_t aclLen = 0, aclNum = 0;
uint8_t scoLen = 0;
uint16_t scoNum = 0;
if (Hci::ReadBufferSize(&aclLen, &scoLen, &aclNum, &scoNum)) {
KernelLogStream(INFO, "BT") << "ACL buffer: " << (uint64_t)aclLen
<< " bytes x " << (uint64_t)aclNum;
}
// Set local name
Hci::WriteLocalName("MontaukOS");
// Class of Device. The A2DP spec MANDATES the Capturing service bit
// (0x080000) for a source; Audio (0x200000) is customary. Major/minor
// class: Computer/Laptop (0x010C) -- what we actually are. The old
// value 0x200408 (Audio/Video major class, minor "hands-free device",
// no Capturing bit) presented MontaukOS to the headset as ANOTHER
// HEADSET. A sink's connection manager classifies peers by CoD (it
// arrives in its Connection Request event and is cached at pairing),
// and an A2DP/AVRCP dial from a "hands-free unit" is a credible reason
// for it to park those channels at "authorization pending" forever.
// NOTE: the headset caches this from pairing -- it must FORGET the
// device and re-pair to observe the new class.
Hci::WriteClassOfDevice(0x28010C);
// Enable Simple Secure Pairing
Hci::WriteSSPMode(1);
// Allow role switch + sniff on new connections. The controller default
// link policy is 0x0000 (deny both). A multipoint headset (Bose QC
// Ultra) that also holds a link to a phone requests a role switch to
// master on our link to avoid a scatternet; with the switch denied,
// some sink firmwares never grant the A2DP media path. Sniff denial
// similarly upsets CSR-derived stacks that sniff idle links.
uint8_t linkPolicy[2] = {0x05, 0x00}; // bit0 role switch, bit2 sniff
Hci::SendCommand(Hci::OP_WRITE_DEFAULT_LP, linkPolicy, 2);
Hci::WaitCommandComplete(Hci::OP_WRITE_DEFAULT_LP);
// Set event mask to receive relevant events. Octet 6 (events 0x31-0x38)
// MUST be enabled for Secure Simple Pairing: IO Capability Request
// (0x31, bit 48), IO Capability Response (0x32), User Confirmation
// Request (0x33), Simple Pairing Complete (0x36) all live there. It was
// 0x00 -> the controller started SSP but the IO-Capability Request event
// never reached us, so pairing always timed out with auth failure 0x05.
uint8_t eventMask[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0x20};
Hci::SendCommand(Hci::OP_SET_EVENT_MASK, eventMask, 8);
Hci::WaitCommandComplete(Hci::OP_SET_EVENT_MASK);
// Enable inquiry + page scan (discoverable and connectable)
Hci::WriteScanEnable(0x03);
// Load persisted bonds so previously-paired devices reconnect without
// re-pairing (VFS is up by the time CompleteInit runs).
Hci::LoadLinkKeys();
g_initialized = true;
KernelLogStream(OK, "BT") << "Bluetooth adapter initialized successfully";
}
// =========================================================================
// ServiceDeferredInit — run boot-deferred bring-up once VFS is ready
// =========================================================================
void ServiceDeferredInit() {
if (!g_initPending || g_initialized) return;
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk still not mounted
g_initPending = false;
KernelLogStream(INFO, "BT") << "Ramdisk mounted; completing Bluetooth init";
CompleteInit();
}
// =========================================================================
// ServiceEvents — steady-state event pump (idle loop)
// =========================================================================
void ServiceEvents() {
if (!g_initialized) return;
if (Xhci::InPollContext()) return; // never nest under PollEvents
Xhci::PollEvents();
Hci::DrainEvents();
Hci::ProcessPendingCommands();
A2dp::PumpMedia(); // feed queued media to the link (no-op when idle)
}
// =========================================================================
// Public queries
// =========================================================================
bool IsInitialized() {
return g_initialized;
}
uint8_t GetSlotId() {
return g_slotId;
}
const uint8_t* GetBdAddr() {
return g_bdAddr;
}
// =========================================================================
// SetAddress — live BD_ADDR change
// =========================================================================
bool SetAddress(const uint8_t* addr) {
if (!g_initialized || !addr) return false;
// Reject the obviously-invalid addresses (all-zero, broadcast).
bool allZero = true, allOnes = true;
for (int i = 0; i < 6; i++) {
if (addr[i] != 0x00) allZero = false;
if (addr[i] != 0xFF) allOnes = false;
}
if (allZero || allOnes) return false;
// Drop any active link first: its pairing/encryption was negotiated
// against the old address, so it cannot survive the change.
bool droppedLink = false;
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
auto* conn = Hci::GetConnectionByIndex(i);
if (conn && conn->Active) {
Hci::Disconnect(conn->Handle, 0x13); // Remote User Terminated
droppedLink = true;
}
}
// Let the disconnection(s) complete before reprogramming the address.
if (droppedLink) {
uint64_t t0 = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - t0 < 300) {
Xhci::PollEvents();
Hci::DrainEvents();
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
}
}
// Program the new address. Do NOT issue an HCI Reset afterwards: the
// Intel 0xFC31 override is volatile and a reset reverts it.
if (!Hci::SetBdAddr(addr)) return false;
// Re-read to confirm and refresh the cache; fall back to the requested
// bytes if the read fails.
uint8_t readback[6] = {};
if (Hci::ReadBdAddr(readback)) {
memcpy(g_bdAddr, readback, 6);
} else {
memcpy(g_bdAddr, addr, 6);
}
// Re-assert discoverable + connectable with the new address (no reset
// happened, so name/CoD/SSP/event-mask all persist).
Hci::WriteScanEnable(0x03);
for (int i = 0; i < 6; i++) {
if (g_bdAddr[i] != addr[i]) return false;
}
KernelLogStream(OK, "BT") << "BD_ADDR changed at runtime";
return true;
}
// =========================================================================
// Scan — blocking inquiry
// =========================================================================
int Scan(Hci::InquiryDevice* buf, int maxCount, uint32_t timeoutMs) {
if (!g_initialized || !buf || maxCount <= 0) return -1;
Hci::ClearInquiryResults();
// Convert timeout to 1.28s units (min 1, max 30)
uint8_t duration = (uint8_t)(timeoutMs / 1280);
if (duration < 1) duration = 1;
if (duration > 30) duration = 30;
if (!Hci::StartInquiry(duration)) return -1;
// Poll until inquiry completes or timeout
uint64_t start = Timekeeping::GetMilliseconds();
while (Hci::IsInquiryActive() && (Timekeeping::GetMilliseconds() - start < timeoutMs)) {
Xhci::PollEvents();
Hci::DrainEvents();
for (int j = 0; j < 200; j++) {
asm volatile("pause" ::: "memory");
}
}
// Cancel if still running
if (Hci::IsInquiryActive()) {
Hci::CancelInquiry();
}
return Hci::GetInquiryResults(buf, maxCount);
}
// =========================================================================
// Connect — initiate ACL connection
// =========================================================================
int Connect(const uint8_t* bdAddr, uint32_t timeoutMs) {
if (!g_initialized || !bdAddr) return -1;
Hci::ResetEventTrace(); // capture the pairing/SSP event sequence
if (!Hci::CreateConnection(bdAddr)) return -1;
// Wait for Connection Complete event
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
Xhci::PollEvents();
Hci::DrainEvents();
// Check connection table for matching BD_ADDR
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
auto* conn = Hci::GetConnectionByIndex(i);
if (conn && conn->Active) {
bool match = true;
for (int j = 0; j < 6; j++) {
if (conn->BdAddr[j] != bdAddr[j]) { match = false; break; }
}
if (match) {
// The headset (in pairing mode) drives Secure Simple
// Pairing itself right after the ACL link comes up. Let
// authentication + encryption finish BEFORE opening any
// L2CAP/AVDTP channels: doing A2DP on a not-yet-
// authenticated link races with the pairing handshake and
// the headset drops us (reason 0x05). Drain events here
// so the IO-capability / user-confirm / link-key / encrypt
// events all get serviced.
//
// We are the initiator: request authentication so the
// controller starts Secure Simple Pairing (Link Key
// Request -> our negative reply -> IO Capability Request
// -> ... ). The headset does not start this on its own.
// NB this only works now that octet 6 of the event mask
// is enabled so the IO-Capability Request event actually
// reaches us; before that this produced 03 17 06 05.
Hci::AuthenticateLink(conn->Handle);
uint64_t t0 = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
Xhci::PollEvents();
Hci::DrainEvents();
// Send queued pairing replies reliably (top-level,
// not nested under PollEvents).
Hci::ProcessPendingCommands();
if (!conn->Active) break; // link dropped during pairing
if (conn->Encrypted) break; // authenticated + encrypted -> ready
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
}
// Bring up the A2DP source stream (signaling + media
// channels, SBC negotiation) only once the link is
// secured. Without a media stream the headset also drops
// the link (reason 0x13), so this keeps it engaged too.
if (conn->Active) {
// Let the link settle after Encryption Change before
// dialing L2CAP: some sinks ignore a CONN_REQ that
// arrives the instant encryption completes. Drain
// (don't blind-sleep) so the ACL RX ring stays live.
uint64_t st = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - st < 300) {
Xhci::PollEvents();
Hci::DrainEvents();
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
}
A2dp::StartSource();
}
// Persist any new link key now (process context), even if
// the link later dropped, so the disk write never stalls
// the nested pairing event handler.
Hci::FlushLinkKeys();
Hci::DumpEventTrace(); // show the pairing/SSP sequence
return 0;
}
}
}
for (int j = 0; j < 200; j++) {
asm volatile("pause" ::: "memory");
}
}
Hci::DumpEventTrace(); // show whatever events did arrive
return -1; // Timeout
}
// =========================================================================
// Disconnect — disconnect a device by BD_ADDR
// =========================================================================
int Disconnect(const uint8_t* bdAddr) {
if (!g_initialized || !bdAddr) return -1;
// Find connection with matching BD_ADDR
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
auto* conn = Hci::GetConnectionByIndex(i);
if (conn && conn->Active) {
bool match = true;
for (int j = 0; j < 6; j++) {
if (conn->BdAddr[j] != bdAddr[j]) { match = false; break; }
}
if (match) {
Hci::Disconnect(conn->Handle, 0x13); // 0x13 = Remote User Terminated
return 0;
}
}
}
return -1; // Not found
}
// =========================================================================
// ListConnected — list active connections
// =========================================================================
int ListConnected(Hci::ConnectionInfo* buf, int maxCount) {
if (!g_initialized || !buf || maxCount <= 0) return 0;
int count = 0;
for (int i = 0; i < Hci::MAX_CONNECTIONS && count < maxCount; i++) {
auto* conn = Hci::GetConnectionByIndex(i);
if (conn && conn->Active) {
buf[count] = *conn;
count++;
}
}
return count;
}
// =========================================================================
// ListBonded / ForgetDevice — paired-device management
// =========================================================================
int ListBonded(Hci::BondInfo* buf, int maxCount) {
if (!g_initialized || !buf || maxCount <= 0) return 0;
return Hci::ListBonds(buf, maxCount);
}
int ForgetDevice(const uint8_t* bdAddr) {
if (!g_initialized || !bdAddr) return -1;
// Best-effort: tear down an active link first so we don't keep a live
// connection whose key we just discarded.
Disconnect(bdAddr);
return Hci::ForgetBond(bdAddr) ? 0 : -1;
}
}