Files
SC-F001/main/bt_hid.c
2026-03-12 20:37:04 -05:00

608 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* bt_hid.c
*
* BLE HID host for SC-F001.
*
* Architecture
* ------------
* bt_hid_scan_task (priority 4, 6 KB)
* - On boot: checks NVS for a saved BDA. If found, attempts
* esp_hidh_dev_open() directly and waits BT_HID_BOND_TIMEOUT_MS.
* If that times out (device not nearby) falls through to scan.
* - While disconnected: runs a BLE scan for BT_HID_SCAN_DURATION_S
* seconds, picks the HID device with best RSSI, opens it.
* - While connected: sleeps 500 ms between checks.
* - Every BT_HID_REPEAT_MS the held-button state is sampled and
* pulse_override() is called if a mapped key is down.
*
* hidh_callback (runs in esp_hidh event task)
* - OPEN: saves BDA to NVS, marks connected.
* - CLOSE: clears connected flag, clears held key.
* - INPUT: decodes usage code, sets s_held_key.
*
* ble_gap_event_handler (runs in BT stack task)
* - Collects HID-UUID scan results.
* - Signals scan_semaphore when scan params set / scan complete.
* - Handles pairing/auth confirmations.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_bt.h"
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_hidh.h"
#include "esp_hid_common.h"
#include "esp_timer.h"
#include "bt_hid.h"
#include "comms_events.h"
#include "control_fsm.h"
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define TAG "BT_HID"
#define BT_HID_SCAN_DURATION_S 3
#define BT_HID_MIN_RSSI (-70) /* dBm — ignore devices weaker than this */
#define BT_HID_RECONNECT_MS 2000
#define BT_HID_BOND_TIMEOUT_MS 5000 /* wait for saved device before scanning */
#define BT_HID_CONNECT_WAIT_MS 5000 /* wait after open() before next loop */
#define BT_HID_NVS_NAMESPACE "bt_hid"
#define BT_HID_NVS_BDA_KEY "bda"
// ---------------------------------------------------------------------------
// HID usage codes -> machine actions
// ---------------------------------------------------------------------------
/*
* Usage codes sent by generic BLE media remotes (HID Consumer Control page).
* Only the four we care about are mapped; everything else is ignored.
*/
#define USAGE_VOL_UP 0x00E9u /* -> jack up */
#define USAGE_VOL_DOWN 0x00EAu /* -> jack down */
#define USAGE_PREV 0x00B6u /* -> reverse */
#define USAGE_NEXT 0x00B5u /* -> forward */
#define USAGE_NONE 0x0000u
// ---------------------------------------------------------------------------
// Shared remote state
// ---------------------------------------------------------------------------
typedef enum {
REMOTE_DISCONNECTED = 0,
REMOTE_CONNECTED,
} remote_conn_state_t;
typedef struct {
remote_conn_state_t conn_state;
uint16_t held_key; /* current usage code, 0 = nothing held */
esp_bd_addr_t bda; /* address of the connected device */
} remote_state_t;
static remote_state_t s_remote = { .conn_state = REMOTE_DISCONNECTED };
static portMUX_TYPE s_mux = portMUX_INITIALIZER_UNLOCKED;
/* Semaphore signalled by GAP callback to unblock the scan task. */
static SemaphoreHandle_t s_scan_sem = NULL;
/* Set once we have a saved BDA from NVS (direct reconnect path). */
static bool s_has_saved_bda = false;
static esp_bd_addr_t s_saved_bda = {0};
static esp_ble_addr_type_t s_saved_addr_type = BLE_ADDR_TYPE_RANDOM;
/* Address type used for the most recent esp_hidh_dev_open() call.
* Set immediately before open so the OPEN callback can persist it. */
static esp_ble_addr_type_t s_connect_addr_type = BLE_ADDR_TYPE_RANDOM;
// ---------------------------------------------------------------------------
// Scan-result list (heap-allocated, freed after each scan round)
// ---------------------------------------------------------------------------
typedef struct scan_result_s {
esp_bd_addr_t bda;
esp_ble_addr_type_t addr_type;
int rssi;
char name[64];
struct scan_result_s *next;
} scan_result_t;
static scan_result_t *s_scan_results = NULL;
static size_t s_num_scan_results = 0;
static TaskHandle_t s_scan_task_handle = NULL;
static scan_result_t *find_scan_result(const esp_bd_addr_t bda)
{
scan_result_t *r = s_scan_results;
while (r) {
if (memcmp(bda, r->bda, sizeof(esp_bd_addr_t)) == 0) return r;
r = r->next;
}
return NULL;
}
static void add_scan_result(const esp_bd_addr_t bda,
esp_ble_addr_type_t addr_type,
const char *name,
int rssi)
{
if (find_scan_result(bda)) return; /* deduplicate */
scan_result_t *r = (scan_result_t *)calloc(1, sizeof(scan_result_t));
if (!r) return;
memcpy(r->bda, bda, sizeof(esp_bd_addr_t));
r->addr_type = addr_type;
r->rssi = rssi;
if (name) strncpy(r->name, name, sizeof(r->name) - 1);
r->next = s_scan_results;
s_scan_results = r;
s_num_scan_results++;
}
static void free_scan_results(void)
{
scan_result_t *r;
while (s_scan_results) {
r = s_scan_results;
s_scan_results = s_scan_results->next;
free(r);
}
s_num_scan_results = 0;
}
// ---------------------------------------------------------------------------
// NVS helpers — store / load the last-connected BDA
// ---------------------------------------------------------------------------
static void nvs_save_bda(const esp_bd_addr_t bda, esp_ble_addr_type_t addr_type)
{
nvs_handle_t h;
if (nvs_open(BT_HID_NVS_NAMESPACE, NVS_READWRITE, &h) != ESP_OK) return;
nvs_set_blob(h, BT_HID_NVS_BDA_KEY, bda, sizeof(esp_bd_addr_t));
nvs_set_u8(h, "addr_type", (uint8_t)addr_type);
nvs_commit(h);
nvs_close(h);
ESP_LOGI(TAG, "Saved BDA %02x:%02x:%02x:%02x:%02x:%02x (addr_type=%d) to NVS",
bda[0], bda[1], bda[2], bda[3], bda[4], bda[5], (int)addr_type);
}
static bool nvs_load_bda(esp_bd_addr_t out_bda, esp_ble_addr_type_t *out_addr_type)
{
nvs_handle_t h;
if (nvs_open(BT_HID_NVS_NAMESPACE, NVS_READONLY, &h) != ESP_OK) return false;
size_t len = sizeof(esp_bd_addr_t);
bool ok = (nvs_get_blob(h, BT_HID_NVS_BDA_KEY, out_bda, &len) == ESP_OK);
if (ok) {
uint8_t at = (uint8_t)BLE_ADDR_TYPE_RANDOM;
nvs_get_u8(h, "addr_type", &at); /* missing key → stays RANDOM, safe default */
*out_addr_type = (esp_ble_addr_type_t)at;
}
nvs_close(h);
return ok;
}
// ---------------------------------------------------------------------------
// Map a HID usage code to an FSM override command
// Returns false if the usage is not mapped.
// ---------------------------------------------------------------------------
static bool usage_to_override(uint16_t usage, fsm_override_t *out)
{
switch (usage) {
case USAGE_VOL_UP: *out = FSM_OVERRIDE_JACK_UP; return true;
case USAGE_VOL_DOWN: *out = FSM_OVERRIDE_JACK_DOWN; return true;
case USAGE_PREV: *out = FSM_OVERRIDE_DRIVE_REV; return true;
case USAGE_NEXT: *out = FSM_OVERRIDE_DRIVE_FWD; return true;
default: return false;
}
}
// ---------------------------------------------------------------------------
// HID Host event callback
// Runs in the esp_hidh internal event task — keep it short, no blocking.
// ---------------------------------------------------------------------------
static void hidh_callback(void *handler_args,
esp_event_base_t base,
int32_t id,
void *event_data)
{
esp_hidh_event_t event = (esp_hidh_event_t)id;
esp_hidh_event_data_t *p = (esp_hidh_event_data_t *)event_data;
switch (event) {
case ESP_HIDH_OPEN_EVENT: {
if (p->open.status == ESP_OK) {
const uint8_t *bda = esp_hidh_dev_bda_get(p->open.dev);
ESP_LOGI(TAG, "CONNECTED: %02x:%02x:%02x:%02x:%02x:%02x '%s'",
bda[0], bda[1], bda[2], bda[3], bda[4], bda[5],
esp_hidh_dev_name_get(p->open.dev));
esp_hidh_dev_dump(p->open.dev, stdout);
taskENTER_CRITICAL(&s_mux);
s_remote.conn_state = REMOTE_CONNECTED;
s_remote.held_key = USAGE_NONE;
memcpy(s_remote.bda, bda, sizeof(esp_bd_addr_t));
taskEXIT_CRITICAL(&s_mux);
nvs_save_bda(bda, s_connect_addr_type);
} else {
ESP_LOGE(TAG, "OPEN failed, status=%d", p->open.status);
/* Free the failed device handle — not doing so leaks a BT
* resource and can block future connection attempts. */
esp_hidh_dev_free(p->open.dev);
}
break;
}
case ESP_HIDH_CLOSE_EVENT: {
taskENTER_CRITICAL(&s_mux);
s_remote.conn_state = REMOTE_DISCONNECTED;
s_remote.held_key = USAGE_NONE;
taskEXIT_CRITICAL(&s_mux);
ESP_LOGW(TAG, "DISCONNECTED — will rescan");
break;
}
case ESP_HIDH_BATTERY_EVENT:
ESP_LOGI(TAG, "Battery: %d%%", p->battery.level);
break;
case ESP_HIDH_INPUT_EVENT: {
/*
* Decode usage code from the first two bytes of the input report.
* BLE consumer-control remotes typically send a 16-bit usage on
* report ID 3 (or similar), zero on key-up.
*/
uint16_t usage = USAGE_NONE;
if (p->input.length >= 2) {
usage = (uint16_t)(p->input.data[0] | ((uint16_t)p->input.data[1] << 8));
} else if (p->input.length == 1) {
usage = p->input.data[0];
}
taskENTER_CRITICAL(&s_mux);
s_remote.held_key = usage;
taskEXIT_CRITICAL(&s_mux);
/* Log mapped presses only (not key-up noise). */
if (usage != USAGE_NONE) {
fsm_override_t dummy;
if (usage_to_override(usage, &dummy)) {
ESP_LOGI(TAG, "KEY DOWN 0x%04X", usage);
} else {
ESP_LOGD(TAG, "KEY 0x%04X (unmapped)", usage);
}
} else {
ESP_LOGD(TAG, "KEY UP");
}
break;
}
default:
break;
}
}
// ---------------------------------------------------------------------------
// BLE GAP event handler
// ---------------------------------------------------------------------------
static void ble_gap_event_handler(esp_gap_ble_cb_event_t event,
esp_ble_gap_cb_param_t *param)
{
switch (event) {
/* Scan parameters accepted — safe to start scanning now. */
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
xSemaphoreGive(s_scan_sem);
break;
case ESP_GAP_BLE_SCAN_RESULT_EVT: {
switch (param->scan_rst.search_evt) {
case ESP_GAP_SEARCH_INQ_RES_EVT: {
uint8_t *adv = param->scan_rst.ble_adv;
/* Only keep devices that advertise the HID service UUID (0x1812). */
uint8_t uuid_len = 0;
uint8_t *uuid_d = esp_ble_resolve_adv_data(
adv, ESP_BLE_AD_TYPE_16SRV_CMPL, &uuid_len);
uint16_t uuid = 0;
if (uuid_d && uuid_len >= 2) {
uuid = (uint16_t)(uuid_d[0] | ((uint16_t)uuid_d[1] << 8));
}
if (uuid != ESP_GATT_UUID_HID_SVC) break;
/* Parse device name (prefer complete, fall back to short). */
uint8_t name_len = 0;
uint8_t *name_d = esp_ble_resolve_adv_data(
adv, ESP_BLE_AD_TYPE_NAME_CMPL, &name_len);
if (!name_d || !name_len) {
name_d = esp_ble_resolve_adv_data(
adv, ESP_BLE_AD_TYPE_NAME_SHORT, &name_len);
}
char name[64] = {0};
if (name_d && name_len > 0) {
size_t copy = name_len < (sizeof(name) - 1) ? name_len : (sizeof(name) - 1);
memcpy(name, name_d, copy);
}
if (param->scan_rst.rssi < BT_HID_MIN_RSSI) {
ESP_LOGD(TAG, "SCAN %02x:%02x:%02x:%02x:%02x:%02x RSSI:%d '%s' (too weak, skipping)",
param->scan_rst.bda[0], param->scan_rst.bda[1],
param->scan_rst.bda[2], param->scan_rst.bda[3],
param->scan_rst.bda[4], param->scan_rst.bda[5],
param->scan_rst.rssi, name);
break;
}
ESP_LOGI(TAG, "SCAN %02x:%02x:%02x:%02x:%02x:%02x RSSI:%d '%s' <<< HID",
param->scan_rst.bda[0], param->scan_rst.bda[1],
param->scan_rst.bda[2], param->scan_rst.bda[3],
param->scan_rst.bda[4], param->scan_rst.bda[5],
param->scan_rst.rssi, name);
add_scan_result(param->scan_rst.bda,
param->scan_rst.ble_addr_type,
name,
param->scan_rst.rssi);
break;
}
/* Scan complete — unblock the scan task. */
case ESP_GAP_SEARCH_INQ_CMPL_EVT:
xSemaphoreGive(s_scan_sem);
break;
default:
break;
}
break;
}
/* Pairing / security events — auto-accept so bonding completes. */
case ESP_GAP_BLE_AUTH_CMPL_EVT:
if (!param->ble_security.auth_cmpl.success) {
ESP_LOGE(TAG, "AUTH ERROR 0x%x", param->ble_security.auth_cmpl.fail_reason);
} else {
ESP_LOGI(TAG, "AUTH SUCCESS");
}
break;
case ESP_GAP_BLE_SEC_REQ_EVT:
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
case ESP_GAP_BLE_NC_REQ_EVT:
esp_ble_confirm_reply(param->ble_security.key_notif.bd_addr, true);
break;
default:
break;
}
}
// ---------------------------------------------------------------------------
// Scan + reconnect task
// ---------------------------------------------------------------------------
static void bt_hid_scan_task(void *pvParameters)
{
esp_ble_scan_params_t scan_params = {
.scan_type = BLE_SCAN_TYPE_ACTIVE,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL,
.scan_interval = 0x50,
.scan_window = 0x30,
.scan_duplicate = BLE_SCAN_DUPLICATE_ENABLE,
};
TickType_t repeat_tick = xTaskGetTickCount();
/* ------------------------------------------------------------------
* On the very first iteration, try to reconnect to the saved BDA.
* ------------------------------------------------------------------ */
if (s_has_saved_bda) {
ESP_LOGI(TAG, "Trying saved device %02x:%02x:%02x:%02x:%02x:%02x ...",
s_saved_bda[0], s_saved_bda[1], s_saved_bda[2],
s_saved_bda[3], s_saved_bda[4], s_saved_bda[5]);
/*
* We don't know the address type of the saved device at this point.
* BLE_ADDR_TYPE_RANDOM is the most common for consumer remotes; if it
* fails the outer scan loop will find it correctly.
*/
s_connect_addr_type = s_saved_addr_type;
esp_hidh_dev_open(s_saved_bda, ESP_HID_TRANSPORT_BLE, s_saved_addr_type);
vTaskDelay(pdMS_TO_TICKS(BT_HID_BOND_TIMEOUT_MS));
/* If the open succeeded the state will be CONNECTED — skip to main loop. */
}
/* ------------------------------------------------------------------
* Main loop: scan → connect → wait → repeat if disconnected.
* ------------------------------------------------------------------ */
while (1) {
/* ---- While connected: check held key and call pulse_override() ---- */
taskENTER_CRITICAL(&s_mux);
remote_conn_state_t conn = s_remote.conn_state;
uint16_t held_key = s_remote.held_key;
taskEXIT_CRITICAL(&s_mux);
if (conn == REMOTE_CONNECTED) {
/* Fire pulse_override every BT_HID_REPEAT_MS while a key is held. */
if (xTaskGetTickCount() - repeat_tick >= pdMS_TO_TICKS(BT_HID_REPEAT_MS)) {
repeat_tick = xTaskGetTickCount();
fsm_override_t override_cmd;
if (held_key != USAGE_NONE && usage_to_override(held_key, &override_cmd)) {
pulse_override(override_cmd);
}
}
vTaskDelay(pdMS_TO_TICKS(10)); /* Short sleep to yield; repeat timer governs rate. */
continue;
}
/* ---- Not connected — run a scan ---- */
ESP_LOGI(TAG, "Scanning for HID devices (%ds)...", BT_HID_SCAN_DURATION_S);
free_scan_results();
esp_ble_gap_set_scan_params(&scan_params);
xSemaphoreTake(s_scan_sem, portMAX_DELAY); /* wait: SCAN_PARAM_SET_COMPLETE */
esp_ble_gap_start_scanning(BT_HID_SCAN_DURATION_S);
xSemaphoreTake(s_scan_sem, portMAX_DELAY); /* wait: SCAN_INQ_CMPL */
ESP_LOGI(TAG, "Found %u HID device(s)", (unsigned)s_num_scan_results);
if (s_num_scan_results > 0) {
/* Pick the device with the strongest signal. */
scan_result_t *r = s_scan_results;
scan_result_t *best = r;
while (r) {
if (r->rssi > best->rssi) best = r;
r = r->next;
}
ESP_LOGI(TAG, "Connecting to %02x:%02x:%02x:%02x:%02x:%02x '%s' (RSSI:%d)",
best->bda[0], best->bda[1], best->bda[2],
best->bda[3], best->bda[4], best->bda[5],
best->name, best->rssi);
s_connect_addr_type = best->addr_type;
esp_hidh_dev_open(best->bda, ESP_HID_TRANSPORT_BLE, best->addr_type);
free_scan_results();
/* Give the connection time to establish before looping. */
vTaskDelay(pdMS_TO_TICKS(BT_HID_CONNECT_WAIT_MS));
} else {
ESP_LOGI(TAG, "No HID devices found, retrying in %dms...", BT_HID_RECONNECT_MS);
vTaskDelay(pdMS_TO_TICKS(BT_HID_RECONNECT_MS));
}
repeat_tick = xTaskGetTickCount(); /* reset repeat timer after reconnect */
}
}
// ---------------------------------------------------------------------------
// Public init
// ---------------------------------------------------------------------------
esp_err_t bt_hid_init(void)
{
esp_err_t ret;
/* Release classic BT memory — we only use BLE. */
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ret = esp_bt_controller_init(&bt_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "bt_controller_init failed: %s", esp_err_to_name(ret));
return ret;
}
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "bt_controller_enable failed: %s", esp_err_to_name(ret));
return ret;
}
esp_bluedroid_config_t bd_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
ret = esp_bluedroid_init_with_cfg(&bd_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "bluedroid_init failed: %s", esp_err_to_name(ret));
return ret;
}
ret = esp_bluedroid_enable();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "bluedroid_enable failed: %s", esp_err_to_name(ret));
return ret;
}
/* esp_hidh posts events to the default event loop — create it if the
* webserver hasn't done so yet. ESP_ERR_INVALID_STATE means it already
* exists, which is fine. */
ret = esp_event_loop_create_default();
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "event_loop_create failed: %s", esp_err_to_name(ret));
return ret;
}
ESP_ERROR_CHECK(esp_ble_gap_register_callback(ble_gap_event_handler));
ESP_ERROR_CHECK(esp_ble_gattc_register_callback(esp_hidh_gattc_event_handler));
esp_hidh_config_t hidh_cfg = {
.callback = hidh_callback,
.event_stack_size = 4096,
.callback_arg = NULL,
};
ret = esp_hidh_init(&hidh_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "hidh_init failed: %s", esp_err_to_name(ret));
return ret;
}
/* Try to load a previously bonded device address from NVS. */
s_has_saved_bda = nvs_load_bda(s_saved_bda, &s_saved_addr_type);
if (s_has_saved_bda) {
ESP_LOGI(TAG, "Found saved BDA in NVS — will try direct reconnect first");
}
s_scan_sem = xSemaphoreCreateBinary();
if (!s_scan_sem) {
ESP_LOGE(TAG, "Failed to create scan semaphore");
return ESP_ERR_NO_MEM;
}
/*
* Priority 4: above webserver (typically 56 on core 0) is fine; below
* the FSM (10) and RF task (5) so control always wins CPU.
*/
xTaskCreate(bt_hid_scan_task, "bt_hid_scan", 6 * 1024, NULL, 4, &s_scan_task_handle);
if (comms_event_group) xEventGroupSetBits(comms_event_group, BT_READY_BIT);
ESP_LOGI(TAG, "BLE HID host initialised");
return ESP_OK;
}
void bt_hid_stop(void)
{
if (s_scan_task_handle != NULL) {
vTaskSuspend(s_scan_task_handle);
ESP_LOGI(TAG, "BT HID scan task suspended");
}
if (comms_event_group) xEventGroupClearBits(comms_event_group, BT_READY_BIT);
}
void bt_hid_resume(void)
{
if (s_scan_task_handle != NULL) {
vTaskResume(s_scan_task_handle);
ESP_LOGI(TAG, "BT HID scan task resumed");
}
if (comms_event_group) xEventGroupSetBits(comms_event_group, BT_READY_BIT);
}