feat: production-ready firmware with BLE memory management, device_id fixes, and docs
- Reduce debug level to 1 (errors only) for production builds - Replace BLE pause/resume with full deinit/reinit during HTTP uploads (~25KB freed) - Add 60s boot report delay for fast post-deploy connectivity verification - Add device_id to BLE batch and heartbeat request bodies - Correct API host to http:// (plain HTTP, not HTTPS) - Add HTTP response logging and CV entry/exit serial logging - Create root README.md with operator setup and architecture overview - Update design spec: HMAC format, BLE memory approach, request body shapes, reporting intervals Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ board_build.partitions = partitions_4mb_ota.csv
|
||||
build_flags =
|
||||
-DBOARD_HAS_PSRAM
|
||||
-mfix-esp32-psram-cache-issue
|
||||
-DCORE_DEBUG_LEVEL=3
|
||||
-DCORE_DEBUG_LEVEL=1
|
||||
-DCONFIG_BT_NIMBLE_ENABLED=1
|
||||
-DCONFIG_SPIRAM_USE_MALLOC=1
|
||||
-DCONFIG_ARDUINO_LOOP_STACK_SIZE=16384
|
||||
|
||||
@@ -51,6 +51,7 @@ class ScanCallback : public NimBLEAdvertisedDeviceCallbacks {
|
||||
std::lock_guard<std::mutex> lock(s_mutex);
|
||||
auto it = s_seen.find(hash);
|
||||
if (it == s_seen.end()) {
|
||||
Serial.printf("[BLE] new device: %s (rssi %d)\n", hash.c_str(), rssi);
|
||||
s_seen[hash] = {rssi, 1};
|
||||
} else {
|
||||
it->second.rssi_sum += rssi;
|
||||
@@ -75,9 +76,19 @@ void ble_scanner_start() {
|
||||
s_scan->start(0, nullptr, false); // 0 = continuous
|
||||
}
|
||||
|
||||
void ble_scanner_pause() { if (s_scan) s_scan->stop(); }
|
||||
void ble_scanner_pause() { if (s_scan) s_scan->stop(); }
|
||||
void ble_scanner_resume() { if (s_scan) s_scan->start(0, nullptr, 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;
|
||||
|
||||
@@ -17,9 +17,13 @@ struct BLEHourlyRecord {
|
||||
// Start continuous passive BLE scan (call once at boot).
|
||||
void ble_scanner_start();
|
||||
|
||||
// Pause scan for ~3s during HTTP upload.
|
||||
// Pause/resume scan (lightweight — stack stays initialized).
|
||||
void ble_scanner_pause();
|
||||
void ble_scanner_resume();
|
||||
|
||||
// Full deinit/reinit — frees ~25KB NimBLE heap for SSL, then restarts scan.
|
||||
void ble_scanner_deinit();
|
||||
void ble_scanner_reinit();
|
||||
|
||||
// Collect current hour's record and reset accumulators. Thread-safe.
|
||||
BLEHourlyRecord ble_scanner_collect(uint32_t period_start, uint32_t period_end);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#define CAM_FPS 5
|
||||
#define CAM_INTERVAL_MS (1000 / CAM_FPS)
|
||||
#define REPORT_INTERVAL_S 3600
|
||||
#define BOOT_REPORT_DELAY_S 60 // first report fires 60s after NTP sync
|
||||
|
||||
static DeviceConfig g_cfg;
|
||||
static CVState g_cv;
|
||||
@@ -44,7 +45,9 @@ static void task_camera(void*) {
|
||||
while (true) {
|
||||
if (camera_capture_96(frame)) {
|
||||
if (xSemaphoreTake(s_cv_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
cv_process(g_cv, frame, g_cfg.line_offset);
|
||||
CVResult r = cv_process(g_cv, frame, g_cfg.line_offset);
|
||||
if (r.entries_delta) Serial.printf("[CV] entry +%d (total %d)\n", r.entries_delta, g_cv.entries);
|
||||
if (r.exits_delta) Serial.printf("[CV] exit +%d (total %d)\n", r.exits_delta, g_cv.exits);
|
||||
xSemaphoreGive(s_cv_mutex);
|
||||
}
|
||||
}
|
||||
@@ -62,8 +65,11 @@ static void task_reporter(void*) {
|
||||
uint32_t now = (uint32_t)(time(nullptr));
|
||||
if (now < 1700000000UL) continue; // NTP not synced
|
||||
|
||||
// First valid timestamp — initialize without reporting
|
||||
if (last_report_ts == 0) { last_report_ts = now; continue; }
|
||||
// First valid timestamp — schedule boot report 60s from now
|
||||
if (last_report_ts == 0) {
|
||||
last_report_ts = now - (REPORT_INTERVAL_S - BOOT_REPORT_DELAY_S);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((now - last_report_ts) < REPORT_INTERVAL_S) continue;
|
||||
|
||||
@@ -71,9 +77,9 @@ static void task_reporter(void*) {
|
||||
uint32_t period_end = now;
|
||||
last_report_ts = now;
|
||||
|
||||
// Pause BLE during upload
|
||||
ble_scanner_pause();
|
||||
led_set(true); // yellow indicator (single LED: on = uploading)
|
||||
// Deinit BLE to free ~25KB heap for SSL handshakes
|
||||
ble_scanner_deinit();
|
||||
led_set(true); // on = uploading
|
||||
|
||||
CameraHourlyRecord cam_rec;
|
||||
if (xSemaphoreTake(s_cv_mutex, pdMS_TO_TICKS(500)) == pdTRUE) {
|
||||
@@ -82,7 +88,7 @@ static void task_reporter(void*) {
|
||||
xSemaphoreGive(s_cv_mutex);
|
||||
} else {
|
||||
// Failed to acquire — skip this cycle, will report next hour
|
||||
ble_scanner_resume();
|
||||
ble_scanner_reinit();
|
||||
led_set(false);
|
||||
continue;
|
||||
}
|
||||
@@ -93,7 +99,7 @@ static void task_reporter(void*) {
|
||||
reporter_submit_ble(g_cfg, ble_rec);
|
||||
reporter_heartbeat(g_cfg, millis() / 1000, WiFi.RSSI());
|
||||
|
||||
ble_scanner_resume();
|
||||
ble_scanner_reinit();
|
||||
led_set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ static bool post_json(const DeviceConfig& cfg, const char* path, const String& b
|
||||
|
||||
HTTPClient http;
|
||||
String url = String(REPORTER_API_HOST) + path;
|
||||
// NOTE: Certificate validation is disabled — connection is encrypted but
|
||||
// server identity is not verified. To enable validation, use WiFiClientSecure
|
||||
// with setCACert() before calling http.begin(client, url).
|
||||
// Acceptable for this deployment: devices operate on store WiFi, not public internet.
|
||||
http.begin(url);
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
http.addHeader("X-Device-Id", cfg.device_id);
|
||||
@@ -42,6 +38,7 @@ static bool post_json(const DeviceConfig& cfg, const char* path, const String& b
|
||||
|
||||
int code = http.POST(body);
|
||||
http.end();
|
||||
Serial.printf("[HTTP] POST %s → %d\n", url.c_str(), code);
|
||||
return (code == 200);
|
||||
}
|
||||
|
||||
@@ -64,6 +61,7 @@ static String build_camera_batch(const DeviceConfig& cfg,
|
||||
static String build_ble_batch(const DeviceConfig& cfg,
|
||||
const std::vector<BLEHourlyRecord>& recs) {
|
||||
JsonDocument doc;
|
||||
doc["device_id"] = cfg.device_id;
|
||||
doc["location_id"] = cfg.location_id;
|
||||
JsonArray arr = doc["records"].to<JsonArray>();
|
||||
for (const auto& r : recs) {
|
||||
@@ -151,6 +149,7 @@ void reporter_submit_ble(const DeviceConfig& cfg, const BLEHourlyRecord& rec) {
|
||||
|
||||
void reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rssi) {
|
||||
JsonDocument doc;
|
||||
doc["device_id"] = cfg.device_id;
|
||||
doc["firmware_version"] = "1.0.0";
|
||||
doc["free_storage_pct"] = 100;
|
||||
doc["wifi_rssi"] = wifi_rssi;
|
||||
|
||||
@@ -12,7 +12,7 @@ struct CameraHourlyRecord {
|
||||
};
|
||||
|
||||
static const int REPORTER_MAX_BUFFER = 24;
|
||||
static const char* REPORTER_API_HOST = "https://logs.research.bike";
|
||||
static const char* REPORTER_API_HOST = "http://logs.research.bike";
|
||||
|
||||
void reporter_init();
|
||||
void reporter_submit_camera(const DeviceConfig& cfg, const CameraHourlyRecord& rec);
|
||||
|
||||
Reference in New Issue
Block a user