// firmware/src/reporter.cpp #include "reporter.h" #include "hmac.h" #include #include #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() { 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& recs) { JsonDocument doc; doc["location_id"] = cfg.location_id; JsonArray arr = doc["records"].to(); for (const auto& r : recs) { JsonObject o = arr.add(); 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& recs) { JsonDocument doc; doc["device_id"] = cfg.device_id; doc["location_id"] = cfg.location_id; JsonArray arr = doc["records"].to(); for (const auto& r : recs) { JsonObject o = arr.add(); 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(); 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 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 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 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 (!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); } } }