711 lines
31 KiB
C++
711 lines
31 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/Audio/Mixer.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>
|
|
#include <Hal/SmpBoot.hpp>
|
|
#include <atomic>
|
|
|
|
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; claimed (atomically -- the pickup runs from the
|
|
// idle loop, concurrently with the rest of the system) by
|
|
// ServiceDeferredInit() once VFS is up.
|
|
static std::atomic<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() {
|
|
KernelLogStream(INFO, "BT") << "Intel Bluetooth adapter detected";
|
|
|
|
// Read the Intel TLV version first. It is the mode probe accepted by
|
|
// both Intel's bootloader and operational firmware. A standard HCI
|
|
// Reset must not precede it: the bootloader answers Reset with status
|
|
// 0x01 (Unknown HCI Command), which is normal rather than a fatal
|
|
// transport failure.
|
|
//
|
|
// If the controller is in bootloader mode, this loads the matching SFI
|
|
// image from the ramdisk, secure-sends it, boots operational firmware,
|
|
// and applies DDC parameters. Returns true if the controller ends up
|
|
// operational (either already loaded, or freshly downloaded).
|
|
if (!DownloadIntelFirmware()) {
|
|
// Older Intel parts may lack the TLV command while already running
|
|
// usable operational firmware. Preserve that compatibility only
|
|
// when standard HCI proves it is genuinely operational.
|
|
if (Hci::Reset()) {
|
|
KernelLogStream(WARNING, "BT")
|
|
<< "Intel firmware query failed; continuing with operational HCI";
|
|
return true;
|
|
}
|
|
KernelLogStream(ERROR, "BT")
|
|
<< "Intel controller remains in bootloader mode";
|
|
return false;
|
|
}
|
|
|
|
// Whether it was already present or was just booted, operational
|
|
// firmware must now accept standard HCI. Do not mark the adapter ready
|
|
// if this transition did not actually happen.
|
|
if (!Hci::Reset()) {
|
|
KernelLogStream(ERROR, "BT")
|
|
<< "Operational firmware did not accept HCI Reset";
|
|
return false;
|
|
}
|
|
if (!Hci::IntelSetEventMask()) {
|
|
KernelLogStream(WARNING, "BT")
|
|
<< "Intel vendor event mask was not accepted";
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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. Deliberately done HERE, at
|
|
// enumeration time, not in the deferred bring-up: this preserves the
|
|
// exact transport timing of the original synchronous boot path.
|
|
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.
|
|
// The idle loop (ServiceDeferredInit) picks it up after boot, keeping
|
|
// the multi-second firmware download off the boot-critical path.
|
|
if (!Fs::Vfs::IsDriveRegistered(0)) {
|
|
g_initPending.store(true, std::memory_order_release);
|
|
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()) {
|
|
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
|
} else {
|
|
// A recognized Intel part that failed its vendor initialization
|
|
// is normally still a bootloader, where a second standard Reset
|
|
// only repeats status 0x01. Stop with an accurate failure
|
|
// instead of pretending a basic-HCI fallback exists.
|
|
KernelLogStream(ERROR, "BT") << "Intel BT initialization failed";
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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: do NOT override the BD_ADDR via 0xFC31 here. 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;
|
|
}
|
|
// Bulk IN was armed before Intel firmware loading, but bootloader runts
|
|
// are not HCI ACL traffic and an absorbed firmware-phase USB error may
|
|
// have stopped the endpoint. Start framed ACL reception only now.
|
|
Hci::EnableAclDataReception();
|
|
|
|
// Set local name
|
|
Hci::WriteLocalName("MontaukOS");
|
|
|
|
// 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);
|
|
|
|
// Enable Secure Connections host support (P-256 / AES-CCM). Link
|
|
// keys are procedure-bound: a bond minted over Secure Connections
|
|
// (BlueZ always negotiates SC -> key Type=7) CANNOT authenticate a
|
|
// legacy link -- the controller must fail with status 5. Without
|
|
// this, a key shared with a Linux install (dual boot, see
|
|
// scripts/import-bluez-bond.sh) is cryptographically fine yet
|
|
// unusable, and our own pairings mint legacy P-192 keys that Linux
|
|
// then silently replaces. Must follow Write SSP Mode.
|
|
uint8_t scOn = 0x01;
|
|
Hci::SendCommand(Hci::OP_WRITE_SC_HOST_SUPPORT, &scOn, 1);
|
|
Hci::WaitCommandComplete(Hci::OP_WRITE_SC_HOST_SUPPORT);
|
|
|
|
// 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, 0xFF, 0xFF, 0x20};
|
|
Hci::SendCommand(Hci::OP_SET_EVENT_MASK, eventMask, 8);
|
|
Hci::WaitCommandComplete(Hci::OP_SET_EVENT_MASK);
|
|
|
|
// Request Extended Inquiry Results so scan entries carry EIR names and
|
|
// RSSI. Older controllers may support only mode 1 (RSSI); the parser
|
|
// handles both result layouts and the fallback preserves discovery.
|
|
if (!Hci::WriteInquiryMode(2)) Hci::WriteInquiryMode(1);
|
|
|
|
// Enable inquiry + page scan (discoverable and connectable)
|
|
Hci::WriteScanEnable(0x03);
|
|
|
|
// 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.load(std::memory_order_relaxed) || g_initialized) return;
|
|
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk still not mounted
|
|
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
|
|
|
// Claim the pending init (this runs from the idle loop; make sure only
|
|
// one pass performs the bring-up). The firmware download inside takes
|
|
// seconds -- running it here instead of on the boot path is what keeps
|
|
// boot fast.
|
|
bool expected = true;
|
|
if (!g_initPending.compare_exchange_strong(expected, false,
|
|
std::memory_order_acquire)) return;
|
|
|
|
KernelLogStream(INFO, "BT") << "Completing deferred Bluetooth init in background";
|
|
|
|
// Reserve this CPU for the duration. The bring-up overlaps desktop
|
|
// startup, and if the scheduler tick pulls this idle context away
|
|
// whenever a process is ready, the HCI waits' wall-clock timeouts
|
|
// expire with almost no polling done. Reserved, the bring-up runs
|
|
// uninterrupted here while processes use other CPUs; on a single-CPU
|
|
// system this briefly pauses userspace, matching the old synchronous
|
|
// behavior minus the boot-path stall.
|
|
auto* cpu = Smp::GetCurrentCpuData();
|
|
bool wasReserved = cpu && cpu->reservedForKernelWork;
|
|
if (cpu) cpu->reservedForKernelWork = true;
|
|
Hci::SetFwTrace(true); // bounded per-completion event-pipe trace
|
|
CompleteInit();
|
|
Hci::DumpFwTrace(); // flush remaining records (process context)
|
|
Hci::SetFwTrace(false);
|
|
if (cpu) cpu->reservedForKernelWork = wasReserved;
|
|
}
|
|
|
|
// =========================================================================
|
|
// ServiceEvents — steady-state event pump (idle loop)
|
|
// =========================================================================
|
|
|
|
void ServiceEvents() {
|
|
if (!g_initialized) return;
|
|
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
|
A2dp::ServiceMedia(); // reap events and feed queued media
|
|
// DrainEvents may just have queued an accept/auth/encryption reply.
|
|
// Send it in this same service pass instead of adding a scheduler-turn
|
|
// delay to the controller's security timeout.
|
|
Hci::ProcessPendingCommands();
|
|
Drivers::Audio::Mixer::OnBluetoothWritable();
|
|
int requestedVolume;
|
|
if (A2dp::ConsumeVolumeRequest(&requestedVolume))
|
|
Drivers::Audio::Mixer::SetMasterVolume(requestedVolume);
|
|
if (A2dp::ConsumeRouteChange())
|
|
Drivers::Audio::Mixer::OnBluetoothStateChanged();
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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.
|
|
// A fixed 300 ms pause was merely hopeful and could change the identity
|
|
// under a still-live encrypted link.
|
|
if (droppedLink) {
|
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
|
bool anyActive = true;
|
|
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
|
|
Xhci::PollEvents();
|
|
Hci::DrainEvents();
|
|
anyActive = false;
|
|
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
|
auto* conn = Hci::GetConnectionByIndex(i);
|
|
if (conn && conn->Active) { anyActive = true; break; }
|
|
}
|
|
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
|
}
|
|
if (anyActive) return false;
|
|
}
|
|
|
|
// Program the new address. Do NOT issue an HCI Reset afterwards: the
|
|
// 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)
|
|
uint32_t durationUnits = timeoutMs / 1280;
|
|
if (durationUnits < 1) durationUnits = 1;
|
|
if (durationUnits > 30) durationUnits = 30;
|
|
uint8_t duration = (uint8_t)durationUnits;
|
|
|
|
if (!Hci::StartInquiry(duration)) return -1;
|
|
|
|
// 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()) {
|
|
// A failed cancel must not be hidden: Create Connection while the
|
|
// controller is still in Inquiry is commonly rejected as Command
|
|
// Disallowed. Give a command that briefly lost HCI ownership one
|
|
// retry, continuing to service the completion event in between.
|
|
if (!Hci::CancelInquiry()) {
|
|
uint64_t cancelStart = Timekeeping::GetMilliseconds();
|
|
while (Hci::IsInquiryActive()
|
|
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
|
|
Xhci::PollEvents();
|
|
Hci::DrainEvents();
|
|
for (int j = 0; j < 100; j++)
|
|
asm volatile("pause" ::: "memory");
|
|
}
|
|
if (Hci::IsInquiryActive() && !Hci::CancelInquiry()) return -1;
|
|
}
|
|
}
|
|
|
|
return Hci::GetInquiryResults(buf, maxCount);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Connect — initiate ACL connection
|
|
// =========================================================================
|
|
|
|
static bool SameAddress(const uint8_t* a, const uint8_t* b) {
|
|
if (!a || !b) return false;
|
|
for (int i = 0; i < 6; i++) if (a[i] != b[i]) return false;
|
|
return true;
|
|
}
|
|
|
|
static Hci::ConnectionInfo* FindAclConnection(const uint8_t* bdAddr) {
|
|
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
|
auto* conn = Hci::GetConnectionByIndex(i);
|
|
if (conn && conn->Active && conn->LinkType == 0x01
|
|
&& SameAddress(conn->BdAddr, bdAddr)) return conn;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int Connect(const uint8_t* bdAddr, uint32_t timeoutMs) {
|
|
if (!g_initialized || !bdAddr) return -1;
|
|
|
|
// A previous attempt can leave a healthy encrypted ACL link with A2DP
|
|
// incomplete. Treat another click as an A2DP repair attempt on that
|
|
// link; issuing HCI Create Connection again just returns "connection
|
|
// already exists" and made manual recovery impossible.
|
|
Hci::ConnectionInfo* target = FindAclConnection(bdAddr);
|
|
bool reusedAcl = target != nullptr;
|
|
|
|
if (!target && !Hci::CreateConnection(bdAddr)) return -1;
|
|
|
|
// Wait for Connection Complete event
|
|
uint64_t start = Timekeeping::GetMilliseconds();
|
|
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
|
Xhci::PollEvents();
|
|
Hci::DrainEvents();
|
|
target = FindAclConnection(bdAddr);
|
|
for (int j = 0; j < 200; j++) {
|
|
asm volatile("pause" ::: "memory");
|
|
}
|
|
}
|
|
if (!target) return -1;
|
|
|
|
// Connection Complete queues authentication for both incoming and
|
|
// outgoing ACLs. Deliver that common request and wait for encryption;
|
|
// a manual A2DP repair on an encrypted ACL skips this entire wait.
|
|
if (!target->Encrypted) {
|
|
// A link that predates this syscall may have exhausted or missed its
|
|
// earlier security attempt; explicitly restart it. A freshly-created
|
|
// link already has the request queued by Connection Complete.
|
|
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
|
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
|
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
|
Xhci::PollEvents();
|
|
Hci::DrainEvents();
|
|
Hci::ProcessPendingCommands();
|
|
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
|
|
if (target->Encrypted) break;
|
|
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
|
}
|
|
}
|
|
|
|
bool targetAlive = target->Active
|
|
&& SameAddress(target->BdAddr, bdAddr);
|
|
bool a2dpReady = false;
|
|
if (targetAlive) {
|
|
a2dpReady = A2dp::StartSource();
|
|
Drivers::Audio::Mixer::OnBluetoothStateChanged();
|
|
}
|
|
|
|
// Persist any new link key now (process context), even if the link later
|
|
// dropped, so disk I/O never stalls the nested pairing event handler.
|
|
Hci::FlushLinkKeys();
|
|
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) return -1;
|
|
if (!a2dpReady) {
|
|
KernelLogStream(WARNING, "BT")
|
|
<< "ACL connected but A2DP source setup failed";
|
|
return -2;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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) {
|
|
if (SameAddress(conn->BdAddr, bdAddr)) {
|
|
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;
|
|
}
|
|
|
|
}
|