Files
DoorCounter/firmware/src/ble_scanner.cpp
Peter Woolery a585a56cff fix(firmware): upgrade NimBLE to 2.x + DNS fallback for unreliable resolvers
NimBLE-Arduino 1.4.2 had an init/fire race in its FreeRTOS callout porting
layer where os_callout_timer_cb dispatched a queued TimerHandle expiry
against a not-yet-initialized event (NULL fn pointer), causing PC=0
InstrFetchProhibited within ~1s of boot when the camera task starved the
timer service. Confirmed by ets_printf instrumentation. Upgrading to
^2.0.0 rewrites the porting layer and eliminates the race; verified clean
on the customer network for 1+ hour.

Also rolls in DNS-resilience work that surfaced the BLE crash during
provisioning: pin lwIP/esp-netif resolvers to 1.1.1.1/8.8.8.8 across DHCP
renewals, add three-tier resolver fallback in reporter with a hardcoded
IP of last resort, and switch to raw WiFiClient with manual Host header
to bypass HTTPClient's brittle DNS path.

Migration touches for NimBLE 2.x:
- NimBLEAdvertisedDeviceCallbacks -> NimBLEScanCallbacks
- onResult signature now takes const NimBLEAdvertisedDevice*
- setAdvertisedDeviceCallbacks -> setScanCallbacks
- start(0, nullptr, false) -> start(0, false, false)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:34:17 -07:00

119 lines
3.6 KiB
C++

// firmware/src/ble_scanner.cpp
#include "ble_scanner.h"
#include <NimBLEDevice.h>
#include <NimBLEScan.h>
#include <NimBLEAdvertisedDevice.h>
#include "mbedtls/md.h"
#include <map>
#include <mutex>
#define RSSI_NEAR -65
#define RSSI_MID -80
static std::mutex s_mutex;
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) {
const mbedtls_md_info_t* info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
if (!info) return String(); // SHA256 not available
uint8_t hash[32] = {};
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
if (mbedtls_md_setup(&ctx, info, 0) != 0) {
mbedtls_md_free(&ctx);
return String();
}
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 NimBLEScanCallbacks {
void onResult(const NimBLEAdvertisedDevice* dev) override {
String mac = String(dev->getAddress().toString().c_str());
String hash = sha256_prefix(mac);
int rssi = dev->getRSSI();
std::lock_guard<std::mutex> lock(s_mutex);
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;
}
};
static ScanCallback s_callback;
static NimBLEScan* s_scan = nullptr;
void ble_scanner_start() {
NimBLEDevice::init("");
s_scan = NimBLEDevice::getScan();
s_scan->setScanCallbacks(&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, false, false); // duration=0 (forever), isContinue=false, restart=false
}
void ble_scanner_pause() { if (s_scan) s_scan->stop(); }
void ble_scanner_resume() { if (s_scan) s_scan->start(0, false, false); }
void ble_scanner_deinit() {
if (s_scan) s_scan->stop();
s_scan = nullptr;
NimBLEDevice::deinit(true); // frees NimBLE heap (~25KB)
}
void ble_scanner_reinit() {
ble_scanner_start(); // re-init stack and restart scan
}
BLEHourlyRecord ble_scanner_collect(uint32_t period_start, uint32_t period_end) {
// Swap accumulators under lock — minimise time with lock held
std::map<String, DeviceObs> local_seen;
int local_max = 0;
{
std::lock_guard<std::mutex> lock(s_mutex);
std::swap(local_seen, s_seen);
local_max = s_max_concurrent;
s_max_concurrent = 0;
}
// Process outside the lock — heap allocation safe here
BLEHourlyRecord rec;
rec.period_start = period_start;
rec.period_end = period_end;
rec.unique_devices = (int)local_seen.size();
rec.max_concurrent = local_max;
rec.near_count = 0; rec.mid_count = 0; rec.far_count = 0;
for (auto& kv : local_seen) {
float avg_rssi = kv.second.rssi_sum / (float)kv.second.count;
if (avg_rssi > RSSI_NEAR) rec.near_count++;
else if (avg_rssi > RSSI_MID) rec.mid_count++;
else rec.far_count++;
rec.device_hashes.push_back(kv.first);
}
return rec;
}