diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 27f3768..54e6cda 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -1,3 +1,144 @@ +// firmware/src/main.cpp #include -void setup() {} -void loop() {} +#include +#include "config.h" +#include "provisioning.h" +#include "camera.h" +#include "cv.h" +#include "ble_scanner.h" +#include "reporter.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 + +static DeviceConfig g_cfg; +static CVState g_cv; + +// LED: simple on/off — blink patterns can be added later +static void led_set(bool on) { digitalWrite(LED_PIN, on ? HIGH : LOW); } + +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) { + config_clear_wifi(); + ESP.restart(); + } + delay(50); + } +} + +// Camera + CV task — runs on core 1 at 5 fps +static void task_camera(void*) { + uint8_t frame[CV_PIXELS]; + while (true) { + if (camera_capture_96(frame)) { + cv_process(g_cv, frame, g_cfg.line_offset); + } + vTaskDelay(pdMS_TO_TICKS(CAM_INTERVAL_MS)); + } +} + +// Hourly reporter task — runs on core 0 +static void task_reporter(void*) { + uint32_t last_report_ts = (uint32_t)(time(nullptr)); + while (true) { + vTaskDelay(pdMS_TO_TICKS(10000)); // check every 10s + + uint32_t now = (uint32_t)(time(nullptr)); + // Skip if NTP not synced or hour not elapsed + if (now < 1700000000UL || (now - last_report_ts) < REPORT_INTERVAL_S) continue; + + uint32_t period_start = last_report_ts; + uint32_t period_end = now; + last_report_ts = now; + + // Pause BLE during upload + ble_scanner_pause(); + led_set(true); // yellow indicator (single LED: on = uploading) + + CameraHourlyRecord cam_rec = {period_start, period_end, + g_cv.entries, g_cv.exits}; + cv_reset_counts(g_cv); + + 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_resume(); + led_set(false); + } +} + +void setup() { + Serial.begin(115200); + pinMode(LED_PIN, OUTPUT); + pinMode(BUTTON_PIN, INPUT_PULLUP); + led_set(true); // on = booting + + 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(); + 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(); + ESP.restart(); + } + + 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); + } + + ble_scanner_start(); + + xTaskCreatePinnedToCore(task_camera, "cam", 4096, nullptr, 2, nullptr, 1); + xTaskCreatePinnedToCore(task_reporter, "rep", 8192, nullptr, 1, nullptr, 0); +} + +void loop() { + check_factory_reset(); + + if (WiFi.status() != WL_CONNECTED) { + led_set(true); // on = no WiFi + WiFi.reconnect(); + delay(5000); + if (WiFi.status() == WL_CONNECTED) { + led_set(false); + reporter_flush(g_cfg); + } + } + delay(1000); +}