This commit is contained in:
Thaddeus Hughes
2026-06-10 16:40:27 -05:00
parent 85206e1dca
commit 20afd3d9ef
78 changed files with 3047 additions and 42944 deletions

View File

@@ -0,0 +1,96 @@
# This is the CMakeCache file.
# For build in directory: d:/SC/SC-F001/main/build/default
# It was generated by CMake: C:/Espressif/tools/cmake/3.24.0/bin/cmake.exe
# You can edit this file to change values found and used by cmake.
# If you do not want to change any of the values, simply exit the editor.
# If you do want to change a value, simply edit, save, and exit the editor.
# The syntax for the file is as follows:
# KEY:TYPE=VALUE
# KEY is the name of a variable in the cache.
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
# VALUE is the current value for the KEY.
########################
# EXTERNAL cache entries
########################
//For backwards compatibility, what version of CMake commands and
// syntax should this version of CMake try to support.
CMAKE_BACKWARDS_COMPATIBILITY:STRING=2.4
//No help, variable specified on the command line.
CMAKE_EXPORT_COMPILE_COMMANDS:UNINITIALIZED=ON
//Value Computed by CMake.
CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=D:/SC/SC-F001/main/build/default/CMakeFiles/pkgRedirects
//Path to a program.
CMAKE_MAKE_PROGRAM:FILEPATH=CMAKE_MAKE_PROGRAM-NOTFOUND
//Value Computed by CMake
CMAKE_PROJECT_DESCRIPTION:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_NAME:STATIC=Project
//Single output directory for building all executables.
EXECUTABLE_OUTPUT_PATH:PATH=
//Single output directory for building all libraries.
LIBRARY_OUTPUT_PATH:PATH=
//Value Computed by CMake
Project_BINARY_DIR:STATIC=D:/SC/SC-F001/main/build/default
//Value Computed by CMake
Project_IS_TOP_LEVEL:STATIC=ON
//Value Computed by CMake
Project_SOURCE_DIR:STATIC=D:/SC/SC-F001/main
########################
# INTERNAL cache entries
########################
//This is the directory where this CMakeCache.txt was created
CMAKE_CACHEFILE_DIR:INTERNAL=d:/SC/SC-F001/main/build/default
//Major version of cmake used to create the current loaded cache
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3
//Minor version of cmake used to create the current loaded cache
CMAKE_CACHE_MINOR_VERSION:INTERNAL=24
//Patch version of cmake used to create the current loaded cache
CMAKE_CACHE_PATCH_VERSION:INTERNAL=0
//Path to CMake executable.
CMAKE_COMMAND:INTERNAL=C:/Espressif/tools/cmake/3.24.0/bin/cmake.exe
//Path to cpack program executable.
CMAKE_CPACK_COMMAND:INTERNAL=C:/Espressif/tools/cmake/3.24.0/bin/cpack.exe
//Path to ctest program executable.
CMAKE_CTEST_COMMAND:INTERNAL=C:/Espressif/tools/cmake/3.24.0/bin/ctest.exe
//Path to cache edit program executable.
CMAKE_EDIT_COMMAND:INTERNAL=C:/Espressif/tools/cmake/3.24.0/bin/cmake-gui.exe
//Name of external makefile project generator.
CMAKE_EXTRA_GENERATOR:INTERNAL=
//Name of generator.
CMAKE_GENERATOR:INTERNAL=MinGW Makefiles
//Generator instance identifier.
CMAKE_GENERATOR_INSTANCE:INTERNAL=
//Name of generator platform.
CMAKE_GENERATOR_PLATFORM:INTERNAL=
//Name of generator toolset.
CMAKE_GENERATOR_TOOLSET:INTERNAL=
//Source directory with the top level CMakeLists.txt file for this
// project
CMAKE_HOME_DIRECTORY:INTERNAL=D:/SC/SC-F001/main
//ADVANCED property for variable: CMAKE_MAKE_PROGRAM
CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1
//number of local generators
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
//Platform information initialized
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
//Path to CMake installation.
CMAKE_ROOT:INTERNAL=C:/Espressif/tools/cmake/3.24.0/share/cmake-3.24

View File

@@ -0,0 +1,15 @@
set(CMAKE_HOST_SYSTEM "Windows-10.0.19045")
set(CMAKE_HOST_SYSTEM_NAME "Windows")
set(CMAKE_HOST_SYSTEM_VERSION "10.0.19045")
set(CMAKE_HOST_SYSTEM_PROCESSOR "AMD64")
set(CMAKE_SYSTEM "Windows-10.0.19045")
set(CMAKE_SYSTEM_NAME "Windows")
set(CMAKE_SYSTEM_VERSION "10.0.19045")
set(CMAKE_SYSTEM_PROCESSOR "AMD64")
set(CMAKE_CROSSCOMPILING "FALSE")
set(CMAKE_SYSTEM_LOADED 1)

View File

@@ -0,0 +1 @@
The system is: Windows - 10.0.19045 - AMD64

View File

@@ -0,0 +1 @@
# This file is generated by cmake for dependency checking of the CMakeCache.txt file

View File

