Files
esp32-cluster/firmware/ble_node/ble_node.ino
T

265 lines
9.1 KiB
Arduino
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.
#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
);
}