- 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>
185 lines
6.3 KiB
C++
185 lines
6.3 KiB
C++
// firmware/src/reporter.cpp
|
|
#include "reporter.h"
|
|
#include "hmac.h"
|
|
#include <HTTPClient.h>
|
|
#include <ArduinoJson.h>
|
|
#include <WiFi.h>
|
|
#include <vector>
|
|
#include <time.h>
|
|
#include <freertos/semphr.h>
|
|
|
|
static std::vector<CameraHourlyRecord> s_cam_buf;
|
|
static std::vector<BLEHourlyRecord> 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() {
|
|
return (uint32_t)time(nullptr);
|
|
}
|
|
|
|
static bool post_json(const DeviceConfig& cfg, const char* path, const String& body) {
|
|
uint32_t ts = now_ts();
|
|
// Reject if NTP hasn't synced yet (timestamp would be near epoch 0)
|
|
if (ts < 1700000000UL) return false; // pre-2023 → clock not valid
|
|
String sig = hmac_sign(cfg.hmac_secret, "POST", path, ts, body);
|
|
if (sig.isEmpty()) return false; // HMAC failed
|
|
|
|
HTTPClient http;
|
|
String url = String(REPORTER_API_HOST) + path;
|
|
http.begin(url);
|
|
http.addHeader("Content-Type", "application/json");
|
|
http.addHeader("X-Device-Id", cfg.device_id);
|
|
http.addHeader("X-Timestamp", String(ts));
|
|
http.addHeader("X-Signature", sig);
|
|
|
|
int code = http.POST(body);
|
|
http.end();
|
|
Serial.printf("[HTTP] POST %s → %d\n", url.c_str(), code);
|
|
return (code == 200);
|
|
}
|
|
|
|
static String build_camera_batch(const DeviceConfig& cfg,
|
|
const std::vector<CameraHourlyRecord>& recs) {
|
|
JsonDocument doc;
|
|
doc["location_id"] = cfg.location_id;
|
|
JsonArray arr = doc["records"].to<JsonArray>();
|
|
for (const auto& r : recs) {
|
|
JsonObject o = arr.add<JsonObject>();
|
|
o["period_start"] = r.period_start;
|
|
o["period_end"] = r.period_end;
|
|
o["entries"] = r.entries;
|
|
o["exits"] = r.exits;
|
|
}
|
|
String out; serializeJson(doc, out);
|
|
return out;
|
|
}
|
|
|
|
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) {
|
|
JsonObject o = arr.add<JsonObject>();
|
|
o["period_start"] = r.period_start;
|
|
o["period_end"] = r.period_end;
|
|
o["unique_devices"] = r.unique_devices;
|
|
o["max_concurrent"] = r.max_concurrent;
|
|
o["near_count"] = r.near_count;
|
|
o["mid_count"] = r.mid_count;
|
|
o["far_count"] = r.far_count;
|
|
if (!r.device_hashes.empty()) {
|
|
JsonArray ha = o["device_hashes"].to<JsonArray>();
|
|
for (const auto& h : r.device_hashes) ha.add(h);
|
|
}
|
|
}
|
|
String out; serializeJson(doc, out);
|
|
return out;
|
|
}
|
|
|
|
static void buf_add_cam(const CameraHourlyRecord& r) {
|
|
if ((int)s_cam_buf.size() < REPORTER_MAX_BUFFER) s_cam_buf.push_back(r);
|
|
}
|
|
static void buf_add_ble(const BLEHourlyRecord& r) {
|
|
if ((int)s_ble_buf.size() < REPORTER_MAX_BUFFER) s_ble_buf.push_back(r);
|
|
}
|
|
|
|
void reporter_submit_camera(const DeviceConfig& cfg, const CameraHourlyRecord& rec) {
|
|
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<CameraHourlyRecord> 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) {
|
|
batch.erase(batch.begin(),
|
|
batch.begin() + ((int)batch.size() - REPORTER_MAX_BUFFER));
|
|
}
|
|
|
|
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) {
|
|
xSemaphoreTake(s_buf_mutex, portMAX_DELAY);
|
|
buf_add_ble(rec);
|
|
xSemaphoreGive(s_buf_mutex);
|
|
return;
|
|
}
|
|
|
|
xSemaphoreTake(s_buf_mutex, portMAX_DELAY);
|
|
std::vector<BLEHourlyRecord> 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) {
|
|
batch.erase(batch.begin(),
|
|
batch.begin() + ((int)batch.size() - REPORTER_MAX_BUFFER));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
doc["pending_records"] = (int)(s_cam_buf.size() + s_ble_buf.size());
|
|
doc["uptime_seconds"] = uptime_s;
|
|
String body; serializeJson(doc, body);
|
|
post_json(cfg, "/api/v1/heartbeat", body);
|
|
}
|
|
|
|
void reporter_flush(const DeviceConfig& cfg) {
|
|
xSemaphoreTake(s_buf_mutex, portMAX_DELAY);
|
|
std::vector<CameraHourlyRecord> cam_snap = s_cam_buf;
|
|
std::vector<BLEHourlyRecord> 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 (!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);
|
|
}
|
|
}
|
|
}
|