30s TWDT subscribes all three long-running tasks and panics on hang. The reporter task's retry loop explicitly feeds between attempts so the 3-try sequence (worst case 52s) does not itself trip the dog. Reset reason on next boot is visible via esp_reset_reason() which EVT_BOOT already logs.
236 lines
7.6 KiB
C++
236 lines
7.6 KiB
C++
// firmware/src/main.cpp
|
|
#include <Arduino.h>
|
|
#include <WiFi.h>
|
|
#include <ArduinoOTA.h>
|
|
#include "config.h"
|
|
#include "provisioning.h"
|
|
#include "camera.h"
|
|
#include "cv.h"
|
|
#include "ble_scanner.h"
|
|
#include "reporter.h"
|
|
#include "event_log.h"
|
|
#include "net_guard.h"
|
|
#include <esp_system.h>
|
|
#include <esp_task_wdt.h>
|
|
|
|
// LED on GPIO2 (TimerCamera-F built-in LED) — verify against board schematic
|
|
// Factory reset: hold GPIO37 (BOOT button) for 5 seconds
|
|
#define LED_PIN 2
|
|
#define BUTTON_PIN 37
|
|
#define FACTORY_RESET_HOLD_MS 5000
|
|
|
|
#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;
|
|
static SemaphoreHandle_t s_cv_mutex = nullptr;
|
|
|
|
static void led_set(bool on) { digitalWrite(LED_PIN, on ? HIGH : LOW); }
|
|
|
|
// Non-blocking-ish detection blink. Saves and restores the current LED state
|
|
// so it doesn't clobber upload/no-wifi indicators. Total duration: ~60ms per
|
|
// pulse + 80ms gap between pulses.
|
|
static void led_blink_pattern(int pulses) {
|
|
bool prev = digitalRead(LED_PIN);
|
|
for (int i = 0; i < pulses; i++) {
|
|
led_set(true);
|
|
vTaskDelay(pdMS_TO_TICKS(60));
|
|
led_set(false);
|
|
if (i < pulses - 1) vTaskDelay(pdMS_TO_TICKS(80));
|
|
}
|
|
led_set(prev);
|
|
}
|
|
|
|
static void check_factory_reset() {
|
|
if (digitalRead(BUTTON_PIN) != LOW) return;
|
|
uint32_t held = millis();
|
|
while (digitalRead(BUTTON_PIN) == LOW) {
|
|
if (millis() - held >= FACTORY_RESET_HOLD_MS) {
|
|
event_log_write(EVT_REBOOT, REBOOT_FACTORY_RESET, 0);
|
|
config_clear_wifi();
|
|
ESP.restart();
|
|
}
|
|
delay(50);
|
|
esp_task_wdt_reset();
|
|
}
|
|
}
|
|
|
|
// Camera + CV task — runs on core 1 at 5 fps
|
|
static void task_camera(void*) {
|
|
static uint8_t frame[CV_PIXELS]; // static: avoids 9KB on task stack
|
|
int last_logged_track_id = 0; // diagnostic: log each new track once
|
|
esp_task_wdt_add(nullptr);
|
|
while (true) {
|
|
if (camera_capture_96(frame)) {
|
|
if (xSemaphoreTake(s_cv_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
CVResult r = cv_process(g_cv, frame, g_cfg.line_offset);
|
|
for (const auto& t : g_cv.tracks) {
|
|
if (t.id > last_logged_track_id) {
|
|
last_logged_track_id = t.id;
|
|
Serial.printf("[CV] spawn id=%d y=%.1f\n", t.id, t.spawn_y);
|
|
}
|
|
}
|
|
if (r.fg_count > 0) {
|
|
Serial.printf("[F] n=%d y=%d..%d c=%.1f\n",
|
|
r.fg_count, r.fg_min_y, r.fg_max_y, r.fg_centroid_y);
|
|
}
|
|
if (r.entries_delta) Serial.printf("[CV] entry +%d (total %d) first=%.1f min=%.1f max=%.1f last=%.1f dur=%d\n",
|
|
r.entries_delta, g_cv.entries,
|
|
r.fire_first_c, r.fire_min_c, r.fire_max_c, r.fire_last_c, r.fire_duration);
|
|
if (r.exits_delta) Serial.printf("[CV] exit +%d (total %d) first=%.1f min=%.1f max=%.1f last=%.1f dur=%d\n",
|
|
r.exits_delta, g_cv.exits,
|
|
r.fire_first_c, r.fire_min_c, r.fire_max_c, r.fire_last_c, r.fire_duration);
|
|
xSemaphoreGive(s_cv_mutex);
|
|
if (r.entries_delta) led_blink_pattern(1);
|
|
if (r.exits_delta) led_blink_pattern(2);
|
|
}
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(CAM_INTERVAL_MS));
|
|
esp_task_wdt_reset();
|
|
}
|
|
}
|
|
|
|
// Hourly reporter task — runs on core 0
|
|
static void task_reporter(void*) {
|
|
uint32_t last_report_ts = 0; // 0 = not initialized yet
|
|
esp_task_wdt_add(nullptr);
|
|
|
|
while (true) {
|
|
vTaskDelay(pdMS_TO_TICKS(10000)); // check every 10s
|
|
esp_task_wdt_reset();
|
|
|
|
uint32_t now = (uint32_t)(time(nullptr));
|
|
if (now < 1700000000UL) continue; // NTP not synced
|
|
|
|
// 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;
|
|
|
|
uint32_t period_start = last_report_ts;
|
|
uint32_t period_end = now;
|
|
last_report_ts = now;
|
|
|
|
// 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) {
|
|
cam_rec = {period_start, period_end, g_cv.entries, g_cv.exits};
|
|
cv_reset_counts(g_cv);
|
|
xSemaphoreGive(s_cv_mutex);
|
|
} else {
|
|
// Failed to acquire — skip this cycle, will report next hour
|
|
ble_scanner_reinit();
|
|
led_set(false);
|
|
continue;
|
|
}
|
|
|
|
BLEHourlyRecord ble_rec = ble_scanner_collect(period_start, period_end);
|
|
|
|
reporter_submit_camera(g_cfg, cam_rec);
|
|
reporter_submit_ble(g_cfg, ble_rec);
|
|
reporter_heartbeat(g_cfg, millis() / 1000, WiFi.RSSI());
|
|
|
|
ble_scanner_reinit();
|
|
led_set(false);
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
pinMode(LED_PIN, OUTPUT);
|
|
pinMode(BUTTON_PIN, INPUT_PULLUP);
|
|
led_set(true); // on = booting
|
|
|
|
event_log_init();
|
|
event_log_write(EVT_BOOT, (uint16_t)esp_reset_reason(), 0);
|
|
|
|
if (!config_load(g_cfg)) {
|
|
Serial.println("FATAL: device_id/location_id/hmac_secret not provisioned");
|
|
while (true) { delay(500); led_set(!digitalRead(LED_PIN)); } // fast blink
|
|
}
|
|
|
|
// Connect to WiFi
|
|
if (!config_has_wifi()) {
|
|
provisioning_run();
|
|
event_log_write(EVT_REBOOT, REBOOT_WIFI_REPROV, 0);
|
|
ESP.restart();
|
|
}
|
|
|
|
WiFi.begin(g_cfg.wifi_ssid.c_str(), g_cfg.wifi_pass.c_str());
|
|
uint32_t wifi_start = millis();
|
|
while (WiFi.status() != WL_CONNECTED && millis() - wifi_start < 15000) {
|
|
check_factory_reset();
|
|
delay(200);
|
|
}
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
// Saved creds failed — re-provision
|
|
provisioning_run();
|
|
event_log_write(EVT_REBOOT, REBOOT_WIFI_REPROV, 0);
|
|
ESP.restart();
|
|
}
|
|
|
|
net_guard_start(g_cfg);
|
|
led_set(false); // off = connected
|
|
|
|
// NTP sync (UTC)
|
|
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
|
|
|
|
cv_init(g_cv);
|
|
|
|
if (!camera_init()) {
|
|
Serial.println("FATAL: camera init failed");
|
|
while (true) delay(1000);
|
|
}
|
|
|
|
reporter_init();
|
|
|
|
ble_scanner_start();
|
|
|
|
// OTA update support
|
|
ArduinoOTA.setHostname(g_cfg.device_id.c_str());
|
|
ArduinoOTA.onStart([]() { ble_scanner_pause(); });
|
|
ArduinoOTA.onEnd([]() {
|
|
ble_scanner_resume();
|
|
event_log_write(EVT_REBOOT, REBOOT_OTA, 0);
|
|
ESP.restart();
|
|
});
|
|
ArduinoOTA.onError([](ota_error_t e) { ble_scanner_resume(); });
|
|
ArduinoOTA.begin();
|
|
|
|
s_cv_mutex = xSemaphoreCreateMutex();
|
|
|
|
// Task watchdog: 30s timeout, panic on trigger so we reboot and log
|
|
// via esp_reset_reason() in EVT_BOOT on the next boot.
|
|
esp_task_wdt_init(30, /*panic=*/true);
|
|
esp_task_wdt_add(nullptr); // subscribe the Arduino loopTask
|
|
|
|
xTaskCreatePinnedToCore(task_camera, "cam", 8192, nullptr, 2, nullptr, 1);
|
|
xTaskCreatePinnedToCore(task_reporter, "rep", 8192, nullptr, 1, nullptr, 0);
|
|
}
|
|
|
|
void loop() {
|
|
esp_task_wdt_reset();
|
|
ArduinoOTA.handle();
|
|
check_factory_reset();
|
|
net_guard_tick();
|
|
|
|
static bool s_was_up = true;
|
|
bool up = net_guard_is_up();
|
|
if (up != s_was_up) {
|
|
led_set(!up); // LED on when NOT up
|
|
if (up) reporter_flush(g_cfg);
|
|
s_was_up = up;
|
|
}
|
|
delay(200);
|
|
}
|