feat: BLE passive scanner with RSSI bucketing and MAC hashing
Add passive BLE scan module using NimBLE for WiFi coexistence. Tracks unique devices per hour with SHA256-hashed MACs, RSSI bucketing (near/mid/far), max concurrent count, and thread-safe collect/reset. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
98
firmware/src/ble_scanner.cpp
Normal file
98
firmware/src/ble_scanner.cpp
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// firmware/src/ble_scanner.cpp
|
||||||
|
#include "ble_scanner.h"
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
#include <NimBLEScan.h>
|
||||||
|
#include <NimBLEAdvertisedDevice.h>
|
||||||
|
#include "mbedtls/md.h"
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#define RSSI_NEAR -65
|
||||||
|
#define RSSI_MID -80
|
||||||
|
|
||||||
|
static portMUX_TYPE s_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||||
|
|
||||||
|
struct DeviceObs {
|
||||||
|
int rssi_sum;
|
||||||
|
int count;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::map<String, DeviceObs> s_seen;
|
||||||
|
static int s_max_concurrent = 0;
|
||||||
|
|
||||||
|
static String sha256_prefix(const String& input) {
|
||||||
|
uint8_t hash[32];
|
||||||
|
mbedtls_md_context_t ctx;
|
||||||
|
const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||||
|
mbedtls_md_init(&ctx);
|
||||||
|
mbedtls_md_setup(&ctx, info, 0);
|
||||||
|
mbedtls_md_starts(&ctx);
|
||||||
|
mbedtls_md_update(&ctx, (const uint8_t*)input.c_str(), input.length());
|
||||||
|
mbedtls_md_finish(&ctx, hash);
|
||||||
|
mbedtls_md_free(&ctx);
|
||||||
|
String hex = "";
|
||||||
|
char buf[3];
|
||||||
|
for (int i = 0; i < 16; i++) { snprintf(buf, 3, "%02x", hash[i]); hex += buf; }
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanCallback : public NimBLEAdvertisedDeviceCallbacks {
|
||||||
|
void onResult(NimBLEAdvertisedDevice* dev) override {
|
||||||
|
String mac = String(dev->getAddress().toString().c_str());
|
||||||
|
String hash = sha256_prefix(mac);
|
||||||
|
int rssi = dev->getRSSI();
|
||||||
|
|
||||||
|
portENTER_CRITICAL(&s_mux);
|
||||||
|
auto it = s_seen.find(hash);
|
||||||
|
if (it == s_seen.end()) {
|
||||||
|
s_seen[hash] = {rssi, 1};
|
||||||
|
} else {
|
||||||
|
it->second.rssi_sum += rssi;
|
||||||
|
it->second.count++;
|
||||||
|
}
|
||||||
|
int concurrent = (int)s_seen.size();
|
||||||
|
if (concurrent > s_max_concurrent) s_max_concurrent = concurrent;
|
||||||
|
portEXIT_CRITICAL(&s_mux);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static ScanCallback s_callback;
|
||||||
|
static NimBLEScan* s_scan = nullptr;
|
||||||
|
|
||||||
|
void ble_scanner_start() {
|
||||||
|
NimBLEDevice::init("");
|
||||||
|
s_scan = NimBLEDevice::getScan();
|
||||||
|
s_scan->setAdvertisedDeviceCallbacks(&s_callback, true); // true = allow duplicates
|
||||||
|
s_scan->setActiveScan(false); // passive
|
||||||
|
s_scan->setInterval(100);
|
||||||
|
s_scan->setWindow(99);
|
||||||
|
s_scan->setMaxResults(0); // don't store results — callback-only
|
||||||
|
s_scan->start(0, nullptr, false); // 0 = continuous
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_scanner_pause() { if (s_scan) s_scan->stop(); }
|
||||||
|
void ble_scanner_resume() { if (s_scan) s_scan->start(0, nullptr, false); }
|
||||||
|
|
||||||
|
BLEHourlyRecord ble_scanner_collect(uint32_t period_start, uint32_t period_end) {
|
||||||
|
portENTER_CRITICAL(&s_mux);
|
||||||
|
|
||||||
|
BLEHourlyRecord rec;
|
||||||
|
rec.period_start = period_start;
|
||||||
|
rec.period_end = period_end;
|
||||||
|
rec.unique_devices = (int)s_seen.size();
|
||||||
|
rec.max_concurrent = s_max_concurrent;
|
||||||
|
rec.near_count = 0; rec.mid_count = 0; rec.far_count = 0;
|
||||||
|
|
||||||
|
for (auto& kv : s_seen) {
|
||||||
|
float avg = kv.second.rssi_sum / (float)kv.second.count;
|
||||||
|
if (avg > RSSI_NEAR) rec.near_count++;
|
||||||
|
else if (avg > RSSI_MID) rec.mid_count++;
|
||||||
|
else rec.far_count++;
|
||||||
|
rec.device_hashes.push_back(kv.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
s_seen.clear();
|
||||||
|
s_max_concurrent = 0;
|
||||||
|
|
||||||
|
portEXIT_CRITICAL(&s_mux);
|
||||||
|
return rec;
|
||||||
|
}
|
||||||
25
firmware/src/ble_scanner.h
Normal file
25
firmware/src/ble_scanner.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// firmware/src/ble_scanner.h
|
||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct BLEHourlyRecord {
|
||||||
|
uint32_t period_start;
|
||||||
|
uint32_t period_end;
|
||||||
|
int unique_devices;
|
||||||
|
int max_concurrent;
|
||||||
|
int near_count; // RSSI > -65 dBm
|
||||||
|
int mid_count; // RSSI -65 to -80 dBm
|
||||||
|
int far_count; // RSSI < -80 dBm
|
||||||
|
std::vector<String> device_hashes; // SHA256(MAC) first 16 hex chars
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start continuous passive BLE scan (call once at boot).
|
||||||
|
void ble_scanner_start();
|
||||||
|
|
||||||
|
// Pause scan for ~3s during HTTP upload.
|
||||||
|
void ble_scanner_pause();
|
||||||
|
void ble_scanner_resume();
|
||||||
|
|
||||||
|
// Collect current hour's record and reset accumulators. Thread-safe.
|
||||||
|
BLEHourlyRecord ble_scanner_collect(uint32_t period_start, uint32_t period_end);
|
||||||
Reference in New Issue
Block a user