@@ -15,6 +15,32 @@
static const char *TAG = "COMMS";
/* Decode a single JSON value into the parameter table. Returns true on
* success (param updated), false if the JSON node type doesn't match the
* parameter type (caller bumps params_failed). All numeric integer types
* funnel through valueint, floats through valuedouble — matches what
* cJSON_AddNumberToObject produced on the way out. */
static bool set_param_from_json(param_idx_t idx, cJSON *value_json) {
if (get_param_type(idx) == PARAM_TYPE_str) {
if (!cJSON_IsString(value_json)) return false;
set_param_string(idx, value_json->valuestring);
return true;
}
if (!cJSON_IsNumber(value_json)) return false;
param_value_t v = {0};
switch (get_param_type(idx)) {
case PARAM_TYPE_u16: v.u16 = (uint16_t)value_json->valueint; break;
case PARAM_TYPE_i16: v.i16 = (int16_t)value_json->valueint; break;
case PARAM_TYPE_u32: v.u32 = (uint32_t)value_json->valueint; break;
case PARAM_TYPE_i32: v.i32 = (int32_t)value_json->valueint; break;
case PARAM_TYPE_f32: v.f32 = (float)value_json->valuedouble; break;
case PARAM_TYPE_f64: v.f64 = value_json->valuedouble; break;
default: return false;
}
set_param_value_t(idx, v);
return true;
}
/**
* Build a JSON object containing complete system status
*/
@@ -44,7 +70,7 @@ cJSON* comms_handle_get(void) {
// Structured error flags (match LED error code bits)
cJSON *errors = cJSON_CreateObject();
bool efuse_trip = efuse_get(BRIDGE_AUX) || efuse_get(BRIDGE_JACK) || efuse_get(BRIDGE_DRIVE);
bool efuse_trip = any_efuse_tripped();
float bat_v = get_battery_V();
float low_v = get_param_value_t(PARAM_LOW_PROTECTION_V).f32;
bool low_bat = (bat_v > 0 && bat_v < low_v);
@@ -89,12 +115,17 @@ cJSON* comms_handle_get(void) {
if (leash_hit)
cJSON_AddItemToArray(msg_array, cJSON_CreateString("DISTANCE LIMIT HIT"));
if (efuse_get(BRIDGE_AUX))
cJSON_AddItemToArray(msg_array, cJSON_CreateString("AUX EFUSE TRIP"));
if (efuse_get(BRIDGE_JACK))
cJSON_AddItemToArray(msg_array, cJSON_CreateString("JACK EFUSE TRIP"));
if (efuse_get(BRIDGE_DRIVE))
cJSON_AddItemToArray(msg_array, cJSON_CreateString("DRIVE EFUSE TRIP"));
// Per-bridge efuse messages. Preserve the original AUX → JACK → DRIVE
// order via an explicit walk; bridge_t enum order is the opposite.
static const bridge_t efuse_msg_order[] = { BRIDGE_AUX, BRIDGE_JACK, BRIDGE_DRIVE };
for (size_t i = 0; i < sizeof(efuse_msg_order)/sizeof(efuse_msg_order[0]); i++) {
bridge_t b = efuse_msg_order[i];
if (efuse_get(b)) {
char msg[32];
snprintf(msg, sizeof(msg), "%s EFUSE TRIP", bridge_names[b]);
cJSON_AddItemToArray(msg_array, cJSON_CreateString(msg));
}
}
if (low_bat)
cJSON_AddItemToArray(msg_array, cJSON_CreateString("LOW BATTERY"));
if (!rtc_is_set())
@@ -112,36 +143,15 @@ cJSON* comms_handle_get(void) {
return NULL;
}
// Add all parameters
// Add all parameters. Numeric params funnel through param_to_double() —
// cJSON stores all numbers as double internally, so the type-specific
// accessor was just feeding the same final value.
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
const char *name = get_param_name(i);
param_value_t value = get_param_value_t(i);
switch (get_param_type(i)) {
case PARAM_TYPE_f32:
cJSON_AddNumberToObject(parameters, name, value.f32);
break;
case PARAM_TYPE_f64:
cJSON_AddNumberToObject(parameters, name, value.f64);
break;
case PARAM_TYPE_i32:
cJSON_AddNumberToObject(parameters, name, value.i32);
break;
case PARAM_TYPE_i16:
cJSON_AddNumberToObject(parameters, name, value.i16);
break;
case PARAM_TYPE_u32:
cJSON_AddNumberToObject(parameters, name, value.u32);
break;
case PARAM_TYPE_u16:
cJSON_AddNumberToObject(parameters, name, value.u16);
break;
case PARAM_TYPE_str:
cJSON_AddStringToObject(parameters, name, get_param_string(i));
break;
default:
cJSON_AddNullToObject(parameters, name);
break;
if (get_param_type(i) == PARAM_TYPE_str) {
cJSON_AddStringToObject(parameters, name, get_param_string(i));
} else {
cJSON_AddNumberToObject(parameters, name, param_to_double(i));
}
}
@@ -376,76 +386,12 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
}
cJSON *value_json = cJSON_GetObjectItem(parameters, key);
// Set parameter value based on type
switch (get_param_type(param_idx)) {
case PARAM_TYPE_f32:
if (cJSON_IsNumber(value_json)) {
set_param_value_t(param_idx, (param_value_t){.f32 = value_json->valuedouble});
params_updated++;
} else {
ESP_LOGW(TAG, "Type mismatch for parameter: %s", key);
params_failed++;
}
break;
case PARAM_TYPE_f64:
if (cJSON_IsNumber(value_json)) {
set_param_value_t(param_idx, (param_value_t){.f64 = value_json->valuedouble});
params_updated++;
} else {
ESP_LOGW(TAG, "Type mismatch for parameter: %s", key);
params_failed++;
}
break;
case PARAM_TYPE_i32:
if (cJSON_IsNumber(value_json)) {
set_param_value_t(param_idx, (param_value_t){.i32 = value_json->valueint});
params_updated++;
} else {
ESP_LOGW(TAG, "Type mismatch for parameter: %s", key);
params_failed++;
}
break;
case PARAM_TYPE_i16:
if (cJSON_IsNumber(value_json)) {
set_param_value_t(param_idx, (param_value_t){.i16 = value_json->valueint});
params_updated++;
} else {
ESP_LOGW(TAG, "Type mismatch for parameter: %s", key);
params_failed++;
}
break;
case PARAM_TYPE_u32:
if (cJSON_IsNumber(value_json)) {
set_param_value_t(param_idx, (param_value_t){.u32 = value_json->valueint});
params_updated++;
} else {
ESP_LOGW(TAG, "Type mismatch for parameter: %s", key);
params_failed++;
}
break;
case PARAM_TYPE_u16:
if (cJSON_IsNumber(value_json)) {
set_param_value_t(param_idx, (param_value_t){.u16 = value_json->valueint});
params_updated++;
} else {
ESP_LOGW(TAG, "Type mismatch for parameter: %s", key);
params_failed++;
}
break;
case PARAM_TYPE_str:
if (cJSON_IsString(value_json)) {
set_param_string(param_idx, value_json->valuestring);
params_updated++;
} else {
ESP_LOGW(TAG, "Type mismatch for parameter: %s", key);
params_failed++;
}
break;
default:
ESP_LOGW(TAG, "Unknown type for parameter: %s", key);
params_failed++;
break;
if (set_param_from_json(param_idx, value_json)) {
params_updated++;
} else {
ESP_LOGW(TAG, "Type mismatch for parameter: %s", key);
params_failed++;
}
}

View File

@@ -33,7 +33,7 @@
static QueueHandle_t fsm_cmd_queue = NULL;
// AUDIT: fsm_init() does not zero these — they persist across panics/WDT resets.
// fsm_init() does not zero these — they persist across panics/WDT resets.
// Only cleared by explicit user action (fsm_clear_error, fsm_set_remaining_distance).
RTC_DATA_ATTR esp_err_t fsm_error = ESP_OK;
esp_err_t fsm_get_error() { return fsm_error; }
@@ -108,6 +108,71 @@ int64_t fsm_cal_t, fsm_cal_e;
int64_t fsm_get_cal_t(){return fsm_cal_t;}
int64_t fsm_get_cal_e(){return fsm_cal_e;}
const char *sc_err_str(esp_err_t e) {
switch (e) {
case ESP_OK: return "OK";
case SC_ERR_EFUSE_TRIP_1: return "EFUSE 1 TRIP";
case SC_ERR_EFUSE_TRIP_2: return "EFUSE 2 TRIP";
case SC_ERR_EFUSE_TRIP_3: return "EFUSE 3 TRIP";
case SC_ERR_SAFETY_TRIP: return "SAFETY NOT SET";
case SC_ERR_LEASH_HIT: return "NO REMAINING DISTANCE";
case SC_ERR_RTC_NOT_SET: return "CLOCK NOT SET";
case SC_ERR_LOW_BATTERY: return "INSUFFICIENT VOLTAGE";
default: return "UNKNOWN";
}
}
const char *fsm_state_str(fsm_state_t s) {
switch (s) {
case STATE_IDLE: return "IDLE";
case STATE_MOVE_START_DELAY: return "MOVE_START_DELAY";
case STATE_JACK_UP_START: return "JACK_UP_START";
case STATE_JACK_UP: return "JACK_UP";
case STATE_DRIVE_START_DELAY: return "DRIVE_START_DELAY";
case STATE_DRIVE_FLUFF_START: return "DRIVE_FLUFF_START";
case STATE_DRIVE: return "DRIVE";
case STATE_DRIVE_END_DELAY: return "DRIVE_END_DELAY";
case STATE_JACK_DOWN: return "JACK_DOWN";
case STATE_UNDO_JACK_START: return "UNDO_JACK_START";
case STATE_CALIBRATE_JACK_DELAY: return "CALIBRATE_JACK_DELAY";
case STATE_CALIBRATE_JACK_MOVE: return "CALIBRATE_JACK_MOVE";
case STATE_CALIBRATE_DRIVE_DELAY: return "CALIBRATE_DRIVE_DELAY";
case STATE_CALIBRATE_DRIVE_MOVE: return "CALIBRATE_DRIVE_MOVE";
default: return "UNKNOWN";
}
}
/* Preconditions for accepting a START command. Returns ESP_OK if every gate
* passes, otherwise the SC_ERR_* code of the first failing gate. Caller is
* expected to assign the returned code into `fsm_error` and skip the start.
* Order matters: most-actionable error first (voltage → safety → efuses) so
* the operator sees the dominant fault when more than one is true. */
static esp_err_t fsm_check_start_preconditions(void) {
esp_err_t code = ESP_OK;
if (get_battery_V() < get_param_value_t(PARAM_LOW_PROTECTION_V).f32) code = SC_ERR_LOW_BATTERY;
else if (!get_is_safe()) code = SC_ERR_SAFETY_TRIP;
else if (efuse_get(BRIDGE_DRIVE)) code = SC_ERR_EFUSE_TRIP_1;
else if (efuse_get(BRIDGE_JACK)) code = SC_ERR_EFUSE_TRIP_2;
else if (efuse_get(BRIDGE_AUX)) code = SC_ERR_EFUSE_TRIP_3;
if (code != ESP_OK) ESP_LOGI(TAG, "FAILED TO START; %s", sc_err_str(code));
return code;
}
/* Gate a calibrate-mode state transition: only accepts the transition from
* `expected` to `next`, optionally requiring battery above LOW_PROTECTION_V.
* Returns true if the transition was made; caller then does per-case work
* (set_timer / save cal data / reset sensor counter) that doesn't fit a
* uniform helper. Battery gate is on for PREP and START (we are about to
* energize a motor); off for END (no motor action). */
static bool fsm_calibrate_transition(fsm_state_t expected, fsm_state_t next,
bool require_battery) {
if (current_state != expected) return false;
if (require_battery &&
get_battery_V() <= get_param_value_t(PARAM_LOW_PROTECTION_V).f32) return false;
current_state = next;
return true;
}
void fsm_request(fsm_cmd_t cmd)
{
// STOP always goes through (safety). All other commands are blocked during soft idle —
@@ -117,7 +182,6 @@ void fsm_request(fsm_cmd_t cmd)
rtc_reset_shutdown_timer(); // any accepted command extends the wake period
if (fsm_cmd_queue != NULL)
xQueueSend(fsm_cmd_queue, &cmd, 0); // safe from any context
// TODO: Make sure this is threadsafe
}
int8_t fsm_get_current_progress(int8_t denominator) {
@@ -188,9 +252,8 @@ esp_err_t send_fsm_log() {
float be_voltage = get_battery_V();
memcpy(&entry[8], &be_voltage, 4);
float current_A = get_bridge_raw_A(BRIDGE_DRIVE)
+ get_bridge_raw_A(BRIDGE_JACK)
+ get_bridge_raw_A(BRIDGE_AUX);
float current_A = 0.0f;
for (bridge_t b = 0; b < N_BRIDGES; b++) current_A += get_bridge_raw_A(b);
memcpy(&entry[12], &current_A, 4);
int16_t be_counter = get_sensor_counter(SENSOR_DRIVE);
@@ -198,11 +261,7 @@ esp_err_t send_fsm_log() {
entry[18] = pack_sensors();
float heat = efuse_get_heat(BRIDGE_DRIVE);
float h2 = efuse_get_heat(BRIDGE_JACK);
float h3 = efuse_get_heat(BRIDGE_AUX);
if (h2 > heat) heat = h2;
if (h3 > heat) heat = h3;
float heat = max_efuse_heat();
memcpy(&entry[19], &heat, 4);
uint16_t i2c_out = i2c_get_outputs();
@@ -254,7 +313,7 @@ void control_task(void *param) {
case FSM_CMD_START:
// Check if we have remaining distance before starting
if (remaining_distance <= 0.0f) {
ESP_LOGI(TAG, "FAILED TO START; NO REMAINING DISTANCE");
ESP_LOGI(TAG, "FAILED TO START; %s", sc_err_str(SC_ERR_LEASH_HIT));
fsm_error = SC_ERR_LEASH_HIT;
log = true;
continue;
@@ -264,34 +323,16 @@ void control_task(void *param) {
case FSM_CMD_START_IGNORE_OVERTRAVEL:
this_move_dist = get_param_value_t(PARAM_DRIVE_DIST).f32;
do_start:
if (current_state == STATE_IDLE) {
if (get_battery_V() < get_param_value_t(PARAM_LOW_PROTECTION_V).f32) {
ESP_LOGI(TAG, "FAILED TO START; INSUFFICIENT VOLTAGE");
fsm_error = SC_ERR_LOW_BATTERY;
/* Silently drop START commands received in any non-idle state
* (e.g. duplicate request while already moving). Preconditions
* are checked only once we know the state is acceptable. */
if (current_state != STATE_IDLE) break;
{
esp_err_t guard = fsm_check_start_preconditions();
if (guard != ESP_OK) {
fsm_error = guard;
continue;
}
if (!get_is_safe()) {
ESP_LOGI(TAG, "FAILED TO START; SAFETY NOT SET");
fsm_error = SC_ERR_SAFETY_TRIP;
continue;
}
if (efuse_get(BRIDGE_DRIVE)) {
ESP_LOGI(TAG, "FAILED TO START; EFUSE 1 TRIP");
fsm_error = SC_ERR_EFUSE_TRIP_1;
continue;
}
if (efuse_get(BRIDGE_JACK)) {
ESP_LOGI(TAG, "FAILED TO START; EFUSE 2 TRIP");
fsm_error = SC_ERR_EFUSE_TRIP_2;
continue;
}
if (efuse_get(BRIDGE_AUX)) {
ESP_LOGI(TAG, "FAILED TO START; EFUSE 3 TRIP");
fsm_error = SC_ERR_EFUSE_TRIP_3;
continue;
}
ESP_LOGI(TAG, "STARTING");
fsm_error = ESP_OK; // if everything is OK now, we're OK.
/* Zero jack timestamps so JACK_DOWN_TIME on this cycle
@@ -300,9 +341,9 @@ void control_task(void *param) {
jack_trans_us = 0;
jack_finish_us = 0;
current_state = STATE_MOVE_START_DELAY;
log = true;
log = true;
set_timer(TRANSITION_DELAY_US);
}
}
break;
case FSM_CMD_STOP:
current_state = STATE_IDLE;
@@ -318,58 +359,54 @@ void control_task(void *param) {
enabled = false;
break;
/* Calibration sub-FSM: PREP arms (IDLE → DELAY), START energizes
* the motor with a hard timeout (DELAY → MOVE), END records
* the result and returns to idle (MOVE → IDLE). PREP/START
* require battery; END doesn't (no motor action). */
case FSM_CMD_CALIBRATE_JACK_PREP:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP");
if (current_state == STATE_IDLE
&& get_battery_V() > get_param_value_t(PARAM_LOW_PROTECTION_V).f32) {
current_state = STATE_CALIBRATE_JACK_DELAY;
log = true;
}
if (fsm_calibrate_transition(STATE_IDLE, STATE_CALIBRATE_JACK_DELAY, true))
log = true;
break;
case FSM_CMD_CALIBRATE_JACK_START:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_START");
if (current_state == STATE_CALIBRATE_JACK_DELAY
&& get_battery_V() > get_param_value_t(PARAM_LOW_PROTECTION_V).f32) {
current_state = STATE_CALIBRATE_JACK_MOVE;
log = true;
if (fsm_calibrate_transition(STATE_CALIBRATE_JACK_DELAY,
STATE_CALIBRATE_JACK_MOVE, true)) {
set_timer(CALIBRATE_JACK_MAX_TIME);
log = true;
}
break;
case FSM_CMD_CALIBRATE_JACK_END:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_END");
if (current_state == STATE_CALIBRATE_JACK_MOVE) {
if (fsm_calibrate_transition(STATE_CALIBRATE_JACK_MOVE,
STATE_IDLE, false)) {
fsm_cal_t = fsm_now - timer_start;
current_state = STATE_IDLE;
log = true;
log = true;
}
break;
case FSM_CMD_CALIBRATE_DRIVE_PREP:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
if (current_state == STATE_IDLE
&& get_battery_V() > get_param_value_t(PARAM_LOW_PROTECTION_V).f32) {
current_state = STATE_CALIBRATE_DRIVE_DELAY;
log = true;
}
if (fsm_calibrate_transition(STATE_IDLE, STATE_CALIBRATE_DRIVE_DELAY, true))
log = true;
break;
case FSM_CMD_CALIBRATE_DRIVE_START:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_START");
if (current_state == STATE_CALIBRATE_DRIVE_DELAY
&& get_battery_V() > get_param_value_t(PARAM_LOW_PROTECTION_V).f32) {
current_state = STATE_CALIBRATE_DRIVE_MOVE;
log = true;
if (fsm_calibrate_transition(STATE_CALIBRATE_DRIVE_DELAY,
STATE_CALIBRATE_DRIVE_MOVE, true)) {
set_timer(CALIBRATE_DRIVE_MAX_TIME);
set_sensor_counter(SENSOR_DRIVE, 0);
log = true;
}
break;
case FSM_CMD_CALIBRATE_DRIVE_END:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_END");
if (current_state == STATE_CALIBRATE_DRIVE_MOVE) {
if (fsm_calibrate_transition(STATE_CALIBRATE_DRIVE_MOVE,
STATE_IDLE, false)) {
fsm_cal_t = fsm_now - timer_start;
fsm_cal_e = get_sensor_counter(SENSOR_DRIVE);
current_state = STATE_IDLE;
log = true;
log = true;
}
break;
}

View File

@@ -112,6 +112,10 @@ int8_t fsm_get_current_progress(int8_t remainder);
fsm_state_t fsm_get_state();
bool fsm_is_idle(void);
/* Human-readable name for a fsm_state_t value — used by logs and any web
* UI surface that wants to render a friendly state name. Returns a literal. */
const char *fsm_state_str(fsm_state_t s);
int8_t get_bridge_state(bridge_t bridge);
#endif /* MAIN_CONTROL_FSM_H_ */

View File

@@ -1,797 +0,0 @@
/*
* lcd.c
*
* Created on: Dec 12, 2025
* Author: Thad
*/
/* NOTICE: THIS IS A DUMPING GROUND FOR OBSOLETE CODE SINCE WE NO LONGER HAVE AN LCD
NONE OF THIS IS TESTED.
*/
// Debounce & Repeat Settings
#define DEBOUNCE_MS 50
#define REPEAT_MS 200
#define REPEAT_START_MS 700
static uint8_t lcd_col = 0;
static uint8_t lcd_row = 0;
static bool debounced_state[4] = {false};
static bool last_known_state[4] = {false};
static uint64_t last_stable_time[4] = {0};
static uint64_t last_change_time[4] = {0};
static uint8_t claimed_repeats[4] = {0};
// === DELAY HELPERS ===
static inline void delay_us(uint32_t us) {
esp_rom_delay_us(us);
}
static esp_err_t tca_write_word_16(uint8_t reg, uint16_t value) {
uint8_t data[3] = { reg, (uint8_t)(value & 0xFF), (uint8_t)(value >> 8) };
return i2c_master_write_to_device(I2C_PORT, TCA_ADDR, data, 3, pdMS_TO_TICKS(1000));
}
// === TCA9555 PORT CONTROL ===
static esp_err_t tca_set_config_port0(uint16_t config_port0) {
return tca_write_word_16(TCA_REG_CONFIG0, config_port0);
}
static esp_err_t tca_port_write(uint8_t value) {
return tca_write_word_8(TCA_REG_OUTPUT1, value);
}
static esp_err_t tca_port_read(uint16_t *value) {
uint16_t low, high;
ESP_ERROR_CHECK(tca_read_word(TCA_REG_INPUT0, &low));
ESP_ERROR_CHECK(tca_read_word(TCA_REG_INPUT1, &high));
*value = low | (high << 8);
return ESP_OK;
}
// === LCD NIBBLE & COMMAND ===
static esp_err_t lcd_write_nibble(uint8_t nibble, bool rs) {
uint8_t data_state = 0;
if (rs) data_state |= (1 << LCD_RS);
if (nibble & 0x01) data_state |= (1 << LCD_D4);
if (nibble & 0x02) data_state |= (1 << LCD_D5);
if (nibble & 0x04) data_state |= (1 << LCD_D6);
if (nibble & 0x08) data_state |= (1 << LCD_D7);
ESP_ERROR_CHECK(tca_port_write(data_state));
ESP_ERROR_CHECK(tca_port_write(data_state | (1 << LCD_E)));
ESP_ERROR_CHECK(tca_port_write(data_state));
return ESP_OK;
}
static esp_err_t lcd_command(uint8_t cmd) {
ESP_ERROR_CHECK(lcd_write_nibble(cmd >> 4, false));
ESP_ERROR_CHECK(lcd_write_nibble(cmd & 0x0F, false));
return ESP_OK;
}
static esp_err_t lcd_data(uint8_t data) {
ESP_ERROR_CHECK(lcd_write_nibble(data >> 4, true));
ESP_ERROR_CHECK(lcd_write_nibble(data & 0x0F, true));
return ESP_OK;
}
void lcd_set_cursor(uint8_t row, uint8_t col) {
uint8_t addr = (row == 0) ? 0x00 : 0x40;
addr += col;
lcd_row = row;
lcd_col = col;
lcd_command(0x80 | addr);
delay_us(50);
}
void lcd_printf(const char *fmt, ...) {
char buf[64];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
lcd_set_cursor(0, 0);
for (int i = 0; i < 32 && buf[i]; i++) {
if (i == 16) lcd_set_cursor(1, 0);
lcd_data((uint8_t)buf[i]);
delay_us(50);
}
}
void lcd_print(const char *str) {
lcd_set_cursor(0, 0);
for (int i = 0; i < 32 && str[i]; i++) {
if (i == 16) lcd_set_cursor(1, 0);
lcd_data((uint8_t)str[i]);
delay_us(50);
}
}
void lcd_off(void) {
if (i2c_initted) lcd_command(0x08);
}
esp_err_t lcd_init_4bit(void) {
ESP_LOGI("I2C", "Starting LCD init...");
ESP_ERROR_CHECK(tca_set_config_port0(0xFF));
tca_port_write(0x00);
delay_us(50000);
ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(4500);
ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(150);
ESP_ERROR_CHECK(lcd_write_nibble(0x3, false)); delay_us(150);
ESP_ERROR_CHECK(lcd_write_nibble(0x2, false)); delay_us(150);
ESP_ERROR_CHECK(lcd_command(0x28)); delay_us(150);
ESP_ERROR_CHECK(lcd_command(0x08)); delay_us(150);
ESP_ERROR_CHECK(lcd_command(0x01)); delay_us(2000);
ESP_ERROR_CHECK(lcd_command(0x06)); delay_us(150);
ESP_ERROR_CHECK(lcd_command(0x0C)); delay_us(150);
ESP_LOGI("I2C", "LCD init complete.");
return ESP_OK;
}
// === BUTTON DEBOUNCE & REPEAT ===
void update_buttons(void) {
for (uint8_t btn = 0; btn < 4; ++btn) {
last_known_state[btn] = debounced_state[btn];
}
uint16_t port_val;
ESP_ERROR_CHECK(tca_port_read(&port_val));
uint8_t raw_buttons = (uint8_t)(port_val & 0x0F);
uint8_t raw_states = ~raw_buttons & 0x0F;
uint64_t now = esp_timer_get_time() / 1000;
for (uint8_t btn = 0; btn < 4; ++btn) {
bool raw_pressed = (raw_states & (1 << btn)) != 0;
if (raw_pressed != debounced_state[btn]) {
if (now - last_stable_time[btn] >= DEBOUNCE_MS) {
debounced_state[btn] = raw_pressed;
last_stable_time[btn] = now;
last_change_time[btn] = now;
claimed_repeats[btn] = 0;
}
} else {
last_stable_time[btn] = now;
}
}
}
bool get_button_tripped(uint8_t button) {
return (button < 4) && debounced_state[button] && !last_known_state[button];
}
bool get_button_released(uint8_t button) {
return (button < 4) && !debounced_state[button] && last_known_state[button];
}
bool get_button_state(uint8_t button) {
return (button < 4) && debounced_state[button];
}
bool get_button_repeat(uint8_t btn) {
if (btn >= 4 || !debounced_state[btn]) return false;
uint64_t now = esp_timer_get_time() / 1000;
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) {
claimed_repeats[btn]++;
return true;
}
return false;
}
int8_t get_button_repeats(uint8_t btn) {
if (!get_button_state(btn))
return 0;
if (btn >= 4 || !debounced_state[btn]) return false;
uint64_t now = esp_timer_get_time() / 1000;
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) {
claimed_repeats[btn]++;
if (claimed_repeats[btn] > 100)
claimed_repeats[btn] = 100;
ESP_LOGI("BTN", "RPT %d", (uint8_t)claimed_repeats[btn]+2);
return claimed_repeats[btn]+1;
}
if (debounced_state[btn] && !last_known_state[btn]) {
ESP_LOGI("BTN", "FST %d", 1);
return 1;
}
//ESP_LOGI("BTN", "RPT %d", 0);
return 0;
}
int64_t get_button_ms(uint8_t btn) {
if (!get_button_state(btn))
return 0;
uint64_t now = esp_timer_get_time() / 1000;
return now - last_change_time[btn];
}
// Parameter descriptor structure
typedef struct {
const char key[24]; // NVS key name (null-terminated)
uint8_t type_size; // Size in bytes: 1=uint8_t, 2=uint16_t, 4=uint32_t/float, 8=uint64_t/double
uint8_t type_flags; // Bitfield: [0:1] signed, [2] float, [3:7] reserved
const void *default_val; // Pointer to default value (matches type)
} param_desc_t;
typedef struct param_group_s param_group_t;
typedef struct param_group_s {
char* (*formatter)(const param_group_t*, uint8_t idx);
const uint8_t num_keys;
const uint8_t indices[8][2];
const char keys[8][20];
void (*launch_functions[8])(char* key, int8_t dir);
} param_group_t;
// temp buffer for formatting stuff onto the LCD
static char formatting_buf[LCD_BUFLEN];
/* MENU DIALOG CONFIG */
char* schedule_format(const param_group_t *pg, uint8_t idx);
char* dist_format (const param_group_t *pg, uint8_t idx);
char* reprog_format (const param_group_t *pg, uint8_t idx);
char* override_format(const param_group_t *pg, uint8_t idx);
char* status_format (const param_group_t *pg, uint8_t idx);
char* cal_format (const param_group_t *pg, uint8_t idx);
char* efuse_format (const param_group_t *pg, uint8_t idx);
char* ftp_format (const param_group_t *pg, uint8_t idx);
// Launch functions (forward declarations)
void trigger_move(char* key, int8_t dir);
void rf_reprogram_remote(char* key, int8_t dir);
void adjust_hour (char* key, int8_t dir);
void adjust_i8_0_99 (char* key, int8_t dir);
void adjust_generic (int idx, int8_t amt);
void dummy_adjuster (char* key, int8_t dir) {}; // do nothing
void launch_ftp (char* key, int8_t dir);
void adjust_i32_smart_0_99999(char* key, int8_t dir);
void adjust_i32_smart_0_999 (char* key, int8_t dir);
// Parameter table (legible, declarative)
const param_desc_t param_table[] = {
{
.key = "sched_start",
.type_size = TYPE_SIZE_1,
.type_flags = TYPE_SIGNED,
.default_val = &(int8_t){0}
},
{
.key = "sched_end",
.type_size = TYPE_SIZE_1,
.type_flags = TYPE_SIGNED,
.default_val = &(int8_t){0}
},
{
.key = "sched_num",
.type_size = TYPE_SIZE_1,
.type_flags = TYPE_SIGNED,
.default_val = &(int8_t){0}
},
{
.key = "efuse_drive_A",
.type_size = TYPE_SIZE_1,
.type_flags = TYPE_SIGNED,
.default_val = &(int8_t){99}
},{
.key = "efuse_jack_A",
.type_size = TYPE_SIZE_1,
.type_flags = TYPE_SIGNED,
.default_val = &(int8_t){99}
},{
.key = "efuse_aux_A",
.type_size = TYPE_SIZE_1,
.type_flags = TYPE_SIGNED,
.default_val = &(int8_t){99}
},
{
.key = "drive_dist",
.type_size = TYPE_SIZE_1,
.type_flags = TYPE_SIGNED,
.default_val = &(int16_t){10}
},{
.key = "drive_tpdf",
.type_size = TYPE_SIZE_4,
.type_flags = 0,
.default_val = &(int32_t){70}
},{
.key = "drive_mspf",
.type_size = TYPE_SIZE_4,
.type_flags = 0,
.default_val = &(int32_t){1000}
},{
.key = "jack_mspi",
.type_size = TYPE_SIZE_4,
.type_flags = 0,
.default_val = &(int32_t){1000}
},{
.key = "jack_dist",
.type_size = TYPE_SIZE_1,
.type_flags = TYPE_SIGNED,
.default_val = &(uint8_t){7}
},
{
.key = "keycode0",
.type_size = TYPE_SIZE_8,
.type_flags = 0,
.default_val = &(uint8_t){0}
},{
.key = "keycode1",
.type_size = TYPE_SIZE_8,
.type_flags = 0,
.default_val = &(uint8_t){0}
},{
.key = "keycode2",
.type_size = TYPE_SIZE_8,
.type_flags = 0,
.default_val = &(uint8_t){0}
},{
.key = "keycode3",
.type_size = TYPE_SIZE_8,
.type_flags = 0,
.default_val = &(uint8_t){0}
}
};
#define PARAM_COUNT (sizeof(param_table)/sizeof(param_table[0]))
// Runtime parameter values
static param_value_t param_values[PARAM_COUNT];
const param_group_t param_group_table[] = {
{
.formatter = status_format,
.num_keys = 3,
.keys = {"","",""},
.launch_functions = {trigger_move, adjust_rtc_hour, adjust_rtc_min}
},{
.formatter = schedule_format,
.num_keys = 3,
.keys = {"sched_start", "sched_end", "sched_num"},
.launch_functions = {adjust_hour, adjust_hour, adjust_i8_0_99}
},
{
.formatter = dist_format,
.num_keys = 2,
.keys = {"drive_dist", "jack_dist"},
.launch_functions = {adjust_i8_0_99, adjust_i8_0_99}
},
{
.formatter = cal_format,
.num_keys = 3,
.keys = { "jack_mspi", "drive_mspf", "drive_tpdf"},
.launch_functions = {adjust_i32_smart_0_99999, adjust_i32_smart_0_99999, adjust_i32_smart_0_999}
},
{
.formatter = efuse_format,
.num_keys = 3,
.keys = { "efuse_aux_A", "efuse_jack_A", "efuse_drive_A"},
.launch_functions = {adjust_i8_0_99, adjust_i8_0_99, adjust_i8_0_99}
},
{
.formatter = override_format,
.num_keys = 3,
.keys = {"","",""},
.launch_functions = {dummy_adjuster, dummy_adjuster, dummy_adjuster}
},
{
.formatter = reprog_format,
.num_keys = 1,
.keys = {""},
.launch_functions = {rf_reprogram_remote}
},
{
.formatter = ftp_format,
.num_keys = 1,
.keys = {""},
.launch_functions = {launch_ftp}
}
};
#define PARAM_GROUP_RUNMTR 5
#define PARAM_GROUP_FTP 7
#define PARAM_GROUP_COUNT (sizeof(param_group_table)/sizeof(param_group_table[0]))
static const char schedule_fmts[3][3][LCD_BUFLEN] = {
{
"Start/End xTimes [-] - x%-2d ",
"Start/End xTimes - [-] x%-2d ",
"Start/End xTimes - - [x%-2d]"
},{
"Start/End xTimes[%2d%cM] - x%-2d ",
"Start/End xTimes %2d%cM [-] x%-2d ",
"Start/End xTimes %2d%cM - [x%-2d]"
},{
"Start/End xTimes[%2d%cM]-%2d%cM x%-2d",
"Start/End xTimes %2d%cM-[%2d%cM] x%-2d",
"Start/End xTimes %2d%cM-%2d%cM [x%-2d]"
}
};
static const char dist_fmts[3][LCD_BUFLEN] = {
"Dist. Drive/Jack[%2d ft] / %2d in ",
"Dist. Drive/Jack %2d ft / [%2d in]"
};
static const char override_fmts[3][LCD_BUFLEN] = {
" Run Motors [AUX]JACK DRIVE ",
" Run Motors AUX[JACK]DRIVE ",
" Run Motors AUX JACK[DRIVE]"
};
static const char cal_fmts[3][LCD_BUFLEN] = {
"Jack ms/in: [%4ld]%4ld %4ld ",
"Drive ms/ft: %4ld[%4ld]%4ld ",
"Drive t/10ft: %4ld %4ld[%4ld]"
};
static const char efuse_fmts[3][LCD_BUFLEN] = {
"E-fuse Aux: [%2dA] %2dA %2dA ",
"E-fuse Jack: %2dA [%2dA] %2dA ",
"E-fuse Drive: %2dA %2dA [%2dA]"
};
/* All function implementations remain unchanged and appear here in original form */
char* schedule_format(const param_group_t *pg, uint8_t idx)
{
/* pg->keys[0..2] → "sched_start", "sched_end", "sched_num" */
int8_t start = (int8_t)get_param_i8(pg->keys[0]); // helper, see below
int8_t end = (int8_t)get_param_i8(pg->keys[1]);
int8_t num = (int8_t)get_param_i8(pg->keys[2]);
char startAP = start<12 ? 'A':'P';
char endAP = end<12 ? 'A':'P';
start %= 12;
end %= 12;
if (start == 0) start = 12;
if (end == 0) end = 12;
if (num == 0) {
snprintf(formatting_buf, sizeof(formatting_buf),
schedule_fmts[0][idx], num);
return formatting_buf;
} else if (num == 1) {
snprintf(formatting_buf, sizeof(formatting_buf),
schedule_fmts[1][idx], start, startAP, num);
return formatting_buf;
} else {
snprintf(formatting_buf, sizeof(formatting_buf),
schedule_fmts[2][idx], start, startAP, end, endAP, num);
return formatting_buf;
}
}
char* dist_format(const param_group_t *pg, uint8_t idx) {
int8_t drive = (int8_t)get_param_i8(pg->keys[0]); // helper, see below
int8_t jack = (int8_t)get_param_i8(pg->keys[1]);
snprintf(formatting_buf, sizeof(formatting_buf),
dist_fmts[idx], drive, jack);
return formatting_buf;
}
char* reprog_format(const param_group_t *pg, uint8_t idx) {
return "Reprogram Keyfob [Press ^ / v ] ";
}
char* override_format(const param_group_t *pg, uint8_t idx) {
return override_fmts[idx];
}
char* ftp_format(const param_group_t *pg, uint8_t idx) {
return " Start Wifi/FTP [Press ^ / v ] ";
}
char charge_indicators[N_CHARGE_STATES] = {
[CHG_STATE_OFF] ='-',
[CHG_STATE_FLOAT] ='F',
[CHG_STATE_BULK] ='B'
};
static const char status_fmts[4][LCD_BUFLEN] = {
"%-6s%2dA %2lu.%02luV[MOVE] %2d:%02d %cM",
"%-6s%2dA %2lu.%02luV MOVE [%2d]:%02d %cM",
"%-6s%2dA %2lu.%02luV MOVE %2d:[%02d]%cM",
"%-6s%2dA %2lu.%02luV[ SET TIME ^/v ]",
};
char* status_format(const param_group_t *pg, uint8_t idx) {
uint32_t vbat = get_battery_mV();
struct tm timeinfo;
rtc_get_time(&timeinfo);
// --- Build 7-char time: " 9:05PM" or "10:05PM" ---
int hour12 = timeinfo.tm_hour % 12;
if (hour12 == 0) hour12 = 12; // 12-hour format
int current_draw = abs(get_bridge_mA(BRIDGE_DRIVE)/1000) + abs(get_bridge_mA(BRIDGE_JACK)/1000) + abs(get_bridge_mA(BRIDGE_AUX)/1000);
if (rtc_is_set())
snprintf(formatting_buf, sizeof(formatting_buf),
status_fmts[idx],
"Idle",
current_draw,
(unsigned long)(vbat / 1000),
(unsigned long)((vbat % 1000) + 99) / 100,
hour12,
timeinfo.tm_min,
timeinfo.tm_hour < 12 ? 'A':'P'
);
else
snprintf(formatting_buf, sizeof(formatting_buf),
status_fmts[3],
"Idle",
current_draw,
(unsigned long)(vbat / 1000),
(unsigned long)((vbat % 1000) + 99) / 100
);
return formatting_buf;
}
char* cal_format(const param_group_t *pg, uint8_t idx) {
int32_t x1 = get_param_i32(pg->keys[0]);
int32_t x2 = get_param_i32(pg->keys[1]);
int32_t x3 = get_param_i32(pg->keys[2]);
snprintf(formatting_buf, sizeof(formatting_buf),
cal_fmts[idx], x1, x2, x3);
return formatting_buf;
}
char* efuse_format(const param_group_t *pg, uint8_t idx) {
int32_t x1 = get_param_i32(pg->keys[0]);
int32_t x2 = get_param_i32(pg->keys[1]);
int32_t x3 = get_param_i32(pg->keys[2]);
snprintf(formatting_buf, sizeof(formatting_buf),
efuse_fmts[idx], x1, x2, x3);
return formatting_buf;
}
// Generic adjustment fallback
void adjust_generic(int idx, int8_t amt) {
const param_desc_t *p = &param_table[idx];
if (p->type_flags & TYPE_FLOAT) {
float step = 0.1f;
param_values[idx].f32 += amt;
} else {
switch (p->type_size) {
case 1: {
int8_t v = (int8_t)param_values[idx].u8;
v += amt;
param_values[idx].u8 = (int8_t)v;
break;
}
case 2: {
int16_t v = (int16_t)param_values[idx].u16;
v += amt;
param_values[idx].u16 = (int16_t)v;
break;
}
}
}
params_save(idx);
}
/**
* adjust_time - Shared adjuster for any time parameter (HH:MM format)
* @idx: Index in param_table[]
* @dir: +1 = increment, -1 = decrement
*
* Assumes value stored as minutes since 00:00 (01439)
* Displays as "HH:MM"
*/
void adjust_hour(char* key, int8_t dir) {
int8_t idx = params_find(key);
if (idx<0) return;
if (dir>0) param_values[idx].i8 += +1;
if (dir<0) param_values[idx].i8 += -1;
// wraparound
if (param_values[idx].i8 > 23) param_values[idx].i8 = 0;
if (param_values[idx].i8 < 0) param_values[idx].i8 = 23;
params_save(idx);
set_next_alarm();
}
void adjust_i8_0_99(char* key, int8_t dir) {
int8_t idx = params_find(key);
if (idx<0) return;
if (dir>0) param_values[idx].i8 += +1;
if (dir<0) param_values[idx].i8 += -1;
// clamp
if (param_values[idx].i8 > 99) param_values[idx].i8 = 99;
if (param_values[idx].i8 < 0) param_values[idx].i8 = 0;
params_save(idx);
set_next_alarm();
}
void adjust_i16_0_9990_by_10(char* key, int8_t dir) {
int8_t idx = params_find(key);
if (idx<0) return;
if (dir>0) param_values[idx].i16 += +1;
if (dir<0) param_values[idx].i16 += -1;
// clamp
if (param_values[idx].i16 > 9990) param_values[idx].i16 = 9990;
if (param_values[idx].i16 < 0) param_values[idx].i16 = 0;
params_save(idx);
set_next_alarm();
}
//inline static int8_t abs(int8_t x) { return x<0?-x:x; }
void adjust_i32_smart_0_99999(char* key, int8_t dir) {
int8_t idx = params_find(key);
if (idx<0) return;
int32_t inc = 1;
if (abs(dir) > 5) inc = 5;
if (abs(dir) > 10) inc = 10;
if (abs(dir) > 13) inc = 50;
if (abs(dir) > 16) inc = 100;
if (abs(dir) > 19) inc = 200;
if (abs(dir) > 22) inc = 1000;
if (dir>0) param_values[idx].i32 += +inc;
if (dir<0) param_values[idx].i32 += -inc;
param_values[idx].i32 = (param_values[idx].i32/inc)*inc;
ESP_LOGI("ADJ", "P[%d] += %d => %ld", (int)idx, (int)inc, (long)param_values[idx].i32);
// clamp
if (param_values[idx].i32 > 99999) param_values[idx].i32 = 99999;
if (param_values[idx].i32 < 0) param_values[idx].i32 = 0;
params_save(idx);
set_next_alarm();
}
void adjust_i32_smart_0_999(char* key, int8_t dir) {
int8_t idx = params_find(key);
if (idx<0) return;
int32_t inc = 1;
if (abs(dir) > 5) inc = 5;
if (abs(dir) > 10) inc = 10;
if (abs(dir) > 13) inc = 50;
if (abs(dir) > 16) inc = 100;
if (abs(dir) > 19) inc = 200;
if (abs(dir) > 22) inc = 1000;
if (dir>0) param_values[idx].i32 += +inc;
if (dir<0) param_values[idx].i32 += -inc;
param_values[idx].i32 = (param_values[idx].i32/inc)*inc;
ESP_LOGI("ADJ", "p[%d] += %d => %ld", (int)idx, (int)inc, (long)param_values[idx].i32);
// clamp
if (param_values[idx].i32 > 999) param_values[idx].i32 = 999;
if (param_values[idx].i32 < 0) param_values[idx].i32 = 0;
params_save(idx);
set_next_alarm();
}
static int8_t group_idx=0, entry_idx=0;
void run_parameter_ui() {
if (get_button_repeats(BTN_L)) {
reset_shutdown_timer();
entry_idx--;
if (entry_idx < 0) {
group_idx--;
if (group_idx < 0) {
group_idx = PARAM_GROUP_COUNT-1;
}
entry_idx = param_group_table[group_idx].num_keys-1;
}
}
if (get_button_repeats(BTN_R)) {
reset_shutdown_timer();
entry_idx++;
if (entry_idx >= param_group_table[group_idx].num_keys) {
group_idx++;
if (group_idx >= PARAM_GROUP_COUNT) {
group_idx = 0;
}
entry_idx = 0;
}
}
// Forbid user from doing anything until they set the time
if (!rtc_is_set()) {
group_idx=0;
entry_idx=1;
}
param_group_t pg = param_group_table[group_idx];
lcd_print(pg.formatter(&pg, entry_idx)); // Formatted with botfmt + values
int8_t n;
if ((n=get_button_repeats(BTN_U))) {
reset_shutdown_timer();
pg.launch_functions[entry_idx](
pg.keys[entry_idx], +n
);
}
if ((n=get_button_repeats(BTN_D))) {
reset_shutdown_timer();
pg.launch_functions[entry_idx](
pg.keys[entry_idx], -n
);
}
/*int64_t ut = get_button_ms(BTN_U);
if (ut) {
reset_shutdown_timer();
pg.launch_functions[entry_idx](
pg.keys[entry_idx], +ut
);
}
int64_t dt = get_button_ms(BTN_D);
if (ut) {
reset_shutdown_timer();
pg.launch_functions[entry_idx](
pg.keys[entry_idx], -ut
);
}*/
}
int8_t parameter_ux_in_override() {
if(group_idx != PARAM_GROUP_RUNMTR)
return -1;
return entry_idx;
}
bool parameter_ux_in_ftp() {
return group_idx == PARAM_GROUP_FTP;
}

View File

@@ -23,13 +23,13 @@
#include "version.h"
#include <string.h>
EventGroupHandle_t comms_event_group = NULL;
EventGroupHandle_t comms_event_group = NULL; // synchronizing tasks
#define TAG "MAIN"
#define POST_MAX_RETRIES 3
#define OTA_ROLLBACK_THRESHOLD 5
#define FACTORY_RESET_HOLD_MS 10000
#define POST_MAX_RETRIES 3 // how many times to try an init function
#define OTA_ROLLBACK_THRESHOLD 5 // how many resets in a row required to deem the boot partition faulty and switch to the other
#define FACTORY_RESET_HOLD_MS 10000 // how many ms is required to hold the button during cold boot to initialize factory reset
// Survives resets (panic, WDT, sw reset) but NOT power-on or external reset
RTC_DATA_ATTR static uint8_t ota_reset_counter = 0;
@@ -85,9 +85,7 @@ esp_err_t send_bat_log() {
// 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
if (any_efuse_tripped()) 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
@@ -154,9 +152,9 @@ 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);
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);
@@ -168,7 +166,6 @@ void app_main(void) {
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();
@@ -238,8 +235,8 @@ void app_main(void) {
//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();
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]
{
@@ -320,7 +317,7 @@ void app_main(void) {
esp_ota_mark_app_valid_cancel_rollback();
/*** MAIN LOOP ***/
uint8_t tap_count = 0;
uint8_t tap_count = 0;
int64_t tap_window_start = 0;
TickType_t xLastWakeTime = xTaskGetTickCount();
@@ -332,23 +329,31 @@ void app_main(void) {
/* Bring-up tool owns the LEDs, buttons, and relays while active. */
if (bringup_mode_is_active()) {
esp_task_wdt_reset();
continue;
continue; // while in bringup, don't do anything more
}
/* 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));
// Button wake: just exit idle and fall through to the normal main
// loop. The user is physically present, so any actual movement
// happens later via triple-tap / RF / web — by then WiFi+BT have
// had plenty of time to come back up on their own.
if (soft_idle_button_raw()) {
rtc_reset_shutdown_timer();
soft_idle_exit();
i2c_poll_buttons(); /* sync TCA9555 state after idle */
xLastWakeTime = xTaskGetTickCount();
}
// Alarm wake: must immediately issue FSM_CMD_START — nobody is
// here to press a button. soft_idle_enter() tore down WiFi+BT
// (see rtc.c soft_idle_enter); soft_idle_exit() restarts them
// but they come up asynchronously. Block up to 5 s for both
// event-group bits so telemetry/abort paths are live before the
// automated move begins. Past timeout we start anyway — the
// physical safety/efuse interlocks still protect the hardware.
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));
@@ -360,14 +365,14 @@ void app_main(void) {
solar_run_fsm();
rtc_check_shutdown_timer();
esp_task_wdt_reset();
continue;
continue; // while in idle, don't do anything more
}
i2c_poll_buttons();
if (i2c_get_button_state(0)) {
rtc_reset_shutdown_timer();
soft_idle_exit();
// soft_idle_exit(); // this should be superfluous
}
// --- Button logic: triple-tap, hold-to-reboot, cancel/stop ---
@@ -435,9 +440,7 @@ void app_main(void) {
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 &&
!any_efuse_tripped() &&
fsm_get_error() == ESP_OK
) {
drive_leds(LED_IDLE);

View File

@@ -433,13 +433,13 @@ esp_err_t process_bridge_current(bridge_t bridge) {
int adc_raw = 0;
int voltage_mv = 0;
adc_channel_t pin;
switch(bridge) {
case BRIDGE_DRIVE: pin = PIN_V_ISENS1; break;
case BRIDGE_JACK: pin = PIN_V_ISENS2; break;
case BRIDGE_AUX: pin = PIN_V_ISENS3; break;
default: return ESP_ERR_INVALID_ARG;
}
static const adc_channel_t bridge_isens_pins[N_BRIDGES] = {
[BRIDGE_DRIVE] = PIN_V_ISENS1,
[BRIDGE_JACK] = PIN_V_ISENS2,
[BRIDGE_AUX] = PIN_V_ISENS3,
};
if (bridge >= N_BRIDGES) return ESP_ERR_INVALID_ARG;
adc_channel_t pin = bridge_isens_pins[bridge];
if (adc_oneshot_read(adc1_handle, pin, &adc_raw) != ESP_OK) {
return 0;
@@ -528,19 +528,14 @@ esp_err_t process_bridge_current(bridge_t bridge) {
// PARAM_EFUSE_TAUCOOL : speed of cooldown for heating (units are 1/s; bigger = faster cooldown)
// Monitor E-fusing
float I_nominal = NAN;
switch(bridge) {
case BRIDGE_DRIVE:
I_nominal = get_param_value_t(PARAM_EFUSE_INOM_1).f32;
break;
case BRIDGE_JACK:
I_nominal = get_param_value_t(PARAM_EFUSE_INOM_2).f32;
break;
case BRIDGE_AUX:
I_nominal = get_param_value_t(PARAM_EFUSE_INOM_3).f32;
break;
default: break;
}
static const param_idx_t bridge_inom[N_BRIDGES] = {
[BRIDGE_DRIVE] = PARAM_EFUSE_INOM_1,
[BRIDGE_JACK] = PARAM_EFUSE_INOM_2,
[BRIDGE_AUX] = PARAM_EFUSE_INOM_3,
};
float I_nominal = (bridge < N_BRIDGES)
? get_param_value_t(bridge_inom[bridge]).f32
: NAN;
// Normalize the current as a fraction of rated current
float I_norm = fabsf(channel->current / I_nominal);
@@ -675,4 +670,26 @@ void efuse_set(bridge_t bridge, efuse_trip_t state)
if (bridge >= N_BRIDGES) return;
isens[bridge].tripped = state;
isens[bridge].trip_time = fsm_now;
}
const char *const bridge_names[N_BRIDGES] = {
[BRIDGE_DRIVE] = "DRIVE",
[BRIDGE_JACK] = "JACK",
[BRIDGE_AUX] = "AUX",
};
bool any_efuse_tripped(void) {
for (bridge_t b = 0; b < N_BRIDGES; b++) {
if (efuse_get(b)) return true;
}
return false;
}
float max_efuse_heat(void) {
float m = efuse_get_heat(0);
for (bridge_t b = 1; b < N_BRIDGES; b++) {
float h = efuse_get_heat(b);
if (h > m) m = h;
}
return m;
}

View File

@@ -26,6 +26,15 @@ efuse_trip_t efuse_get (bridge_t bridge); // Query if bridge is currently
float efuse_get_heat(bridge_t bridge);
void efuse_set(bridge_t bridge, efuse_trip_t state);
/* True if any of the N_BRIDGES bridges is currently tripped. */
bool any_efuse_tripped(void);
/* Max heat across all bridges — used for telemetry logging. */
float max_efuse_heat(void);
/* Human name per bridge_t index ("DRIVE" / "JACK" / "AUX"). */
extern const char *const bridge_names[N_BRIDGES];
float get_bridge_A(bridge_t bridge);
float get_bridge_raw_A(bridge_t bridge);
float get_battery_V();

View File

@@ -50,7 +50,7 @@ static uint64_t rtc_hw_time_us(void)
uint64_t last_activity_tick = 0;
// RTC_DATA_ATTR keeps these in RTC memory; persists across software resets (panics, WDT).
// AUDIT: no init path zeroes these — rtc_restore_time() recovers via RTC HW counter,
// no init path zeroes these — rtc_restore_time() recovers via RTC HW counter,
// rtc_set_s() is only called explicitly by the user. Verified 2026-03-12.
RTC_DATA_ATTR int64_t next_alarm_time_s = -1;
RTC_DATA_ATTR bool rtc_set = false;
@@ -308,62 +308,50 @@ void adjust_rtc_min(char *key, int8_t dir)
void rtc_schedule_next_alarm(void) {
int64_t start_sec = get_param_value_t(PARAM_MOVE_START).u32;
int64_t end_sec = get_param_value_t(PARAM_MOVE_END).u32;
int16_t num = get_param_value_t(PARAM_NUM_MOVES).i16;
/* Walk MOVE_TIME_0..MOVE_TIME_(NUM_MOVE_TIMES-1). Each slot is either
* -1 (disabled) or a 0..86399 seconds-into-day offset. For each enabled
* slot we compute its absolute Unix time for today and tomorrow, keep
* whichever is the soonest still-future timestamp, and take the minimum
* across all enabled slots. If no slot is enabled, the device has no
* schedule and next_alarm_time_s is set to -1 (web UI renders DISABLED).
*
* The slots are sorted by commit_params() so the first non-negative is
* the smallest seconds-into-day, but we DON'T short-circuit on the first
* future hit — slot[0]'s "today" can already be past while a later slot
* (smaller offset that wrapped) could be the soonest. Walking all 12 is
* cheap (~1µs) and removes that subtlety entirely. */
if (num <= 0) {
if (!rtc_is_set()) {
/* Without a valid wall clock, "seconds into day" is meaningless. */
next_alarm_time_s = -1;
return;
}
// Current time info
int64_t s_into_day = rtc_get_s_in_day();
time_t current_time = rtc_get_s();
time_t today_midnight = current_time - s_into_day;
int64_t s_into_day = rtc_get_s_in_day();
time_t current_time = rtc_get_s();
time_t today_midnight = current_time - s_into_day;
bool overnight = (start_sec > end_sec);
int64_t total_duration = overnight ? (86400 - start_sec) + end_sec : end_sec - start_sec;
time_t best = -1;
for (int i = 0; i < NUM_MOVE_TIMES; i++) {
int32_t slot = get_param_value_t(PARAM_MOVE_TIME_0 + i).i32;
if (slot < 0) continue; // disabled
// Determine period start
time_t period_start;
if (overnight && s_into_day < end_sec) {
// Current time is within overnight period → started yesterday
period_start = (today_midnight - 86400) + start_sec;
/* Candidate is today's occurrence if still in the future, else
* tomorrow's occurrence. >= keeps a "fire exactly at the matching
* second" semantic without arming twice. */
time_t candidate = today_midnight + slot;
if (candidate <= current_time) candidate += 86400;
if (best < 0 || candidate < best) best = candidate;
}
next_alarm_time_s = best;
if (best > 0) {
ESP_LOGI("ALARM", "SET FOR %lld (in %lld s)", (long long)best, (long long)(best - current_time));
} else {
// Normal or after end → starts today
period_start = today_midnight + start_sec;
ESP_LOGI("ALARM", "No enabled MOVE_TIME_* slots — schedule disabled");
}
//time_t period_end = period_start + total_duration;
if (num == 1) {
// Single alarm: at period start, if passed, next day
next_alarm_time_s = (current_time < period_start) ? period_start : period_start + 86400;
ESP_LOGI("ALARM", "SET FOR %lld (in %lld s)", next_alarm_time_s, next_alarm_time_s - current_time);
return;
}
// Find next alarm
int64_t spacing = total_duration / (num - 1);
time_t next_alarm = -1;
for (int16_t i = 0; i < num; i++) {
time_t alarm_time = period_start + spacing * i;
if (alarm_time > current_time) {
next_alarm = alarm_time;
break;
}
}
// If all passed, first of next period
if (next_alarm == -1) {
next_alarm = period_start + 86400;
}
next_alarm_time_s = next_alarm;
ESP_LOGI("ALARM", "SET FOR %lld (in %lld s)", next_alarm_time_s, next_alarm_time_s - current_time);
}
int64_t rtc_get_next_alarm_s() {

View File

@@ -22,5 +22,12 @@
#define SC_ERR_RTC_NOT_SET 0x220
#define SC_ERR_LOW_BATTERY 0x230
#include "esp_err.h"
/* Human-readable name for an SC_ERR_* code (or ESP_OK). Used by FSM log
* messages and the web UI so error strings live in one place. Returns a
* literal — no allocation, safe to use anywhere. */
const char *sc_err_str(esp_err_t e);
#endif /* MAIN_SC_ERR_H_ */

View File

@@ -149,52 +149,23 @@ static bool validate_param(param_idx_t id) {
// Clamp to [min, max] per type
bool clamped = false;
#define CLAMP_FIELD(field) do { \
if (parameter_table[id].field < parameter_mins[id].field) { \
parameter_table[id].field = parameter_mins[id].field; clamped = true; \
} else if (parameter_table[id].field > parameter_maxs[id].field) { \
parameter_table[id].field = parameter_maxs[id].field; clamped = true; \
} \
} while (0)
switch (type) {
case PARAM_TYPE_u16:
if (parameter_table[id].u16 < parameter_mins[id].u16) {
parameter_table[id].u16 = parameter_mins[id].u16; clamped = true;
} else if (parameter_table[id].u16 > parameter_maxs[id].u16) {
parameter_table[id].u16 = parameter_maxs[id].u16; clamped = true;
}
break;
case PARAM_TYPE_i16:
if (parameter_table[id].i16 < parameter_mins[id].i16) {
parameter_table[id].i16 = parameter_mins[id].i16; clamped = true;
} else if (parameter_table[id].i16 > parameter_maxs[id].i16) {
parameter_table[id].i16 = parameter_maxs[id].i16; clamped = true;
}
break;
case PARAM_TYPE_u32:
if (parameter_table[id].u32 < parameter_mins[id].u32) {
parameter_table[id].u32 = parameter_mins[id].u32; clamped = true;
} else if (parameter_table[id].u32 > parameter_maxs[id].u32) {
parameter_table[id].u32 = parameter_maxs[id].u32; clamped = true;
}
break;
case PARAM_TYPE_i32:
if (parameter_table[id].i32 < parameter_mins[id].i32) {
parameter_table[id].i32 = parameter_mins[id].i32; clamped = true;
} else if (parameter_table[id].i32 > parameter_maxs[id].i32) {
parameter_table[id].i32 = parameter_maxs[id].i32; clamped = true;
}
break;
case PARAM_TYPE_f32:
if (parameter_table[id].f32 < parameter_mins[id].f32) {
parameter_table[id].f32 = parameter_mins[id].f32; clamped = true;
} else if (parameter_table[id].f32 > parameter_maxs[id].f32) {
parameter_table[id].f32 = parameter_maxs[id].f32; clamped = true;
}
break;
case PARAM_TYPE_f64:
if (parameter_table[id].f64 < parameter_mins[id].f64) {
parameter_table[id].f64 = parameter_mins[id].f64; clamped = true;
} else if (parameter_table[id].f64 > parameter_maxs[id].f64) {
parameter_table[id].f64 = parameter_maxs[id].f64; clamped = true;
}
break;
default:
break;
case PARAM_TYPE_u16: CLAMP_FIELD(u16); break;
case PARAM_TYPE_i16: CLAMP_FIELD(i16); break;
case PARAM_TYPE_u32: CLAMP_FIELD(u32); break;
case PARAM_TYPE_i32: CLAMP_FIELD(i32); break;
case PARAM_TYPE_f32: CLAMP_FIELD(f32); break;
case PARAM_TYPE_f64: CLAMP_FIELD(f64); break;
default: break;
}
#undef CLAMP_FIELD
if (clamped) {
ESP_LOGW(TAG, "Param %s: out of range, clamped", parameter_names[id]);
@@ -387,70 +358,72 @@ const char* get_param_unit(param_idx_t id) {
}
// ============================================================================
// STORAGE HELPER: Pack parameter value into buffer
// STORAGE HELPERS: Pack / unpack parameter value into a fixed buffer
// ============================================================================
// All param_value_t fields share offset 0 of the union, so a single memcpy of
// param_type_size() bytes covers every type. Strings get an explicit
// null-termination on unpack to defend against a corrupted flash entry.
static void pack_param(uint8_t *dest, param_idx_t id) {
param_type_e type = parameter_types[id];
switch(type) {
case PARAM_TYPE_u16:
memcpy(dest, &parameter_table[id].u16, 2);
break;
case PARAM_TYPE_i16:
memcpy(dest, &parameter_table[id].i16, 2);
break;
case PARAM_TYPE_u32:
memcpy(dest, &parameter_table[id].u32, 4);
break;
case PARAM_TYPE_i32:
memcpy(dest, &parameter_table[id].i32, 4);
break;
case PARAM_TYPE_f32:
memcpy(dest, &parameter_table[id].f32, 4);
break;
case PARAM_TYPE_f64:
memcpy(dest, &parameter_table[id].f64, 8);
break;
case PARAM_TYPE_str:
memcpy(dest, parameter_table[id].str, 16);
break;
default:
memset(dest, 0, 16);
break;
size_t sz = param_type_size(type);
if (sz == 0 || sz > sizeof(param_value_t)) { memset(dest, 0, sizeof(param_value_t)); return; }
memcpy(dest, &parameter_table[id], sz);
}
static void unpack_param(const uint8_t *src, param_idx_t id) {
param_type_e type = parameter_types[id];
size_t sz = param_type_size(type);
if (sz == 0 || sz > sizeof(param_value_t)) return;
memcpy(&parameter_table[id], src, sz);
if (type == PARAM_TYPE_str) parameter_table[id].str[PARAM_STR_SIZE - 1] = '\0';
}
// ============================================================================
// Promote a numeric parameter to double for callers that don't care about
// the underlying integer/float width (cJSON, UI display). Returns 0.0 for
// string params — caller must check get_param_type() first when that matters.
// ============================================================================
double param_to_double(param_idx_t id) {
if (id >= NUM_PARAMS) return 0.0;
param_value_t v = parameter_table[id];
switch (parameter_types[id]) {
case PARAM_TYPE_u16: return (double)v.u16;
case PARAM_TYPE_i16: return (double)v.i16;
case PARAM_TYPE_u32: return (double)v.u32;
case PARAM_TYPE_i32: return (double)v.i32;
case PARAM_TYPE_f32: return (double)v.f32;
case PARAM_TYPE_f64: return v.f64;
default: return 0.0;
}
}
// ============================================================================
// STORAGE HELPER: Unpack parameter value from buffer
// Sort the 12 MOVE_TIME_* slots: ascending non-negative values first, then
// every -1 (disabled) entry. Insertion sort over a tiny fixed-width array
// — N=12 makes algorithmic complexity irrelevant. Treating -1 as +infinity
// keeps both the "rank by time" and "disabled goes last" behavior in a
// single comparator. Called by commit_params() so on-disk layout is always
// the canonical sorted order; UI can read MOVE_TIME_0..N back in order.
// ============================================================================
static void unpack_param(const uint8_t *src, param_idx_t id) {
param_type_e type = parameter_types[id];
switch(type) {
case PARAM_TYPE_u16:
memcpy(&parameter_table[id].u16, src, 2);
break;
case PARAM_TYPE_i16:
memcpy(&parameter_table[id].i16, src, 2);
break;
case PARAM_TYPE_u32:
memcpy(&parameter_table[id].u32, src, 4);
break;
case PARAM_TYPE_i32:
memcpy(&parameter_table[id].i32, src, 4);
break;
case PARAM_TYPE_f32:
memcpy(&parameter_table[id].f32, src, 4);
break;
case PARAM_TYPE_f64:
memcpy(&parameter_table[id].f64, src, 8);
break;
case PARAM_TYPE_str:
memcpy(parameter_table[id].str, src, 16);
parameter_table[id].str[15] = '\0'; // Ensure null termination
break;
default:
break;
void sort_move_schedule(void) {
int32_t v[NUM_MOVE_TIMES];
for (int i = 0; i < NUM_MOVE_TIMES; i++) {
v[i] = parameter_table[PARAM_MOVE_TIME_0 + i].i32;
}
for (int i = 1; i < NUM_MOVE_TIMES; i++) {
int32_t key = v[i];
int32_t key_rank = (key < 0) ? INT32_MAX : key;
int j = i - 1;
while (j >= 0) {
int32_t j_rank = (v[j] < 0) ? INT32_MAX : v[j];
if (j_rank <= key_rank) break;
v[j + 1] = v[j];
j--;
}
v[j + 1] = key;
}
for (int i = 0; i < NUM_MOVE_TIMES; i++) {
parameter_table[PARAM_MOVE_TIME_0 + i].i32 = v[i];
}
}
@@ -463,6 +436,11 @@ esp_err_t commit_params(void) {
return ESP_ERR_INVALID_STATE;
}
/* Canonicalize the schedule before writing — on-disk layout is always
* sorted, so anything that reads MOVE_TIME_0..N back can iterate in
* time order without re-sorting. */
sort_move_schedule();
ESP_LOGI(TAG, "Committing %d parameters to flash...", NUM_PARAMS);
// Erase entire params partition
@@ -1157,10 +1135,6 @@ static uint32_t log_read_cursor = 0;
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if no more entries, error code otherwise
*/
esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type) {
// NOTE: log_read_cursor must be declared as a file-scope static variable
// Add this declaration near the other log static variables in storage.c:
// static uint32_t log_read_cursor = 0;
if (!log_initialized || log_partition == NULL) {
ESP_LOGE(TAG, "Logging not initialized");
return ESP_ERR_INVALID_STATE;

View File

@@ -4,8 +4,6 @@
#include <stdint.h>
#include "esp_err.h"
// TODO: Sanity check that the EEPROM is working (sacrifice sector 0?)
// ============================================================================
// FLASH LAYOUT CONSTANTS
// ============================================================================
@@ -118,7 +116,31 @@ typedef struct {
PARAM_DEF(SAFETY_BREAK_US, u32, 300000, "", 0, 10000000) \
PARAM_DEF(SAFETY_MAKE_US, u32, 1000000, "", 0, 10000000) \
PARAM_DEF(JACK_IS_DOWN, f32, 8.0, "A", 0.0, 200.0) /* deprecated: may duplicate JACK_I_DOWN */ \
PARAM_DEF(FLUFF_PREDRIVE_MS, u32, 2000, "ms", 0, 60000)
PARAM_DEF(FLUFF_PREDRIVE_MS, u32, 2000, "ms", 0, 60000) \
/* Tabular schedule: up to 12 daily move times (seconds since local midnight). \
* A value of -1 marks the slot as disabled — the default. Slots are sorted \
* by commit_params() so non-negative values appear first in ascending order, \
* with -1 entries pushed to the end. Range allows the validator to clamp \
* out-of-band negatives (e.g. -2) back to -1 = disabled. Appended at the \
* end of PARAM_LIST so existing flash layout / CRC offsets stay stable. \
* MOVE_START / MOVE_END / NUM_MOVES above are deprecated; the scheduler \
* no longer reads them and the web UI no longer surfaces them. */ \
PARAM_DEF(MOVE_TIME_0, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_1, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_2, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_3, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_4, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_5, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_6, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_7, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_8, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_9, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_10, i32, -1, "s", -1, 86399) \
PARAM_DEF(MOVE_TIME_11, i32, -1, "s", -1, 86399)
/* Tabular schedule width. The enum entries above must remain contiguous so
* PARAM_MOVE_TIME_0 + i indexes slot i. */
#define NUM_MOVE_TIMES 12
// Generate enum for parameter indices
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_##name,
@@ -179,10 +201,17 @@ const char* get_param_name(param_idx_t id);
param_value_t get_param_default(param_idx_t id);
const char* get_param_unit(param_idx_t id);
const char* get_param_json_string(param_idx_t id, char* buffer, size_t buf_size);
double param_to_double(param_idx_t id); // numeric params only — see storage.c
// Parameter commit to flash
esp_err_t commit_params(void);
/* In-place sort of MOVE_TIME_0..MOVE_TIME_(NUM_MOVE_TIMES-1) so that
* non-negative entries come first in ascending order, with -1 (disabled)
* entries pushed to the end. Called automatically by commit_params() —
* exposed separately for tests / migrations. */
void sort_move_schedule(void);
// Logging functions
esp_err_t log_init(void);
esp_err_t log_write(const uint8_t* buf, uint8_t len, uint8_t type);

File diff suppressed because one or more lines are too long

View File

@@ -270,16 +270,19 @@
<button id="now_btn" onclick="setTimeToNow()">Sync Time</button></td>
</tr>
<tr>
<td>Schedule Start</td>
<td><input type="time" id="UX_MOVE_START" onchange="changeSchedule(this)"/></td>
</tr>
<tr>
<td>Schedule End</td>
<td><input type="time" id="UX_MOVE_END" onchange="changeSchedule(this)"/></td>
</tr>
<tr>
<td># Moves/Day</td>
<td><input type="number" min="0" id="UX_NUM_MOVES" onchange="changeSchedule(this)"/></td>
<td>Schedule</td>
<td>
<!-- Visible rows are rendered by renderSchedule() from the
12 hidden PARAM_MOVE_TIME_N inputs below. -->
<div id="schedule_rows"></div>
<button id="schedule_add_btn" onclick="addScheduleSlot()">+ Add move</button>
<!-- Hidden inputs hold the seconds-of-day value for each of
the 12 schedule slots (-1 = disabled). They participate
in the regular PARAM_* save flow: when the user edits
a row we mark the corresponding hidden input .changed,
and commitParams() picks it up like any other param. -->
<div style="display:none" id="schedule_hidden_inputs"></div>
</td>
</tr>
<tr>
<td>Next Move At</td>
@@ -839,7 +842,140 @@
markChanged(input);
}
const scheduleInputs = ['MOVE_START', 'MOVE_END', 'NUM_MOVES', 'DRIVE_DIST', 'JACK_DIST'];
const scheduleInputs = ['DRIVE_DIST', 'JACK_DIST'];
// ============================================================
// Tabular schedule (up to 12 daily move times)
// ============================================================
// The user thinks in "list of times-of-day". The firmware stores
// 12 i32 params (MOVE_TIME_0..MOVE_TIME_11), each holding either
// seconds-since-local-midnight (0..86399) or -1 = disabled. The
// device sorts the array ascending after every commit so on every
// poll the enabled slots arrive packed at the front in time order.
//
// We back the UI with 12 hidden `<input id="PARAM_MOVE_TIME_N">`
// elements so the values plug into the existing PARAM_* save flow:
// editing a row marks its hidden input .changed, commitParams()
// picks it up like any other param.
const NUM_MOVE_TIMES = 12;
(function initScheduleHiddenInputs() {
const host = ge('schedule_hidden_inputs');
if (!host) return;
for (let i = 0; i < NUM_MOVE_TIMES; i++) {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.id = `PARAM_MOVE_TIME_${i}`;
inp.value = -1;
host.appendChild(inp);
}
})();
function readScheduleSlot(i) {
const inp = ge(`PARAM_MOVE_TIME_${i}`);
return inp ? parseInt(inp.value, 10) : -1;
}
function writeScheduleSlot(i, value) {
const inp = ge(`PARAM_MOVE_TIME_${i}`);
if (!inp) return;
inp.value = value;
markChanged(inp);
}
// Format seconds-of-day as HH:MM for an <input type="time">.
function secondsToHM(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;
}
function renderSchedule() {
const host = ge('schedule_rows');
if (!host) return;
// If any time input inside the schedule is currently focused,
// the user is mid-edit; re-rendering would steal focus on every
// 2-second poll. Skip — the next poll after blur picks it up.
if (host.contains(document.activeElement)
&& document.activeElement.type === 'time') return;
host.innerHTML = '';
let enabledCount = 0;
for (let i = 0; i < NUM_MOVE_TIMES; i++) {
const v = readScheduleSlot(i);
if (v < 0) continue;
enabledCount++;
const row = document.createElement('div');
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.gap = '4px';
const t = document.createElement('input');
t.type = 'time';
t.value = secondsToHM(v);
// Capture the slot index — at render time `i` is the slot,
// which is also where the hidden input lives.
t.oninput = ((slot) => () => onScheduleTimeEdit(slot, t.value))(i);
row.appendChild(t);
const x = document.createElement('button');
x.textContent = '✕'; // ✕
x.style.width = '40px';
x.style.flexShrink = '0';
x.onclick = ((slot) => () => onScheduleRemove(slot))(i);
row.appendChild(x);
host.appendChild(row);
}
// The + Add button is disabled when all 12 slots are in use.
const addBtn = ge('schedule_add_btn');
if (addBtn) addBtn.disabled = enabledCount >= NUM_MOVE_TIMES;
}
function onScheduleTimeEdit(slot, hhmm) {
if (!hhmm) return; // empty edit — leave previous value
const parts = hhmm.split(':').map(Number);
if (parts.length < 2 || Number.isNaN(parts[0]) || Number.isNaN(parts[1])) return;
writeScheduleSlot(slot, parts[0] * 3600 + parts[1] * 60);
// Don't re-render: the visible <input> already shows the new
// value and a rebuild would steal focus mid-edit.
}
function onScheduleRemove(slot) {
writeScheduleSlot(slot, -1);
renderSchedule();
}
function addScheduleSlot() {
// Find the first disabled slot and arm it with a default of
// 12:00. Visible rows are sorted alphabetically by storage
// index so the new row appears at whatever position holds the
// first -1 — the device will re-sort canonically on commit.
for (let i = 0; i < NUM_MOVE_TIMES; i++) {
if (readScheduleSlot(i) < 0) {
writeScheduleSlot(i, 12 * 3600);
renderSchedule();
return;
}
}
}
// Called once per /get poll from updateUI(). Syncs the 12 hidden
// inputs from the server's `parameters` object using _safeSet so
// any slot the user has edited (marked .changed) survives the
// sync. renderSchedule() then redraws the visible rows.
function updateScheduleFromServer() {
if (!data.parameters) return;
for (let i = 0; i < NUM_MOVE_TIMES; i++) {
const inp = ge(`PARAM_MOVE_TIME_${i}`);
const v = data.parameters[`MOVE_TIME_${i}`];
if (typeof v === 'number') _safeSet(inp, v);
}
renderSchedule();
}
function changeSchedule(ux_input) {
@@ -1074,7 +1210,8 @@
const seconds = String(dt.getUTCSeconds()).padStart(2, '0');
timeOutput.value = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
} else {
// <=0 means firmware has no alarm scheduled (e.g. NUM_MOVES=0).
// <=0 means firmware has no alarm scheduled — either every
// MOVE_TIME_* slot is -1, or the RTC isn't set yet.
if (timeOutput.type !== 'text') timeOutput.type = 'text';
timeOutput.value = 'DISABLED';
}
@@ -1084,6 +1221,7 @@
if (data.parameters) {
updateParamTable();
updateScheduleInputs();
updateScheduleFromServer();
}
// Update remaining distance (special parameter)
@@ -1095,11 +1233,24 @@
}
// Keys whose inputs live OUTSIDE the auto-generated DANGER ZONE
// table (in the dedicated WiFi section, or commented out entirely).
// Must be filtered in BOTH the build and update paths — otherwise a
// poll sees "no input for NET_SSID" and force-rebuilds the whole
// table every tick, wiping every in-progress edit.
const PARAM_TABLE_SKIP = new Set(['NET_SSID', 'NET_PASS', 'WIFI_SSID', 'WIFI_PASS']);
// table (in the dedicated WiFi section, in the Schedule section, or
// commented out entirely). Must be filtered in BOTH the build and
// update paths — otherwise a poll sees "no input for X" and force-
// rebuilds the whole table every tick, wiping every in-progress edit.
//
// MOVE_START / MOVE_END / NUM_MOVES are kept in firmware for storage
// compatibility but no longer read by the scheduler or surfaced here.
const PARAM_TABLE_SKIP = new Set([
'NET_SSID', 'NET_PASS', 'WIFI_SSID', 'WIFI_PASS',
'MOVE_START', 'MOVE_END', 'NUM_MOVES',
]);
// MOVE_TIME_0..11 are rendered in the dedicated Schedule section;
// exclude them from the DANGER ZONE table the same way (kept as a
// prefix check so we don't have to maintain a 12-entry list).
function paramSkipped(key) {
return PARAM_TABLE_SKIP.has(key) || key.startsWith('MOVE_TIME_');
}
function updateParamTable() {
const table = ge('table');
@@ -1107,7 +1258,7 @@
// Sort parameters alphabetically, pre-filtering keys that don't
// belong in the auto-generated DANGER ZONE table.
const sortedParams = Object.entries(data.parameters)
.filter(([k]) => !PARAM_TABLE_SKIP.has(k))
.filter(([k]) => !paramSkipped(k))
.sort((a, b) => a[0].localeCompare(b[0]));
if (!paramTableCreated) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -71,6 +71,68 @@ static httpd_handle_t http_server_instance = NULL;
char http_buffer[4096];
static SemaphoreHandle_t http_buffer_mutex = NULL;
/* Run an httpd call; on non-OK, log a "Failed to <what>" message and
* return the error code from the enclosing function. Replaces the
* three-line set_type / log / return triples that dominated handlers. */
#define HTTPD_RET_ON_ERR(expr, what) do { \
esp_err_t _e = (expr); \
if (_e != ESP_OK) { \
ESP_LOGE(TAG, "Failed to %s: %s", what, esp_err_to_name(_e)); \
return _e; \
} \
} while (0)
/* Same but only warns (used when the handler can plausibly proceed even
* if the call failed — typically the trailing Connection: close header). */
#define HTTPD_WARN_ON_ERR(expr, what) do { \
esp_err_t _e = (expr); \
if (_e != ESP_OK) { \
ESP_LOGW(TAG, "Failed to %s: %s", what, esp_err_to_name(_e)); \
} \
} while (0)
/* Read [from, to) from `part` into http_buffer in chunks and stream each
* chunk as an HTTP body fragment. The shared http_buffer is already held
* by the caller via with_http_buffer(). Returns the first error to abort
* the caller's response (and arranges a 500 if the partition read fails). */
static esp_err_t stream_partition_range(httpd_req_t *req,
const esp_partition_t *part,
int32_t from, int32_t to) {
int32_t offset = from;
while (offset < to) {
size_t to_read = MIN(sizeof(http_buffer), (size_t)(to - offset));
esp_err_t err = esp_partition_read(part, offset, http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to read storage");
}
err = httpd_resp_send_chunk(req, (const char *)http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return err;
}
offset += to_read;
}
return ESP_OK;
}
/* Schedule a one-shot esp_timer to invoke `cb` `delay_us` from now. Used
* to defer sleep/hibernate/wifi_restart calls until the httpd handler has
* returned — calling httpd_stop()/webserver_stop() inside a handler dead-
* locks because httpd_stop waits for all handlers to drain. `*slot` is
* cached across calls so we don't leak timer handles on repeated requests. */
static void defer_call(esp_timer_handle_t *slot, const char *name,
esp_timer_cb_t cb, uint64_t delay_us) {
if (*slot == NULL) {
esp_timer_create_args_t ta = { .callback = cb, .name = name };
esp_timer_create(&ta, slot);
}
if (*slot != NULL) esp_timer_start_once(*slot, delay_us);
}
static esp_err_t with_http_buffer(httpd_req_t *req,
esp_err_t (*body)(httpd_req_t *)) {
if (http_buffer_mutex != NULL) {
@@ -99,33 +161,13 @@ static esp_err_t root_get_handler(httpd_req_t *req) {
}
bringup_notify_http_request();
// Send the HTML response
esp_err_t err = httpd_resp_set_type(req, "text/html");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set response type: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set content encoding header: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_send(req, (const char *)html_content, html_content_len);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send HTML response: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_set_hdr(req, "Connection", "close");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
// Continue anyway
}
return err;
HTTPD_RET_ON_ERR(httpd_resp_set_type(req, "text/html"), "set response type");
HTTPD_RET_ON_ERR(httpd_resp_set_hdr(req, "Content-Encoding", "gzip"), "set content encoding header");
HTTPD_RET_ON_ERR(httpd_resp_send(req, (const char *)html_content, html_content_len),
"send HTML response");
HTTPD_WARN_ON_ERR(httpd_resp_set_hdr(req, "Connection", "close"), "set connection header");
return ESP_OK;
}
// Cache the storage partition pointer to avoid repeated lookups
@@ -294,92 +336,24 @@ static esp_err_t log_handler_locked(httpd_req_t *req) {
return err;
}
// Send log data (same as before)
int32_t offset = tail;
if (tail == head) {
// Empty log, nothing more to send
// Send log data. Three cases:
// tail == head: log is empty, nothing more to stream.
// tail < head: contiguous run [tail, head).
// tail > head: wrapped — stream [tail, partition_end) then [log_start, head).
if (tail < head) {
err = stream_partition_range(req, log_part, tail, head);
if (err != ESP_OK) return err;
} else if (tail > head) {
err = stream_partition_range(req, log_part, tail, (int32_t)log_part->size);
if (err != ESP_OK) return err;
err = stream_partition_range(req, log_part, log_start, head);
if (err != ESP_OK) return err;
}
else if (tail < head) {
// Normal case: tail before head
while (offset < head) {
size_t to_read = MIN(sizeof(http_buffer), head - offset);
err = esp_partition_read(log_part, offset, http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to read storage");
}
err = httpd_resp_send_chunk(req, (const char *)http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return err;
}
offset += to_read;
}
}
else {
// Wrapped case: tail after head, read from tail to end, then start to head
while (offset < (int32_t)log_part->size) {
size_t to_read = MIN(sizeof(http_buffer), log_part->size - offset);
err = esp_partition_read(log_part, offset, http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to read storage");
}
err = httpd_resp_send_chunk(req, (const char *)http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return err;
}
offset += to_read;
}
// Now read from start to head
offset = log_start;
while (offset < head) {
size_t to_read = MIN(sizeof(http_buffer), head - offset);
err = esp_partition_read(log_part, offset, http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to read storage");
}
err = httpd_resp_send_chunk(req, (const char *)http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return err;
}
offset += to_read;
}
}
// Send empty chunk to signal end
err = httpd_resp_send_chunk(req, NULL, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send final chunk: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_set_hdr(req, "Connection", "close");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
}
return err;
HTTPD_RET_ON_ERR(httpd_resp_send_chunk(req, NULL, 0), "send final chunk");
HTTPD_WARN_ON_ERR(httpd_resp_set_hdr(req, "Connection", "close"), "set connection header");
return ESP_OK;
}
/**
@@ -567,66 +541,30 @@ static esp_err_t post_handler_locked(httpd_req_t *req) {
return ESP_OK; // Never reached
}
/* All three of these have the same deadlock concern: calling
* httpd_stop() (directly or via webserver_stop / soft_idle_enter /
* hibernate_enter) from inside a handler blocks because httpd_stop()
* waits for all handlers to drain. Defer to a one-shot timer so this
* handler returns first and the httpd task becomes free. */
if (should_restart_wifi) {
/* Same deadlock risk as should_sleep — httpd_stop() inside
* webserver_restart_wifi() cannot be called from within a handler. */
static esp_timer_handle_t s_wifi_restart_timer = NULL;
if (s_wifi_restart_timer == NULL) {
esp_timer_create_args_t ta = {
.callback = webserver_restart_wifi_cb,
.name = "wifi_restart",
};
esp_timer_create(&ta, &s_wifi_restart_timer);
}
if (s_wifi_restart_timer != NULL) {
esp_timer_start_once(s_wifi_restart_timer, 500 * 1000); /* 500 ms in µs */
}
defer_call(&s_wifi_restart_timer, "wifi_restart", webserver_restart_wifi_cb, 500 * 1000);
return ESP_OK;
}
if (should_sleep) {
ESP_LOGI(TAG, "Entering soft idle in 2 seconds...");
/* Cannot call soft_idle_enter() (→ httpd_stop()) from within an httpd
* handler — httpd_stop() waits for all handlers to finish, causing a
* deadlock. Schedule via a one-shot timer so this handler returns
* first and the httpd task is free. */
static esp_timer_handle_t s_sleep_timer = NULL;
if (s_sleep_timer == NULL) {
esp_timer_create_args_t ta = {
.callback = soft_idle_enter_cb,
.name = "soft_idle",
};
esp_timer_create(&ta, &s_sleep_timer);
}
if (s_sleep_timer != NULL) {
esp_timer_start_once(s_sleep_timer, 2000 * 1000); /* 2 s in µs */
}
defer_call(&s_sleep_timer, "soft_idle", soft_idle_enter_cb, 2000 * 1000);
return ESP_OK;
}
if (should_hibernate) {
ESP_LOGI(TAG, "Hibernating in 2 seconds...");
static esp_timer_handle_t s_hibernate_timer = NULL;
defer_call(&s_hibernate_timer, "hibernate", hibernate_enter_cb, 2000 * 1000);
return ESP_OK;
}
if (should_hibernate) {
ESP_LOGI(TAG, "Hibernating in 2 seconds...");
/* Same deadlock concern as soft_idle: webserver_stop() inside
* hibernate_enter() blocks on the httpd task; defer via timer. */
static esp_timer_handle_t s_hibernate_timer = NULL;
if (s_hibernate_timer == NULL) {
esp_timer_create_args_t ta = {
.callback = hibernate_enter_cb,
.name = "hibernate",
};
esp_timer_create(&ta, &s_hibernate_timer);
}
if (s_hibernate_timer != NULL) {
esp_timer_start_once(s_hibernate_timer, 2000 * 1000); /* 2 s in µs */
}
return ESP_OK;
}
err = httpd_resp_set_hdr(req, "Connection", "close");
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
}
HTTPD_WARN_ON_ERR(httpd_resp_set_hdr(req, "Connection", "close"), "set connection header");
return err;
}