Files
tactility/Tactility/Source/bluetooth/Bluetooth.cpp
T
Adolfo Reyna 80bd1c9f20 fix(bluetooth): auto-reconnect to paired devices after reboot
- Auto-set enableOnBoot=true when BT is enabled or device is paired,
  so board restarts with BT on and can reconnect
- Make RADIO_STATE_ON handler non-exclusive: scan for HID Host,
  start HID Device, SPP and MIDI all independently (was else-if)
- Improve HID Host auto-connect for RPA: direct addr match, name-match
  fallback, and direct connect to stored identity using resolving list
- Persist addrType in PairedDevice file for more reliable reconnection
- Add verbose logging for loadAll/save to debug missing files

Fixes issue where board restart did not reconnect to previously
connected BT devices. Also preserves f94cc160 fullscreen flag feature.
2026-07-19 19:31:57 -04:00

496 lines
18 KiB
C++

#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/bluetooth/BluetoothPairedDevice.h>
#include <Tactility/bluetooth/BluetoothSettings.h>
#include <Tactility/bluetooth/BluetoothPrivate.h>
#include <Tactility/Mutex.h>
#include <Tactility/Tactility.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/bluetooth.h>
#include <tactility/drivers/bluetooth_hid_device.h>
#include <tactility/drivers/bluetooth_midi.h>
#include <tactility/drivers/bluetooth_serial.h>
#include <tactility/log.h>
#include <array>
#include <cstring>
#include <vector>
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<PeerRecord> scan_results_cache;
struct CachedAddr {
uint8_t addr[6];
uint8_t addr_type;
};
static std::vector<CachedAddr> 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<Device**>(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<uint8_t, 6> 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<uint8_t, 6> 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<uint8_t, 6> 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<PeerRecord> getScanResults() {
auto lock = scan_cache_mutex.asScopedLock();
lock.lock();
return scan_results_cache;
}
std::vector<PeerRecord> getPairedPeers() {
auto stored = settings::loadAll();
std::vector<PeerRecord> result;
result.reserve(stored.size());
std::array<uint8_t, 6> 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<uint8_t, 6>& /*addr*/) {
// Pairing is handled automatically during connection by NimBLE SM.
}
void unpair(const std::array<uint8_t, 6>& 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<uint8_t, 6>& 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<uint8_t, 6>& 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