502 lines
17 KiB
C
502 lines
17 KiB
C
#include "esp_task_wdt.h"
|
|
#include "esp_system.h"
|
|
#include "esp_ota_ops.h"
|
|
#include "i2c.h"
|
|
#include "log_test.h"
|
|
#include "partition_test.h"
|
|
#include "storage.h"
|
|
#include "uart_comms.h"
|
|
#include "esp_err.h"
|
|
#include "esp_log.h"
|
|
#include "endian.h"
|
|
#include "control_fsm.h"
|
|
#include "sc_err.h"
|
|
#include "power_mgmt.h"
|
|
#include "rtc.h"
|
|
#include "sensors.h"
|
|
#include "solar.h"
|
|
#include "rf_433.h"
|
|
#include "bt_hid.h"
|
|
#include "webserver.h"
|
|
#include "bringup.h"
|
|
#include "comms_events.h"
|
|
#include "version.h"
|
|
#include <string.h>
|
|
|
|
EventGroupHandle_t comms_event_group = NULL;
|
|
|
|
#define TAG "MAIN"
|
|
|
|
#define POST_MAX_RETRIES 3
|
|
#define OTA_ROLLBACK_THRESHOLD 5
|
|
#define FACTORY_RESET_HOLD_MS 10000
|
|
|
|
// Survives resets (panic, WDT, sw reset) but NOT power-on or external reset
|
|
RTC_DATA_ATTR static uint8_t ota_reset_counter = 0;
|
|
|
|
// Try an init function up to POST_MAX_RETRIES times. On final failure, reboot.
|
|
// Critical inits (ADC, I2C, storage, FSM, sensors) use this — a permanent failure
|
|
// feeds the OTA rollback reset counter via the panic→reboot path.
|
|
static void init_critical(const char *name, esp_err_t (*fn)(void)) {
|
|
for (int attempt = 1; attempt <= POST_MAX_RETRIES; attempt++) {
|
|
esp_err_t err = fn();
|
|
if (err == ESP_OK) return;
|
|
ESP_LOGE(TAG, "%s FAILED (attempt %d/%d): %s", name, attempt, POST_MAX_RETRIES, esp_err_to_name(err));
|
|
if (attempt < POST_MAX_RETRIES) vTaskDelay(pdMS_TO_TICKS(100));
|
|
}
|
|
ESP_LOGE(TAG, "%s FAILED after %d attempts — rebooting", name, POST_MAX_RETRIES);
|
|
vTaskDelay(pdMS_TO_TICKS(500));
|
|
esp_restart();
|
|
}
|
|
|
|
int64_t last_bat_log_time = 0;
|
|
esp_err_t send_bat_log() {
|
|
if(!rtc_is_set()) return ESP_OK;
|
|
|
|
uint8_t entry[12] = {};
|
|
|
|
// Pack 64-bit timestamp into bytes 1-8
|
|
uint64_t be_timestamp = rtc_get_ms();
|
|
memcpy(&entry[0], &be_timestamp, 8);
|
|
|
|
// Pack 32-bit voltages/currents into bytes 9-24
|
|
float be_voltage = get_battery_V();
|
|
memcpy(&entry[8], &be_voltage, 4);
|
|
|
|
last_bat_log_time = esp_timer_get_time();
|
|
|
|
log_write(entry, 12, LOG_TYPE_BAT);
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
|
|
// --- LED Status Indicators ---
|
|
// See docs/button_behavior.md for button LED feedback.
|
|
// Status LEDs:
|
|
// IDLE: LED1 blinks 0.5Hz (1s on / 1s off)
|
|
// ERROR: 5Hz rapid blink 1s, then hold error code 2s (3s cycle)
|
|
// Error code bits: LED1=efuse, LED2=RTC/battery, LED3=safety/leash/FSM
|
|
// WATERFALL: 001→011→111→110→100→000, ~1 cycle/s (moving, delays)
|
|
// CALIBRATING: all LEDs flash 1Hz (500ms on / 500ms off)
|
|
// UNDO: solid all LEDs on
|
|
// BOOTING: LED1 solid
|
|
|
|
// LED error code bits: LED1=efuse/battery, LED2=RTC, LED3=safety/leash
|
|
static uint8_t error_code_from_state(void) {
|
|
uint8_t code = 0;
|
|
if (efuse_get(BRIDGE_JACK) != EFUSE_OK ||
|
|
efuse_get(BRIDGE_AUX) != EFUSE_OK ||
|
|
efuse_get(BRIDGE_DRIVE) != EFUSE_OK) code |= 0b001; // LED1: efuse
|
|
float bat_v = get_battery_V();
|
|
float low_v = get_param_value_t(PARAM_LOW_PROTECTION_V).f32;
|
|
if (bat_v > 0 && bat_v < low_v) code |= 0b001; // LED1: low battery
|
|
if (!rtc_is_set()) code |= 0b010; // LED2: RTC not set
|
|
esp_err_t fe = fsm_get_error();
|
|
if (fe == SC_ERR_SAFETY_TRIP) code |= 0b100; // LED3: safety
|
|
if (fe == SC_ERR_LEASH_HIT) code |= 0b100; // LED3: leash
|
|
if (fe != ESP_OK && code == 0) code = 0b111; // unknown error
|
|
return code;
|
|
}
|
|
|
|
typedef enum {
|
|
LED_IDLE,
|
|
LED_ERROR,
|
|
LED_WATERFALL,
|
|
LED_CALIBRATING,
|
|
LED_UNDO,
|
|
LED_BOOTING
|
|
} led_mode_t;
|
|
|
|
void drive_leds(led_mode_t mode) {
|
|
static const uint8_t waterfall[] = {0b001, 0b011, 0b111, 0b110, 0b100, 0b000};
|
|
int64_t now_us = esp_timer_get_time();
|
|
|
|
switch (mode) {
|
|
case LED_IDLE:
|
|
// 0.5Hz: 1s on, 1s off
|
|
i2c_set_led1((now_us / 1000000) % 2 ? 0b000 : 0b001);
|
|
break;
|
|
|
|
case LED_ERROR: {
|
|
// 3s cycle: 1s rapid blink (5Hz) then 2s hold error code
|
|
int64_t phase_us = now_us % 3000000;
|
|
if (phase_us < 1000000) {
|
|
// Rapid blink at 5Hz (100ms per half-cycle)
|
|
i2c_set_led1((phase_us / 100000) % 2 ? 0b000 : 0b111);
|
|
} else {
|
|
i2c_set_led1(error_code_from_state());
|
|
}
|
|
break;
|
|
}
|
|
|
|
case LED_WATERFALL:
|
|
// ~1 cycle/s: 6 steps at ~167ms each
|
|
i2c_set_led1(waterfall[(now_us / 167000) % 6]);
|
|
break;
|
|
|
|
case LED_CALIBRATING:
|
|
// 1Hz: 500ms on, 500ms off
|
|
i2c_set_led1((now_us / 500000) % 2 ? 0b000 : 0b111);
|
|
break;
|
|
|
|
case LED_UNDO:
|
|
i2c_set_led1(0b111);
|
|
break;
|
|
|
|
case LED_BOOTING:
|
|
i2c_set_led1(0b001);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void app_main(void) {
|
|
esp_task_wdt_add(NULL);
|
|
|
|
ESP_LOGI(TAG, "Firmware: %s", FIRMWARE_STRING);
|
|
ESP_LOGI(TAG, "Version: %s", FIRMWARE_VERSION);
|
|
ESP_LOGI(TAG, "Branch: %s", FIRMWARE_BRANCH);
|
|
ESP_LOGI(TAG, "Built: %s", BUILD_DATE);
|
|
|
|
// I2C first so we can light the LED immediately
|
|
init_critical("I2C", i2c_init);
|
|
drive_leds(LED_BOOTING); // LED on ASAP after I2C is up
|
|
i2c_post(); // verify TCA9555 responds
|
|
/* Sensors powered from boot; FSM will keep P10 high on every tick.
|
|
* Drops back to 0 on soft_idle_enter() (sleep). */
|
|
i2c_relays_idle();
|
|
|
|
if (rtc_xtal_init() != ESP_OK) ESP_LOGE(TAG, "RTC FAILED");
|
|
|
|
|
|
// Factory reset: cold boot + button held for 10s
|
|
// LEDs flash while waiting, go solid when triggered
|
|
esp_reset_reason_t boot_reset_reason = esp_reset_reason();
|
|
if ((boot_reset_reason == ESP_RST_POWERON || boot_reset_reason == ESP_RST_EXT)
|
|
&& gpio_get_level(GPIO_NUM_13) == 0) {
|
|
ESP_LOGW(TAG, "Button held on cold boot — hold %ds for factory reset", FACTORY_RESET_HOLD_MS / 1000);
|
|
|
|
// Flash all LEDs while user holds button (100ms on/off cycle)
|
|
int held_ms = 0;
|
|
while (gpio_get_level(GPIO_NUM_13) == 0 && held_ms < FACTORY_RESET_HOLD_MS) {
|
|
i2c_set_led1((held_ms / 100) % 2 ? 0b111 : 0b000);
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
held_ms += 100;
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
if (held_ms < FACTORY_RESET_HOLD_MS) {
|
|
ESP_LOGI(TAG, "Button released early (%dms) — skipping factory reset", held_ms);
|
|
i2c_set_led1(0b000);
|
|
} else {
|
|
// Solid LEDs = reset triggered
|
|
i2c_set_led1(0b111);
|
|
ESP_LOGW(TAG, "FACTORY RESET TRIGGERED");
|
|
|
|
// Initialize storage so we can erase it
|
|
if (storage_init() != ESP_OK) ESP_LOGE(TAG, "STORAGE FAILED");
|
|
|
|
esp_err_t reset_err = factory_reset();
|
|
if (reset_err == ESP_OK) {
|
|
ESP_LOGI(TAG, "Factory reset completed successfully");
|
|
// Success: green blink
|
|
for (int i = 0; i < 5; i++) {
|
|
i2c_set_led1(0b010);
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
i2c_set_led1(0b000);
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
}
|
|
} else {
|
|
ESP_LOGE(TAG, "Factory reset failed!");
|
|
// Error: red blink
|
|
for (int i = 0; i < 5; i++) {
|
|
i2c_set_led1(0b100);
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
i2c_set_led1(0b000);
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
}
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Rebooting system...");
|
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
esp_restart();
|
|
}
|
|
}
|
|
|
|
// Critical inits — retry up to 3 times, then reboot
|
|
init_critical("ADC", adc_init);
|
|
init_critical("STORAGE", storage_init);
|
|
rtc_restore_time(); // After NVS is up: try RTC_DATA_ATTR, then NVS fallback
|
|
init_critical("LOG", log_init);
|
|
|
|
// POST checks — verify hardware is responding correctly
|
|
adc_post(); // ADC channels readable and not frozen
|
|
storage_post(); // flash write-read-verify on test sector
|
|
|
|
//run_all_log_tests();
|
|
|
|
esp_reset_reason_t reset_reason = esp_reset_reason();
|
|
esp_sleep_wakeup_cause_t wake_cause = esp_sleep_get_wakeup_cause();
|
|
|
|
// Log every boot: boot_info = wake_cause[7:4] | reset_reason[3:0]
|
|
{
|
|
uint8_t boot_entry[9] = {};
|
|
uint64_t ts = rtc_get_ms();
|
|
memcpy(&boot_entry[0], &ts, 8);
|
|
boot_entry[8] = ((uint8_t)wake_cause << 4) | ((uint8_t)reset_reason & 0x0F);
|
|
log_write(boot_entry, sizeof(boot_entry), LOG_TYPE_BOOT);
|
|
}
|
|
|
|
// OTA rollback: count consecutive abnormal resets (panic/WDT).
|
|
// Power-on and external resets clear the counter; crashes increment it.
|
|
// After OTA_ROLLBACK_THRESHOLD consecutive crashes, roll back to the
|
|
// previous OTA partition (if available).
|
|
if (reset_reason == ESP_RST_POWERON || reset_reason == ESP_RST_EXT) {
|
|
ota_reset_counter = 0;
|
|
} else if (reset_reason == ESP_RST_PANIC ||
|
|
reset_reason == ESP_RST_INT_WDT ||
|
|
reset_reason == ESP_RST_TASK_WDT ||
|
|
reset_reason == ESP_RST_WDT) {
|
|
ota_reset_counter++;
|
|
ESP_LOGW(TAG, "Crash detected (reason=%d), reset counter=%d/%d",
|
|
reset_reason, ota_reset_counter, OTA_ROLLBACK_THRESHOLD);
|
|
|
|
uint8_t crash_entry[9] = {};
|
|
uint64_t ts = rtc_get_ms();
|
|
memcpy(&crash_entry[0], &ts, 8);
|
|
crash_entry[8] = (uint8_t)reset_reason;
|
|
log_write(crash_entry, sizeof(crash_entry), LOG_TYPE_CRASH);
|
|
|
|
if (ota_reset_counter >= OTA_ROLLBACK_THRESHOLD) {
|
|
ESP_LOGE(TAG, "Rollback threshold reached — marking app invalid");
|
|
esp_ota_mark_app_invalid_rollback_and_reboot();
|
|
// Does not return — reboots into previous OTA slot
|
|
}
|
|
}
|
|
|
|
if (solar_run_fsm() != ESP_OK) ESP_LOGE(TAG, "SOLAR FAILED");
|
|
|
|
send_bat_log();
|
|
|
|
/*** FULL BOOT ***/
|
|
// Critical — must succeed or reboot
|
|
init_critical("UART", uart_init);
|
|
init_critical("SENSORS", sensors_init);
|
|
init_critical("FSM", fsm_init);
|
|
|
|
// Create event group before non-critical inits (they set bits on it)
|
|
comms_event_group = xEventGroupCreate();
|
|
|
|
// Non-critical — retry once on failure, then log and continue.
|
|
// Set event bits even on failure so alarm-wake doesn't block forever.
|
|
if (rf_433_init() != ESP_OK) {
|
|
ESP_LOGW(TAG, "RF init failed, retrying...");
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
if (rf_433_init() != ESP_OK) ESP_LOGE(TAG, "RF FAILED (continuing without RF)");
|
|
}
|
|
if (bt_hid_init() != ESP_OK) {
|
|
ESP_LOGW(TAG, "BT init failed, retrying...");
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
if (bt_hid_init() != ESP_OK) {
|
|
ESP_LOGE(TAG, "BT HID FAILED (continuing without BT)");
|
|
if (comms_event_group) xEventGroupSetBits(comms_event_group, BT_READY_BIT);
|
|
}
|
|
}
|
|
if (webserver_init() != ESP_OK) {
|
|
ESP_LOGW(TAG, "Webserver init failed, retrying...");
|
|
vTaskDelay(pdMS_TO_TICKS(500));
|
|
if (webserver_init() != ESP_OK) {
|
|
ESP_LOGE(TAG, "WEBSERVER FAILED (continuing without WiFi)");
|
|
if (comms_event_group) xEventGroupSetBits(comms_event_group, WIFI_READY_BIT);
|
|
}
|
|
}
|
|
|
|
// POST + FSM started successfully — this firmware is good.
|
|
// Clear the rollback counter and mark the OTA partition as valid.
|
|
ota_reset_counter = 0;
|
|
esp_ota_mark_app_valid_cancel_rollback();
|
|
|
|
/*** MAIN LOOP ***/
|
|
uint8_t tap_count = 0;
|
|
int64_t tap_window_start = 0;
|
|
|
|
TickType_t xLastWakeTime = xTaskGetTickCount();
|
|
const TickType_t xFrequency = pdMS_TO_TICKS(50);
|
|
|
|
while(true) {
|
|
vTaskDelayUntil(&xLastWakeTime, xFrequency);
|
|
|
|
/* Bring-up tool owns the LEDs, buttons, and relays while active. */
|
|
if (bringup_mode_is_active()) {
|
|
esp_task_wdt_reset();
|
|
continue;
|
|
}
|
|
|
|
/* In soft idle: slow poll (5s) via direct GPIO, no I2C. */
|
|
// TODO: Critique & confirm what we do in idle
|
|
if (soft_idle_is_active()) {
|
|
//vTaskDelay(pdMS_TO_TICKS(1000));
|
|
if (soft_idle_button_raw()) {
|
|
rtc_reset_shutdown_timer();
|
|
soft_idle_exit();
|
|
i2c_poll_buttons(); /* sync TCA9555 state after idle */
|
|
xLastWakeTime = xTaskGetTickCount();
|
|
}
|
|
if (rtc_alarm_tripped()) {
|
|
soft_idle_exit();
|
|
xLastWakeTime = xTaskGetTickCount();
|
|
// Wait for WiFi + BT to come back up (or timeout after 5s)
|
|
if (comms_event_group) {
|
|
xEventGroupWaitBits(comms_event_group, COMMS_ALL_BITS,
|
|
pdFALSE, pdTRUE, pdMS_TO_TICKS(5000));
|
|
}
|
|
esp_task_wdt_reset();
|
|
fsm_request(FSM_CMD_START);
|
|
rtc_schedule_next_alarm();
|
|
}
|
|
solar_run_fsm();
|
|
rtc_check_shutdown_timer();
|
|
esp_task_wdt_reset();
|
|
continue;
|
|
}
|
|
|
|
i2c_poll_buttons();
|
|
|
|
if (i2c_get_button_state(0)) {
|
|
rtc_reset_shutdown_timer();
|
|
soft_idle_exit();
|
|
}
|
|
|
|
// --- Button logic: triple-tap, hold-to-reboot, cancel/stop ---
|
|
// See docs/button_behavior.md for full spec.
|
|
fsm_state_t cur_state = fsm_get_state();
|
|
bool btn_pressed = i2c_get_button_state(0);
|
|
bool btn_tripped = i2c_get_button_tripped(0);
|
|
bool btn_released = i2c_get_button_released(0);
|
|
int64_t btn_held = i2c_get_button_ms(0);
|
|
|
|
// Hold-to-reboot (active in IDLE and CALIBRATE states only)
|
|
bool hold_reboot_active = (cur_state == STATE_IDLE ||
|
|
cur_state == STATE_CALIBRATE_JACK_DELAY ||
|
|
cur_state == STATE_CALIBRATE_JACK_MOVE ||
|
|
cur_state == STATE_CALIBRATE_DRIVE_DELAY ||
|
|
cur_state == STATE_CALIBRATE_DRIVE_MOVE);
|
|
|
|
if (hold_reboot_active && btn_pressed && btn_held > 3000) {
|
|
// Flash all LEDs then reboot
|
|
ESP_LOGW(TAG, "Hold-to-reboot triggered");
|
|
rtc_save_time();
|
|
for (int i = 0; i < 6; i++) {
|
|
i2c_set_led1(i % 2 ? 0b000 : 0b111);
|
|
vTaskDelay(pdMS_TO_TICKS(150));
|
|
}
|
|
esp_restart();
|
|
}
|
|
|
|
// LED feedback while holding: off → 1 → 1+2 → 1+2+3
|
|
if (hold_reboot_active && btn_pressed && btn_held > 100) {
|
|
if (btn_held > 2250) i2c_set_led1(0b111);
|
|
else if (btn_held > 1500) i2c_set_led1(0b011);
|
|
else if (btn_held > 750) i2c_set_led1(0b001);
|
|
else i2c_set_led1(0b000);
|
|
}
|
|
|
|
// Tap processing — uses release edge so it doesn't conflict with hold
|
|
switch (cur_state) {
|
|
case STATE_IDLE:
|
|
// Triple-tap to start (count on release, ignore long presses)
|
|
if (btn_released && btn_held < 1000) {
|
|
tap_count++;
|
|
if (tap_count == 1) tap_window_start = esp_timer_get_time();
|
|
ESP_LOGI(TAG, "Tap %d/3", tap_count);
|
|
if (tap_count >= 3) {
|
|
ESP_LOGI(TAG, "Triple-tap → START");
|
|
tap_count = 0;
|
|
fsm_request(FSM_CMD_START);
|
|
}
|
|
}
|
|
|
|
// Tap window LED feedback + expiry
|
|
if (tap_count > 0) {
|
|
if (esp_timer_get_time() - tap_window_start > 2000000) {
|
|
ESP_LOGI(TAG, "Tap window expired at %d/3", tap_count);
|
|
tap_count = 0; // window expired
|
|
} else if (!btn_pressed) {
|
|
uint8_t led = (tap_count >= 2) ? 0b011 : 0b001;
|
|
i2c_set_led1(led);
|
|
break; // skip default LED while showing tap feedback
|
|
}
|
|
}
|
|
|
|
// Default idle LEDs (only when not holding or tap-counting)
|
|
if (!btn_pressed && tap_count == 0) {
|
|
if (
|
|
rtc_is_set() &&
|
|
efuse_get(BRIDGE_JACK)==EFUSE_OK &&
|
|
efuse_get(BRIDGE_AUX)==EFUSE_OK &&
|
|
efuse_get(BRIDGE_DRIVE)==EFUSE_OK &&
|
|
fsm_get_error() == ESP_OK
|
|
) {
|
|
drive_leds(LED_IDLE);
|
|
} else {
|
|
drive_leds(LED_ERROR);
|
|
}
|
|
}
|
|
|
|
// when not actively moving we log at a low frequency (every 120s)
|
|
if ((esp_timer_get_time() > last_bat_log_time + 120000000ULL))
|
|
send_bat_log();
|
|
break;
|
|
|
|
case STATE_UNDO_JACK_START:
|
|
drive_leds(LED_UNDO);
|
|
if (btn_tripped) {
|
|
ESP_LOGI(TAG, "STOP");
|
|
fsm_request(FSM_CMD_STOP);
|
|
}
|
|
break;
|
|
|
|
case STATE_CALIBRATE_JACK_DELAY:
|
|
drive_leds(LED_CALIBRATING);
|
|
if (btn_tripped)
|
|
fsm_request(FSM_CMD_CALIBRATE_JACK_START);
|
|
break;
|
|
case STATE_CALIBRATE_JACK_MOVE:
|
|
drive_leds(LED_CALIBRATING);
|
|
if (btn_tripped)
|
|
fsm_request(FSM_CMD_CALIBRATE_JACK_END);
|
|
break;
|
|
case STATE_CALIBRATE_DRIVE_DELAY:
|
|
drive_leds(LED_CALIBRATING);
|
|
if (btn_tripped)
|
|
fsm_request(FSM_CMD_CALIBRATE_DRIVE_START);
|
|
break;
|
|
case STATE_CALIBRATE_DRIVE_MOVE:
|
|
drive_leds(LED_CALIBRATING);
|
|
if (btn_tripped)
|
|
fsm_request(FSM_CMD_CALIBRATE_DRIVE_END);
|
|
break;
|
|
|
|
default:
|
|
// Moving — any press cancels
|
|
drive_leds(LED_WATERFALL);
|
|
if (btn_tripped) {
|
|
ESP_LOGI(TAG, "UNDO");
|
|
fsm_request(FSM_CMD_UNDO);
|
|
}
|
|
break;
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rtc_alarm_tripped()) {
|
|
fsm_request(FSM_CMD_START);
|
|
rtc_schedule_next_alarm();
|
|
}
|
|
|
|
solar_run_fsm();
|
|
rtc_check_shutdown_timer();
|
|
esp_task_wdt_reset();
|
|
}
|
|
} |