// firmware/src/ble_scanner.cpp #include "ble_scanner.h" #include #include #include #include "mbedtls/md.h" #include #include #define RSSI_NEAR -65 #define RSSI_MID -80 static std::mutex s_mutex; struct DeviceObs { int rssi_sum; int count; }; static std::map 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 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 local_seen; int local_max = 0; { std::lock_guard 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; }