Add BLE layer: firmware, coordinator support, dashboard tab
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
#include <WiFi.h>
|
||||
#include <WiFiUdp.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLEScan.h>
|
||||
#include <BLEAdvertisedDevice.h>
|
||||
#include "esp_efuse.h"
|
||||
#include "esp_mac.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "config.h"
|
||||
|
||||
WiFiUDP udp;
|
||||
String nodeId;
|
||||
|
||||
unsigned long lastHeartbeat = 0;
|
||||
unsigned long lastFlush = 0;
|
||||
|
||||
static volatile bool scanDone = false;
|
||||
static volatile uint32_t bleDrops = 0;
|
||||
|
||||
// ─── BLE event ───────────────────────────────────────────────────────────────
|
||||
|
||||
struct BleEvent {
|
||||
char mac[18];
|
||||
uint8_t addr_type; // 0 = public, 1 = random
|
||||
char name[32];
|
||||
int8_t rssi;
|
||||
int32_t mfr_id; // company ID (little-endian), -1 if absent
|
||||
char mfr_data[33]; // hex string of bytes after company ID, up to 16 bytes
|
||||
};
|
||||
|
||||
static QueueHandle_t bleQueue;
|
||||
|
||||
// ─── Dedup ───────────────────────────────────────────────────────────────────
|
||||
|
||||
struct BleDedup { char mac[18]; uint32_t last_ms; };
|
||||
static BleDedup dedupCache[BLE_DEDUP_CACHE_SIZE];
|
||||
static int dedupNext = 0;
|
||||
|
||||
static bool isDuplicate(const char* mac) {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < BLE_DEDUP_CACHE_SIZE; i++) {
|
||||
if (dedupCache[i].last_ms == 0) continue;
|
||||
if (strcmp(dedupCache[i].mac, mac) == 0) {
|
||||
if ((now - dedupCache[i].last_ms) < (uint32_t)(BLE_DEDUP_SECS * 1000))
|
||||
return true;
|
||||
dedupCache[i].last_ms = now;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
strncpy(dedupCache[dedupNext].mac, mac, 17);
|
||||
dedupCache[dedupNext].mac[17] = '\0';
|
||||
dedupCache[dedupNext].last_ms = now;
|
||||
dedupNext = (dedupNext + 1) % BLE_DEDUP_CACHE_SIZE;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── Advertisement callback ──────────────────────────────────────────────────
|
||||
// Runs in the BLE task — must be fast and must not call UDP.
|
||||
|
||||
class AdvCallback : public BLEAdvertisedDeviceCallbacks {
|
||||
void onResult(BLEAdvertisedDevice dev) override {
|
||||
char mac[18];
|
||||
strncpy(mac, dev.getAddress().toString().c_str(), 17);
|
||||
mac[17] = '\0';
|
||||
|
||||
if (isDuplicate(mac)) return;
|
||||
|
||||
BleEvent ev;
|
||||
memcpy(ev.mac, mac, 18);
|
||||
|
||||
// BLE_ADDR_TYPE_PUBLIC = 0, everything else treated as random
|
||||
ev.addr_type = (dev.getAddressType() == BLE_ADDR_PUBLIC) ? 0 : 1;
|
||||
ev.rssi = (int8_t)dev.getRSSI();
|
||||
ev.mfr_id = -1;
|
||||
ev.mfr_data[0] = '\0';
|
||||
ev.name[0] = '\0';
|
||||
|
||||
// Device name — strip non-printable bytes
|
||||
if (dev.haveName()) {
|
||||
String raw = dev.getName();
|
||||
int j = 0;
|
||||
for (int i = 0; i < (int)raw.length() && j < 31; i++) {
|
||||
char c = raw[i];
|
||||
ev.name[j++] = (c >= 32 && c <= 126) ? c : '?';
|
||||
}
|
||||
ev.name[j] = '\0';
|
||||
}
|
||||
|
||||
// Manufacturer specific data (AD type 0xFF)
|
||||
// First 2 bytes (little-endian) are the company ID; rest is payload.
|
||||
if (dev.haveManufacturerData()) {
|
||||
String md = dev.getManufacturerData();
|
||||
if (md.length() >= 2) {
|
||||
ev.mfr_id = (int32_t)((uint8_t)md[0] | ((uint8_t)md[1] << 8));
|
||||
int payload_len = min((int)16, (int)md.length() - 2);
|
||||
for (int i = 0; i < payload_len; i++) {
|
||||
snprintf(ev.mfr_data + i * 2, 3, "%02x", (uint8_t)md[2 + i]);
|
||||
}
|
||||
ev.mfr_data[payload_len * 2] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
if (xQueueSend(bleQueue, &ev, 0) != pdTRUE) bleDrops++;
|
||||
}
|
||||
};
|
||||
|
||||
static AdvCallback advCallback;
|
||||
static BLEScan* pBLEScan = nullptr;
|
||||
|
||||
// Called from BLE task when a scan window completes.
|
||||
static void onScanComplete(BLEScanResults) {
|
||||
pBLEScan->clearResults();
|
||||
scanDone = true;
|
||||
}
|
||||
|
||||
// ─── Setup ───────────────────────────────────────────────────────────────────
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(500);
|
||||
|
||||
uint8_t mac[6];
|
||||
esp_efuse_mac_get_default(mac);
|
||||
char macBuf[9];
|
||||
snprintf(macBuf, sizeof(macBuf), "%02X%02X%02X%02X", mac[2], mac[3], mac[4], mac[5]);
|
||||
nodeId = String(macBuf);
|
||||
|
||||
Serial.printf("\n[BLE NODE] ID: %s\n", nodeId.c_str());
|
||||
|
||||
memset(dedupCache, 0, sizeof(dedupCache));
|
||||
bleQueue = xQueueCreate(BLE_QUEUE_SIZE, sizeof(BleEvent));
|
||||
|
||||
connectWiFi();
|
||||
|
||||
BLEDevice::init("");
|
||||
pBLEScan = BLEDevice::getScan();
|
||||
pBLEScan->setAdvertisedDeviceCallbacks(&advCallback, true); // true = keep duplicates in scan window
|
||||
pBLEScan->setActiveScan(false); // passive — do not send scan requests
|
||||
pBLEScan->setInterval(100); // scan interval (ms × 0.625 = 62.5ms)
|
||||
pBLEScan->setWindow(99); // scan window — near-continuous coverage
|
||||
|
||||
pBLEScan->start(BLE_SCAN_DURATION_SECS, onScanComplete, false);
|
||||
Serial.println("[BLE] Passive scan started");
|
||||
}
|
||||
|
||||
// ─── Loop ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void loop() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("[WIFI] Lost connection, reconnecting...");
|
||||
connectWiFi();
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
if (now - lastHeartbeat >= HEARTBEAT_INTERVAL_MS) {
|
||||
lastHeartbeat = now;
|
||||
sendHeartbeat();
|
||||
}
|
||||
|
||||
if (now - lastFlush >= BLE_FLUSH_INTERVAL_MS) {
|
||||
lastFlush = now;
|
||||
flushBleQueue();
|
||||
}
|
||||
|
||||
// Restart scan after each window completes
|
||||
if (scanDone) {
|
||||
scanDone = false;
|
||||
pBLEScan->start(BLE_SCAN_DURATION_SECS, onScanComplete, false);
|
||||
}
|
||||
|
||||
delay(10);
|
||||
}
|
||||
|
||||
// ─── WiFi ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void connectWiFi() {
|
||||
Serial.printf("[WIFI] Connecting to %s\n", WIFI_SSID);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
|
||||
int attempts = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.printf("\n[WIFI] Connected — IP: %s\n", WiFi.localIP().toString().c_str());
|
||||
} else {
|
||||
Serial.println("\n[WIFI] Failed to connect, will retry in loop");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Send ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void sendBleEvent(const BleEvent& ev) {
|
||||
// Escape name for JSON
|
||||
char nameEsc[66];
|
||||
int j = 0;
|
||||
for (int i = 0; ev.name[i] && j < 64; i++) {
|
||||
if (ev.name[i] == '"' || ev.name[i] == '\\') nameEsc[j++] = '\\';
|
||||
nameEsc[j++] = ev.name[i];
|
||||
}
|
||||
nameEsc[j] = '\0';
|
||||
|
||||
char buf[320];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"node_id\":\"%s\",\"ts\":%lu,\"type\":\"ble_adv\","
|
||||
"\"mac\":\"%s\",\"addr_type\":%u,"
|
||||
"\"name\":\"%s\",\"rssi\":%d,"
|
||||
"\"mfr_id\":%ld,\"mfr_data\":\"%s\"}",
|
||||
nodeId.c_str(), millis(),
|
||||
ev.mac, (unsigned)ev.addr_type,
|
||||
nameEsc, (int)ev.rssi,
|
||||
(long)ev.mfr_id, ev.mfr_data
|
||||
);
|
||||
|
||||
udp.beginPacket(COORDINATOR_IP, COORDINATOR_PORT);
|
||||
udp.print(buf);
|
||||
udp.endPacket();
|
||||
|
||||
const char* at = ev.addr_type == 0 ? "pub" : "rnd";
|
||||
Serial.printf(" [BLE] %s (%s) \"%s\" %ddBm mfr=0x%04lX data=%s\n",
|
||||
ev.mac, at, ev.name[0] ? ev.name : "<anon>",
|
||||
(int)ev.rssi, (long)(ev.mfr_id >= 0 ? ev.mfr_id : 0), ev.mfr_data);
|
||||
}
|
||||
|
||||
void flushBleQueue() {
|
||||
if (WiFi.status() != WL_CONNECTED) return;
|
||||
BleEvent ev;
|
||||
while (xQueueReceive(bleQueue, &ev, 0) == pdTRUE) {
|
||||
sendBleEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Heartbeat ───────────────────────────────────────────────────────────────
|
||||
|
||||
void sendHeartbeat() {
|
||||
unsigned long now = millis();
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"{\"node_id\":\"%s\",\"ts\":%lu,\"type\":\"heartbeat\","
|
||||
"\"uptime_ms\":%lu,\"free_heap\":%lu,\"wifi_rssi\":%d,"
|
||||
"\"probe_drops\":0,\"deauth_drops\":0,\"assoc_drops\":0,"
|
||||
"\"ble_drops\":%lu}",
|
||||
nodeId.c_str(), now,
|
||||
now,
|
||||
(unsigned long)ESP.getFreeHeap(),
|
||||
WiFi.RSSI(),
|
||||
(unsigned long)bleDrops
|
||||
);
|
||||
udp.beginPacket(COORDINATOR_IP, COORDINATOR_PORT);
|
||||
udp.print(buf);
|
||||
udp.endPacket();
|
||||
Serial.printf("[HB] uptime=%lus heap=%luK ap=%ddBm ble_drops=%lu\n",
|
||||
now / 1000,
|
||||
(unsigned long)ESP.getFreeHeap() / 1024,
|
||||
WiFi.RSSI(),
|
||||
(unsigned long)bleDrops
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
// ─── WiFi (for UDP transport only) ───────────────────────────────────────────
|
||||
#define WIFI_SSID "sandbox"
|
||||
#define WIFI_PASSWORD "Jaunsgads11!!"
|
||||
|
||||
// ─── Coordinator ─────────────────────────────────────────────────────────────
|
||||
#define COORDINATOR_IP "192.168.1.133"
|
||||
#define COORDINATOR_PORT 5005
|
||||
|
||||
// ─── BLE scan ────────────────────────────────────────────────────────────────
|
||||
#define BLE_SCAN_DURATION_SECS 5 // each passive scan window (seconds)
|
||||
#define BLE_FLUSH_INTERVAL_MS 5000 // how often to flush queue to coordinator (ms)
|
||||
#define HEARTBEAT_INTERVAL_MS 10000 // heartbeat cadence (ms) — same as WiFi nodes
|
||||
|
||||
// ─── Dedup ───────────────────────────────────────────────────────────────────
|
||||
// Suppress the same MAC within this window. Randomised-MAC devices will
|
||||
// reappear when their MAC rotates (typically every 10–15 min), which is fine.
|
||||
#define BLE_DEDUP_SECS 30 // seconds before the same MAC is allowed again
|
||||
#define BLE_DEDUP_CACHE_SIZE 300 // ring-buffer size; BLE sees far more MACs than WiFi
|
||||
|
||||
// ─── Queue ───────────────────────────────────────────────────────────────────
|
||||
#define BLE_QUEUE_SIZE 256 // events buffered between flushes
|
||||
Reference in New Issue
Block a user