diff --git a/firmware/lib/hmac/hmac.cpp b/firmware/lib/hmac/hmac.cpp index fee48d5..6e174b2 100644 --- a/firmware/lib/hmac/hmac.cpp +++ b/firmware/lib/hmac/hmac.cpp @@ -15,8 +15,8 @@ static HString bytes_to_hex(const uint8_t* bytes, size_t len) { } static void hex_to_bytes(const HString& hex, uint8_t* out, size_t out_len) { - if (hex.size() % 2 != 0) return; // malformed — odd-length hex - for (size_t i = 0; i < out_len && (i * 2 + 1) < hex.size(); i++) { + if (hex.length() % 2 != 0) return; // malformed — odd-length hex + for (size_t i = 0; i < out_len && (i * 2 + 1) < hex.length(); i++) { char byte_str[3] = {hex[i*2], hex[i*2+1], 0}; out[i] = (uint8_t)strtol(byte_str, nullptr, 16); } @@ -39,7 +39,7 @@ HString hmac_sign(const HString& secret_hex, const HString& device_id, uint32_t timestamp, const HString& body) { // 1. SHA256(body) uint8_t body_hash[32] = {}; - if (!sha256((const uint8_t*)body.c_str(), body.size(), body_hash)) { + if (!sha256((const uint8_t*)body.c_str(), body.length(), body_hash)) { return HString{}; } HString body_hash_hex = bytes_to_hex(body_hash, 32); @@ -50,7 +50,7 @@ HString hmac_sign(const HString& secret_hex, const HString& device_id, HString message = device_id + ":" + ts_buf + ":" + body_hash_hex; // 3. Decode secret from hex - size_t secret_len = secret_hex.size() / 2; + size_t secret_len = secret_hex.length() / 2; uint8_t secret[64] = {}; hex_to_bytes(secret_hex, secret, secret_len); @@ -62,7 +62,7 @@ HString hmac_sign(const HString& secret_hex, const HString& device_id, int ret2 = mbedtls_md_setup(&ctx, info, 1); if (ret2 != 0) { mbedtls_md_free(&ctx); return HString{}; } mbedtls_md_hmac_starts(&ctx, secret, secret_len); - mbedtls_md_hmac_update(&ctx, (const uint8_t*)message.c_str(), message.size()); + mbedtls_md_hmac_update(&ctx, (const uint8_t*)message.c_str(), message.length()); mbedtls_md_hmac_finish(&ctx, hmac_result); mbedtls_md_free(&ctx); diff --git a/firmware/src/reporter.cpp b/firmware/src/reporter.cpp new file mode 100644 index 0000000..4fe2ec0 --- /dev/null +++ b/firmware/src/reporter.cpp @@ -0,0 +1,130 @@ +// firmware/src/reporter.cpp +#include "reporter.h" +#include "hmac.h" +#include +#include +#include +#include +#include + +static std::vector s_cam_buf; +static std::vector s_ble_buf; + +// 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(); + String sig = hmac_sign(cfg.hmac_secret, cfg.device_id, 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-HMAC-Signature", sig); + + int code = http.POST(body); + http.end(); + 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["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) { buf_add_cam(rec); return; } + + std::vector batch; + batch.insert(batch.end(), s_cam_buf.begin(), s_cam_buf.end()); + batch.push_back(rec); + s_cam_buf.clear(); + + String body = build_camera_batch(cfg, batch); + if (!post_json(cfg, "/api/v1/camera/events/batch", body)) { + for (const auto& r : batch) buf_add_cam(r); + } +} + +void reporter_submit_ble(const DeviceConfig& cfg, const BLEHourlyRecord& rec) { + if (WiFi.status() != WL_CONNECTED) { buf_add_ble(rec); return; } + + std::vector batch; + batch.insert(batch.end(), s_ble_buf.begin(), s_ble_buf.end()); + batch.push_back(rec); + s_ble_buf.clear(); + + String body = build_ble_batch(cfg, batch); + if (!post_json(cfg, "/api/v1/events/batch", body)) { + for (const auto& r : batch) buf_add_ble(r); + } +} + +void reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rssi) { + JsonDocument doc; + 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) { + 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(); + } + 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(); + } +} diff --git a/firmware/src/reporter.h b/firmware/src/reporter.h new file mode 100644 index 0000000..167ac3e --- /dev/null +++ b/firmware/src/reporter.h @@ -0,0 +1,20 @@ +// firmware/src/reporter.h +#pragma once +#include +#include "config.h" +#include "ble_scanner.h" + +struct CameraHourlyRecord { + uint32_t period_start; + uint32_t period_end; + int entries; + int exits; +}; + +static const int REPORTER_MAX_BUFFER = 24; +static const char* REPORTER_API_HOST = "https://logs.research.bike"; + +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); +void reporter_flush(const DeviceConfig& cfg);