stashing
This commit is contained in:
96
main/build/default/CMakeCache.txt
Normal file
96
main/build/default/CMakeCache.txt
Normal 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
|
||||
|
||||
15
main/build/default/CMakeFiles/3.24.0/CMakeSystem.cmake
Normal file
15
main/build/default/CMakeFiles/3.24.0/CMakeSystem.cmake
Normal 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)
|
||||
1
main/build/default/CMakeFiles/CMakeOutput.log
Normal file
1
main/build/default/CMakeFiles/CMakeOutput.log
Normal file
@@ -0,0 +1 @@
|
||||
The system is: Windows - 10.0.19045 - AMD64
|
||||
1
main/build/default/CMakeFiles/cmake.check_cache
Normal file
1
main/build/default/CMakeFiles/cmake.check_cache
Normal file
@@ -0,0 +1 @@
|
||||
# This file is generated by cmake for dependency checking of the CMakeCache.txt file
|
||||
156
main/comms.c
156
main/comms.c
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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], ¤t_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;
|
||||
}
|
||||
|
||||
@@ -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_ */
|
||||
797
main/hard_ui.c
797
main/hard_ui.c
@@ -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 = ¶m_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 (0–1439)
|
||||
* 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;
|
||||
}
|
||||
51
main/main.c
51
main/main.c
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
84
main/rtc.c
84
main/rtc.c
@@ -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() {
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
182
main/storage.c
182
main/storage.c
@@ -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, ¶meter_table[id].u16, 2);
|
||||
break;
|
||||
case PARAM_TYPE_i16:
|
||||
memcpy(dest, ¶meter_table[id].i16, 2);
|
||||
break;
|
||||
case PARAM_TYPE_u32:
|
||||
memcpy(dest, ¶meter_table[id].u32, 4);
|
||||
break;
|
||||
case PARAM_TYPE_i32:
|
||||
memcpy(dest, ¶meter_table[id].i32, 4);
|
||||
break;
|
||||
case PARAM_TYPE_f32:
|
||||
memcpy(dest, ¶meter_table[id].f32, 4);
|
||||
break;
|
||||
case PARAM_TYPE_f64:
|
||||
memcpy(dest, ¶meter_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, ¶meter_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(¶meter_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(¶meter_table[id].u16, src, 2);
|
||||
break;
|
||||
case PARAM_TYPE_i16:
|
||||
memcpy(¶meter_table[id].i16, src, 2);
|
||||
break;
|
||||
case PARAM_TYPE_u32:
|
||||
memcpy(¶meter_table[id].u32, src, 4);
|
||||
break;
|
||||
case PARAM_TYPE_i32:
|
||||
memcpy(¶meter_table[id].i32, src, 4);
|
||||
break;
|
||||
case PARAM_TYPE_f32:
|
||||
memcpy(¶meter_table[id].f32, src, 4);
|
||||
break;
|
||||
case PARAM_TYPE_f64:
|
||||
memcpy(¶meter_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;
|
||||
|
||||
@@ -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
@@ -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
1394
main/webpage_gzip.h
1394
main/webpage_gzip.h
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
260
main/webserver.c
260
main/webserver.c
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user