fix(firmware): add HTTP timeouts + 3-try retry, report heartbeat status

Unbounded TLS/HTTP POSTs were blocking the reporter task indefinitely
on weak WiFi. Now: 5s connect timeout, 10s response timeout, 3 attempts
with 0/2s/5s backoff. Every attempt logs HTTP_OK or HTTP_FAIL to the
event log. reporter_heartbeat now returns bool so the caller can count
consecutive misses.
This commit is contained in:
2026-04-23 13:44:17 -07:00
parent 57129ba078
commit 8f8ad0b1b0
2 changed files with 31 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
// firmware/src/reporter.cpp // firmware/src/reporter.cpp
#include "reporter.h" #include "reporter.h"
#include "hmac.h" #include "hmac.h"
#include "event_log.h"
#include <HTTPClient.h> #include <HTTPClient.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <WiFi.h> #include <WiFi.h>
@@ -21,25 +22,46 @@ static uint32_t now_ts() {
return (uint32_t)time(nullptr); return (uint32_t)time(nullptr);
} }
static bool post_json(const DeviceConfig& cfg, const char* path, const String& body) { static bool post_json_once(const DeviceConfig& cfg, const char* path, const String& body) {
uint32_t ts = now_ts(); uint32_t ts = now_ts();
// Reject if NTP hasn't synced yet (timestamp would be near epoch 0) if (ts < 1700000000UL) return false;
if (ts < 1700000000UL) return false; // pre-2023 → clock not valid
String sig = hmac_sign(cfg.hmac_secret, "POST", path, ts, body); String sig = hmac_sign(cfg.hmac_secret, "POST", path, ts, body);
if (sig.isEmpty()) return false; // HMAC failed if (sig.isEmpty()) return false;
HTTPClient http; HTTPClient http;
String url = String(REPORTER_API_HOST) + path; String url = String(REPORTER_API_HOST) + path;
http.begin(url); http.begin(url);
http.setConnectTimeout(5000); // DNS + TCP connect
http.setTimeout(10000); // per-transaction response timeout
http.addHeader("Content-Type", "application/json"); http.addHeader("Content-Type", "application/json");
http.addHeader("X-Device-Id", cfg.device_id); http.addHeader("X-Device-Id", cfg.device_id);
http.addHeader("X-Timestamp", String(ts)); http.addHeader("X-Timestamp", String(ts));
http.addHeader("X-Signature", sig); http.addHeader("X-Signature", sig);
uint32_t t0 = millis();
int code = http.POST(body); int code = http.POST(body);
uint32_t elapsed = millis() - t0;
http.end(); http.end();
Serial.printf("[HTTP] POST %s → %d\n", url.c_str(), code); uint16_t phash = event_log_path_hash(path);
return (code == 200); Serial.printf("[HTTP] POST %s -> %d (%u ms)\n", url.c_str(), code, (unsigned)elapsed);
if (code == 200) {
event_log_write(EVT_HTTP_OK, phash, (uint16_t)((elapsed > 65535) ? 65535 : elapsed));
return true;
}
event_log_write(EVT_HTTP_FAIL, phash, (uint16_t)code);
return false;
}
static bool post_json(const DeviceConfig& cfg, const char* path, const String& body) {
// 3 attempts with 0/2s/5s delays. Total worst case ~ 2x(5+10)s + 7s = 37s;
// acceptable for an hourly task and within the 30s TWDT once Task 6 lands
// IF TWDT is fed between attempts (see Task 6).
static const uint16_t DELAYS_MS[] = { 0, 2000, 5000 };
for (int i = 0; i < 3; i++) {
if (DELAYS_MS[i]) vTaskDelay(pdMS_TO_TICKS(DELAYS_MS[i]));
if (post_json_once(cfg, path, body)) return true;
}
return false;
} }
static String build_camera_batch(const DeviceConfig& cfg, static String build_camera_batch(const DeviceConfig& cfg,
@@ -147,7 +169,7 @@ void reporter_submit_ble(const DeviceConfig& cfg, const BLEHourlyRecord& rec) {
} }
} }
void reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rssi) { bool reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rssi) {
JsonDocument doc; JsonDocument doc;
doc["device_id"] = cfg.device_id; doc["device_id"] = cfg.device_id;
doc["firmware_version"] = "1.0.0"; doc["firmware_version"] = "1.0.0";
@@ -156,7 +178,7 @@ void reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rss
doc["pending_records"] = (int)(s_cam_buf.size() + s_ble_buf.size()); doc["pending_records"] = (int)(s_cam_buf.size() + s_ble_buf.size());
doc["uptime_seconds"] = uptime_s; doc["uptime_seconds"] = uptime_s;
String body; serializeJson(doc, body); String body; serializeJson(doc, body);
post_json(cfg, "/api/v1/heartbeat", body); return post_json(cfg, "/api/v1/heartbeat", body);
} }
void reporter_flush(const DeviceConfig& cfg) { void reporter_flush(const DeviceConfig& cfg) {

View File

@@ -17,5 +17,5 @@ static const char* REPORTER_API_HOST = "http://logs.research.bike";
void reporter_init(); void reporter_init();
void reporter_submit_camera(const DeviceConfig& cfg, const CameraHourlyRecord& rec); void reporter_submit_camera(const DeviceConfig& cfg, const CameraHourlyRecord& rec);
void reporter_submit_ble(const DeviceConfig& cfg, const BLEHourlyRecord& rec); void reporter_submit_ble(const DeviceConfig& cfg, const BLEHourlyRecord& rec);
void reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rssi); bool reporter_heartbeat(const DeviceConfig& cfg, uint32_t uptime_s, int wifi_rssi);
void reporter_flush(const DeviceConfig& cfg); void reporter_flush(const DeviceConfig& cfg);