From 8a00665e4c4c16b88ee16f6b8f4da0fca4d24cf6 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Tue, 14 Apr 2026 10:33:23 -0700 Subject: [PATCH] fix: ArduinoOTA init, reporter mutex, BLE lock scope, NVS type Co-Authored-By: Claude Sonnet 4.6 --- firmware/src/ble_scanner.cpp | 46 +++++++++++++++++-------------- firmware/src/main.cpp | 11 ++++++++ firmware/src/reporter.cpp | 53 ++++++++++++++++++++++++++++++------ firmware/src/reporter.h | 1 + tools/flash_device.py | 2 +- 5 files changed, 83 insertions(+), 30 deletions(-) diff --git a/firmware/src/ble_scanner.cpp b/firmware/src/ble_scanner.cpp index 6534980..b465ba1 100644 --- a/firmware/src/ble_scanner.cpp +++ b/firmware/src/ble_scanner.cpp @@ -5,11 +5,12 @@ #include #include "mbedtls/md.h" #include +#include #define RSSI_NEAR -65 #define RSSI_MID -80 -static portMUX_TYPE s_mux = portMUX_INITIALIZER_UNLOCKED; +static std::mutex s_mutex; struct DeviceObs { int rssi_sum; @@ -47,7 +48,7 @@ class ScanCallback : public NimBLEAdvertisedDeviceCallbacks { String hash = sha256_prefix(mac); int rssi = dev->getRSSI(); - portENTER_CRITICAL(&s_mux); + std::lock_guard lock(s_mutex); auto it = s_seen.find(hash); if (it == s_seen.end()) { s_seen[hash] = {rssi, 1}; @@ -57,7 +58,6 @@ class ScanCallback : public NimBLEAdvertisedDeviceCallbacks { } int concurrent = (int)s_seen.size(); if (concurrent > s_max_concurrent) s_max_concurrent = concurrent; - portEXIT_CRITICAL(&s_mux); } }; @@ -79,26 +79,30 @@ 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); + // 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; } - s_seen.clear(); - 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; - portEXIT_CRITICAL(&s_mux); + 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; } diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 8fdf7f6..d474d02 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -1,6 +1,7 @@ // firmware/src/main.cpp #include #include +#include #include "config.h" #include "provisioning.h" #include "camera.h" @@ -139,8 +140,17 @@ void setup() { while (true) delay(1000); } + reporter_init(); + ble_scanner_start(); + // OTA update support + ArduinoOTA.setHostname(g_cfg.device_id.c_str()); + ArduinoOTA.onStart([]() { ble_scanner_pause(); }); + ArduinoOTA.onEnd([]() { ble_scanner_resume(); ESP.restart(); }); + ArduinoOTA.onError([](ota_error_t e) { ble_scanner_resume(); }); + ArduinoOTA.begin(); + s_cv_mutex = xSemaphoreCreateMutex(); xTaskCreatePinnedToCore(task_camera, "cam", 4096, nullptr, 2, nullptr, 1); @@ -148,6 +158,7 @@ void setup() { } void loop() { + ArduinoOTA.handle(); check_factory_reset(); if (WiFi.status() != WL_CONNECTED) { diff --git a/firmware/src/reporter.cpp b/firmware/src/reporter.cpp index a78b23f..865d028 100644 --- a/firmware/src/reporter.cpp +++ b/firmware/src/reporter.cpp @@ -6,9 +6,15 @@ #include #include #include +#include static std::vector s_cam_buf; static std::vector s_ble_buf; +static SemaphoreHandle_t s_buf_mutex = nullptr; + +void reporter_init() { + s_buf_mutex = xSemaphoreCreateMutex(); +} // Returns current Unix timestamp (NTP-synced via configTime in main.cpp). static uint32_t now_ts() { @@ -86,12 +92,19 @@ static void buf_add_ble(const BLEHourlyRecord& r) { } void reporter_submit_camera(const DeviceConfig& cfg, const CameraHourlyRecord& rec) { - if (WiFi.status() != WL_CONNECTED) { buf_add_cam(rec); return; } + if (WiFi.status() != WL_CONNECTED) { + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); + buf_add_cam(rec); + xSemaphoreGive(s_buf_mutex); + return; + } + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); std::vector batch; batch.insert(batch.end(), s_cam_buf.begin(), s_cam_buf.end()); batch.push_back(rec); s_cam_buf.clear(); + xSemaphoreGive(s_buf_mutex); // Cap to MAX_BUFFER: drop oldest to make room for newest if ((int)batch.size() > REPORTER_MAX_BUFFER) { @@ -101,17 +114,26 @@ void reporter_submit_camera(const DeviceConfig& cfg, const CameraHourlyRecord& r String body = build_camera_batch(cfg, batch); if (!post_json(cfg, "/api/v1/camera/events/batch", body)) { + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); s_cam_buf = batch; // re-buffer the whole capped batch + xSemaphoreGive(s_buf_mutex); } } void reporter_submit_ble(const DeviceConfig& cfg, const BLEHourlyRecord& rec) { - if (WiFi.status() != WL_CONNECTED) { buf_add_ble(rec); return; } + if (WiFi.status() != WL_CONNECTED) { + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); + buf_add_ble(rec); + xSemaphoreGive(s_buf_mutex); + return; + } + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); std::vector batch; batch.insert(batch.end(), s_ble_buf.begin(), s_ble_buf.end()); batch.push_back(rec); s_ble_buf.clear(); + xSemaphoreGive(s_buf_mutex); // Cap to MAX_BUFFER: drop oldest to make room for newest if ((int)batch.size() > REPORTER_MAX_BUFFER) { @@ -121,7 +143,9 @@ void reporter_submit_ble(const DeviceConfig& cfg, const BLEHourlyRecord& rec) { String body = build_ble_batch(cfg, batch); if (!post_json(cfg, "/api/v1/events/batch", body)) { + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); s_ble_buf = batch; // re-buffer the whole capped batch + xSemaphoreGive(s_buf_mutex); } } @@ -137,12 +161,25 @@ void reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rss } void reporter_flush(const DeviceConfig& cfg) { - if (!s_cam_buf.empty()) { - String body = build_camera_batch(cfg, s_cam_buf); - if (post_json(cfg, "/api/v1/camera/events/batch", body)) s_cam_buf.clear(); + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); + std::vector cam_snap = s_cam_buf; + std::vector ble_snap = s_ble_buf; + xSemaphoreGive(s_buf_mutex); + + if (!cam_snap.empty()) { + String body = build_camera_batch(cfg, cam_snap); + if (post_json(cfg, "/api/v1/camera/events/batch", body)) { + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); + s_cam_buf.clear(); + xSemaphoreGive(s_buf_mutex); + } } - if (!s_ble_buf.empty()) { - String body = build_ble_batch(cfg, s_ble_buf); - if (post_json(cfg, "/api/v1/events/batch", body)) s_ble_buf.clear(); + if (!ble_snap.empty()) { + String body = build_ble_batch(cfg, ble_snap); + if (post_json(cfg, "/api/v1/events/batch", body)) { + xSemaphoreTake(s_buf_mutex, portMAX_DELAY); + s_ble_buf.clear(); + xSemaphoreGive(s_buf_mutex); + } } } diff --git a/firmware/src/reporter.h b/firmware/src/reporter.h index 167ac3e..28c1b55 100644 --- a/firmware/src/reporter.h +++ b/firmware/src/reporter.h @@ -14,6 +14,7 @@ struct CameraHourlyRecord { static const int REPORTER_MAX_BUFFER = 24; static const char* REPORTER_API_HOST = "https://logs.research.bike"; +void reporter_init(); void reporter_submit_camera(const DeviceConfig& cfg, const CameraHourlyRecord& rec); void reporter_submit_ble(const DeviceConfig& cfg, const BLEHourlyRecord& rec); void reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rssi); diff --git a/tools/flash_device.py b/tools/flash_device.py index 223c744..a32a268 100755 --- a/tools/flash_device.py +++ b/tools/flash_device.py @@ -34,7 +34,7 @@ def build_nvs_csv(device_id, location_id, hmac_secret, f"device_id,data,string,{device_id}", f"location_id,data,string,{location_id}", f"hmac_secret,data,string,{hmac_secret}", - f"line_offset,data,u8,{line_offset}", + f"line_offset,data,u32,{line_offset}", ] if wifi_ssid is not None: rows.append(f"wifi_ssid,data,string,{wifi_ssid}")