#include "ble_scanner.h" #include "esp_bt.h" #include "esp_bt_main.h" #include "esp_gap_ble_api.h" #include "esp_log.h" #include #include #define TAG "ble_scanner" static ble_scanner_cb_t s_cb = NULL; /* Passive scan: 100ms interval, 50ms window (50% duty cycle) */ static esp_ble_scan_params_t s_scan_params = { .scan_type = BLE_SCAN_TYPE_PASSIVE, .own_addr_type = BLE_ADDR_TYPE_PUBLIC, .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, .scan_interval = 0xA0, /* 100ms: 0xA0 * 0.625ms */ .scan_window = 0x50, /* 50ms: 0x50 * 0.625ms */ .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE, }; /* Eddystone URL expansion codes 0x00-0x0D per spec */ static const char *const s_url_expansions[] = { ".com/", ".org/", ".edu/", ".net/", ".info/", ".biz/", ".gov/", ".com", ".org", ".edu", ".net", ".info", ".biz", ".gov", }; /* * iBeacon: AD type 0xFF, company {0x4C,0x00}, subtype {0x02,0x15} * value layout: company[2] subtype[2] UUID[16] major[2] minor[2] tx_power[1] */ static bool try_ibeacon(const uint8_t *val, uint8_t vlen, ble_beacon_t *out) { if (vlen < 25 || val[0] != 0x4C || val[1] != 0x00 || val[2] != 0x02 || val[3] != 0x15) return false; const uint8_t *u = val + 4; snprintf(out->id, sizeof(out->id), "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x" "-%02x%02x%02x%02x%02x%02x-%u-%u", u[0],u[1],u[2],u[3], u[4],u[5], u[6],u[7], u[8],u[9], u[10],u[11],u[12],u[13],u[14],u[15], (unsigned)((u[16] << 8) | u[17]), (unsigned)((u[18] << 8) | u[19])); out->tx_power = (int8_t)val[24]; out->type = BLE_BEACON_IBEACON; return true; } /* * AltBeacon: AD type 0xFF, bytes[2..3]=={0xBE,0xAC} * value layout: mfg_id[2] beacon_code[2] beacon_id[20] tx_power[1] reserved[1] */ static bool try_altbeacon(const uint8_t *val, uint8_t vlen, ble_beacon_t *out) { if (vlen < 26 || val[2] != 0xBE || val[3] != 0xAC) return false; const uint8_t *id = val + 4; for (int i = 0; i < 20; i++) snprintf(out->id + i * 2, 3, "%02x", id[i]); out->id[40] = '\0'; out->tx_power = (int8_t)val[24]; out->type = BLE_BEACON_ALTBEACON; return true; } /* * Eddystone: AD type 0x16, service UUID {0xAA,0xFE} * value layout: uuid[2] frame_type[1] ... * UID frame (0x00): tx_power[1] namespace[10] instance[6] * URL frame (0x10): tx_power[1] scheme[1] encoded_url[...] */ static bool try_eddystone(const uint8_t *val, uint8_t vlen, ble_beacon_t *out) { if (vlen < 4 || val[0] != 0xAA || val[1] != 0xFE) return false; uint8_t frame = val[2]; if (frame == 0x00) { if (vlen < 20) return false; out->tx_power = (int8_t)val[3]; const uint8_t *ns = val + 4; const uint8_t *inst = val + 14; snprintf(out->id, sizeof(out->id), "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x" ".%02x%02x%02x%02x%02x%02x", ns[0],ns[1],ns[2],ns[3],ns[4],ns[5],ns[6],ns[7],ns[8],ns[9], inst[0],inst[1],inst[2],inst[3],inst[4],inst[5]); out->type = BLE_BEACON_EDDYSTONE_UID; return true; } if (frame == 0x10) { if (vlen < 5) return false; out->tx_power = (int8_t)val[3]; static const char *const schemes[] = { "http://www.", "https://www.", "http://", "https://", }; const char *scheme = (val[4] < 4) ? schemes[val[4]] : ""; int pos = snprintf(out->id, sizeof(out->id), "%s", scheme); for (uint8_t i = 5; i < vlen; i++) { uint8_t c = val[i]; int rem = (int)sizeof(out->id) - pos; if (rem <= 1) break; int n; if (c < (uint8_t)(sizeof(s_url_expansions) / sizeof(s_url_expansions[0]))) { n = snprintf(out->id + pos, rem, "%s", s_url_expansions[c]); } else { out->id[pos] = (char)c; n = 1; } pos += (n >= rem) ? rem - 1 : n; } out->id[sizeof(out->id) - 1] = '\0'; out->type = BLE_BEACON_EDDYSTONE_URL; return true; } return false; } /* * Walk BLE advertisement data (sequence of length-type-value records) and * attempt to match iBeacon, AltBeacon, or Eddystone. Returns true on match. */ static bool parse_beacon(const uint8_t *data, uint8_t len, ble_beacon_t *out) { int pos = 0; while (pos < (int)len) { uint8_t ad_len = data[pos]; if (ad_len == 0 || pos + ad_len >= (int)len) break; uint8_t ad_type = data[pos + 1]; const uint8_t *val = data + pos + 2; uint8_t vlen = ad_len - 1; if (ad_type == 0xFF) { if (try_ibeacon(val, vlen, out)) return true; if (try_altbeacon(val, vlen, out)) return true; } else if (ad_type == 0x16) { if (try_eddystone(val, vlen, out)) return true; } pos += 1 + ad_len; } return false; } static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { esp_ble_gap_start_scanning(0); /* 0 = scan indefinitely */ } else { ESP_LOGE(TAG, "Scan param set failed: %d", param->scan_param_cmpl.status); } break; case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { ESP_LOGE(TAG, "Scan start failed: %d", param->scan_start_cmpl.status); } break; case ESP_GAP_BLE_SCAN_RESULT_EVT: { esp_ble_gap_cb_param_t *p = param; if (p->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT && s_cb) { ble_beacon_t beacon = {0}; beacon.rssi = (int8_t)p->scan_rst.rssi; if (parse_beacon(p->scan_rst.ble_adv, p->scan_rst.adv_data_len, &beacon)) { s_cb(&beacon); } } break; } case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: ESP_LOGI(TAG, "Scan stopped"); break; default: break; } } void ble_scanner_init(ble_scanner_cb_t cb) { s_cb = cb; 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(); ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg)); ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); ESP_ERROR_CHECK(esp_bluedroid_init()); ESP_ERROR_CHECK(esp_bluedroid_enable()); ESP_ERROR_CHECK(esp_ble_gap_register_callback(gap_event_handler)); ESP_LOGI(TAG, "BLE scanner initialised"); } void ble_scanner_start(void) { ESP_LOGI(TAG, "Starting BLE scan"); ESP_ERROR_CHECK(esp_ble_gap_set_scan_params(&s_scan_params)); /* Scanning begins in the param-set callback once confirmed */ } void ble_scanner_stop(void) { esp_ble_gap_stop_scanning(); }