/* * 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 #include #include #include #include #include #include #include #include #include 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 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; } }