Files
SC-F001/main/main.c
2026-03-12 19:12:42 -05:00

370 lines
12 KiB
C

#include "esp_task_wdt.h"
#include "esp_system.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 "power_mgmt.h"
#include "rtc.h"
#include "sensors.h"
#include "solar.h"
#include "rf_433.h"
#include "bt_hid.h"
#include "webserver.h"
#include "version.h"
#include <string.h>
#define TAG "MAIN"
#define POST_MAX_RETRIES 3
// 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;
}
typedef enum {
LED_STATE_DRIVING,
LED_STATE_ERROR,
LED_STATE_AWAKE,
LED_STATE_CANCELLING,
LED_STATE_ERRORED,
LED_STATE_START1,
LED_STATE_START2,
LED_STATE_START3,
LED_STATE_START4,
LED_STATE_BOOTING
} led_state_t;
void drive_leds(led_state_t state) {
uint8_t patterns[5][12] = {
{1,3,7,6,4,0},
{0b101,0b001},
{1,1,1,1,1,1, 1,1,1,3},
{4,2},
{0b001, 0b101},
};
switch(state) {
case LED_STATE_DRIVING:
i2c_set_led1(patterns[state][(esp_timer_get_time()/100000) % 6]);
break;
case LED_STATE_ERROR:
//ESP_LOGE(TAG, "SOME SORT OF ERROR");
i2c_set_led1(patterns[state][(esp_timer_get_time()/1000000) % 2]);
break;
case LED_STATE_AWAKE:
i2c_set_led1(patterns[state][(esp_timer_get_time()/200000) % 10]);
break;
case LED_STATE_CANCELLING:
i2c_set_led1(patterns[state][(esp_timer_get_time()/200000) % 2]);
break;
case LED_STATE_ERRORED:
i2c_set_led1(patterns[state][(esp_timer_get_time()/200000) % 2]);
break;
case LED_STATE_BOOTING:
i2c_set_led1(0b001);
break;
case LED_STATE_START1:
i2c_set_led1(0b000);
break;
case LED_STATE_START2:
i2c_set_led1(0b001);
break;
case LED_STATE_START3:
i2c_set_led1(0b011);
break;
case LED_STATE_START4:
i2c_set_led1(0b111);
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);
// TODO: Confirm whether external RTC crystal can be dropped (see TODO.md #13)
if (rtc_xtal_init() != ESP_OK) ESP_LOGE(TAG, "RTC FAILED");
rtc_restore_time(); // Recover time from RTC domain if we crashed
// Critical inits — retry up to 3 times, then reboot (feeds OTA rollback counter)
init_critical("I2C", i2c_init);
i2c_post(); // verify TCA9555 responds
i2c_set_relays((relay_port_t){.raw=0});
drive_leds(LED_STATE_BOOTING);
// Check for factory reset condition: Cold boot (power-on/ext-reset) + button held
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, "FACTORY RESET TRIGGERED - Button held on cold boot");
// Flash LED pattern to indicate factory reset
for (int i = 0; i < 10; i++) {
i2c_set_led1(0b111);
vTaskDelay(pdMS_TO_TICKS(100));
i2c_set_led1(0b000);
vTaskDelay(pdMS_TO_TICKS(100));
}
// Initialize minimal components needed for factory reset
if (storage_init() != ESP_OK) ESP_LOGE(TAG, "STORAGE FAILED");
// Perform factory reset
esp_err_t reset_err = factory_reset();
if (reset_err == ESP_OK) {
ESP_LOGI(TAG, "Factory reset completed successfully");
// Flash success pattern
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!");
// Flash error pattern
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));
}
}
// Reboot the system
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);
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);
}
// TODO: OTA rollback counter (see TODO.md #3)
// Write a crash log entry if we rebooted unexpectedly
if (reset_reason == ESP_RST_PANIC ||
reset_reason == ESP_RST_INT_WDT ||
reset_reason == ESP_RST_TASK_WDT ||
reset_reason == ESP_RST_WDT) {
ESP_LOGW(TAG, "Crash detected! Reset reason: %d", reset_reason);
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 (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("FSM", fsm_init);
// sensors_init() is called inside control_task() — see control_fsm.c:185
// Non-critical — log error but continue booting
if (rf_433_init() != ESP_OK) ESP_LOGE(TAG, "RF FAILED");
if (bt_hid_init() != ESP_OK) ESP_LOGE(TAG, "BT HID FAILED");
if (webserver_init() != ESP_OK) ESP_LOGE(TAG, "WEBSERVER FAILED");
/*** MAIN LOOP ***/
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(50);
while(true) {
vTaskDelayUntil(&xLastWakeTime, xFrequency);
/* 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();
vTaskDelay(pdMS_TO_TICKS(500));
// TODO: do a hard wait until wifi and bluetooth come up, not just blindly wait; might be better to be non-blocking
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();
}
// TODO: Make sure all ISRs are clean (very tight, no blocking functions)
switch (fsm_get_state()) {
case STATE_IDLE:
// LED cue for user
if (i2c_get_button_ms(0) > 1600){
drive_leds(LED_STATE_START4);
} else if (i2c_get_button_ms(0) > 1100){
drive_leds(LED_STATE_START3);
} else if (i2c_get_button_ms(0) > 600){
drive_leds(LED_STATE_START2);
} else if (i2c_get_button_ms(0) > 100){
drive_leds(LED_STATE_START1);
} else {
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_STATE_AWAKE);
} else {
drive_leds(LED_STATE_ERROR);
}
/*int8_t state = 0b001;
if (get_is_safe()) state |= 0b010;
if (get_sensor(SENSOR_SAFETY)) state |= 0b100;
i2c_set_led1(state);*/
}
// 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();
if(i2c_get_button_ms(0) > 2100)
fsm_request(FSM_CMD_START);
break;
//case STATE_UNDO_JACK:
case STATE_UNDO_JACK_START:
// it's running the jack, but undoing
//send_log();
drive_leds(LED_STATE_CANCELLING);
if (i2c_get_button_tripped(0)) {
ESP_LOGI(TAG, "AAAAH STOP!!!");
fsm_request(FSM_CMD_STOP);
}
break;
case STATE_CALIBRATE_JACK_DELAY:
//send_log();
if (i2c_get_button_tripped(0))
fsm_request(FSM_CMD_CALIBRATE_JACK_START);
break;
case STATE_CALIBRATE_JACK_MOVE:
//send_log();
if (i2c_get_button_tripped(0))
fsm_request(FSM_CMD_CALIBRATE_JACK_END);
break;
case STATE_CALIBRATE_DRIVE_DELAY:
//send_log();
if (i2c_get_button_tripped(0))
fsm_request(FSM_CMD_CALIBRATE_DRIVE_START);
break;
case STATE_CALIBRATE_DRIVE_MOVE:
//send_log();
if (i2c_get_button_tripped(0))
fsm_request(FSM_CMD_CALIBRATE_DRIVE_END);
break;
default:
// it's running in every other case
//send_log();
drive_leds(LED_STATE_DRIVING);
if (i2c_get_button_tripped(0)) {
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(); // TODO: Will esp timer overflow? Handle overflow if needed (this used to be handled by the fact that we were in deep sleep)
esp_task_wdt_reset();
}
}