From 8992e311baf28ef6a7ea5feef3eb05b7446fb8f2 Mon Sep 17 00:00:00 2001 From: dvdrw Date: Sat, 16 May 2026 20:52:50 +0200 Subject: [PATCH] feat: parse and filter ibeacon/altbeacon/eddystone ble advertisements --- components/ble_scanner/include/ble_scanner.h | 25 +++- components/ble_scanner/src/ble_scanner.c | 141 +++++++++++++++++- .../mqtt_publisher/include/mqtt_publisher.h | 3 + .../mqtt_publisher/src/mqtt_publisher.c | 11 ++ main/main.c | 16 +- 5 files changed, 182 insertions(+), 14 deletions(-) diff --git a/components/ble_scanner/include/ble_scanner.h b/components/ble_scanner/include/ble_scanner.h index 7505dc9..d296791 100644 --- a/components/ble_scanner/include/ble_scanner.h +++ b/components/ble_scanner/include/ble_scanner.h @@ -2,12 +2,25 @@ #include -/** - * Called for every unique BLE advertisement received. - * tag_id is a null-terminated string: "aa:bb:cc:dd:ee:ff" - * rssi is in dBm (negative). - */ -typedef void (*ble_scanner_cb_t)(const char *tag_id, int8_t rssi); +typedef enum { + BLE_BEACON_IBEACON, + BLE_BEACON_ALTBEACON, + BLE_BEACON_EDDYSTONE_UID, + BLE_BEACON_EDDYSTONE_URL, +} ble_beacon_type_t; + +typedef struct { + ble_beacon_type_t type; + char id[64]; /* beacon identifier; encoding varies by type: + iBeacon: "UUID-MAJOR-MINOR" + AltBeacon: 40-char hex beacon ID + Eddystone UID: "NAMESPACE.INSTANCE" hex + Eddystone URL: decoded URL string */ + int8_t tx_power; /* calibrated TX power from beacon payload */ + int8_t rssi; /* measured signal strength */ +} ble_beacon_t; + +typedef void (*ble_scanner_cb_t)(const ble_beacon_t *beacon); /** * Initialise the Bluedroid BLE stack and register the scan callback. diff --git a/components/ble_scanner/src/ble_scanner.c b/components/ble_scanner/src/ble_scanner.c index fcd567a..31be448 100644 --- a/components/ble_scanner/src/ble_scanner.c +++ b/components/ble_scanner/src/ble_scanner.c @@ -20,6 +20,136 @@ static esp_ble_scan_params_t s_scan_params = { .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) { @@ -41,12 +171,11 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, 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) { - char tag_id[18]; - uint8_t *a = p->scan_rst.bda; - snprintf(tag_id, sizeof(tag_id), - "%02x:%02x:%02x:%02x:%02x:%02x", - a[0], a[1], a[2], a[3], a[4], a[5]); - s_cb(tag_id, p->scan_rst.rssi); + 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; } diff --git a/components/mqtt_publisher/include/mqtt_publisher.h b/components/mqtt_publisher/include/mqtt_publisher.h index 9b43dae..721b4fd 100644 --- a/components/mqtt_publisher/include/mqtt_publisher.h +++ b/components/mqtt_publisher/include/mqtt_publisher.h @@ -41,5 +41,8 @@ esp_err_t mqtt_publisher_init(const char *sensor_id, /** Publish an RSSI reading. Non-blocking (QoS 1). */ void mqtt_publisher_send_rssi(const char *tag_id, int8_t rssi); +/** Publish a parsed beacon reading with type, id, tx_power and rssi. Non-blocking (QoS 1). */ +void mqtt_publisher_send_beacon(const char *type, const char *id, int8_t tx_power, int8_t rssi); + /** Publish the announce message (empty payload). */ void mqtt_publisher_announce(void); diff --git a/components/mqtt_publisher/src/mqtt_publisher.c b/components/mqtt_publisher/src/mqtt_publisher.c index b4cf18e..799d767 100644 --- a/components/mqtt_publisher/src/mqtt_publisher.c +++ b/components/mqtt_publisher/src/mqtt_publisher.c @@ -139,3 +139,14 @@ void mqtt_publisher_send_rssi(const char *tag_id, int8_t rssi) "{\"tag_id\":\"%s\",\"rssi\":%d}", tag_id, (int)rssi); esp_mqtt_client_publish(s_client, s_topic_rssi, payload, len, 1, 0); } + +void mqtt_publisher_send_beacon(const char *type, const char *id, int8_t tx_power, int8_t rssi) +{ + if (!s_client) return; + + char payload[128]; + int len = snprintf(payload, sizeof(payload), + "{\"type\":\"%s\",\"id\":\"%s\",\"tx_power\":%d,\"rssi\":%d}", + type, id, (int)tx_power, (int)rssi); + esp_mqtt_client_publish(s_client, s_topic_rssi, payload, len, 1, 0); +} diff --git a/main/main.c b/main/main.c index 9e2378a..a8bc5f4 100644 --- a/main/main.c +++ b/main/main.c @@ -73,9 +73,21 @@ static void wifi_event_handler(void *arg, esp_event_base_t base, } } -static void on_ble_scan_result(const char *tag_id, int8_t rssi) +static const char *beacon_type_str(ble_beacon_type_t t) { - mqtt_publisher_send_rssi(tag_id, rssi); + switch (t) { + case BLE_BEACON_IBEACON: return "ibeacon"; + case BLE_BEACON_ALTBEACON: return "altbeacon"; + case BLE_BEACON_EDDYSTONE_UID: return "eddystone_uid"; + case BLE_BEACON_EDDYSTONE_URL: return "eddystone_url"; + default: return "unknown"; + } +} + +static void on_ble_scan_result(const ble_beacon_t *beacon) +{ + mqtt_publisher_send_beacon(beacon_type_str(beacon->type), + beacon->id, beacon->tx_power, beacon->rssi); } static void wifi_init_sta(void)