feat: parse and filter ibeacon/altbeacon/eddystone ble advertisements
This commit is contained in:
@@ -2,12 +2,25 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+14
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user