#ifdef ESP_PLATFORM #include #endif #if defined(CONFIG_BT_NIMBLE_ENABLED) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace tt::bluetooth { constexpr auto* TAG = "Bluetooth"; // ---- Scan result cache (C++ PeerRecord list, updated from BT_EVENT_PEER_FOUND) ---- static Mutex scan_cache_mutex; static std::vector scan_results_cache; struct CachedAddr { uint8_t addr[6]; uint8_t addr_type; }; static std::vector scan_addr_cache; // parallel to scan_results_cache // ---- Device accessor ---- Device* findFirstRegisteredDevice() { Device* found = nullptr; device_for_each_of_type(&BLUETOOTH_TYPE, &found, [](Device* dev, void* ctx) -> bool { *static_cast(ctx) = dev; return true; }); return found; } // ---- Scan cache helpers ---- void cacheScanAddr(const uint8_t addr[6], uint8_t addr_type) { auto lock = scan_cache_mutex.asScopedLock(); lock.lock(); for (auto& entry : scan_addr_cache) { if (memcmp(entry.addr, addr, 6) == 0) { entry.addr_type = addr_type; return; } } CachedAddr e = {}; memcpy(e.addr, addr, 6); e.addr_type = addr_type; scan_addr_cache.push_back(e); } bool getCachedScanAddrType(const uint8_t addr[6], uint8_t* addr_type_out) { auto lock = scan_cache_mutex.asScopedLock(); lock.lock(); for (const auto& entry : scan_addr_cache) { if (memcmp(entry.addr, addr, 6) == 0) { if (addr_type_out) *addr_type_out = entry.addr_type; return true; } } if (addr_type_out) *addr_type_out = 0; return false; } static void cachePeerRecord(const BtPeerRecord& krecord) { PeerRecord rec; memcpy(rec.addr.data(), krecord.addr, 6); rec.name = krecord.name[0] != '\0' ? krecord.name : ""; rec.rssi = krecord.rssi; rec.paired = krecord.paired; rec.connected = krecord.connected; rec.profileId = 0; cacheScanAddr(krecord.addr, krecord.addr_type); auto lock = scan_cache_mutex.asScopedLock(); lock.lock(); for (auto& existing : scan_results_cache) { if (existing.addr == rec.addr) { if (!rec.name.empty()) existing.name = rec.name; existing.rssi = rec.rssi; return; } } scan_results_cache.push_back(std::move(rec)); } // ---- Bridge callback (registered with kernel driver) ---- // This callback listens to platform driver events to perform auto-start logic // and settings management. Consumers should register their own callbacks via // bluetooth_add_event_callback() to receive events directly. static void bt_event_bridge(Device*, void* /*context*/, BtEvent event) { switch (event.type) { case BT_EVENT_RADIO_STATE_CHANGED: switch (event.radio_state) { case BT_RADIO_STATE_ON: getMainDispatcher().dispatch([] { auto peers = settings::loadAll(); LOG_I(TAG, "RADIO ON: loaded %d paired peers", (int)peers.size()); for (const auto& p : peers) { LOG_I(TAG, " - peer %s name='%s' profile=%d auto=%d type=%d", settings::addrToHex(p.addr).c_str(), p.name.c_str(), p.profileId, (int)p.autoConnect, (int)p.addrType); } bool has_hid_host_auto = false; bool has_hid_device_auto = false; for (const auto& p : peers) { if (!p.autoConnect) continue; if (p.profileId == BT_PROFILE_HID_HOST) has_hid_host_auto = true; if (p.profileId == BT_PROFILE_HID_DEVICE) has_hid_device_auto = true; } LOG_I(TAG, "RADIO ON: has_hid_host_auto=%d has_hid_device_auto=%d spp=%d midi=%d", (int)has_hid_host_auto, (int)has_hid_device_auto, (int)settings::shouldSppAutoStart(), (int)settings::shouldMidiAutoStart()); // Start all auto-connect/auto-start roles that are configured. // Scanning (central) and advertising (peripheral) can run concurrently // on ESP32 NimBLE, so we no longer use exclusive else-if. if (has_hid_host_auto) { LOG_I(TAG, "HID host auto-connect peer found — starting scan"); if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) { bluetooth_scan_start(dev); } } if (has_hid_device_auto) { LOG_I(TAG, "HID device auto-start (bonded peer found)"); if (Device* dev = bluetooth_hid_device_get_device()) { bluetooth_hid_device_start(dev, BT_HID_DEVICE_MODE_KEYBOARD); } } // SPP and MIDI are peripheral servers; start them if their global // auto-start flags are set, regardless of HID roles. if (settings::shouldSppAutoStart()) { LOG_I(TAG, "Auto-starting SPP server"); if (Device* dev = bluetooth_serial_get_device()) { bluetooth_serial_start(dev); } } if (settings::shouldMidiAutoStart()) { LOG_I(TAG, "Auto-starting MIDI server"); if (Device* dev = bluetooth_midi_get_device()) { bluetooth_midi_start(dev); } } }); break; default: break; } break; case BT_EVENT_SCAN_STARTED: { auto lock = scan_cache_mutex.asScopedLock(); lock.lock(); scan_results_cache.clear(); scan_addr_cache.clear(); } break; case BT_EVENT_SCAN_FINISHED: getMainDispatcher().dispatch([] { autoConnectHidHost(); }); break; case BT_EVENT_PEER_FOUND: cachePeerRecord(event.peer); break; case BT_EVENT_PAIR_RESULT: if (event.pair_result.result == BT_PAIR_RESULT_SUCCESS) { uint8_t addr_buf[6]; int profile_copy = event.pair_result.profile; memcpy(addr_buf, event.pair_result.addr, 6); getMainDispatcher().dispatch([addr_buf, profile_copy]() mutable { // Ensure Bluetooth auto-enables on boot after successful pairing, // so previously connected devices reconnect after a restart. settings::setEnableOnBoot(true); std::array peer_addr; memcpy(peer_addr.data(), addr_buf, 6); const auto hex = settings::addrToHex(peer_addr); if (!settings::hasFileForDevice(hex)) { settings::PairedDevice dev; dev.addr = peer_addr; dev.name = ""; dev.autoConnect = true; dev.profileId = profile_copy; // Try to preserve addrType from scan cache if available uint8_t cached_type = 0; if (getCachedScanAddrType(peer_addr.data(), &cached_type)) { dev.addrType = cached_type; } else { dev.addrType = 0; } if (settings::save(dev)) { LOG_I(TAG, "Saved paired peer %s (profile=%d)", hex.c_str(), profile_copy); } } }); } else if (event.pair_result.result == BT_PAIR_RESULT_BOND_LOST) { uint8_t addr_buf[6]; memcpy(addr_buf, event.pair_result.addr, 6); getMainDispatcher().dispatch([addr_buf]() mutable { std::array peer_addr; memcpy(peer_addr.data(), addr_buf, 6); settings::remove(settings::addrToHex(peer_addr)); }); } break; case BT_EVENT_PROFILE_STATE_CHANGED: if (event.profile_state.state == BT_PROFILE_STATE_CONNECTED) { uint8_t addr_buf[6]; int profile_copy = (int)event.profile_state.profile; memcpy(addr_buf, event.profile_state.addr, 6); getMainDispatcher().dispatch([addr_buf, profile_copy]() mutable { std::array peer_addr; memcpy(peer_addr.data(), addr_buf, 6); const auto hex = settings::addrToHex(peer_addr); settings::PairedDevice stored; if (settings::load(hex, stored) && stored.profileId != profile_copy) { stored.profileId = profile_copy; settings::save(stored); } }); // TODO: Fix auto reconnect if user manually disconnects } else if (event.profile_state.state == BT_PROFILE_STATE_IDLE && event.profile_state.profile == BT_PROFILE_HID_HOST) { // HID host disconnected — check if any peer has autoConnect and re-scan // so that autoConnectHidHost() fires when the scan finishes. getMainDispatcher().dispatch([] { auto peers = settings::loadAll(); bool has_auto = false; for (const auto& p : peers) { if (p.autoConnect && p.profileId == BT_PROFILE_HID_HOST) { has_auto = true; break; } } if (has_auto) { if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) { if (!bluetooth_is_scanning(dev)) { bluetooth_scan_start(dev); } } } }); } break; default: break; } } // ---- systemStart ---- void systemStart() { Device* dev = findFirstRegisteredDevice(); if (dev == nullptr) { LOG_W(TAG, "systemStart: no BLE device found"); return; } if (settings::shouldEnableOnBoot()) { start(dev); } } bool isRadioOnOrPending(Device* dev) { if (!device_is_ready(dev)) return false; BtRadioState state; if (bluetooth_get_radio_state(dev, &state) != ERROR_NONE) return false; return state == BT_RADIO_STATE_ON || state == BT_RADIO_STATE_ON_PENDING; } bool start(Device* dev) { LOG_I(TAG, "Auto-enabling BLE on boot"); if (!device_is_ready(dev)) { LOG_I(TAG, "Starting BLE device"); if (device_start(dev) != ERROR_NONE) { LOG_E(TAG, "Failed to start BLE device"); return false; } } // TODO: Fix bug where repeatedly calling start would add this callback multiple times if (bluetooth_add_event_callback(dev, nullptr, bt_event_bridge) != ERROR_NONE) { LOG_E(TAG, "Failed to set BLE callback"); } LOG_I(TAG, "Enabling BT radio"); if (bluetooth_set_radio_enabled(dev, true) != ERROR_NONE) { LOG_E(TAG, "Failed to enable BLE radio"); // Add bridge again bluetooth_remove_event_callback(dev, bt_event_bridge); return false; } // Persist enable-on-boot so that paired devices auto-reconnect after a restart. // The settings file is written from the main task to avoid blocking the NimBLE host // task, but the dispatcher may run immediately, so we also set it here. settings::setEnableOnBoot(true); LOG_I(TAG, "BT enabled"); return true; } bool stop(Device* dev) { BtRadioState state; if (bluetooth_get_radio_state(dev, &state) != ERROR_NONE) { return false; } if (state == BT_RADIO_STATE_OFF || state == BT_RADIO_STATE_OFF_PENDING) { return true; } if (bluetooth_remove_event_callback(dev, bt_event_bridge) != ERROR_NONE) { LOG_E(TAG, "Failed to remove BLE callback"); } if (bluetooth_set_radio_enabled(dev, false) != ERROR_NONE) { LOG_E(TAG, "Failed to disable BT radio"); // Re-register bridge bluetooth_add_event_callback(dev, nullptr, bt_event_bridge); return false; } if (device_stop(dev) != ERROR_NONE) { LOG_E(TAG, "Failed to stop BT device"); return false; } return true; } // ---- Public API ---- const char* radioStateToString(RadioState state) { switch (state) { using enum RadioState; case Off: return "Off"; case OnPending: return "OnPending"; case On: return "On"; case OffPending: return "OffPending"; } check(false, "not implemented"); } RadioState getRadioState() { Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE); if (dev == nullptr) return RadioState::Off; BtRadioState state = BT_RADIO_STATE_OFF; bluetooth_get_radio_state(dev, &state); switch (state) { case BT_RADIO_STATE_OFF: return RadioState::Off; case BT_RADIO_STATE_ON_PENDING: return RadioState::OnPending; case BT_RADIO_STATE_ON: return RadioState::On; case BT_RADIO_STATE_OFF_PENDING: return RadioState::OffPending; } return RadioState::Off; } std::vector getScanResults() { auto lock = scan_cache_mutex.asScopedLock(); lock.lock(); return scan_results_cache; } std::vector getPairedPeers() { auto stored = settings::loadAll(); std::vector result; result.reserve(stored.size()); std::array connected_addr = {}; bool hid_host_connected = hidHostGetConnectedPeer(connected_addr); for (const auto& device : stored) { PeerRecord record; record.addr = device.addr; record.name = device.name; record.rssi = 0; record.paired = true; record.profileId = device.profileId; record.connected = hid_host_connected && device.addr == connected_addr; result.push_back(std::move(record)); } // Synthesize fallback: LittleFS readdir can lag behind fwrite by one tick, so the // connected peer may not appear in loadAll() yet. Always ensure it is in the list. if (hid_host_connected) { bool found = false; for (const auto& r : result) { if (r.addr == connected_addr) { found = true; break; } } if (!found) { PeerRecord record; record.addr = connected_addr; record.rssi = 0; record.paired = true; record.connected = true; record.profileId = BT_PROFILE_HID_HOST; // Try to get the name from the scan cache. { auto lock = scan_cache_mutex.asScopedLock(); lock.lock(); for (const auto& sr : scan_results_cache) { if (sr.addr == connected_addr) { record.name = sr.name; break; } } } result.push_back(std::move(record)); } } return result; } void pair(const std::array& /*addr*/) { // Pairing is handled automatically during connection by NimBLE SM. } void unpair(const std::array& addr) { Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE); if (dev != nullptr) { bluetooth_unpair(dev, addr.data()); } settings::remove(settings::addrToHex(addr)); } void connect(const std::array& addr, int profileId) { LOG_I(TAG, "connect(profile=%d)", profileId); // Ensure BT restarts in the same mode after reboot, so previously connected // devices can be re-found (scan) or reconnected to (advertising). settings::setEnableOnBoot(true); if (profileId == BT_PROFILE_HID_HOST) { hidHostConnect(addr); } else if (profileId == BT_PROFILE_HID_DEVICE) { if (Device* dev = bluetooth_hid_device_get_device()) { bluetooth_hid_device_start(dev, BT_HID_DEVICE_MODE_KEYBOARD); } } else if (profileId == BT_PROFILE_SPP) { if (Device* dev = bluetooth_serial_get_device()) { bluetooth_serial_start(dev); settings::setSppAutoStart(true); } } else if (profileId == BT_PROFILE_MIDI) { if (Device* dev = bluetooth_midi_get_device()) { bluetooth_midi_start(dev); settings::setMidiAutoStart(true); } } } void disconnect(const std::array& addr, int profileId) { LOG_I(TAG, "disconnect(profile=%d)", profileId); if (profileId == BT_PROFILE_HID_HOST) { hidHostDisconnect(); } else if (profileId == BT_PROFILE_HID_DEVICE) { if (Device* dev = bluetooth_hid_device_get_device()) { bluetooth_hid_device_stop(dev); } } else { Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE); if (dev == nullptr) return; bluetooth_disconnect(dev, addr.data(), (BtProfileId)profileId); } } bool isProfileSupported(int profileId) { return profileId == BT_PROFILE_HID_HOST || profileId == BT_PROFILE_HID_DEVICE || profileId == BT_PROFILE_SPP || profileId == BT_PROFILE_MIDI; } } // namespace tt::bluetooth #endif // CONFIG_BT_NIMBLE_ENABLED