i think we're basically done
This commit is contained in:
150
main/bringup.c
150
main/bringup.c
@@ -80,15 +80,29 @@ static float elapsed_s(void)
|
||||
|
||||
/* -------- output helpers -------- */
|
||||
|
||||
/* Build the line in a stack buffer and emit with a single write so concurrent
|
||||
* ESP_LOGx output (notably the wifi driver during BU.WIFI.START) cannot slice
|
||||
* into the middle of it. Leading '\n' protects against partial lines that
|
||||
* another task may have written without a terminator. */
|
||||
__attribute__((format(printf, 2, 3)))
|
||||
static void emit(const char *kind, const char *fmt, ...)
|
||||
{
|
||||
printf("BU.%s ", kind);
|
||||
char buf[256];
|
||||
/* Reserve one byte at the end for the trailing '\n' so a long line is
|
||||
* truncated within the body rather than dropping the newline. Without
|
||||
* this, a body that filled the buffer would produce a line glued to
|
||||
* whatever came next on the wire. */
|
||||
const int cap = (int)sizeof(buf) - 1; // room for '\n'
|
||||
int n = snprintf(buf, cap, "\nBU.%s ", kind);
|
||||
if (n < 0) n = 0;
|
||||
if (n > cap) n = cap;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vprintf(fmt, ap);
|
||||
int m = vsnprintf(buf + n, cap - n, fmt, ap);
|
||||
va_end(ap);
|
||||
printf("\n");
|
||||
if (m > 0) n += (m < cap - n) ? m : cap - n;
|
||||
buf[n++] = '\n';
|
||||
fwrite(buf, 1, n, stdout);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
@@ -295,7 +309,12 @@ static void cmd_led_watch(char *args)
|
||||
static void cmd_adc_once(void)
|
||||
{
|
||||
int bat_mv = get_bat_raw_mv();
|
||||
float bat_V = get_battery_V();
|
||||
/* Bypass the EMA — process_battery_voltage() runs in the FSM task,
|
||||
* which is paused while bring-up is active, so get_battery_V() returns
|
||||
* a stale value that never reflects V_SENS_K / V_SENS_OFFSET writes
|
||||
* issued during calibration. Compute fresh from raw mV + current params. */
|
||||
float bat_V = bat_mv * get_param_value_t(PARAM_V_SENS_K).f32
|
||||
+ get_param_value_t(PARAM_V_SENS_OFFSET).f32;
|
||||
#ifdef BOARD_V5
|
||||
/* VOC and FAULT pins are unusable on V5 (input-only ESP32 GPIOs
|
||||
* without external pulls — see README "V5 hardware caveats"); skip. */
|
||||
@@ -345,6 +364,9 @@ static void cmd_sensors_watch(char *args)
|
||||
* on UART0 (operator hit Enter on the host side).
|
||||
* sec > 0 → watch for that many seconds, then return.
|
||||
*/
|
||||
/* Force the sensor rail (P10) up before we observe — covers cases where
|
||||
* the FSM or sensor task drove it low between boot and BU.BEGIN. */
|
||||
i2c_relays_idle();
|
||||
char *s = args;
|
||||
char *t = next_tok(&s);
|
||||
int sec = t ? atoi(t) : 0;
|
||||
@@ -462,7 +484,12 @@ static void cmd_relay(char *args)
|
||||
if (ms < 10) ms = 10;
|
||||
if (ms > 2000) ms = 2000;
|
||||
|
||||
if (!get_is_safe()) { SKIP("relay reason=\"safety open\""); return; }
|
||||
/* Read SAFETY directly: sensors_check() runs in the FSM task, which is
|
||||
* paused while bring-up is active, so is_safe / get_is_safe() are stale.
|
||||
* Safety pin is active-LOW. */
|
||||
extern uint8_t sensor_pins[N_SENSORS];
|
||||
#define _BU_SAFETY_OPEN() (gpio_get_level(sensor_pins[SENSOR_SAFETY]) != 0)
|
||||
if (_BU_SAFETY_OPEN()) { SKIP("relay reason=\"safety open\""); return; }
|
||||
|
||||
/* P10 / sensor power rail. Default is ON; pulse it OFF to prove the line
|
||||
* can be driven, then restore. */
|
||||
@@ -511,11 +538,44 @@ static void cmd_relay(char *args)
|
||||
last_relay_state = rs;
|
||||
i2c_set_relays(rs);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(ms / 2));
|
||||
fsm_now = esp_timer_get_time();
|
||||
process_bridge_current(b);
|
||||
float I_mid = get_bridge_A(b);
|
||||
vTaskDelay(pdMS_TO_TICKS(ms - ms / 2));
|
||||
/* JACK DOWN should stop as soon as the JACK sensor goes active (LOW) so
|
||||
* the bring-up pulse can't drive the actuator into its mechanical limit.
|
||||
* Other directions/bridges run for the full requested duration. SAFETY
|
||||
* is checked every iteration regardless of bridge — multi-second pulses
|
||||
* during bring-up must still kill the motor on a safety break. */
|
||||
bool jack_down = (b == BRIDGE_JACK && dir == BRIDGE_REV);
|
||||
bool stopped_by_sensor = false;
|
||||
bool stopped_by_safety = false;
|
||||
|
||||
int64_t pulse_start_us = esp_timer_get_time();
|
||||
int64_t mid_us = pulse_start_us + (int64_t)(ms / 2) * 1000;
|
||||
int64_t end_us = pulse_start_us + (int64_t)ms * 1000;
|
||||
|
||||
float I_mid = NAN;
|
||||
while (esp_timer_get_time() < end_us) {
|
||||
if (_BU_SAFETY_OPEN()) {
|
||||
stopped_by_safety = true;
|
||||
break;
|
||||
}
|
||||
if (jack_down && gpio_get_level(sensor_pins[SENSOR_JACK]) == 0) {
|
||||
stopped_by_sensor = true;
|
||||
break;
|
||||
}
|
||||
if (isnan(I_mid) && esp_timer_get_time() >= mid_us) {
|
||||
fsm_now = esp_timer_get_time();
|
||||
process_bridge_current(b);
|
||||
I_mid = get_bridge_A(b);
|
||||
}
|
||||
esp_task_wdt_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
if (isnan(I_mid)) {
|
||||
/* Sensor tripped before we hit the midpoint; sample current now so
|
||||
* the response still has a meaningful I_mid. */
|
||||
fsm_now = esp_timer_get_time();
|
||||
process_bridge_current(b);
|
||||
I_mid = get_bridge_A(b);
|
||||
}
|
||||
|
||||
relay_port_t idle = {.bridges = {.SENSORS = 1}};
|
||||
last_relay_state = idle;
|
||||
@@ -530,8 +590,15 @@ static void cmd_relay(char *args)
|
||||
? get_sensor_isr_edges(which_sensor) : 0;
|
||||
uint32_t edges = edges_after - edges_before;
|
||||
|
||||
OK("relay bridge=%s dir=%s ms=%d I_before=%+.2f I_mid=%+.2f I_after=%+.2f heat=%.3f tripped=%d edges=%u",
|
||||
t_bridge, t_dir, ms, I_before, I_mid, I_after, heat, tripped, (unsigned)edges);
|
||||
int actual_ms = (int)((esp_timer_get_time() - pulse_start_us) / 1000);
|
||||
const char *stop_reason =
|
||||
stopped_by_safety ? "safety" :
|
||||
stopped_by_sensor ? "sensor" : "time";
|
||||
OK("relay bridge=%s dir=%s ms=%d actual_ms=%d stop=%s "
|
||||
"I_before=%+.2f I_mid=%+.2f I_after=%+.2f heat=%.3f tripped=%d edges=%u",
|
||||
t_bridge, t_dir, ms, actual_ms, stop_reason,
|
||||
I_before, I_mid, I_after, heat, tripped, (unsigned)edges);
|
||||
#undef _BU_SAFETY_OPEN
|
||||
}
|
||||
|
||||
static void cmd_rf_watch(char *args)
|
||||
@@ -588,7 +655,24 @@ static void cmd_wifi_wait(char *args)
|
||||
(void)args; /* no timeout — BRINGUP.md §4 Stage 6. Operator aborts via Ctrl+C. */
|
||||
wifi_sta_list_t sta = {0};
|
||||
int last_n = 0;
|
||||
bool aborted = false;
|
||||
while (1) {
|
||||
/* Abort on any UART input — the host sends a stray byte to break out
|
||||
* of the wait so that a follow-up BU.END is actually dispatched
|
||||
* (otherwise the dispatcher stays blocked here forever). */
|
||||
size_t available = 0;
|
||||
if (uart_get_buffered_data_len(UART_NUM_0, &available) == ESP_OK
|
||||
&& available > 0) {
|
||||
uint8_t drain[64];
|
||||
while (available > 0) {
|
||||
int n = uart_read_bytes(UART_NUM_0, drain, sizeof(drain), 0);
|
||||
if (n <= 0) break;
|
||||
available = (size_t)n < available ? available - (size_t)n : 0;
|
||||
}
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (esp_wifi_ap_get_sta_list(&sta) == ESP_OK && sta.num > last_n) {
|
||||
EVT("wifi.assoc n=%d t=%.2f", sta.num, elapsed_s());
|
||||
last_n = sta.num;
|
||||
@@ -599,7 +683,8 @@ static void cmd_wifi_wait(char *args)
|
||||
esp_task_wdt_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
}
|
||||
OK("wifi.wait clients=%d http_reqs=%d", last_n, s_http_reqs);
|
||||
OK("wifi.wait clients=%d http_reqs=%d aborted=%d",
|
||||
last_n, s_http_reqs, aborted ? 1 : 0);
|
||||
}
|
||||
|
||||
static void cmd_fsm(char *args)
|
||||
@@ -652,6 +737,11 @@ static void cmd_param(char *args)
|
||||
}
|
||||
|
||||
if (strcmp(op, "SET") == 0) {
|
||||
/* SET writes flash. Require BU.BEGIN to prevent accidental persistence
|
||||
* from stray BU.PARAM lines outside of an active bring-up session. */
|
||||
if (!s_active) {
|
||||
ERR("param reason=\"BU.BEGIN required first\""); return;
|
||||
}
|
||||
char *val = next_tok(&s);
|
||||
if (!val) { ERR("param reason=\"missing value\""); return; }
|
||||
esp_err_t e = ESP_OK;
|
||||
@@ -673,6 +763,12 @@ static void cmd_param(char *args)
|
||||
if (e != ESP_OK) { ERR("param reason=\"set failed\" err=%s", esp_err_to_name(e)); return; }
|
||||
e = commit_params();
|
||||
if (e != ESP_OK) { ERR("param reason=\"commit failed\" err=%s", esp_err_to_name(e)); return; }
|
||||
/* If the conversion params changed, refresh the battery EMA so
|
||||
* get_battery_V() returns a value consistent with the new K/OFFSET
|
||||
* immediately rather than decaying through the EMA. */
|
||||
if (idx == PARAM_V_SENS_K || idx == PARAM_V_SENS_OFFSET) {
|
||||
reset_battery_ema();
|
||||
}
|
||||
OK("param key=%s set=ok committed=yes", key);
|
||||
return;
|
||||
}
|
||||
@@ -680,6 +776,33 @@ static void cmd_param(char *args)
|
||||
ERR("param reason=\"unknown op\" op=%s", op);
|
||||
}
|
||||
|
||||
/* BU.FACTORY_RESET — wipe all params back to defaults, erase log partition,
|
||||
* then reboot. Equivalent to the cold-boot button-hold path in main.c, but
|
||||
* reachable from a host without physical access. Destructive — operator
|
||||
* must explicitly invoke it. */
|
||||
static void cmd_factory_reset(char *args)
|
||||
{
|
||||
(void)args;
|
||||
/* Refuse without an explicit BU.BEGIN. Without this guard, any party
|
||||
* that can write to UART0 can wipe params/log just by sending the
|
||||
* command — and uart_comms.c forwards bare BU.* lines to the dispatcher
|
||||
* even when bring-up mode is off. */
|
||||
if (!s_active) {
|
||||
ERR("factory_reset reason=\"BU.BEGIN required first\"");
|
||||
return;
|
||||
}
|
||||
OK("factory_reset stage=start");
|
||||
esp_err_t e = factory_reset();
|
||||
if (e != ESP_OK) {
|
||||
ERR("factory_reset stage=apply err=%s", esp_err_to_name(e));
|
||||
return;
|
||||
}
|
||||
OK("factory_reset stage=done reboot=2s");
|
||||
fflush(stdout);
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
/* -------- dispatcher -------- */
|
||||
|
||||
typedef void (*cmd_fn)(char *args);
|
||||
@@ -707,6 +830,7 @@ static const struct cmd_entry CMDS[] = {
|
||||
{ "FSM", cmd_fsm },
|
||||
{ "SOLAR.TICK", cmd_solar_tick },
|
||||
{ "PARAM", cmd_param },
|
||||
{ "FACTORY_RESET", cmd_factory_reset},
|
||||
};
|
||||
|
||||
void bringup_handle_line(char *line)
|
||||
|
||||
14
main/comms.c
14
main/comms.c
@@ -168,6 +168,7 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
|
||||
bool reboot_requested = false;
|
||||
bool wifi_params_changed = false;
|
||||
bool wifi_restart_requested = false;
|
||||
bool refresh_battery_ema = false;
|
||||
const char *error_msg = NULL;
|
||||
int params_updated = 0;
|
||||
int params_failed = 0;
|
||||
@@ -183,8 +184,8 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
|
||||
// Process remaining_dist if present
|
||||
cJSON *remaining_dist = cJSON_GetObjectItem(root, "remaining_dist");
|
||||
if (cJSON_IsNumber(remaining_dist)) {
|
||||
int64_t new_dist = (int64_t)cJSON_GetNumberValue(remaining_dist);
|
||||
ESP_LOGI(TAG, "Setting remaining_dist to %lld", new_dist);
|
||||
float new_dist = (float)cJSON_GetNumberValue(remaining_dist);
|
||||
ESP_LOGI(TAG, "Setting remaining_dist to %.3f", new_dist);
|
||||
fsm_set_remaining_distance(new_dist);
|
||||
}
|
||||
|
||||
@@ -362,6 +363,12 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
|
||||
param_idx == PARAM_NET_PASS) {
|
||||
wifi_params_changed = true;
|
||||
}
|
||||
|
||||
/* If the battery conversion params change, refresh the EMA
|
||||
* so get_battery_V() reflects the new K/OFFSET immediately. */
|
||||
if (param_idx == PARAM_V_SENS_K || param_idx == PARAM_V_SENS_OFFSET) {
|
||||
refresh_battery_ema = true;
|
||||
}
|
||||
|
||||
cJSON *value_json = cJSON_GetObjectItem(parameters, key);
|
||||
|
||||
@@ -440,6 +447,9 @@ esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
|
||||
if (params_updated > 0) {
|
||||
rtc_schedule_next_alarm();
|
||||
commit_params();
|
||||
if (refresh_battery_ema) {
|
||||
reset_battery_ema();
|
||||
}
|
||||
if (wifi_params_changed) {
|
||||
ESP_LOGI(TAG, "WiFi params changed — restarting WiFi AP");
|
||||
wifi_restart_requested = true;
|
||||
|
||||
@@ -40,8 +40,14 @@ esp_err_t fsm_get_error() { return fsm_error; }
|
||||
void fsm_clear_error() { fsm_error = ESP_OK; }
|
||||
|
||||
|
||||
/* override_time + override_cmd are written from RF/BT/comms tasks and read
|
||||
* from the control task. int64_t isn't atomic on a 32-bit MCU, so we wrap
|
||||
* read/write in a critical section to prevent torn reads (which could land
|
||||
* override_time far in the future and run a motor for seconds longer than
|
||||
* RF_PULSE_LENGTH). */
|
||||
static portMUX_TYPE override_spin = portMUX_INITIALIZER_UNLOCKED;
|
||||
int64_t override_time = -1;
|
||||
fsm_override_t override_cmd;
|
||||
fsm_override_t override_cmd = FSM_OVERRIDE_DRIVE_FWD;
|
||||
bool enabled = false;
|
||||
|
||||
float this_move_dist = 0.0f;
|
||||
@@ -82,11 +88,22 @@ void pulse_override(fsm_override_t cmd) {
|
||||
if (soft_idle_is_active()) return;
|
||||
if (current_state == STATE_IDLE) {
|
||||
rtc_reset_shutdown_timer();
|
||||
int64_t deadline = fsm_now + (int64_t)get_param_value_t(PARAM_RF_PULSE_LENGTH).u32;
|
||||
portENTER_CRITICAL(&override_spin);
|
||||
override_cmd = cmd;
|
||||
override_time = fsm_now + get_param_value_t(PARAM_RF_PULSE_LENGTH).u32;
|
||||
override_time = deadline;
|
||||
portEXIT_CRITICAL(&override_spin);
|
||||
}
|
||||
}
|
||||
|
||||
/* Atomic snapshot of override_time + override_cmd for the control task. */
|
||||
static inline void override_snapshot(int64_t *time_out, fsm_override_t *cmd_out) {
|
||||
portENTER_CRITICAL(&override_spin);
|
||||
*time_out = override_time;
|
||||
*cmd_out = override_cmd;
|
||||
portEXIT_CRITICAL(&override_spin);
|
||||
}
|
||||
|
||||
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;}
|
||||
@@ -130,49 +147,69 @@ int8_t fsm_get_current_progress(int8_t denominator) {
|
||||
|
||||
|
||||
#define JACK_TIME get_param_value_t(PARAM_JACK_KT).f32 * get_param_value_t(PARAM_JACK_DIST ).f32
|
||||
#define JACK_DOWN_TIME (jack_finish_us - jack_start_us) * 105/100
|
||||
|
||||
/* Symmetric jack-down duration: how long jack-up actually ran, plus 5%.
|
||||
* If jack_start_us / jack_finish_us are zero or negative (panic recovery,
|
||||
* or a transition that skipped the normal path) the delta is unsafe — fall
|
||||
* back to the parameter-derived JACK_TIME as a floor so we don't either
|
||||
* (a) cut the jack-down to ~0 and leave the actuator extended, or (b) run
|
||||
* forever. */
|
||||
static inline int64_t _jack_down_time_us(void) {
|
||||
int64_t delta = jack_finish_us - jack_start_us;
|
||||
int64_t floor_us = (int64_t)JACK_TIME;
|
||||
if (delta < floor_us) delta = floor_us;
|
||||
return delta * 105 / 100;
|
||||
}
|
||||
#define JACK_DOWN_TIME _jack_down_time_us()
|
||||
#define DRIVE_TIME get_param_value_t(PARAM_DRIVE_KT).f32 * this_move_dist
|
||||
#define DRIVE_DIST get_param_value_t(PARAM_DRIVE_KE).f32 * this_move_dist
|
||||
|
||||
int64_t last_log_time = 0;
|
||||
#define LOGSIZE 39
|
||||
/* FSM log payload (single current channel — V5 has one shared ACS sensor; V4
|
||||
* had three but the per-bridge values are redundant since only one bridge is
|
||||
* active at a time). Layout:
|
||||
* [0:8] ts_ms u64
|
||||
* [8:12] bat_V f32
|
||||
* [12:16] current_A f32 — sum of bridge currents (mutually exclusive)
|
||||
* [16:18] counter i16
|
||||
* [18:19] sensors u8
|
||||
* [19:23] heat f32 — max bridge heat
|
||||
* [23:25] i2c_out u16 — last 16-bit TCA9555 output state
|
||||
* (high byte = OUTPUT0 / LEDs, low = OUTPUT1 / relays) */
|
||||
#define LOGSIZE 25
|
||||
esp_err_t send_fsm_log() {
|
||||
if(!rtc_is_set()) return ESP_OK;
|
||||
|
||||
|
||||
uint8_t entry[LOGSIZE] = {};
|
||||
|
||||
// Pack 64-bit timestamp into bytes 1-8
|
||||
|
||||
uint64_t be_timestamp = rtc_get_ms();
|
||||
memcpy(&entry[0], &be_timestamp, 8);
|
||||
|
||||
// Pack 32-bit voltages/currents into bytes 9-24
|
||||
|
||||
float be_voltage = get_battery_V();
|
||||
memcpy(&entry[8], &be_voltage, 4);
|
||||
float be_current1 = get_bridge_raw_A(BRIDGE_DRIVE);
|
||||
memcpy(&entry[12], &be_current1, 4);
|
||||
float be_current2 = get_bridge_raw_A(BRIDGE_JACK);
|
||||
memcpy(&entry[16], &be_current2, 4);
|
||||
float be_current3 = get_bridge_raw_A(BRIDGE_AUX);
|
||||
memcpy(&entry[20], &be_current3, 4);
|
||||
|
||||
|
||||
float current_A = get_bridge_raw_A(BRIDGE_DRIVE)
|
||||
+ get_bridge_raw_A(BRIDGE_JACK)
|
||||
+ get_bridge_raw_A(BRIDGE_AUX);
|
||||
memcpy(&entry[12], ¤t_A, 4);
|
||||
|
||||
int16_t be_counter = get_sensor_counter(SENSOR_DRIVE);
|
||||
memcpy(&entry[24], &be_counter, 2);
|
||||
|
||||
entry[26] = pack_sensors();
|
||||
|
||||
|
||||
|
||||
|
||||
float heat1 = efuse_get_heat(BRIDGE_DRIVE);
|
||||
memcpy(&entry[27], &heat1, 4);
|
||||
float heat2 = efuse_get_heat(BRIDGE_JACK);
|
||||
memcpy(&entry[31], &heat2, 4);
|
||||
float heat3 = efuse_get_heat(BRIDGE_AUX);
|
||||
memcpy(&entry[35], &heat3, 4);
|
||||
|
||||
memcpy(&entry[16], &be_counter, 2);
|
||||
|
||||
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;
|
||||
memcpy(&entry[19], &heat, 4);
|
||||
|
||||
uint16_t i2c_out = i2c_get_outputs();
|
||||
memcpy(&entry[23], &i2c_out, 2);
|
||||
|
||||
last_log_time = esp_timer_get_time();
|
||||
|
||||
|
||||
|
||||
log_write(entry, LOGSIZE, fsm_get_state());
|
||||
|
||||
//ESP_LOGI(TAG, "WROTE LOG; %lld / %ld/%ld; %5.2f %5.2f %5.2f", (long long)rtc_get_ms(), (unsigned long)log_get_tail(), (unsigned long)log_get_head(), heat1, heat2, heat3);
|
||||
@@ -257,6 +294,11 @@ void control_task(void *param) {
|
||||
|
||||
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
|
||||
* never inherits a stale value from a prior run. */
|
||||
jack_start_us = 0;
|
||||
jack_trans_us = 0;
|
||||
jack_finish_us = 0;
|
||||
current_state = STATE_MOVE_START_DELAY;
|
||||
log = true;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
@@ -454,12 +496,25 @@ void control_task(void *param) {
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
set_timer(JACK_DOWN_TIME);
|
||||
log = true;
|
||||
} else {
|
||||
} else if (efuse_get(BRIDGE_DRIVE)) {
|
||||
// Fault — deduct actual distance traveled (may be partial).
|
||||
// Checked before the normal-completion branch so a tick
|
||||
// that satisfies both conditions doesn't double-deduct
|
||||
// remaining_distance.
|
||||
int32_t current_encoder = get_sensor_counter(SENSOR_DRIVE);
|
||||
int32_t ticks_traveled = current_encoder - move_start_encoder;
|
||||
float ke = get_param_value_t(PARAM_DRIVE_KE).f32;
|
||||
float distance_traveled = ticks_traveled / ke;
|
||||
|
||||
remaining_distance -= distance_traveled;
|
||||
if (remaining_distance < 0.0f) remaining_distance = 0.0f;
|
||||
|
||||
fsm_error = SC_ERR_EFUSE_TRIP_1;
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
set_timer(JACK_DOWN_TIME);
|
||||
log = true;
|
||||
} else {
|
||||
int32_t current_encoder = get_sensor_counter(SENSOR_DRIVE);
|
||||
if (timer_done() || current_encoder > 0) {
|
||||
// Normal completion — deduct planned distance from leash
|
||||
remaining_distance -= this_move_dist;
|
||||
@@ -468,28 +523,23 @@ void control_task(void *param) {
|
||||
log = true;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
}
|
||||
|
||||
if (efuse_get(BRIDGE_DRIVE)) {
|
||||
// Fault — deduct actual distance traveled (may be partial)
|
||||
remaining_distance -= distance_traveled;
|
||||
if (remaining_distance < 0.0f) remaining_distance = 0.0f;
|
||||
|
||||
fsm_error = SC_ERR_EFUSE_TRIP_1;
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
set_timer(JACK_DOWN_TIME);
|
||||
log = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_DRIVE_END_DELAY:
|
||||
// 1s pause after drive — then lower jack
|
||||
// 1s pause after drive — then lower jack normally.
|
||||
// Goes straight to STATE_JACK_DOWN so the LED/comms message
|
||||
// reads "MOVING…" rather than "CANCELLING MOVE" on a normal
|
||||
// cycle. STATE_UNDO_JACK_START remains the path for explicit
|
||||
// undo / safety-break / efuse-trip recovery.
|
||||
if (!get_is_safe()) {
|
||||
fsm_error = SC_ERR_SAFETY_TRIP;
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
set_timer(JACK_DOWN_TIME);
|
||||
log = true;
|
||||
} else if (timer_done()) {
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
current_state = STATE_JACK_DOWN;
|
||||
set_timer(JACK_DOWN_TIME);
|
||||
log = true;
|
||||
}
|
||||
break;
|
||||
@@ -551,10 +601,14 @@ void control_task(void *param) {
|
||||
|
||||
/**** SET OUTPUTS ****/
|
||||
switch (current_state) {
|
||||
case STATE_IDLE:
|
||||
// In idle we still accept override commands
|
||||
if (override_time > fsm_now) {
|
||||
switch(override_cmd) {
|
||||
case STATE_IDLE: {
|
||||
// In idle we still accept override commands. Snapshot both fields
|
||||
// atomically to defend against the int64 torn read on writers.
|
||||
int64_t local_time;
|
||||
fsm_override_t local_cmd;
|
||||
override_snapshot(&local_time, &local_cmd);
|
||||
if (local_time > fsm_now) {
|
||||
switch(local_cmd) {
|
||||
case FSM_OVERRIDE_DRIVE_FWD:
|
||||
if (efuse_get(BRIDGE_DRIVE)){
|
||||
drive_relays((relay_port_t){.bridges = {
|
||||
@@ -653,6 +707,7 @@ void control_task(void *param) {
|
||||
}});
|
||||
}
|
||||
break;
|
||||
} /* close STATE_IDLE block scope */
|
||||
case STATE_CALIBRATE_JACK_MOVE:
|
||||
case STATE_JACK_UP_START:
|
||||
case STATE_JACK_UP:
|
||||
|
||||
42
main/i2c.c
42
main/i2c.c
@@ -12,6 +12,11 @@
|
||||
static bool i2c_initted = false;
|
||||
//static bool safety_ok = false; // Safety interlock
|
||||
static uint8_t last_relay_request = 0; // Track last relay request
|
||||
/* Cached last-written values for the two TCA9555 output ports. Used by
|
||||
* i2c_get_outputs() so the FSM log can record the full 16-bit output state
|
||||
* without paying for an extra I2C read each tick. */
|
||||
static uint8_t last_output0 = 0;
|
||||
static uint8_t last_output1 = 0;
|
||||
|
||||
// === I2C LOW-LEVEL ===
|
||||
static esp_err_t tca_write_word_8(uint8_t reg, uint8_t value) {
|
||||
@@ -65,6 +70,7 @@ esp_err_t i2c_post(void) {
|
||||
}
|
||||
|
||||
esp_err_t i2c_set_relays(relay_port_t states) {
|
||||
last_output1 = states.raw;
|
||||
return tca_write_word_8(TCA_REG_OUTPUT1, states.raw);
|
||||
}
|
||||
|
||||
@@ -77,12 +83,27 @@ esp_err_t i2c_relays_sleep(void) {
|
||||
}
|
||||
|
||||
esp_err_t i2c_set_led1(uint8_t state) {
|
||||
// push 3 LSB to top
|
||||
return tca_write_word_8(TCA_REG_OUTPUT0, state<<5);
|
||||
/* P05-P07 are LEDs (outputs); P00-P04 are buttons / unused INPUTS
|
||||
* (CONFIG0 = 0b00000011 sets P00/P01 as inputs; P02-P04 are unused
|
||||
* but also configured as inputs). Writing the whole OUTPUT0 register
|
||||
* is therefore safe — the input-bit slots in OUTPUT0 are don't-cares
|
||||
* because the pin direction prevents the value from driving the line.
|
||||
* If P02-P04 ever become outputs, switch this to read-modify-write. */
|
||||
uint8_t v = state << 5;
|
||||
last_output0 = v;
|
||||
return tca_write_word_8(TCA_REG_OUTPUT0, v);
|
||||
}
|
||||
|
||||
uint16_t i2c_get_outputs(void) {
|
||||
/* OUTPUT0 in the high byte (P00..P07), OUTPUT1 in the low byte
|
||||
* (P10..P17). Reflects the last value written by this driver. */
|
||||
return ((uint16_t)last_output0 << 8) | last_output1;
|
||||
}
|
||||
|
||||
esp_err_t i2c_stop() {
|
||||
if (!i2c_initted) return ESP_OK;
|
||||
last_output0 = 0;
|
||||
last_output1 = 0;
|
||||
tca_write_word_8(TCA_REG_OUTPUT0, 0);
|
||||
tca_write_word_8(TCA_REG_OUTPUT1, 0);
|
||||
return ESP_OK;
|
||||
@@ -147,25 +168,26 @@ bool i2c_get_button_repeat(uint8_t btn) {
|
||||
}
|
||||
|
||||
int8_t i2c_get_button_repeats(uint8_t btn) {
|
||||
if (!i2c_get_button_state(btn))
|
||||
return 0;
|
||||
|
||||
if (btn >= N_BTNS || !debounced_state[btn]) return false;
|
||||
/* Returns -1 on out-of-range button index (was previously `false` = 0,
|
||||
* which conflated error with "no repeat"). 0 means button not pressed
|
||||
* or no new repeat this poll. >=1 is a valid repeat count. */
|
||||
if (btn >= N_BTNS) return -1;
|
||||
if (!i2c_get_button_state(btn)) return 0;
|
||||
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
|
||||
if (now + DEBOUNCE_MS < last_change_time[btn]) return 0;
|
||||
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);
|
||||
ESP_LOGI("BTN", "RPT %d", (uint8_t)(claimed_repeats[btn]+1));
|
||||
return claimed_repeats[btn]+1;
|
||||
}
|
||||
if (debounced_state[btn] && !last_known_state[btn]) {
|
||||
|
||||
ESP_LOGI("BTN", "FST %d", 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@ esp_err_t i2c_stop(void);
|
||||
esp_err_t i2c_set_relays(relay_port_t states);
|
||||
esp_err_t i2c_set_led1(uint8_t state);
|
||||
|
||||
/* Returns the last-written 16-bit TCA9555 output state.
|
||||
* High byte: OUTPUT0 (P00..P07, LEDs in P05..P07).
|
||||
* Low byte: OUTPUT1 (P10..P17, relay_port_t.raw). */
|
||||
uint16_t i2c_get_outputs(void);
|
||||
|
||||
/* Normal run state: all bridges off, but P10 (sensor rail) held high.
|
||||
* Use whenever "everything off" is really "all motors off, system is still on". */
|
||||
esp_err_t i2c_relays_idle(void);
|
||||
|
||||
@@ -150,7 +150,8 @@ void drive_leds(led_mode_t mode) {
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void) {esp_task_wdt_add(NULL);
|
||||
void app_main(void) {
|
||||
esp_task_wdt_add(NULL);
|
||||
|
||||
ESP_LOGI(TAG, "Firmware: %s", FIRMWARE_STRING);
|
||||
ESP_LOGI(TAG, "Version: %s", FIRMWARE_VERSION);
|
||||
|
||||
@@ -143,16 +143,11 @@ esp_err_t drive_relays(relay_port_t relay_state) {
|
||||
BRIDGE_TRANSITION_LOGIC(JACK)
|
||||
BRIDGE_TRANSITION_LOGIC(AUX)
|
||||
|
||||
/* Sensor rail (P10) powered only when FSM is actively doing something.
|
||||
* In STATE_IDLE we cut it to save power. Overrides energize a motor
|
||||
* while FSM is technically IDLE — keep the rail up in that case too
|
||||
* so overrides that consult sensor state (e.g. JACK_DOWN, which checks
|
||||
* SENSOR_JACK) still work. */
|
||||
bool motor_requested = (relay_state.bridges.DRIVE != BRIDGE_OFF ||
|
||||
relay_state.bridges.JACK != BRIDGE_OFF ||
|
||||
relay_state.bridges.AUX != BRIDGE_OFF);
|
||||
relay_state.bridges.SENSORS =
|
||||
(fsm_get_state() != STATE_IDLE || motor_requested) ? 1 : 0;
|
||||
/* Sensor rail (P10) is on whenever the device is awake — including
|
||||
* STATE_IDLE — so the SAFETY input can be observed continuously.
|
||||
* It is dropped only in soft_idle_enter() (sleep) via i2c_relays_sleep,
|
||||
* and toggled explicitly by the bring-up tool's BU.RELAY SENSORS cmd. */
|
||||
relay_state.bridges.SENSORS = 1;
|
||||
|
||||
if (!get_is_safe())
|
||||
relay_state.bridges.DRIVE = 0;
|
||||
@@ -287,6 +282,20 @@ float get_raw_battery_voltage(void) {
|
||||
return voltage_mv * get_param_value_t(PARAM_V_SENS_K).f32 + get_param_value_t(PARAM_V_SENS_OFFSET).f32; // same as / 1000.0 * 1150.0 / 150.0;
|
||||
}
|
||||
|
||||
void reset_battery_ema(void)
|
||||
{
|
||||
/* Next process_battery_voltage() call re-seeds from raw. Also refresh
|
||||
* immediately from a fresh raw read so callers that read get_battery_V()
|
||||
* before the FSM ticks again (e.g. during bringup) see the new value. */
|
||||
float raw = get_raw_battery_voltage();
|
||||
if (!isnan(raw)) {
|
||||
ema_battery = raw;
|
||||
ema_battery_init = true;
|
||||
} else {
|
||||
ema_battery_init = false;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t process_battery_voltage(void)
|
||||
{
|
||||
float raw = get_raw_battery_voltage();
|
||||
@@ -364,6 +373,20 @@ static bool v5_any_bridge_active(void) {
|
||||
v5_bridge_is_active(BRIDGE_JACK) ||
|
||||
v5_bridge_is_active(BRIDGE_AUX);
|
||||
}
|
||||
|
||||
/* True if any currently-active bridge is still inside its INRUSH_US window.
|
||||
* The shared ACS reading is unattributable per-bridge during a co-active
|
||||
* inrush — the full combined current is attributed to each active bridge in
|
||||
* process_bridge_current(), so a quieter bridge (e.g. AUX during DRIVE start)
|
||||
* sees an inflated I_norm and would spuriously instant-trip on KINST. */
|
||||
static bool v5_any_bridge_in_inrush(void) {
|
||||
int64_t inrush_us = (int64_t)get_param_value_t(PARAM_EFUSE_INRUSH_US).u32;
|
||||
for (bridge_t b = 0; b < N_BRIDGES; b++) {
|
||||
if (!v5_bridge_is_active(b)) continue;
|
||||
if (fsm_now < isens[b].on_us + inrush_us) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
esp_err_t process_bridge_current(bridge_t bridge) {
|
||||
@@ -440,10 +463,11 @@ esp_err_t process_bridge_current(bridge_t bridge) {
|
||||
} else {
|
||||
float alpha = get_param_value_t(PARAM_ADC_ALPHA_ISENS).f32;
|
||||
if (isnan(channel->raw_current)) {
|
||||
//ESP_LOGI(TAG, "RAW BATTERY IS NAN");
|
||||
channel->ema_current = NAN;
|
||||
} else {
|
||||
if (isnan(ema_battery) || isnan(alpha)) {
|
||||
/* Reset the per-bridge EMA if it (or alpha) is NaN — using
|
||||
* the per-bridge value, not the unrelated battery EMA. */
|
||||
if (isnan(channel->ema_current) || isnan(alpha)) {
|
||||
channel->ema_current = channel->raw_current;
|
||||
} else {
|
||||
channel->ema_current = alpha * channel->raw_current + (1.0f - alpha) * channel->ema_current;
|
||||
@@ -470,9 +494,10 @@ esp_err_t process_bridge_current(bridge_t bridge) {
|
||||
} else {
|
||||
float alpha = get_param_value_t(PARAM_ADC_ALPHA_IAZ).f32;
|
||||
if (isnan(channel->raw_current)) {
|
||||
//ESP_LOGI(TAG, "RAW BATTERY IS NAN");
|
||||
/* skip — no fresh sample */
|
||||
} else {
|
||||
if (isnan(ema_battery) || isnan(alpha)) {
|
||||
/* Reset the autozero offset if it (or alpha) is NaN. */
|
||||
if (isnan(channel->az_offset) || isnan(alpha)) {
|
||||
channel->az_offset = channel->ema_current;
|
||||
} else {
|
||||
channel->az_offset = alpha * channel->ema_current +
|
||||
@@ -512,9 +537,17 @@ esp_err_t process_bridge_current(bridge_t bridge) {
|
||||
// Normalize the current as a fraction of rated current
|
||||
float I_norm = fabsf(channel->current / I_nominal);
|
||||
|
||||
// Instant trip on extreme overcurrent
|
||||
// Instant trip on extreme overcurrent. On V5, also require that no
|
||||
// *other* active bridge is still in its inrush window — during a
|
||||
// co-active inrush the shared ACS reading is attributed to each
|
||||
// active bridge, which inflates the quieter bridge's I_norm and
|
||||
// would otherwise cause a spurious instant-trip there.
|
||||
if (fsm_now > channel->on_us + get_param_value_t(PARAM_EFUSE_INRUSH_US).u32
|
||||
&& I_norm >= get_param_value_t(PARAM_EFUSE_KINST).f32) {
|
||||
&& I_norm >= get_param_value_t(PARAM_EFUSE_KINST).f32
|
||||
#ifdef BOARD_V5
|
||||
&& !v5_any_bridge_in_inrush()
|
||||
#endif
|
||||
) {
|
||||
// Check if overcurrent has persisted long enough
|
||||
channel->tripped = true;
|
||||
channel->trip_time = fsm_now;
|
||||
|
||||
@@ -46,6 +46,13 @@ bool get_bridge_spike(bridge_t bridge, float threshold);
|
||||
esp_err_t process_bridge_current(bridge_t bridge);
|
||||
esp_err_t process_battery_voltage();
|
||||
|
||||
/* Force the battery EMA to re-seed from the next raw read. Call after a
|
||||
* V_SENS_K / V_SENS_OFFSET change so get_battery_V() reflects the new
|
||||
* calibration immediately instead of decaying through the EMA — which
|
||||
* otherwise stays stale across bringup mode (FSM is paused, so
|
||||
* process_battery_voltage doesn't tick) until alpha catches up. */
|
||||
void reset_battery_ema(void);
|
||||
|
||||
esp_err_t adc_init();
|
||||
esp_err_t adc_post(void);
|
||||
esp_err_t power_init();
|
||||
|
||||
@@ -37,8 +37,13 @@ typedef struct {
|
||||
size_t num_symbols;
|
||||
} rf_code_t;
|
||||
|
||||
int learn_flag = -1;
|
||||
bool controls_enabled = true;
|
||||
/* These are written by the comms task (HTTP/UART) and read by the RF
|
||||
* receiver task. `volatile` forces the RF task to re-read each iteration
|
||||
* rather than caching in a register; on a dual-core ESP32 the writer's
|
||||
* memory is also flushed by the FreeRTOS task switch / queue ops the
|
||||
* comms task does around updates. */
|
||||
volatile int learn_flag = -1;
|
||||
volatile bool controls_enabled = true;
|
||||
|
||||
// Temporary storage for learned keycodes (not committed to params yet)
|
||||
static int64_t temp_keycodes[NUM_RF_BUTTONS] = {0};
|
||||
|
||||
@@ -140,28 +140,36 @@ esp_err_t sensors_init() {
|
||||
}
|
||||
|
||||
void sensors_check() {
|
||||
sensor_event_t evt;
|
||||
|
||||
uint8_t i = 0;
|
||||
|
||||
if (xQueueReceive(sensor_event_queue, &evt, pdMS_TO_TICKS(10)) == pdTRUE) {
|
||||
i = evt.sensor_id;
|
||||
|
||||
ESP_LOGI("SENS", "EVENT %d", i);
|
||||
|
||||
bool current_raw = !gpio_get_level(sensor_pins[i]);
|
||||
/* Drain ALL queued events with a non-blocking receive. Previously this
|
||||
* was a single blocking receive with a 10 ms timeout, which (a) chewed up
|
||||
* half the FSM tick window waiting on quiet sensors, and (b) consumed
|
||||
* only one edge per tick — encoder bursts above 50 Hz overflowed the
|
||||
* 16-entry queue and undercounted distance. Now each tick consumes the
|
||||
* full queue contents and applies a per-sensor debounce. */
|
||||
sensor_event_t evt;
|
||||
static uint64_t last_counted_us[N_SENSORS] = {0};
|
||||
while (xQueueReceive(sensor_event_queue, &evt, 0) == pdTRUE) {
|
||||
uint8_t i = evt.sensor_id;
|
||||
if (i >= N_SENSORS) continue;
|
||||
|
||||
/* Use the ISR-captured level (snapshot at edge time) rather than a
|
||||
* fresh GPIO read here — by the time the FSM tick drains the queue,
|
||||
* the line may have toggled again and a re-read would miss the
|
||||
* transition we're processing. */
|
||||
bool current_raw = evt.level;
|
||||
|
||||
/* Software debounce on non-safety sensors. The safety sensor has its
|
||||
* own asymmetric debouncer below, so we don't double-count there. */
|
||||
uint64_t now = esp_timer_get_time();
|
||||
if (i != SENSOR_SAFETY) {
|
||||
if (now - last_counted_us[i] < DEBOUNCE_TIME_US) continue;
|
||||
}
|
||||
last_counted_us[i] = now;
|
||||
|
||||
sensor_stable_state[i] = current_raw;
|
||||
|
||||
if (current_raw && !last_raw_state[i]){
|
||||
ESP_LOGI("SENS", "FALLING");
|
||||
if (current_raw != last_raw_state[i]) {
|
||||
sensor_count[i]++;
|
||||
}
|
||||
if (!current_raw && last_raw_state[i]){
|
||||
ESP_LOGI("SENS", "RISING");
|
||||
sensor_count[i]++;
|
||||
}
|
||||
|
||||
last_raw_state[i] = current_raw;
|
||||
}
|
||||
|
||||
@@ -214,8 +222,7 @@ bool get_sensor(sensor_t i) {
|
||||
}
|
||||
|
||||
bool get_is_safe(void) {
|
||||
return true;
|
||||
//return is_safe;
|
||||
return is_safe;
|
||||
}
|
||||
|
||||
int16_t get_sensor_counter(sensor_t i) {
|
||||
|
||||
57
main/solar.c
57
main/solar.c
@@ -34,40 +34,43 @@ esp_err_t init_solar_gpio() {
|
||||
|
||||
esp_err_t solar_run_fsm() {
|
||||
init_solar_gpio();
|
||||
|
||||
|
||||
int64_t now = rtc_get_ms();
|
||||
|
||||
//ESP_LOGI("BAT", "FSM STATE %d", current_charge_state);
|
||||
|
||||
|
||||
/* `now` is in milliseconds; CHG_BULK_S / CHG_LOW_S are in seconds.
|
||||
* Scale the parameter to ms before comparing. Initialize the timer on
|
||||
* first run rather than leaving it at -1 (which would otherwise make
|
||||
* `now > timer + threshold` true after just `threshold` ms). */
|
||||
if (timer < 0) timer = now;
|
||||
|
||||
float vbat = get_battery_V();
|
||||
|
||||
|
||||
/*
|
||||
The state machine is simple.
|
||||
- After a period of time when battery is low, switch to bulk
|
||||
- After a period of time in bulk, switch to float
|
||||
*/
|
||||
|
||||
//if (rtc_is_set()) {
|
||||
switch(current_charge_state) {
|
||||
case CHG_STATE_BULK:
|
||||
if (now > timer+get_param_value_t(PARAM_CHG_BULK_S).u32) {
|
||||
current_charge_state = CHG_STATE_FLOAT;
|
||||
}
|
||||
|
||||
break;
|
||||
case CHG_STATE_FLOAT:
|
||||
// if we have sufficient voltage, reset the timer
|
||||
if (vbat > get_param_value_t(PARAM_CHG_LOW_V).f32) {
|
||||
timer = now;
|
||||
}
|
||||
|
||||
if (now > timer+get_param_value_t(PARAM_CHG_LOW_S).u32) {
|
||||
timer = now;
|
||||
current_charge_state = CHG_STATE_BULK;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
switch(current_charge_state) {
|
||||
case CHG_STATE_BULK:
|
||||
if (now > timer + (int64_t)get_param_value_t(PARAM_CHG_BULK_S).u32 * 1000) {
|
||||
current_charge_state = CHG_STATE_FLOAT;
|
||||
}
|
||||
|
||||
break;
|
||||
case CHG_STATE_FLOAT:
|
||||
// if we have sufficient voltage, reset the timer
|
||||
if (vbat > get_param_value_t(PARAM_CHG_LOW_V).f32) {
|
||||
timer = now;
|
||||
}
|
||||
|
||||
if (now > timer + (int64_t)get_param_value_t(PARAM_CHG_LOW_S).u32 * 1000) {
|
||||
timer = now;
|
||||
current_charge_state = CHG_STATE_BULK;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
/*} else {
|
||||
reset_solar_fsm();
|
||||
ESP_LOGI(TAG, "RESET SOLAR FSM");
|
||||
|
||||
@@ -37,20 +37,9 @@ typedef struct {
|
||||
uint8_t type;
|
||||
} log_queue_entry_t;
|
||||
|
||||
// ============================================================================
|
||||
// LOG TYPE DEFINITIONS (Magic values 0xC0-0xCF)
|
||||
// ============================================================================
|
||||
#define LOG_TYPE_DATA 0xC0 // Generic data log
|
||||
#define LOG_TYPE_EVENT 0xC1 // Event marker
|
||||
#define LOG_TYPE_ERROR 0xC2 // Error log
|
||||
#define LOG_TYPE_DEBUG 0xC3 // Debug message
|
||||
#define LOG_TYPE_SENSOR 0xC4 // Sensor reading
|
||||
#define LOG_TYPE_COMMAND 0xC5 // Command executed
|
||||
#define LOG_TYPE_STATUS 0xC6 // Status update
|
||||
#define LOG_TYPE_CUSTOM 0xCF // Custom/user-defined
|
||||
|
||||
// Helper macro to check if a byte is a valid log type
|
||||
#define IS_VALID_LOG_TYPE(x) ((x) >= 0xC0 && (x) <= 0xCF)
|
||||
/* LOG_TYPE_* + IS_VALID_LOG_TYPE moved to storage.h as the single source
|
||||
* of truth. The duplicate set previously here disagreed with the header
|
||||
* (different LOG_TYPE_CUSTOM* names). */
|
||||
|
||||
// ============================================================================
|
||||
// PARAMETER TABLE GENERATION
|
||||
@@ -272,6 +261,10 @@ esp_err_t set_param_value_t(param_idx_t id, param_value_t val) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
parameter_table[id] = val;
|
||||
/* Run bounds/NaN validation immediately so callers that read between
|
||||
* set and the next commit_params() see a clamped value rather than a
|
||||
* raw out-of-range one. */
|
||||
validate_param(id);
|
||||
ESP_LOGI(TAG, "Parameter %d (%s) set (not committed)", id, parameter_names[id]);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -518,6 +511,20 @@ esp_err_t commit_params(void) {
|
||||
esp_err_t factory_reset(void) {
|
||||
ESP_LOGI(TAG, "Performing factory reset...");
|
||||
|
||||
/* Stop the log writer task before erasing the log partition so an
|
||||
* in-flight write doesn't race against esp_partition_erase_range. We
|
||||
* also take log_mutex for the rest of this function so any pre-existing
|
||||
* writer that's mid-write completes before we erase, and any new writer
|
||||
* (if log_task_running observation lagged) blocks. The caller is about
|
||||
* to esp_restart, so we never give the mutex back. */
|
||||
if (log_task_running) {
|
||||
log_task_running = false;
|
||||
/* Give the writer task time to observe the flag (its queue receive
|
||||
* has a 100 ms timeout) and finish any in-flight write. */
|
||||
vTaskDelay(pdMS_TO_TICKS(150));
|
||||
}
|
||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||
|
||||
// Reset all parameters to defaults
|
||||
for (int i = 0; i < NUM_PARAMS; i++) {
|
||||
memcpy(¶meter_table[i], ¶meter_defaults[i], sizeof(param_value_t));
|
||||
@@ -753,11 +760,6 @@ static bool is_sector_full(uint32_t x) {
|
||||
return (buf != 0xFF);
|
||||
}
|
||||
|
||||
static inline void find_head_tail(int32_t num_sectors, int32_t *head, int32_t *tail) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Replace log_write with this non-blocking version:
|
||||
esp_err_t log_write(const uint8_t* buf, uint8_t len, uint8_t type) {
|
||||
if (!log_initialized || log_partition == NULL) {
|
||||
@@ -811,11 +813,28 @@ static esp_err_t log_write_blocking(const uint8_t* buf, uint8_t len, uint8_t typ
|
||||
|
||||
// check if we will overrun the sector
|
||||
if (log_head_offset + len+2 >= log_sector_end(log_head_offset)) {
|
||||
// zero the rest of sector
|
||||
char zeros[256] = {0};
|
||||
esp_partition_write(log_partition,
|
||||
log_head_offset, &zeros,
|
||||
log_sector_end(log_head_offset)-log_head_offset);
|
||||
/* Zero the remainder of the current sector. The gap can be up to
|
||||
* one full sector (FLASH_SECTOR_SIZE = 4096); a single 256-byte
|
||||
* stack buffer would have caused the partition driver to read past
|
||||
* the buffer and write arbitrary stack contents to flash. Use a
|
||||
* static-zero buffer (`.bss` is implicitly zero-initialized) and
|
||||
* chunk the write so an oversize buffer is never required. */
|
||||
static const uint8_t zeros[256] = {0};
|
||||
size_t gap = log_sector_end(log_head_offset) - log_head_offset;
|
||||
size_t written = 0;
|
||||
while (written < gap) {
|
||||
size_t chunk = MIN(sizeof(zeros), gap - written);
|
||||
esp_err_t we = esp_partition_write(log_partition,
|
||||
log_head_offset + written,
|
||||
zeros, chunk);
|
||||
if (we != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed zero-pad write: %s", esp_err_to_name(we));
|
||||
/* Bump head to the next sector regardless — readers tolerate
|
||||
* 0xFF or 0x00 padding between entries. */
|
||||
break;
|
||||
}
|
||||
written += chunk;
|
||||
}
|
||||
|
||||
// set head to next sector, and check for wrap
|
||||
log_head_offset = log_sector_end(log_head_offset);
|
||||
@@ -851,15 +870,30 @@ static esp_err_t log_write_blocking(const uint8_t* buf, uint8_t len, uint8_t typ
|
||||
}
|
||||
len++; // account for type bit
|
||||
|
||||
|
||||
esp_partition_write(log_partition, log_head_offset, &len, 1);
|
||||
esp_partition_write(log_partition, log_head_offset+1, buf, len-1);
|
||||
esp_partition_write(log_partition, log_head_offset+len, &type, 1);
|
||||
/* Check each write — a partial failure here leaves a corrupt entry that
|
||||
* the recovery scan has to skip past. On any failure, bump head to the
|
||||
* next sector so the next write starts clean. */
|
||||
esp_err_t we;
|
||||
we = esp_partition_write(log_partition, log_head_offset, &len, 1);
|
||||
if (we == ESP_OK) {
|
||||
we = esp_partition_write(log_partition, log_head_offset+1, buf, len-1);
|
||||
}
|
||||
if (we == ESP_OK) {
|
||||
we = esp_partition_write(log_partition, log_head_offset+len, &type, 1);
|
||||
}
|
||||
if (we != ESP_OK) {
|
||||
ESP_LOGE(TAG, "log_write_blocking partial-write fail: %s",
|
||||
esp_err_to_name(we));
|
||||
log_head_offset = log_sector_end(log_head_offset);
|
||||
if (log_head_offset >= log_partition->size) log_head_offset = 0;
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
return we;
|
||||
}
|
||||
|
||||
log_head_offset+=len+1;
|
||||
ESP_LOGI(TAG, "Wrote; Tail/Head are now %lu/%lu",
|
||||
ESP_LOGI(TAG, "Wrote; Tail/Head are now %lu/%lu",
|
||||
(unsigned long)log_tail_offset, (unsigned long)log_head_offset);
|
||||
|
||||
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -11,26 +11,39 @@
|
||||
// ============================================================================
|
||||
#define FLASH_SECTOR_SIZE 4096
|
||||
|
||||
// ============================================================================
|
||||
// LOG ENTRY TYPE DEFINITIONS (Magic values 0xC0-0xCF)
|
||||
// ============================================================================
|
||||
#define LOG_TYPE_DATA 0xC0 // Generic data log
|
||||
#define LOG_TYPE_EVENT 0xC1 // Event marker
|
||||
#define LOG_TYPE_ERROR 0xC2 // Error log
|
||||
#define LOG_TYPE_DEBUG 0xC3 // Debug message
|
||||
#define LOG_TYPE_SENSOR 0xC4 // Sensor reading
|
||||
#define LOG_TYPE_COMMAND 0xC5 // Command executed
|
||||
#define LOG_TYPE_STATUS 0xC6 // Status update
|
||||
#define LOG_TYPE_CUSTOM_1 0xC7 // Custom type 1
|
||||
#define LOG_TYPE_CUSTOM_2 0xC8 // Custom type 2
|
||||
#define LOG_TYPE_CUSTOM_3 0xC9 // Custom type 3
|
||||
/* LOG ENTRY TYPES.
|
||||
*
|
||||
* Two ranges are emitted by the firmware in practice:
|
||||
* 0..13 — FSM state-tagged data entries (see LOG_FSM_NAMES in
|
||||
* webpage.html and send_fsm_log() in control_fsm.c).
|
||||
* 100..103 — System events: BAT, CRASH, BOOT, TIME_SET (see
|
||||
* control_fsm.h LOG_TYPE_BAT/CRASH/BOOT/TIME_SET).
|
||||
*
|
||||
* Constants in the 0xC0-0xCF range are reserved for legacy/future use; the
|
||||
* webpage parser does not currently understand them.
|
||||
*/
|
||||
#define LOG_TYPE_DATA 0xC0 // reserved
|
||||
#define LOG_TYPE_EVENT 0xC1
|
||||
#define LOG_TYPE_ERROR 0xC2
|
||||
#define LOG_TYPE_DEBUG 0xC3
|
||||
#define LOG_TYPE_SENSOR 0xC4
|
||||
#define LOG_TYPE_COMMAND 0xC5
|
||||
#define LOG_TYPE_STATUS 0xC6
|
||||
#define LOG_TYPE_CUSTOM_1 0xC7
|
||||
#define LOG_TYPE_CUSTOM_2 0xC8
|
||||
#define LOG_TYPE_CUSTOM_3 0xC9
|
||||
// 0xCA-0xCF reserved for future use
|
||||
|
||||
// Maximum payload size per log entry (255 max due to 1-byte size field)
|
||||
#define LOG_MAX_PAYLOAD 200
|
||||
|
||||
// Helper macro to check if a byte is a valid log type
|
||||
#define IS_VALID_LOG_TYPE(x) ((x) >= 0xC0 && (x) <= 0xCF)
|
||||
/* Helper macro to check if a byte is a valid log type. Includes the
|
||||
* 0..13 FSM-state range, the 100..103 system-event range, and the legacy
|
||||
* 0xC0-0xCF magic range. Used as a soft sanity check during log_read —
|
||||
* unknown types are still surfaced to callers, just with a warning. */
|
||||
#define IS_VALID_LOG_TYPE(x) (((x) <= 13) || \
|
||||
((x) >= 100 && (x) <= 103) || \
|
||||
((x) >= 0xC0 && (x) <= 0xCF))
|
||||
|
||||
// ============================================================================
|
||||
// LOG ENTRY STRUCTURE
|
||||
@@ -61,11 +74,11 @@ typedef struct {
|
||||
PARAM_DEF(NUM_MOVES, u32, 0, "", 0, 1000) \
|
||||
PARAM_DEF(MOVE_START, u32, 0, "s", 0, 86400) \
|
||||
PARAM_DEF(MOVE_END, u32, 0, "s", 0, 86400) \
|
||||
PARAM_DEF(DRIVE_DIST, f32, 10, "ft", 0.0, 100.0) \
|
||||
PARAM_DEF(JACK_DIST, f32, 5, "in", 0.0, 10.0) \
|
||||
PARAM_DEF(DRIVE_DIST, f32, 4, "ft", 0.0, 100.0) \
|
||||
PARAM_DEF(JACK_DIST, f32, 1.5, "in", 0.0, 10.0) \
|
||||
PARAM_DEF(DRIVE_KE, f32, 29.2, "n/ft", 1.0, 1e9) \
|
||||
PARAM_DEF(DRIVE_KT, f32, 1440000, "us/ft", 1.0, 1e9) /* div-critical */ \
|
||||
PARAM_DEF(JACK_KT, f32, 1428571, "ms/in", 1.0, 1e9) /* div-critical */ \
|
||||
PARAM_DEF(JACK_KT, f32, 1725698, "ms/in", 1.0, 1e9) /* div-critical */ \
|
||||
PARAM_DEF(KEYCODE_0, u32, 0, "", 0, 0) /* skip */ \
|
||||
PARAM_DEF(KEYCODE_1, u32, 0, "", 0, 0) \
|
||||
PARAM_DEF(KEYCODE_2, u32, 0, "", 0, 0) \
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,55 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
tan: #ba965b
|
||||
light tan: #efede9
|
||||
white: #ffffff
|
||||
green: #2a493d
|
||||
black: #2f2f2f
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Control Panel</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
|
||||
/* === COLOR TOKENS ===
|
||||
* Single source of truth for every color used in the UI. Item 14:
|
||||
* green/red/yellow each appear in exactly one place here and are
|
||||
* referenced everywhere else via var(--…). */
|
||||
:root {
|
||||
--green: #38B000;
|
||||
--red: #C1121F;
|
||||
--yellow: #FFDD00;
|
||||
|
||||
/* Dark theme surfaces (item 15) */
|
||||
--bg: #1a1a1a; /* page background */
|
||||
--surface: #262626; /* cards, inputs, popups */
|
||||
--surface-2: #333333; /* hover / readonly tint */
|
||||
--text: #f0f0f0; /* primary text */
|
||||
--text-dim: #a0a0a0; /* secondary text */
|
||||
--border: #444444;
|
||||
--accent: #ba965b; /* brand tan kept as accent */
|
||||
}
|
||||
|
||||
html, body { background-color: var(--bg); color: var(--text); }
|
||||
body { text-align: center; margin: 0; padding: 0; }
|
||||
#wrapper { text-align: center; box-sizing: border-box; }
|
||||
#content { max-width: 500px; margin: auto; padding: 0 10px; }
|
||||
body { text-align: center; margin: 0; padding: 0; }
|
||||
|
||||
|
||||
* {
|
||||
font-size: 1.2rem;
|
||||
background-color: #ffffff;
|
||||
color: #2f2f2f;
|
||||
font-family: "Noto Sans", "Verdana", sans-serif;
|
||||
font-size: 1.2rem;
|
||||
color: var(--text);
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: transparent;
|
||||
}
|
||||
input, button { width: 100%; }
|
||||
input, button { background-color: #efede9; text-align: right; box-sizing: border-box; border: 1px; border-radius: 5px; margin: 5px;}
|
||||
input[type="text"], input[type="number"],input[type="time"] { font-family: monospace;border: 1px solid #ba965b; border-radius: 5px; }
|
||||
|
||||
|
||||
input[readonly] { background-color: #e4e4e4; border-radius: 5px;}
|
||||
|
||||
|
||||
button { text-align: center; border: 1px solid #ba965b; border-radius: 5px; }
|
||||
.changed, #commit_btn { background-color: #2a493d !important; color: #ffffff !important; }
|
||||
#cancel_btn { background-color: #723 !important; color: #ffffff !important; }
|
||||
#commit_btn, #cancel_btn { width: 45%; margin-top: 10px; padding: 10px; cursor: pointer; border: none; font-weight: bold; }
|
||||
#commit_btn[disabled], #cancel_btn[disabled] { background-color: #444 !important; color: #888; cursor: not-allowed; }
|
||||
|
||||
/* === FORM CONTROLS === */
|
||||
input, button {
|
||||
width: 100%;
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
text-align: right;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
input[type="text"], input[type="number"], input[type="time"] {
|
||||
/* Numerical / freeform-text inputs keep monospace per CHANGELIST. */
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
input[readonly] {
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background-color: var(--surface-2); }
|
||||
|
||||
/* Save / Discard footer buttons. */
|
||||
.changed, #commit_btn { background-color: var(--green) !important; color: #ffffff !important; border-color: var(--green) !important; }
|
||||
#cancel_btn { background-color: var(--red) !important; color: #ffffff !important; border-color: var(--red) !important; }
|
||||
#commit_btn, #cancel_btn { width: 45%; margin-top: 10px; padding: 10px; font-weight: bold; }
|
||||
#commit_btn[disabled], #cancel_btn[disabled] {
|
||||
background-color: var(--surface-2) !important;
|
||||
color: var(--text-dim) !important;
|
||||
border-color: var(--border) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; text-align: left; }
|
||||
td { padding: 8px; border-bottom: 1px solid #efede9; }
|
||||
td { padding: 8px; border-bottom: 1px solid var(--border); }
|
||||
#log_viewer_table td { padding: 2px 6px; font-size: 0.65rem; }
|
||||
#log_viewer_table td:first-child { white-space: nowrap; }
|
||||
summary { border-radius: 5px; font-weight: bold; text-align: left; color: #fff; background-color: #723; padding: 0.3rem;}
|
||||
|
||||
.cmd { font-size: 1.5rem; border: none;}
|
||||
|
||||
#msg {text-align: center;}
|
||||
|
||||
/* Status box: state label plus a flex row of per-error pills. */
|
||||
summary {
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
color: #ffffff;
|
||||
background-color: var(--red);
|
||||
padding: 0.3rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* === ACTION BUTTON ROW (item 12) ===
|
||||
* Flex layout instead of a table: each button gets exactly 50% of
|
||||
* the row width with a 10px gap, regardless of label width. The
|
||||
* `.cmd` text wraps when "START MOVE" doesn't fit on one line. */
|
||||
.action_row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
.action_row > button {
|
||||
flex: 1 1 0; /* equal width — both buttons take 50% */
|
||||
margin: 0;
|
||||
min-height: 64px;
|
||||
}
|
||||
.cmd {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
border: none;
|
||||
line-height: 1.15;
|
||||
}
|
||||
.cmd_action { background-color: var(--green); color: #ffffff; }
|
||||
.cmd_action.moving { background-color: var(--yellow); color: #1a1a1a; }
|
||||
.cmd_estop { background-color: var(--red); color: #ffffff; }
|
||||
|
||||
#msg { text-align: center; }
|
||||
|
||||
/* === STATUS BOX === */
|
||||
#status_box { text-align: center; padding: 6px 0; }
|
||||
#status_state { font-weight: bold; font-size: 1.3rem; padding: 4px 0; color: #2a493d; }
|
||||
#status_state.error { color: #c33; }
|
||||
/* Item 13: status text always uses the default theme color — no
|
||||
* error-class override that flips it red. The pill row below does
|
||||
* the error signaling. */
|
||||
#status_state {
|
||||
font-weight: bold;
|
||||
font-size: 1.3rem;
|
||||
padding: 4px 0;
|
||||
color: var(--text);
|
||||
}
|
||||
#status_indicators { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-top: 4px; }
|
||||
#status_indicators:empty { display: none; }
|
||||
.status_pill {
|
||||
@@ -58,18 +139,19 @@ black: #2f2f2f
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
background-color: #888;
|
||||
background-color: var(--surface-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status_pill.error { background-color: #c33; }
|
||||
.status_pill.warn { background-color: #c90; }
|
||||
.status_pill.info { background-color: #479; }
|
||||
|
||||
.status_pill.error { background-color: var(--red); }
|
||||
.status_pill.warn { background-color: var(--yellow); color: #1a1a1a; }
|
||||
.status_pill.info { background-color: var(--accent); }
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-size: 2.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Popup modal styles */
|
||||
|
||||
/* === POPUP MODAL === */
|
||||
#popup-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -77,101 +159,87 @@ black: #2f2f2f
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#popup-content {
|
||||
background-color: #2a493d;
|
||||
color: #ffffff;
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#popup-content h2 {
|
||||
margin-top: 0;
|
||||
color: #ffffff;
|
||||
background-color: #2a493d;
|
||||
}
|
||||
|
||||
#popup-content h2,
|
||||
#popup-content p {
|
||||
background-color: #2a493d;
|
||||
color: #ffffff;
|
||||
font-size: 1.1rem;
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#popup-content h2 { margin-top: 0; }
|
||||
#popup-content p { font-size: 1.1rem; }
|
||||
|
||||
@media screen and (max-width: 350px) {
|
||||
#content { max-width: 100%; padding: 0 5px; }
|
||||
/* Stack table cells vertically on mobile for better usability,
|
||||
* but exempt the log viewer — it stays as a wide table with
|
||||
* horizontal scroll provided by its wrapping div. */
|
||||
/* Stack table cells vertically on mobile, but exempt the log
|
||||
* viewer — it stays as a wide table with horizontal scroll. */
|
||||
table:not(#log_viewer_table) tr td { display: block; width: 100%; box-sizing: border-box; }
|
||||
table:not(#log_viewer_table) tr { display: block; margin-bottom: 10px; }
|
||||
}
|
||||
|
||||
|
||||
#popup-buttons {
|
||||
background-color: #2a493d;
|
||||
background-color: var(--surface);
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#popup-buttons button {
|
||||
background-color: #efede9;
|
||||
color: #2f2f2f;
|
||||
border: 1px solid #ba965b;
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--accent);
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
#popup-buttons button:hover {
|
||||
background-color: #ba965b;
|
||||
color: #ffffff;
|
||||
background-color: var(--accent);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
#popup-buttons button.primary {
|
||||
background-color: #ba965b;
|
||||
color: #ffffff;
|
||||
background-color: var(--accent);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
#popup-buttons button.primary:hover {
|
||||
background-color: #8a7045;
|
||||
background-color: #d4b27e;
|
||||
}
|
||||
|
||||
|
||||
#popup-input-container {
|
||||
background-color: #2a493d;
|
||||
background-color: var(--surface);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#popup-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ba965b;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 5px;
|
||||
background-color: #efede9;
|
||||
color: #2f2f2f;
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.sqbtn {
|
||||
width: 35%;
|
||||
padding: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sqbtn {
|
||||
width: 35%;
|
||||
padding: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -181,14 +249,11 @@ black: #2f2f2f
|
||||
|
||||
<h1>CluckCommand</h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><button class="cmd" onclick="sendCommand('start')">START</button></td>
|
||||
<td><button class="cmd" onclick="sendCommand('stop')" style="background-color:#723; color: #fff">STOP</button></td>
|
||||
<td><button class="cmd" onclick="sendCommand('undo')">UNDO</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="action_row">
|
||||
<button class="cmd cmd_action" id="action_btn" onclick="actionBtnClick()">START MOVE</button>
|
||||
<button class="cmd cmd_estop" onclick="sendCommand('stop')">E-STOP</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
@@ -325,7 +390,7 @@ black: #2f2f2f
|
||||
<br/>
|
||||
|
||||
<details id="log_viewer_details">
|
||||
<summary style="background-color:#2a493d;">EVENT LOG</summary>
|
||||
<summary style="background-color:var(--surface-2);">EVENT LOG</summary>
|
||||
<!-- Body breaks out of the 500px #content column so the table has
|
||||
room to breathe. The summary above stays inside the column. -->
|
||||
<div id="log_viewer_body" style="position:relative;width:100vw;left:50%;margin-left:-50vw;padding:0 8px;box-sizing:border-box;">
|
||||
@@ -365,8 +430,8 @@ black: #2f2f2f
|
||||
</table>
|
||||
|
||||
<table id="table"></table>
|
||||
<button class="cmd" onclick="sendCommand('reboot')" style="background-color:#723; color: #fff">REBOOT</button>
|
||||
<button class="cmd" onclick="sendCommand('sleep')" style="background-color:#237; color: #fff">SLEEP</button>
|
||||
<button class="cmd" onclick="sendCommand('reboot')" style="background-color:var(--red); color:#fff">REBOOT</button>
|
||||
<button class="cmd" onclick="sendCommand('sleep')" style="background-color:var(--surface-2); color:var(--text)">SLEEP</button>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
@@ -665,6 +730,14 @@ black: #2f2f2f
|
||||
}
|
||||
}
|
||||
|
||||
// Single action-button dispatcher. STATE_IDLE (0) → start, anything else
|
||||
// → undo. Uses last polled FSM state so the click matches what the
|
||||
// operator sees on the button label.
|
||||
function actionBtnClick() {
|
||||
const isIdle = !data.state || data.state === 0;
|
||||
sendCommand(isIdle ? 'start' : 'undo');
|
||||
}
|
||||
|
||||
async function sendCommand(cmdName) {
|
||||
if (cmdName === 'start') {
|
||||
if (!await modalConfirm("Will begin moving - please confirm."))
|
||||
@@ -930,6 +1003,16 @@ black: #2f2f2f
|
||||
stateEl.textContent = state || '—';
|
||||
stateEl.classList.toggle('error', activePills.length > 0);
|
||||
|
||||
// Action button: STATE_IDLE (0) → "START MOVE" (green);
|
||||
// any other state → "UNDO" (yellow). Single button replaces
|
||||
// the old separate START/UNDO controls.
|
||||
const actionBtn = ge('action_btn');
|
||||
if (actionBtn) {
|
||||
const isIdle = !data.state || data.state === 0;
|
||||
actionBtn.textContent = isIdle ? 'START MOVE' : 'UNDO';
|
||||
actionBtn.classList.toggle('moving', !isIdle);
|
||||
}
|
||||
|
||||
const box = ge('status_indicators');
|
||||
box.innerHTML = '';
|
||||
for (const [, label, kind] of activePills) {
|
||||
@@ -1557,15 +1640,22 @@ black: #2f2f2f
|
||||
const payloadStart = i + 1;
|
||||
const entry = { type };
|
||||
try {
|
||||
if (type >= 0 && type <= 13 && payloadSize >= 27) {
|
||||
entry.ts_ms = Number(dv.getBigUint64(payloadStart + 0, true));
|
||||
entry.bat_V = dv.getFloat32(payloadStart + 8, true);
|
||||
entry.drive_A = dv.getFloat32(payloadStart + 12, true);
|
||||
entry.jack_A = dv.getFloat32(payloadStart + 16, true);
|
||||
entry.aux_A = dv.getFloat32(payloadStart + 20, true);
|
||||
entry.counter = dv.getInt16(payloadStart + 24, true);
|
||||
entry.sensors = bytes[payloadStart + 26];
|
||||
entry.name = LOG_FSM_NAMES[type] || `STATE_${type}`;
|
||||
if (type >= 0 && type <= 13 && payloadSize >= 19) {
|
||||
// Single-current FSM payload (25 bytes):
|
||||
// ts(8) bat(4) current(4) counter(2) sensors(1)
|
||||
// heat(4) i2c_out(2)
|
||||
entry.ts_ms = Number(dv.getBigUint64(payloadStart + 0, true));
|
||||
entry.bat_V = dv.getFloat32(payloadStart + 8, true);
|
||||
entry.current_A = dv.getFloat32(payloadStart + 12, true);
|
||||
entry.counter = dv.getInt16(payloadStart + 16, true);
|
||||
entry.sensors = bytes[payloadStart + 18];
|
||||
if (payloadSize >= 23) {
|
||||
entry.heat = dv.getFloat32(payloadStart + 19, true);
|
||||
}
|
||||
if (payloadSize >= 25) {
|
||||
entry.i2c_out = dv.getUint16(payloadStart + 23, true);
|
||||
}
|
||||
entry.name = LOG_FSM_NAMES[type] || `STATE_${type}`;
|
||||
} else if (type === LOG_TYPE_BAT && payloadSize >= 12) {
|
||||
entry.ts_ms = Number(dv.getBigUint64(payloadStart, true));
|
||||
entry.bat_V = dv.getFloat32(payloadStart + 8, true);
|
||||
@@ -1596,16 +1686,22 @@ black: #2f2f2f
|
||||
const sensorHex = e.sensors !== undefined
|
||||
? '0x' + e.sensors.toString(16).padStart(2,'0')
|
||||
: '';
|
||||
const i2cHex = e.i2c_out !== undefined
|
||||
? '0x' + e.i2c_out.toString(16).padStart(4,'0')
|
||||
: '';
|
||||
return [
|
||||
{ text: _logFormatTs(e.ts_ms) },
|
||||
{ text: e.name || '' },
|
||||
{ text: e.bat_V !== undefined ? e.bat_V.toFixed(2) : '' },
|
||||
{ text: e.drive_A !== undefined ? e.drive_A.toFixed(2) : '' },
|
||||
{ text: e.jack_A !== undefined ? e.jack_A.toFixed(2) : '' },
|
||||
{ text: e.aux_A !== undefined ? e.aux_A.toFixed(2) : '' },
|
||||
{ text: e.counter !== undefined ? String(e.counter) : '' },
|
||||
{ text: e.bat_V !== undefined ? e.bat_V.toFixed(2) : '' },
|
||||
{ text: e.current_A !== undefined ? e.current_A.toFixed(2) : '' },
|
||||
{ text: e.counter !== undefined ? String(e.counter) : '' },
|
||||
{ text: sensorHex,
|
||||
title: e.sensors !== undefined ? _decodeSensors(e.sensors) : undefined },
|
||||
{ text: e.heat !== undefined ? e.heat.toFixed(2) : '' },
|
||||
{ text: i2cHex,
|
||||
title: e.i2c_out !== undefined
|
||||
? '0b' + e.i2c_out.toString(2).padStart(16,'0')
|
||||
: undefined },
|
||||
{ text: e.reason || '' },
|
||||
];
|
||||
}
|
||||
@@ -1635,7 +1731,7 @@ black: #2f2f2f
|
||||
function _renderLogEntries(entries) {
|
||||
const table = ge('log_viewer_table');
|
||||
table.innerHTML = '';
|
||||
const cols = ['Time','Type','Battery V','Drive A','Jack A','Aux A','Counter','Sensors','Extra'];
|
||||
const cols = ['Time','Type','Battery V','Current A','Counter','Sensors','Heat','I2C Out','Extra'];
|
||||
_appendRow(table, cols.map(h => ({ text: h, bold: true })));
|
||||
|
||||
// Walk newest-first, group contiguous entries sharing the same
|
||||
@@ -1663,10 +1759,11 @@ black: #2f2f2f
|
||||
const header = _appendRow(table, [{
|
||||
text: `[+] ${name} × ${group.length} ${firstTs} → ${lastTs}`,
|
||||
colSpan: cols.length,
|
||||
style: { cursor: 'pointer', background: '#efede9',
|
||||
style: { cursor: 'pointer', background: 'var(--surface-2)',
|
||||
color: 'var(--text)',
|
||||
fontWeight: 'bold', fontSize: '0.7rem' },
|
||||
}], {
|
||||
style: { background: '#efede9' },
|
||||
style: { background: 'var(--surface-2)' },
|
||||
});
|
||||
// Defer wiring the onclick until we've appended the child rows.
|
||||
const childRows = group.map(e =>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1321
main/webpage_gzip.h
1321
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
104
main/webserver.c
104
main/webserver.c
@@ -21,6 +21,7 @@
|
||||
#include "string.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_system.h"
|
||||
@@ -61,7 +62,32 @@ extern const uint8_t prvtkey_pem_end[] asm("_binary_prvtkey_pem_end");
|
||||
|
||||
static httpd_handle_t http_server_instance = NULL;
|
||||
|
||||
/* Shared scratch buffer used by log/post/ota handlers. The httpd default
|
||||
* config uses a single worker task, so handlers don't normally race — but
|
||||
* with config.lru_purge_enable + 7 sockets some IDF configurations allow
|
||||
* concurrent invocations. The mutex below is defense in depth: each
|
||||
* handler that touches http_buffer takes it on entry via the wrap_*
|
||||
* dispatcher below and releases on exit. */
|
||||
char http_buffer[4096];
|
||||
static SemaphoreHandle_t http_buffer_mutex = NULL;
|
||||
|
||||
static esp_err_t with_http_buffer(httpd_req_t *req,
|
||||
esp_err_t (*body)(httpd_req_t *)) {
|
||||
if (http_buffer_mutex != NULL) {
|
||||
if (xSemaphoreTake(http_buffer_mutex, pdMS_TO_TICKS(2000)) != pdTRUE) {
|
||||
/* esp_http_server's httpd_err_code_t enum doesn't include 503,
|
||||
* so emit it via the lower-level set_status + send pair. */
|
||||
ESP_LOGW(TAG, "http_buffer busy — rejecting request");
|
||||
httpd_resp_set_status(req, "503 Service Unavailable");
|
||||
httpd_resp_set_type(req, "text/plain");
|
||||
httpd_resp_send(req, "busy", HTTPD_RESP_USE_STRLEN);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
esp_err_t ret = body(req);
|
||||
if (http_buffer_mutex != NULL) xSemaphoreGive(http_buffer_mutex);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Handler to serve the HTML page */
|
||||
static esp_err_t root_get_handler(httpd_req_t *req) {
|
||||
@@ -107,7 +133,11 @@ static const esp_partition_t *cached_log_partition = NULL;
|
||||
|
||||
// In webserver.c - Replace the log_handler function
|
||||
|
||||
static esp_err_t log_handler_locked(httpd_req_t *req);
|
||||
static esp_err_t log_handler(httpd_req_t *req) {
|
||||
return with_http_buffer(req, log_handler_locked);
|
||||
}
|
||||
static esp_err_t log_handler_locked(httpd_req_t *req) {
|
||||
if (req == NULL) {
|
||||
ESP_LOGE(TAG, "Null request pointer");
|
||||
return ESP_FAIL;
|
||||
@@ -443,7 +473,11 @@ static void webserver_restart_wifi_cb(void *arg) { webserver_restart_wifi(); }
|
||||
/**
|
||||
* Unified POST handler - handles commands, parameter updates, time updates
|
||||
*/
|
||||
static esp_err_t post_handler_locked(httpd_req_t *req);
|
||||
static esp_err_t post_handler(httpd_req_t *req) {
|
||||
return with_http_buffer(req, post_handler_locked);
|
||||
}
|
||||
static esp_err_t post_handler_locked(httpd_req_t *req) {
|
||||
ESP_LOGI(TAG, "post_handler");
|
||||
|
||||
if (req == NULL) {
|
||||
@@ -575,7 +609,11 @@ static esp_err_t post_handler(httpd_req_t *req) {
|
||||
return err;
|
||||
}
|
||||
|
||||
static esp_err_t ota_post_handler_locked(httpd_req_t *req);
|
||||
static esp_err_t ota_post_handler(httpd_req_t *req) {
|
||||
return with_http_buffer(req, ota_post_handler_locked);
|
||||
}
|
||||
static esp_err_t ota_post_handler_locked(httpd_req_t *req) {
|
||||
ESP_LOGI(TAG, "OTA POST request received");
|
||||
|
||||
if (req == NULL) {
|
||||
@@ -670,8 +708,10 @@ static esp_err_t ota_post_handler(httpd_req_t *req) {
|
||||
remaining -= recv_len;
|
||||
received += recv_len;
|
||||
|
||||
// Log progress every 10%
|
||||
if (total_len > 0 && (received % (total_len / 10)) < recv_len) {
|
||||
// Log progress every 10%. Guard against total_len < 10 (would
|
||||
// otherwise divide by zero in the modulo) and total_len == 0
|
||||
// (chunked transfer with unknown length).
|
||||
if (total_len >= 10 && (received % (total_len / 10)) < recv_len) {
|
||||
ESP_LOGI(TAG, "OTA progress: %d%%", (received * 100) / total_len);
|
||||
}
|
||||
}
|
||||
@@ -834,6 +874,9 @@ static bool s_wifi_initted = false;
|
||||
static esp_err_t start_http_server(void) {
|
||||
if (server_running) return ESP_OK;
|
||||
ESP_LOGI(TAG, "STARTING HTTP");
|
||||
if (http_buffer_mutex == NULL) {
|
||||
http_buffer_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = 80;
|
||||
config.max_open_sockets = 7;
|
||||
@@ -891,20 +934,33 @@ static esp_err_t stop_http_server(void) {
|
||||
|
||||
int n_connected = 0;
|
||||
|
||||
/* Signaled by the WiFi event task when the AP fully stops. Used by
|
||||
* webserver_restart_wifi() to deterministically wait for teardown to
|
||||
* complete instead of racing on a fixed delay. */
|
||||
static SemaphoreHandle_t s_ap_stopped_sem = NULL;
|
||||
|
||||
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void* event_data) {
|
||||
if (event_base == WIFI_EVENT) {
|
||||
if (event_id == WIFI_EVENT_AP_STOP) {
|
||||
ESP_LOGI(TAG, "WIFI_EVENT_AP_STOP");
|
||||
if (s_ap_stopped_sem) xSemaphoreGive(s_ap_stopped_sem);
|
||||
return;
|
||||
}
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
|
||||
ESP_LOGI(TAG, "Station connected, AID=%d", event->aid);
|
||||
rtc_reset_shutdown_timer();
|
||||
n_connected++;
|
||||
if (n_connected > 0) start_http_server();
|
||||
/* HTTP lifecycle is no longer tied to client count — the server
|
||||
* is started once in webserver_init() and stays up. Tying
|
||||
* start/stop to events created races where rapid connect/
|
||||
* disconnect bursts could call httpd_start twice or stop on
|
||||
* the wrong tick. */
|
||||
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
|
||||
ESP_LOGI(TAG, "Station disconnected, AID=%d", event->aid);
|
||||
n_connected--;
|
||||
if (n_connected <= 0) stop_http_server();
|
||||
if (n_connected > 0) n_connected--;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -939,11 +995,24 @@ static esp_err_t wifi_common_init(void) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (s_ap_stopped_sem == NULL) {
|
||||
s_ap_stopped_sem = xSemaphoreCreateBinary();
|
||||
}
|
||||
|
||||
s_wifi_initted = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t launch_soft_ap(void) {
|
||||
/* If WiFi is already up, don't re-issue set_mode/set_config/start —
|
||||
* those modify driver state on a running AP and can de-associate
|
||||
* existing clients. The caller (typically webserver_init or
|
||||
* BU.WIFI.START) just gets ESP_OK as if a fresh launch succeeded. */
|
||||
if (s_wifi_running) {
|
||||
ESP_LOGI(TAG, "AP already running — launch_soft_ap is a no-op");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "AP LAUNCHING");
|
||||
|
||||
if (s_ap_netif == NULL) {
|
||||
@@ -987,7 +1056,15 @@ static esp_err_t launch_soft_ap(void) {
|
||||
wifi_config.ap.channel = 6;
|
||||
}
|
||||
|
||||
wifi_config.ap.ssid_len = strlen(ssid_str);
|
||||
/* Clamp explicitly: the param store currently caps SSIDs at 16 bytes so
|
||||
* the value is always within `wifi_config.ap.ssid` (32-byte) bounds, but
|
||||
* if the param size ever grows the WiFi driver would read past the
|
||||
* buffer. */
|
||||
{
|
||||
size_t len = strlen(ssid_str);
|
||||
if (len > sizeof(wifi_config.ap.ssid)) len = sizeof(wifi_config.ap.ssid);
|
||||
wifi_config.ap.ssid_len = (uint8_t)len;
|
||||
}
|
||||
|
||||
err = esp_wifi_set_mode(WIFI_MODE_AP);
|
||||
if (err != ESP_OK) { ESP_LOGE(TAG, "set_mode AP: %s", esp_err_to_name(err)); return err; }
|
||||
@@ -1049,11 +1126,20 @@ esp_err_t webserver_restart_wifi(void) {
|
||||
|
||||
stop_http_server();
|
||||
if (s_wifi_running) {
|
||||
/* Drain any pre-existing token so xSemaphoreTake below blocks for a
|
||||
* fresh AP_STOP event rather than returning immediately on stale
|
||||
* state. */
|
||||
if (s_ap_stopped_sem) xSemaphoreTake(s_ap_stopped_sem, 0);
|
||||
esp_wifi_stop();
|
||||
s_wifi_running = false;
|
||||
/* Allow the event loop to drain the AP-stop event that
|
||||
* esp_wifi_stop() queues asynchronously before we relaunch. */
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
/* Wait for the WiFi driver to finish tearing the AP down. The
|
||||
* 1-second deadline is generous; on healthy hardware the event
|
||||
* arrives within a few tens of ms. */
|
||||
if (s_ap_stopped_sem) {
|
||||
xSemaphoreTake(s_ap_stopped_sem, pdMS_TO_TICKS(1000));
|
||||
} else {
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t err = start_wifi(false); // called from esp_timer task, not subscribed to WDT
|
||||
|
||||
Reference in New Issue
Block a user