fix(bluetooth): robust HID host autoconnect and combo keyboard parser
- Scan: store nameless directed adv to keep addr_type cache for Fosmon-type keyboards that wake without name -> fixes auto-connect on boot not finding device - HID parser: strip Report ID, support standard 3/4-byte mouse [btn,x8,y8,wheel] instead of mis-detecting as 12-bit packed; heuristic only when std small - HID reconnect: try PUBLIC then RANDOM addr_type with 8s timeout, periodic retry timer (3/10/30s backoff) that exists at boot via ensureAutoConnTimer, direct connect even without advert after empty scans to catch wake-on-keypress - Verified on waveshare-esp32-s3-rlcd /dev/cu.usbmodem1101 - Fosmon mini B00BX0YKX4 combo now moves smoothly and reconnects after board restart Co-authored-by: Hermes Agent <hermes@noreply>
This commit is contained in:
@@ -82,12 +82,20 @@ int ble_gap_disc_event_handler(struct ble_gap_event* event, void* arg) {
|
|||||||
memcpy(record.name, ctx->scan_results[i].name, BT_NAME_MAX + 1);
|
memcpy(record.name, ctx->scan_results[i].name, BT_NAME_MAX + 1);
|
||||||
}
|
}
|
||||||
ctx->scan_results[i].rssi = record.rssi;
|
ctx->scan_results[i].rssi = record.rssi;
|
||||||
|
// Always keep addr_type up to date (RPA can rotate)
|
||||||
|
ctx->scan_addrs[i] = disc.addr;
|
||||||
found = true;
|
found = true;
|
||||||
should_publish = (record.name[0] != '\0');
|
should_publish = (record.name[0] != '\0');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found && record.name[0] != '\0' && ctx->scan_count < 64) {
|
if (!found && ctx->scan_count < 64) {
|
||||||
|
// Store ALL unique advertisers, not only those with names.
|
||||||
|
// This is required for auto-connect: bonded keyboards (e.g. Fosmon
|
||||||
|
// mini keyboard/touchpad combo) may advertise with only Appearance
|
||||||
|
// + flags after waking from sleep, or use RPA without a name.
|
||||||
|
// UI can still filter for display, but cache must contain them so
|
||||||
|
// addr_type is available for ble_gap_connect().
|
||||||
ctx->scan_results[ctx->scan_count] = record;
|
ctx->scan_results[ctx->scan_count] = record;
|
||||||
ctx->scan_addrs[ctx->scan_count] = disc.addr; // full addr (type+val)
|
ctx->scan_addrs[ctx->scan_count] = disc.addr; // full addr (type+val)
|
||||||
ctx->scan_count++;
|
ctx->scan_count++;
|
||||||
|
|||||||
@@ -145,11 +145,30 @@ static void hidHostKeyboardReadCb(lv_indev_t* /*indev*/, lv_indev_data_t* data)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void hidHostHandleKeyboardReport(const uint8_t* data, uint16_t len) {
|
static void hidHostHandleKeyboardReportInternal(const uint8_t* data, uint16_t len, uint8_t expected_report_id);
|
||||||
|
static void hidHostHandleMouseReportInternal(const uint8_t* data, uint16_t len, uint8_t expected_report_id);
|
||||||
|
static void hidHostHandleKeyboardReport(const uint8_t* data, uint16_t len);
|
||||||
|
static void hidHostHandleMouseReport(const uint8_t* data, uint16_t len);
|
||||||
|
|
||||||
|
// ---- Early impls so other funcs can call them ----
|
||||||
|
|
||||||
|
static void hidHostHandleKeyboardReportInternal(const uint8_t* data, uint16_t len, uint8_t expected_report_id) {
|
||||||
if (len < 3 || !hid_host_key_queue) return;
|
if (len < 3 || !hid_host_key_queue) return;
|
||||||
uint8_t mod = data[0];
|
|
||||||
const uint8_t* curr = &data[2];
|
// Strip leading Report ID if present (some HOGP combo devices prepend it)
|
||||||
int nkeys = std::min((int)(len - 2), 6);
|
size_t offset = 0;
|
||||||
|
if (expected_report_id != 0 && len >= 1 && data[0] == expected_report_id) {
|
||||||
|
offset = 1;
|
||||||
|
if (len - offset < 3) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* body = data + offset;
|
||||||
|
uint16_t body_len = len - offset;
|
||||||
|
|
||||||
|
uint8_t mod = body[0];
|
||||||
|
const uint8_t* curr = (body_len >= 2) ? &body[2] : body;
|
||||||
|
int curr_offset = (body_len >= 2) ? 2 : 0;
|
||||||
|
int nkeys = std::min((int)(body_len - curr_offset), 6);
|
||||||
|
|
||||||
for (int i = 0; i < 6; i++) {
|
for (int i = 0; i < 6; i++) {
|
||||||
uint8_t kc = hid_host_prev_keys[i];
|
uint8_t kc = hid_host_prev_keys[i];
|
||||||
@@ -175,6 +194,10 @@ static void hidHostHandleKeyboardReport(const uint8_t* data, uint16_t len) {
|
|||||||
if (nkeys < 6) std::memset(hid_host_prev_keys + nkeys, 0, 6 - nkeys);
|
if (nkeys < 6) std::memset(hid_host_prev_keys + nkeys, 0, 6 - nkeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void hidHostHandleKeyboardReport(const uint8_t* data, uint16_t len) {
|
||||||
|
hidHostHandleKeyboardReportInternal(data, len, 0);
|
||||||
|
}
|
||||||
|
|
||||||
static void hidHostMouseReadCb(lv_indev_t* /*indev*/, lv_indev_data_t* data) {
|
static void hidHostMouseReadCb(lv_indev_t* /*indev*/, lv_indev_data_t* data) {
|
||||||
int32_t cx = hid_host_mouse_x.load();
|
int32_t cx = hid_host_mouse_x.load();
|
||||||
int32_t cy = hid_host_mouse_y.load();
|
int32_t cy = hid_host_mouse_y.load();
|
||||||
@@ -215,29 +238,83 @@ static void hidHostMouseReadCb(lv_indev_t* /*indev*/, lv_indev_data_t* data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void hidHostHandleMouseReport(const uint8_t* data, uint16_t len) {
|
static void hidHostHandleMouseReportInternal(const uint8_t* data, uint16_t len, uint8_t expected_report_id) {
|
||||||
int32_t dx = 0;
|
int32_t dx = 0;
|
||||||
int32_t dy = 0;
|
int32_t dy = 0;
|
||||||
bool btn = false;
|
bool btn = false;
|
||||||
|
|
||||||
if (len >= 5) {
|
if (len == 0) return;
|
||||||
// 12-bit packed coordinate format (Logitech/high-res HID mice)
|
|
||||||
btn = (data[0] & 0x01) != 0;
|
|
||||||
|
|
||||||
int16_t raw_x = data[2] | ((data[3] & 0x0F) << 8);
|
// Strip Report ID prefix if present. Many combo HOGP devices (e.g. Fosmon
|
||||||
if (raw_x & 0x0800) raw_x |= 0xF000; // sign extend 12-bit to 16-bit
|
// mini KB + touchpad: https://a.co/d/0fg4vprm) use Report IDs:
|
||||||
dx = (int16_t)raw_x;
|
// ID 1 = keyboard, ID 2 = mouse/touchpad. NimBLE notifies the raw char
|
||||||
|
// value *including* the ID byte for some stacks. If first byte matches
|
||||||
|
// the expected ID and remaining length looks like a mouse report, strip it.
|
||||||
|
const uint8_t* p = data;
|
||||||
|
uint16_t plen = len;
|
||||||
|
if (expected_report_id != 0 && len >= 2 && data[0] == expected_report_id) {
|
||||||
|
p = data + 1;
|
||||||
|
plen = len - 1;
|
||||||
|
}
|
||||||
|
|
||||||
int16_t raw_y = (data[3] >> 4) | (data[4] << 4);
|
if (plen < 3) return;
|
||||||
if (raw_y & 0x0800) raw_y |= 0xF000; // sign extend 12-bit to 16-bit
|
|
||||||
dy = (int16_t)raw_y;
|
if (plen >= 5) {
|
||||||
} else if (len >= 3) {
|
// Two possible interpretations for 5-byte reports:
|
||||||
// Standard 3-byte boot protocol
|
// A) Standard extended: [buttons, x8, y8, wheel, pan]
|
||||||
btn = (data[0] & 0x01) != 0;
|
// B) Logitech 12-bit packed: [buttons, ?, x_lo, x_hi_nibble|y_lo_nibble, y_hi]
|
||||||
dx = (int8_t)data[1];
|
//
|
||||||
dy = (int8_t)data[2];
|
// Heuristic to avoid breaking cheap touchpads (Fosmon/etc) which send (A):
|
||||||
|
// - Try standard decode first: dx=(int8)p[1], dy=(int8)p[2]
|
||||||
|
// - Try 12-bit decode: raw_x = p[2] | ((p[3]&0x0F)<<8), raw_y = (p[3]>>4)|(p[4]<<4)
|
||||||
|
// If standard dx/dy are small (|dx|,|dy| <= 127 fits int8) and 12-bit result
|
||||||
|
// would imply a huge jump (abs>200) while standard is modest (<50), prefer standard.
|
||||||
|
// This keeps Logitech high-res mice working (they emit true 12-bit values where
|
||||||
|
// standard decode looks like random / large wheel bytes) while not breaking
|
||||||
|
// touchpads that emit [btn, x, y, wheel, 0].
|
||||||
|
int8_t std_dx = (int8_t)p[1];
|
||||||
|
int8_t std_dy = (int8_t)p[2];
|
||||||
|
|
||||||
|
int16_t raw_x12 = p[2] | ((p[3] & 0x0F) << 8);
|
||||||
|
if (raw_x12 & 0x0800) raw_x12 |= 0xF000;
|
||||||
|
int16_t raw_y12 = (int16_t)((p[3] >> 4) | (p[4] << 4));
|
||||||
|
if (raw_y12 & 0x0800) raw_y12 |= 0xF000;
|
||||||
|
|
||||||
|
// Detect 12-bit: if wheel byte position (p[3]) in standard view looks like
|
||||||
|
// plausible wheel (typically 0 or small) but 12-bit gives large coordinate,
|
||||||
|
// AND standard dx/dy are within typical touchpad incremental range, use standard.
|
||||||
|
bool std_small = (std::abs((int)std_dx) <= 48 && std::abs((int)std_dy) <= 48);
|
||||||
|
bool s12_large = (std::abs((int)raw_x12) > 128 || std::abs((int)raw_y12) > 128);
|
||||||
|
|
||||||
|
if (std_small && s12_large) {
|
||||||
|
// Most likely NOT 12-bit packed, it's a standard report with wheel byte
|
||||||
|
btn = (p[0] & 0x01) != 0;
|
||||||
|
dx = std_dx;
|
||||||
|
dy = std_dy;
|
||||||
|
} else if (s12_large || !std_small) {
|
||||||
|
// Could be either, but if standard dx/dy are huge maybe it's really 12-bit
|
||||||
|
// Prefer 12-bit only when standard parse looks pathological AND 12-bit looks
|
||||||
|
// like plausible incremental movement (abs <= 300)
|
||||||
|
if (std::abs((int)raw_x12) <= 300 && std::abs((int)raw_y12) <= 300 &&
|
||||||
|
(std::abs((int)std_dx) > 64 || std::abs((int)std_dy) > 64 || p[1] == 0)) {
|
||||||
|
btn = (p[0] & 0x01) != 0;
|
||||||
|
dx = (int16_t)raw_x12;
|
||||||
|
dy = (int16_t)raw_y12;
|
||||||
|
} else {
|
||||||
|
btn = (p[0] & 0x01) != 0;
|
||||||
|
dx = std_dx;
|
||||||
|
dy = std_dy;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn = (p[0] & 0x01) != 0;
|
||||||
|
dx = std_dx;
|
||||||
|
dy = std_dy;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
// 3 or 4 bytes: standard boot protocol
|
||||||
|
btn = (p[0] & 0x01) != 0;
|
||||||
|
dx = (int8_t)p[1];
|
||||||
|
dy = (int8_t)p[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_display_t* disp = lv_display_get_default();
|
lv_display_t* disp = lv_display_get_default();
|
||||||
@@ -275,6 +352,10 @@ static void hidHostHandleMouseReport(const uint8_t* data, uint16_t len) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void hidHostHandleMouseReport(const uint8_t* data, uint16_t len) {
|
||||||
|
hidHostHandleMouseReportInternal(data, len, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Asynchronous coordination helpers ----
|
// ---- Asynchronous coordination helpers ----
|
||||||
|
|
||||||
static void hidHostOnDscsDiscovered(HidHostCtx& ctx) {
|
static void hidHostOnDscsDiscovered(HidHostCtx& ctx) {
|
||||||
@@ -809,16 +890,40 @@ static int hidHostGapCb(struct ble_gap_event* event, void* /*arg*/) {
|
|||||||
os_mbuf_copydata(event->notify_rx.om, 0, len, buf);
|
os_mbuf_copydata(event->notify_rx.om, 0, len, buf);
|
||||||
for (const auto& rpt : ctx.inputRpts) {
|
for (const auto& rpt : ctx.inputRpts) {
|
||||||
if (rpt.valHandle != event->notify_rx.attr_handle) continue;
|
if (rpt.valHandle != event->notify_rx.attr_handle) continue;
|
||||||
|
// Pass reportId so handlers can strip leading ID byte when needed
|
||||||
|
// (combo keyboard/mouse devices like Fosmon B00BX0YKX4 often include ID)
|
||||||
switch (rpt.type) {
|
switch (rpt.type) {
|
||||||
case HidReportType::Keyboard: hidHostHandleKeyboardReport(buf, len); break;
|
case HidReportType::Keyboard:
|
||||||
case HidReportType::Mouse: hidHostHandleMouseReport(buf, len); break;
|
hidHostHandleKeyboardReportInternal(buf, len, rpt.reportId);
|
||||||
|
break;
|
||||||
|
case HidReportType::Mouse:
|
||||||
|
hidHostHandleMouseReportInternal(buf, len, rpt.reportId);
|
||||||
|
break;
|
||||||
case HidReportType::Consumer:
|
case HidReportType::Consumer:
|
||||||
LOGGER.info("Consumer report len={}", len);
|
LOGGER.info("Consumer report len={}", len);
|
||||||
break;
|
break;
|
||||||
case HidReportType::Unknown:
|
case HidReportType::Unknown: {
|
||||||
if (len >= 6) hidHostHandleKeyboardReport(buf, len);
|
// Unknown type: use length + heuristic. For combo devices
|
||||||
else if (len >= 3) hidHostHandleMouseReport(buf, len);
|
// where reportId identifies type, trust reportId if non-zero.
|
||||||
|
// Also trust buffer length: keyboard is typically 8 bytes
|
||||||
|
// (mod+reserved+6keys) possibly with ID -> 9.
|
||||||
|
// Mouse is 3-5 bytes.
|
||||||
|
if (rpt.reportId != 0) {
|
||||||
|
// If we have a Report Map type parsed later, we may not yet
|
||||||
|
// know type here. Make educated guess from len excluding ID.
|
||||||
|
uint16_t effective_len = len;
|
||||||
|
if (len >= 1 && buf[0] == rpt.reportId) effective_len = len - 1;
|
||||||
|
if (effective_len >= 8 || effective_len == 6) {
|
||||||
|
hidHostHandleKeyboardReportInternal(buf, len, rpt.reportId);
|
||||||
|
} else if (effective_len >= 3) {
|
||||||
|
hidHostHandleMouseReportInternal(buf, len, rpt.reportId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (len >= 6) hidHostHandleKeyboardReport(buf, len);
|
||||||
|
else if (len >= 3) hidHostHandleMouseReport(buf, len);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -834,6 +939,39 @@ static int hidHostGapCb(struct ble_gap_event* event, void* /*arg*/) {
|
|||||||
|
|
||||||
// ---- Public functions ----
|
// ---- Public functions ----
|
||||||
|
|
||||||
|
static esp_timer_handle_t hid_autoconn_retry_timer = nullptr;
|
||||||
|
static int hid_autoconn_empty_scan_count = 0;
|
||||||
|
static constexpr int HID_AUTOCONN_DIRECT_AFTER = 1; // after N empty scans, try direct connect without seeing advert
|
||||||
|
static constexpr int HID_AUTOCONN_MAX_FAST = 20; // ~60s @ 3s interval
|
||||||
|
static constexpr int HID_AUTOCONN_MAX_TOTAL = 100; // then backoff longer but keep trying
|
||||||
|
|
||||||
|
static void ensureAutoConnTimer() {
|
||||||
|
if (hid_autoconn_retry_timer != nullptr) return;
|
||||||
|
esp_timer_create_args_t args = {};
|
||||||
|
args.callback = [](void* /*arg*/) {
|
||||||
|
LOGGER.info("Auto-connect retry timer fired (empty_scans={})", hid_autoconn_empty_scan_count);
|
||||||
|
if (hidHostIsConnected()) {
|
||||||
|
hid_autoconn_empty_scan_count = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If not already scanning, start a scan; autoConnectHidHost() will be called
|
||||||
|
// on SCAN_FINISHED via bt_event_bridge.
|
||||||
|
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||||
|
if (!bluetooth_is_scanning(dev)) {
|
||||||
|
bluetooth_scan_start(dev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keep periodic retry even if we are scanning - as fallback
|
||||||
|
// The timer will be re-armed in autoConnectHidHost()
|
||||||
|
};
|
||||||
|
args.dispatch_method = ESP_TIMER_TASK;
|
||||||
|
args.name = "hid_autoconn_retry";
|
||||||
|
if (esp_timer_create(&args, &hid_autoconn_retry_timer) != ESP_OK) {
|
||||||
|
LOGGER.error("Failed to create hid_autoconn_retry timer");
|
||||||
|
hid_autoconn_retry_timer = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
||||||
if (getRadioState() != RadioState::On) {
|
if (getRadioState() != RadioState::On) {
|
||||||
LOGGER.warn("hidHostConnect: radio not on");
|
LOGGER.warn("hidHostConnect: radio not on");
|
||||||
@@ -852,7 +990,7 @@ void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
|||||||
hid_host_ctx = std::make_unique<HidHostCtx>();
|
hid_host_ctx = std::make_unique<HidHostCtx>();
|
||||||
hid_host_ctx->peerAddr = addr;
|
hid_host_ctx->peerAddr = addr;
|
||||||
|
|
||||||
// Create enc retry timer lazily
|
// Create timers lazily
|
||||||
if (hid_enc_retry_timer == nullptr) {
|
if (hid_enc_retry_timer == nullptr) {
|
||||||
esp_timer_create_args_t args = {};
|
esp_timer_create_args_t args = {};
|
||||||
args.callback = hidEncRetryTimerCb;
|
args.callback = hidEncRetryTimerCb;
|
||||||
@@ -863,17 +1001,26 @@ void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
|||||||
hid_enc_retry_timer = nullptr;
|
hid_enc_retry_timer = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ensureAutoConnTimer();
|
||||||
|
// Cancel any pending auto-connect retry (we are connecting now)
|
||||||
|
if (hid_autoconn_retry_timer) esp_timer_stop(hid_autoconn_retry_timer);
|
||||||
|
hid_autoconn_empty_scan_count = 0;
|
||||||
|
|
||||||
// Notify driver that a HID host central connection is starting.
|
// Notify driver that a HID host central connection is starting.
|
||||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) bluetooth_set_hid_host_active(dev, true);
|
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) bluetooth_set_hid_host_active(dev, true);
|
||||||
|
|
||||||
// Look up the addr_type from the cached scan results.
|
// Look up the addr_type from the cached scan results. Bonded devices may
|
||||||
|
// advertise with RPA (random) or public; cache now includes nameless adverts
|
||||||
|
// so we usually have it. Fall back to trying both types.
|
||||||
ble_addr_t ble_addr = {};
|
ble_addr_t ble_addr = {};
|
||||||
ble_addr.type = BLE_ADDR_PUBLIC;
|
ble_addr.type = BLE_ADDR_PUBLIC;
|
||||||
std::memcpy(ble_addr.val, addr.data(), 6);
|
std::memcpy(ble_addr.val, addr.data(), 6);
|
||||||
uint8_t addr_type = 0;
|
uint8_t addr_type = 0;
|
||||||
if (getCachedScanAddrType(addr.data(), &addr_type)) {
|
if (getCachedScanAddrType(addr.data(), &addr_type)) {
|
||||||
ble_addr.type = addr_type;
|
ble_addr.type = addr_type;
|
||||||
|
LOGGER.info("hidHostConnect: using addr_type={} from scan cache", addr_type);
|
||||||
|
} else {
|
||||||
|
LOGGER.info("hidHostConnect: addr_type unknown, will try PUBLIC then RANDOM");
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t own_addr_type;
|
uint8_t own_addr_type;
|
||||||
@@ -881,7 +1028,23 @@ void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
|||||||
own_addr_type = BLE_OWN_ADDR_PUBLIC;
|
own_addr_type = BLE_OWN_ADDR_PUBLIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
int rc = ble_gap_connect(own_addr_type, &ble_addr, 5000, nullptr, hidHostGapCb, nullptr);
|
int rc = ble_gap_connect(own_addr_type, &ble_addr, 8000, nullptr, hidHostGapCb, nullptr);
|
||||||
|
if (rc != 0 && ble_addr.type == BLE_ADDR_PUBLIC) {
|
||||||
|
// Common fail: device actually uses RANDOM (RPA). Retry with RANDOM.
|
||||||
|
LOGGER.info("ble_gap_connect PUBLIC failed rc={}, retrying RANDOM", rc);
|
||||||
|
ble_addr.type = BLE_ADDR_RANDOM;
|
||||||
|
rc = ble_gap_connect(own_addr_type, &ble_addr, 8000, nullptr, hidHostGapCb, nullptr);
|
||||||
|
}
|
||||||
|
if (rc != 0 && ble_addr.type == BLE_ADDR_RANDOM) {
|
||||||
|
// Try PUBLIC again if RANDOM was first (or both failed, we already tried public)
|
||||||
|
// Only if we originally had a cached RANDOM type, try PUBLIC now.
|
||||||
|
if (addr_type == BLE_ADDR_RANDOM) {
|
||||||
|
LOGGER.info("ble_gap_connect RANDOM failed rc={}, retrying PUBLIC", rc);
|
||||||
|
ble_addr.type = BLE_ADDR_PUBLIC;
|
||||||
|
rc = ble_gap_connect(own_addr_type, &ble_addr, 8000, nullptr, hidHostGapCb, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
LOGGER.warn("ble_gap_connect failed rc={}", rc);
|
LOGGER.warn("ble_gap_connect failed rc={}", rc);
|
||||||
hid_host_ctx.reset();
|
hid_host_ctx.reset();
|
||||||
@@ -895,7 +1058,7 @@ void hidHostConnect(const std::array<uint8_t, 6>& addr) {
|
|||||||
bluetooth_fire_event(dev, e);
|
bluetooth_fire_event(dev, e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOGGER.info("Connecting...");
|
LOGGER.info("Connecting... addr_type={}", (int)ble_addr.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,34 +1083,84 @@ bool hidHostGetConnectedPeer(std::array<uint8_t, 6>& addr_out) {
|
|||||||
void autoConnectHidHost() {
|
void autoConnectHidHost() {
|
||||||
if (hidHostIsConnected()) return;
|
if (hidHostIsConnected()) return;
|
||||||
|
|
||||||
// Connect to the first saved HID host peer that appeared in the last scan.
|
ensureAutoConnTimer();
|
||||||
// cacheScanAddr() is populated during scanning so addr_type is available for ble_gap_connect.
|
|
||||||
|
// Phase 1: connect if we saw it in last scan (preferred: we have fresh addr_type + RSSI)
|
||||||
auto scan = getScanResults();
|
auto scan = getScanResults();
|
||||||
for (const auto& r : scan) {
|
for (const auto& r : scan) {
|
||||||
settings::PairedDevice stored;
|
settings::PairedDevice stored;
|
||||||
if (settings::load(settings::addrToHex(r.addr), stored) &&
|
if (settings::load(settings::addrToHex(r.addr), stored) &&
|
||||||
stored.autoConnect &&
|
stored.autoConnect &&
|
||||||
stored.profileId == BT_PROFILE_HID_HOST) {
|
stored.profileId == BT_PROFILE_HID_HOST) {
|
||||||
LOGGER.info("Auto-connecting HID host to {}", settings::addrToHex(r.addr));
|
LOGGER.info("Auto-connecting HID host to {} (name='{}' rssi={}) [from scan]",
|
||||||
|
settings::addrToHex(r.addr), r.name, (int)r.rssi);
|
||||||
hidHostConnect(r.addr);
|
hidHostConnect(r.addr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device not in the last scan. If we have an autoConnect HID host peer, restart
|
// Phase 2: not in scan. Keep trying.
|
||||||
// scanning so we keep checking until the device powers back on.
|
// This handles boot timing: keyboard may be off, asleep, or using RPA that we
|
||||||
|
// haven't cached yet. We need to:
|
||||||
|
// a) keep scanning periodically so we eventually see wake-up adv
|
||||||
|
// b) after a few empty scans, try DIRECT connect to bonded address — NimBLE
|
||||||
|
// can often connect via directed connection even if not currently advertising,
|
||||||
|
// using the bond store, or it will start scanning internally.
|
||||||
auto peers = settings::loadAll();
|
auto peers = settings::loadAll();
|
||||||
|
bool found_auto_peer = false;
|
||||||
|
std::array<uint8_t, 6> direct_addr = {};
|
||||||
for (const auto& peer : peers) {
|
for (const auto& peer : peers) {
|
||||||
if (peer.autoConnect && peer.profileId == BT_PROFILE_HID_HOST) {
|
if (peer.autoConnect && peer.profileId == BT_PROFILE_HID_HOST) {
|
||||||
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
found_auto_peer = true;
|
||||||
if (!bluetooth_is_scanning(dev)) {
|
direct_addr = peer.addr;
|
||||||
LOGGER.info("Auto-connect HID host: device not in scan, retrying scan");
|
break; // take first
|
||||||
bluetooth_scan_start(dev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!found_auto_peer) {
|
||||||
|
// No autoConnect peers at all — stop retry timer
|
||||||
|
if (hid_autoconn_retry_timer) esp_timer_stop(hid_autoconn_retry_timer);
|
||||||
|
hid_autoconn_empty_scan_count = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hid_autoconn_empty_scan_count++;
|
||||||
|
|
||||||
|
if (hid_autoconn_empty_scan_count >= HID_AUTOCONN_DIRECT_AFTER) {
|
||||||
|
// Try direct connect to saved peer, even though it wasn't in scan.
|
||||||
|
// hidHostConnect() will attempt both PUBLIC and RANDOM addr types.
|
||||||
|
// This is crucial for after reboot where keyboard may be bonded but not
|
||||||
|
// currently advertising in our 5s window, or cache was cleared on SCAN_STARTED.
|
||||||
|
// We only attempt direct every few scans to avoid tight connect loops.
|
||||||
|
if (hid_autoconn_empty_scan_count % 2 == 1 || hid_autoconn_empty_scan_count <= HID_AUTOCONN_DIRECT_AFTER+1) {
|
||||||
|
LOGGER.info("Auto-connect HID host: device not seen after {} scans, trying direct to {}",
|
||||||
|
hid_autoconn_empty_scan_count, settings::addrToHex(direct_addr));
|
||||||
|
hidHostConnect(direct_addr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not yet at direct-connect threshold, or direct failed last time — keep scanning
|
||||||
|
if (Device* dev = device_find_first_active_by_type(&BLUETOOTH_TYPE)) {
|
||||||
|
if (!bluetooth_is_scanning(dev)) {
|
||||||
|
LOGGER.info("Auto-connect HID host: device not in scan (attempt {}/{}), retrying scan",
|
||||||
|
hid_autoconn_empty_scan_count, HID_AUTOCONN_DIRECT_AFTER);
|
||||||
|
bluetooth_scan_start(dev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule next retry
|
||||||
|
uint64_t delay_us = 3 * 1000 * 1000ULL; // 3s fast phase
|
||||||
|
if (hid_autoconn_empty_scan_count >= HID_AUTOCONN_MAX_FAST) {
|
||||||
|
delay_us = 10 * 1000 * 1000ULL; // 10s after 60s
|
||||||
|
if (hid_autoconn_empty_scan_count >= HID_AUTOCONN_MAX_TOTAL) {
|
||||||
|
delay_us = 30 * 1000 * 1000ULL; // 30s after 5min, keep trying forever but slow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hid_autoconn_retry_timer) {
|
||||||
|
esp_timer_stop(hid_autoconn_retry_timer);
|
||||||
|
esp_timer_start_once(hid_autoconn_retry_timer, delay_us);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace tt::bluetooth
|
} // namespace tt::bluetooth
|
||||||
|
|||||||
Reference in New Issue
Block a user