safety interlocking and a bunch of other fun stuff

This commit is contained in:
Thaddeus Hughes
2026-01-05 19:47:51 -06:00
parent 53bea4eb04
commit 15e2145560
19 changed files with 1004 additions and 935 deletions

View File

@@ -4,7 +4,7 @@
include(${CMAKE_CURRENT_LIST_DIR}/version.cmake)
idf_component_register(
SRCS main.c i2c.c rtc.c storage.c uart_comms.c control_fsm.c power_mgmt.c rf_433.c rtc.c sensors.c solar.c webserver.c simple_dns_server.c # list the source files of this component
SRCS main.c i2c.c rtc.c storage.c uart_comms.c control_fsm.c power_mgmt.c rf_433.c rtc.c sensors.c solar.c webserver.c simple_dns_server.c comms.c # list the source files of this component
INCLUDE_DIRS "." "${CMAKE_BINARY_DIR}"
PRIV_INCLUDE_DIRS # optional, add here private include directories

458
main/comms.c Normal file
View File

@@ -0,0 +1,458 @@
#include "comms.h"
#include "cJSON.h"
#include "control_fsm.h"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "power_mgmt.h"
#include "rf_433.h"
#include "rtc.h"
#include "storage.h"
#include "version.h"
#include <string.h>
static const char *TAG = "COMMS";
/**
* Build a JSON object containing complete system status
*/
cJSON* comms_handle_get(void) {
ESP_LOGI(TAG, "GET request");
rtc_reset_shutdown_timer();
// Create root JSON object
cJSON *root = cJSON_CreateObject();
if (root == NULL) {
ESP_LOGE(TAG, "Failed to create JSON object");
return NULL;
}
// Add basic system info
cJSON_AddStringToObject(root, "build_version", FIRMWARE_VERSION);
cJSON_AddStringToObject(root, "build_date", BUILD_DATE);
cJSON_AddNumberToObject(root, "time", (double)rtc_get_s());
cJSON_AddBoolToObject(root, "rtc_set", rtc_is_set());
cJSON_AddNumberToObject(root, "state", fsm_get_state());
cJSON_AddNumberToObject(root, "voltage", get_battery_V());
cJSON_AddNumberToObject(root, "remaining_dist", fsm_get_remaining_distance());
cJSON_AddNumberToObject(root, "next_alarm", (double)rtc_get_next_alarm_s());
cJSON *msg_array = cJSON_CreateArray();
if (msg_array == NULL) {
ESP_LOGE(TAG, "Failed to create msg array");
cJSON_Delete(root);
return NULL;
}
// Add state message
switch(fsm_get_state()) {
case STATE_IDLE:
cJSON_AddItemToArray(msg_array, cJSON_CreateString("IDLE"));
break;
case STATE_UNDO_JACK:
case STATE_UNDO_JACK_START:
cJSON_AddItemToArray(msg_array, cJSON_CreateString("CANCELLING MOVE"));
break;
default:
cJSON_AddItemToArray(msg_array, cJSON_CreateString("MOVING..."));
break;
}
// Add warning/error messages
if (fsm_get_remaining_distance() <= 0) {
cJSON_AddItemToArray(msg_array, cJSON_CreateString("DISTANCE LIMIT HIT"));
}
if (efuse_is_tripped(BRIDGE_AUX)) {
cJSON_AddItemToArray(msg_array, cJSON_CreateString("AUX EFUSE TRIP"));
}
if (efuse_is_tripped(BRIDGE_JACK)) {
cJSON_AddItemToArray(msg_array, cJSON_CreateString("JACK EFUSE TRIP"));
}
if (efuse_is_tripped(BRIDGE_DRIVE)) {
cJSON_AddItemToArray(msg_array, cJSON_CreateString("DRIVE EFUSE TRIP"));
}
if (!rtc_is_set()) {
cJSON_AddItemToArray(msg_array, cJSON_CreateString("CLOCK NOT SET"));
}
cJSON_AddItemToObject(root, "msg", msg_array);
// Add parameters object
cJSON *parameters = cJSON_CreateObject();
if (parameters == NULL) {
ESP_LOGE(TAG, "Failed to create parameters object");
cJSON_Delete(root);
return NULL;
}
// Add all parameters
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;
}
}
cJSON_AddItemToObject(root, "parameters", parameters);
return root;
}
/**
* Process a POST request with JSON data
*/
esp_err_t comms_handle_post(cJSON *root, cJSON **response_json) {
ESP_LOGI(TAG, "POST request");
if (root == NULL || response_json == NULL) {
ESP_LOGE(TAG, "Invalid arguments to comms_handle_post");
return ESP_FAIL;
}
rtc_reset_shutdown_timer();
bool cmd_executed = false;
bool sleep_requested = false;
bool reboot_requested = false;
const char *error_msg = NULL;
int params_updated = 0;
int params_failed = 0;
// Process time if present
cJSON *time = cJSON_GetObjectItem(root, "time");
if (cJSON_IsNumber(time)) {
int64_t new_time = (int64_t)cJSON_GetNumberValue(time);
ESP_LOGI(TAG, "Setting time to %lld", new_time);
rtc_set_s(new_time);
}
// 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);
fsm_set_remaining_distance(new_dist);
}
// Process command if present
cJSON *cmd = cJSON_GetObjectItem(root, "cmd");
if (cJSON_IsString(cmd)) {
const char *cmd_str = cmd->valuestring;
ESP_LOGI(TAG, "Executing command: %s", cmd_str);
if (strcmp(cmd_str, "start") == 0) {
fsm_request(FSM_CMD_START);
cmd_executed = true;
}
else if (strcmp(cmd_str, "stop") == 0) {
fsm_request(FSM_CMD_STOP);
cmd_executed = true;
}
else if (strcmp(cmd_str, "undo") == 0) {
fsm_request(FSM_CMD_UNDO);
cmd_executed = true;
}
else if (strcmp(cmd_str, "fwd") == 0) {
pulseOverride(RELAY_A1);
pulseOverride(RELAY_A3);
cmd_executed = true;
}
else if (strcmp(cmd_str, "rev") == 0) {
pulseOverride(RELAY_B1);
pulseOverride(RELAY_A3);
cmd_executed = true;
}
else if (strcmp(cmd_str, "up") == 0) {
pulseOverride(RELAY_A2);
cmd_executed = true;
}
else if (strcmp(cmd_str, "down") == 0) {
pulseOverride(RELAY_B2);
cmd_executed = true;
}
else if (strcmp(cmd_str, "aux") == 0) {
pulseOverride(RELAY_A3);
cmd_executed = true;
}
else if (strcmp(cmd_str, "reboot") == 0) {
reboot_requested = true;
cmd_executed = true;
}
else if (strcmp(cmd_str, "sleep") == 0) {
sleep_requested = true;
cmd_executed = true;
}
else if (strcmp(cmd_str, "rf_clear_temp") == 0) {
rf_433_clear_temp_keycodes();
cmd_executed = true;
}
else if (strcmp(cmd_str, "rf_disable") == 0) {
rf_433_disable_controls();
cmd_executed = true;
}
else if (strcmp(cmd_str, "rf_enable") == 0) {
rf_433_enable_controls();
cmd_executed = true;
}
else if (strcmp(cmd_str, "rf_learn") == 0) {
// Start learning for a specific channel
cJSON *channel = cJSON_GetObjectItem(root, "channel");
if (cJSON_IsNumber(channel)) {
int ch = (int)cJSON_GetNumberValue(channel);
if (ch >= 0 && ch < NUM_RF_BUTTONS) {
rf_433_learn_keycode(ch);
cmd_executed = true;
} else if (ch == -1) {
rf_433_cancel_learn_keycode();
cmd_executed = true;
} else {
error_msg = "Invalid channel number";
}
} else {
error_msg = "rf_learn requires channel parameter";
}
}
else if (strcmp(cmd_str, "rf_set_temp") == 0) {
cJSON *index = cJSON_GetObjectItem(root, "index");
cJSON *code = cJSON_GetObjectItem(root, "code");
if (cJSON_IsNumber(index) && cJSON_IsNumber(code)) {
int idx = (int)cJSON_GetNumberValue(index);
int32_t rf_code = (int32_t)cJSON_GetNumberValue(code);
rf_433_set_temp_keycode(idx, rf_code);
cmd_executed = true;
} else {
error_msg = "rf_set_temp requires index and code parameters";
}
}
else if (strcmp(cmd_str, "rf_status") == 0) {
// Return current temp keycodes
cJSON *response = cJSON_CreateObject();
cJSON *codes_array = cJSON_CreateArray();
for (int i = 0; i < 4; i++) { // Only return first 4 for web UI
int32_t code = rf_433_get_temp_keycode(i);
cJSON_AddItemToArray(codes_array, cJSON_CreateNumber(code));
}
cJSON_AddItemToObject(response, "codes", codes_array);
*response_json = response;
return ESP_OK;
}
else if (strcmp(cmd_str, "cal_jack_start") == 0) {
fsm_request(FSM_CMD_CALIBRATE_JACK_PREP);
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP");
cmd_executed = true;
}
else if (strcmp(cmd_str, "cal_jack_finish") == 0) {
cJSON *amt = cJSON_GetObjectItem(root, "amt");
if (cJSON_IsNumber(amt) && amt->valuedouble >= 0 && amt->valuedouble < 8) {
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_FINISH");
fsm_set_cal_val(amt->valuedouble);
fsm_request(FSM_CMD_CALIBRATE_JACK_FINISH);
cmd_executed = true;
} else {
error_msg = "cal_jack_finish requires amt parameter (0-8)";
}
}
else if (strcmp(cmd_str, "cal_drive_start") == 0) {
fsm_request(FSM_CMD_CALIBRATE_DRIVE_PREP);
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
cmd_executed = true;
}
else if (strcmp(cmd_str, "cal_drive_finish") == 0) {
cJSON *amt = cJSON_GetObjectItem(root, "amt");
if (cJSON_IsNumber(amt) && amt->valuedouble >= 0 && amt->valuedouble < 8) {
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_FINISH");
fsm_set_cal_val(amt->valuedouble);
fsm_request(FSM_CMD_CALIBRATE_DRIVE_FINISH);
cmd_executed = true;
} else {
error_msg = "cal_drive_finish requires amt parameter (0-8)";
}
}
else if (strcmp(cmd_str, "cal_get") == 0) {
ESP_LOGI(TAG, "CAL_GET");
// Build JSON response with calibration values
cJSON *response = cJSON_CreateObject();
cJSON_AddItemToObject(response, "e", cJSON_CreateNumber(fsm_get_cal_e()));
cJSON_AddItemToObject(response, "t", cJSON_CreateNumber(fsm_get_cal_t()));
*response_json = response;
return ESP_OK;
}
else {
ESP_LOGW(TAG, "Unknown command: %s", cmd_str);
error_msg = "Unknown command";
}
}
// Process batch parameter updates if present
cJSON *parameters = cJSON_GetObjectItem(root, "parameters");
if (cJSON_IsObject(parameters)) {
// Process individual parameter updates (direct key-value pairs)
cJSON *item = NULL;
cJSON_ArrayForEach(item, parameters) {
const char *key = item->string;
ESP_LOGI(TAG, "Processing parameter: %s", key);
// Find parameter index by name
param_idx_t param_idx = NUM_PARAMS;
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (strcmp(get_param_name(i), key) == 0) {
param_idx = i;
break;
}
}
if (param_idx == NUM_PARAMS) {
ESP_LOGW(TAG, "Unknown parameter: %s", key);
params_failed++;
continue;
}
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 (params_updated > 0) {
rtc_schedule_next_alarm();
commit_params();
}
}
// Build response
cJSON *response = cJSON_CreateObject();
if (response == NULL) {
ESP_LOGE(TAG, "Failed to create response JSON");
return ESP_FAIL;
}
if (reboot_requested) {
cJSON_AddStringToObject(response, "status", "ok");
cJSON_AddStringToObject(response, "message", "Rebooting...");
cJSON_AddBoolToObject(response, "reboot", true);
*response_json = response;
return ESP_OK;
}
if (sleep_requested) {
cJSON_AddStringToObject(response, "status", "ok");
cJSON_AddStringToObject(response, "message", "Sleeping...");
cJSON_AddBoolToObject(response, "sleep", true);
*response_json = response;
return ESP_OK;
}
if (error_msg != NULL) {
cJSON_AddStringToObject(response, "status", "error");
cJSON_AddStringToObject(response, "message", error_msg);
*response_json = response;
return ESP_FAIL;
}
cJSON_AddStringToObject(response, "status", "ok");
cJSON_AddNumberToObject(response, "params_updated", params_updated);
cJSON_AddNumberToObject(response, "params_failed", params_failed);
cJSON_AddBoolToObject(response, "cmd_executed", cmd_executed);
*response_json = response;
return ESP_OK;
}

34
main/comms.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef COMMS_H
#define COMMS_H
#include <stdint.h>
#include "esp_err.h"
#include "cJSON.h"
/**
* Unified communications module
*
* This module provides the core GET/POST logic that can be called from
* either the webserver (HTTP) or UART (serial) interfaces.
*/
/**
* Process a GET request - returns complete system status as JSON
*
* @return cJSON object containing system status, or NULL on error
* Caller is responsible for deleting the returned object with cJSON_Delete()
*/
cJSON* comms_handle_get(void);
/**
* Process a POST request - handles commands, parameter updates, time updates
*
* @param request_json The parsed JSON request object
* @param response_json Pointer to store the response JSON object
* @return ESP_OK on success, error code otherwise
* Caller is responsible for deleting the response_json with cJSON_Delete()
*/
esp_err_t comms_handle_post(cJSON *request_json, cJSON **response_json);
#endif // COMMS_H

View File

@@ -12,6 +12,7 @@
#include "power_mgmt.h"
#include "rtc_wdt.h"
#include "driver/gpio.h"
#include "sc_err.h"
#include "storage.h"
#include "rtc.h"
#include "sensors.h"
@@ -25,6 +26,10 @@
#define TAG "FSM"
static QueueHandle_t fsm_cmd_queue = NULL;
RTC_DATA_ATTR esp_err_t error = ESP_OK;
esp_err_t fsm_get_error() { return error; }
void fsm_clear_error() { error = ESP_OK; }
// map from relay number to bridge
bridge_t bridge_map[] = {
@@ -171,19 +176,41 @@ void control_task(void *param) {
fsm_cmd_t cmd;
while (xQueueReceive(fsm_cmd_queue, &cmd, 0) == pdTRUE) {
// if (error != ESP_OK) continue; // don't do anything until error is cleared
switch (cmd) {
case FSM_CMD_START:
if (current_state == STATE_IDLE) {
// Check if we have remaining distance before starting
if (remaining_distance > 0.0f
&& !efuse_is_tripped(BRIDGE_DRIVE)
&& !efuse_is_tripped(BRIDGE_JACK)
&& !efuse_is_tripped(BRIDGE_AUX)) {
current_state = STATE_MOVE_START_DELAY;
set_timer(TRANSITION_DELAY_US);
} else {
ESP_LOGW(TAG, "Cannot start move: no remaining distance (%.2f)", remaining_distance);
if (remaining_distance <= 0.0f) {
error = SC_ERR_LEASH_HIT;
continue;
}
if (get_battery_V() > get_param_value_t(PARAM_LOW_PROTECTION_V).f32) {
error = SC_ERR_LOW_BATTERY;
continue;
}
if (get_safety_sensor()) {
error = SC_ERR_SAFETY_TRIP;
continue;
}
if (efuse_is_tripped(BRIDGE_DRIVE)) {
error = SC_ERR_EFUSE_TRIP_1;
continue;
}
if (efuse_is_tripped(BRIDGE_JACK)) {
error = SC_ERR_EFUSE_TRIP_2;
continue;
}
if (efuse_is_tripped(BRIDGE_AUX)) {
error = SC_ERR_EFUSE_TRIP_3;
continue;
}
error = ESP_OK; // if everything is OK now, we're OK.
current_state = STATE_MOVE_START_DELAY;
set_timer(TRANSITION_DELAY_US);
}
break;
case FSM_CMD_STOP:
@@ -202,14 +229,16 @@ void control_task(void *param) {
case FSM_CMD_CALIBRATE_JACK_PREP:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP");
if (current_state == STATE_IDLE) {
if (current_state == STATE_IDLE
&& get_battery_V() > get_param_value_t(PARAM_LOW_PROTECTION_V).f32) {
current_state = STATE_CALIBRATE_JACK_DELAY;
}
break;
case FSM_CMD_CALIBRATE_JACK_START:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_START");
if (current_state == STATE_CALIBRATE_JACK_DELAY) {
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;
set_timer(CALIBRATE_JACK_MAX_TIME);
}
@@ -231,14 +260,16 @@ void control_task(void *param) {
case FSM_CMD_CALIBRATE_DRIVE_PREP:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
if (current_state == STATE_IDLE) {
if (current_state == STATE_IDLE
&& get_battery_V() > get_param_value_t(PARAM_LOW_PROTECTION_V).f32) {
current_state = STATE_CALIBRATE_DRIVE_DELAY;
}
break;
case FSM_CMD_CALIBRATE_DRIVE_START:
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_START");
if (current_state == STATE_CALIBRATE_DRIVE_DELAY) {
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;
set_timer(CALIBRATE_DRIVE_MAX_TIME);
set_sensor_counter(SENSOR_DRIVE, 0);
@@ -306,6 +337,10 @@ void control_task(void *param) {
}
break;
case STATE_MOVE_START_DELAY:
if (!get_safety_sensor()) {
error = SC_ERR_SAFETY_TRIP;
current_state = STATE_IDLE;
}
if (timer_done()) {
current_state = STATE_JACK_UP_START;
set_timer(JACK_TIME / 2); // First phase is half of total jack time
@@ -313,6 +348,11 @@ void control_task(void *param) {
}
break;
case STATE_JACK_UP_START:
if (!get_safety_sensor()) {
error = SC_ERR_SAFETY_TRIP;
current_state = STATE_UNDO_JACK_START;
}
{
// Track elapsed time
int64_t elapsed = current_time - timer_start;
@@ -332,14 +372,19 @@ void control_task(void *param) {
}
}
// E-fuse trip should still cause undo
// E-fuse trip
if (efuse_is_tripped(BRIDGE_JACK)) {
ESP_LOGI(TAG, "START->UP BY TIME");
current_state = STATE_UNDO_JACK_START;
error = SC_ERR_EFUSE_TRIP_2;
current_state = STATE_IDLE;
}
}
break;
case STATE_JACK_UP:
if (!get_safety_sensor()) {
error = SC_ERR_SAFETY_TRIP;
current_state = STATE_UNDO_JACK_START;
}
{
if (timer_done() || efuse_is_tripped(BRIDGE_JACK)) {
// Track total time including first phase
@@ -348,12 +393,16 @@ void control_task(void *param) {
set_timer(TRANSITION_DELAY_US);
}
if (efuse_is_tripped(BRIDGE_JACK)) {
ESP_LOGE(TAG, "JACK TRIPPED EFUSE");
current_state = STATE_UNDO_JACK_START;
error = SC_ERR_EFUSE_TRIP_2;
current_state = STATE_IDLE;
}
}
break;
case STATE_DRIVE_START_DELAY:
if (!get_safety_sensor()) {
error = SC_ERR_SAFETY_TRIP;
current_state = STATE_UNDO_JACK_START;
}
if (timer_done()) {
current_state = STATE_DRIVE;
set_timer(DRIVE_TIME);
@@ -366,6 +415,10 @@ void control_task(void *param) {
}
break;
case STATE_DRIVE:
if (!get_safety_sensor()) {
error = SC_ERR_SAFETY_TRIP;
current_state = STATE_UNDO_JACK_START;
}
{
int32_t current_encoder = get_sensor_counter(SENSOR_DRIVE);
int32_t ticks_traveled = current_encoder - move_start_encoder;
@@ -407,17 +460,27 @@ void control_task(void *param) {
if (remaining_distance < 0.0f) remaining_distance = 0.0f;
ESP_LOGW(TAG, "Drive fault: traveled %.2f, old_remaining %.2f, new_remaining %.2f",
distance_traveled, old_remaining, remaining_distance);
error = SC_ERR_EFUSE_TRIP_1;
current_state = STATE_UNDO_JACK_START;
}
}
break;
case STATE_DRIVE_END_DELAY:
if (!get_safety_sensor()) {
error = SC_ERR_SAFETY_TRIP;
current_state = STATE_UNDO_JACK_START;
}
if (timer_done()) {
current_state = STATE_JACK_DOWN;
set_timer(jack_up_total_time); // Use the tracked jack up time
}
break;
case STATE_JACK_DOWN:
if (!get_safety_sensor()) {
error = SC_ERR_SAFETY_TRIP;
current_state = STATE_UNDO_JACK_START;
}
{
// Get current sensing parameters
int64_t delay = get_param_value_t(PARAM_EFUSE_INRUSH_US).u32;
@@ -474,7 +537,6 @@ void control_task(void *param) {
break;
case STATE_CALIBRATE_JACK_MOVE:
if (timer_done()) {
ESP_LOGI(TAG, "STATE_CALIBRATE_JACK_END");
current_state = STATE_IDLE;
fsm_cal_t = current_time - timer_start;
}
@@ -485,8 +547,7 @@ void control_task(void *param) {
// no way out of this except a command
break;
case STATE_CALIBRATE_DRIVE_MOVE:
if (timer_done()) {
ESP_LOGI(TAG, "STATE_CALIBRATE_DRIVE_END");
if (!get_safety_sensor() || timer_done()) {
current_state = STATE_IDLE;
fsm_cal_t = current_time - timer_start;
fsm_cal_e = get_sensor_counter(SENSOR_DRIVE);

View File

@@ -44,7 +44,7 @@ typedef enum {
} fsm_state_t;
typedef enum {
RELAY_NONE = 0,
RELAY_SENSORS = 0,
RELAY_C3,
RELAY_B3,
RELAY_A3,
@@ -75,6 +75,9 @@ int64_t fsm_get_cal_t();
int64_t fsm_get_cal_e();
void fsm_request(fsm_cmd_t cmd);
esp_err_t fsm_get_error();
void fsm_clear_error();
float fsm_get_remaining_distance(void);
void fsm_set_remaining_distance(float x);

View File

@@ -28,8 +28,12 @@
#define REPEAT_MS 200
#define REPEAT_START_MS 700
#define SAFETY_MASK 0b00111111
// Static Variables
static bool i2c_initted = false;
static bool safety_ok = false; // Safety interlock
static uint8_t last_relay_request = 0; // Track last relay request
// === I2C LOW-LEVEL ===
static esp_err_t tca_write_word_8(uint8_t reg, uint8_t value) {
@@ -64,12 +68,37 @@ esp_err_t i2c_init(void) {
ESP_ERROR_CHECK(tca_write_word_8(TCA_REG_CONFIG1, 0b00000000));
i2c_initted = true;
safety_ok = false; // Start with safety not OK
last_relay_request = 0;
return ESP_OK;
}
esp_err_t i2c_set_relays(uint8_t states) {
return tca_write_word_8(TCA_REG_OUTPUT1, states);
void i2c_set_safety_status(bool safe) {
safety_ok = safe;
if (!safe) {
// Safety tripped - immediately turn off all relays
ESP_LOGW("I2C", "Safety interlock activated");
tca_write_word_8(TCA_REG_OUTPUT1, last_relay_request & SAFETY_MASK);
} else {
// Safety cleared - restore last requested relay state
ESP_LOGI("I2C", "Safety interlock cleared");
}
}
esp_err_t i2c_set_relays(uint8_t states) {
last_relay_request = states; // Always track the request
if (!safety_ok) {
// Safety interlock active - refuse to energize relays
ESP_LOGW("I2C", "Main relay operation blocked by safety interlock");
return tca_write_word_8(TCA_REG_OUTPUT1, states & SAFETY_MASK);
}
return tca_write_word_8(TCA_REG_OUTPUT1, states);
}
esp_err_t i2c_set_led1(uint8_t state) {
// push 3 LSB to top
return tca_write_word_8(TCA_REG_OUTPUT0, state<<5);

View File

@@ -11,6 +11,7 @@ esp_err_t i2c_stop(void);
esp_err_t i2c_set_relays(uint8_t states);
esp_err_t i2c_set_led1(uint8_t state);
void i2c_set_safety_status(bool safe);
esp_err_t i2c_poll_buttons();

View File

@@ -124,7 +124,7 @@ void app_main(void) {
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, "Built: %s", BUILD_DATE);
// Check for factory reset condition: Cold boot + button held
@@ -240,12 +240,13 @@ void app_main(void) {
driveLEDs(LED_STATE_START2);
} else if (i2c_get_button_ms(0) > 100){
driveLEDs(LED_STATE_START1);
} else{
} else {
if (
rtc_is_set() &&
!efuse_is_tripped(BRIDGE_JACK) &&
!efuse_is_tripped(BRIDGE_AUX) &&
!efuse_is_tripped(BRIDGE_DRIVE)
!efuse_is_tripped(BRIDGE_DRIVE) &&
fsm_get_error() == ESP_OK
) {
driveLEDs(LED_STATE_AWAKE);
} else {

26
main/sc_err.h Normal file
View File

@@ -0,0 +1,26 @@
/*
* sc_errors.h
*
* Created on: Jan 5, 2026
* Author: Thad
*/
#ifndef MAIN_SC_ERR_H_
#define MAIN_SC_ERR_H_
// from esp_err.h, from 0 -> 0x100 is clear
// as is 0x10D -> 0x3000
#define SC_ERR_EFUSE_TRIP_1 0x201
#define SC_ERR_EFUSE_TRIP_2 0x202
#define SC_ERR_EFUSE_TRIP_3 0x203
#define SC_ERR_SAFETY_TRIP 0x210
#define SC_ERR_LEASH_HIT 0x211
#define SC_ERR_RTC_NOT_SET 0x220
#define SC_ERR_LOW_BATTERY 0x230
#endif /* MAIN_SC_ERR_H_ */

View File

@@ -1,10 +1,12 @@
#include "sensors.h"
#include "i2c.h"
#include "esp_log.h"
#include "esp_task_wdt.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "storage.h"
static const char* TAG = "SENS";
@@ -15,6 +17,13 @@ static volatile uint64_t sensor_last_isr_time[N_SENSORS] = {0};
static volatile bool sensor_stable_state[N_SENSORS] = {false};
static QueueHandle_t sensor_event_queue = NULL;
// Safety sensor debouncing
static volatile bool safety_tripped = false;
static volatile uint64_t safety_low_start_time = 0;
static volatile uint64_t safety_high_start_time = 0;
#define SAFETY_TRIP_DEBOUNCE_US get_param_value_t(PARAM_SAFETY_BREAK_US).u32
#define SAFETY_UNTRIP_DEBOUNCE_US get_param_value_t(PARAM_SAFETY_BREAK_US).u32
#define DEBOUNCE_TIME_US 2000 // 2 ms debounce (adjust per switch)
#define DEBOUNCE_TICKS pdMS_TO_TICKS(DEBOUNCE_TIME_MS)
@@ -23,7 +32,7 @@ typedef struct {
bool level;
} sensor_event_t;
// ISR: Minimal work just record timestamp and forward to queue
// ISR: Minimal work just record timestamp and forward to queue
static void IRAM_ATTR sensor_isr_handler(void* arg) {
uint32_t gpio_num = (uint32_t)arg;
uint8_t i;
@@ -56,62 +65,72 @@ static void sensor_debounce_task(void* param) {
last_processed_time[i] = esp_timer_get_time();
}
// Initialize safety sensor
bool safety_current = !gpio_get_level(sensor_pins[SENSOR_SAFETY]);
if (safety_current) {
safety_low_start_time = esp_timer_get_time();
safety_high_start_time = 0;
} else {
safety_low_start_time = 0;
safety_high_start_time = esp_timer_get_time();
}
uint8_t i = 0;
//int64_t now = -1;
uint8_t i = 0;
while (1) {
if (xQueueReceive(sensor_event_queue, &evt, pdMS_TO_TICKS(100)) == pdTRUE) {
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]);
sensor_stable_state[i] = current_raw;
sensor_stable_state[i] = current_raw;
if (current_raw && !last_raw_state[i]){
ESP_LOGI("SENS", "FALLING");
sensor_count[i]++;
}
if (current_raw && !last_raw_state[i]){
ESP_LOGI("SENS", "FALLING");
sensor_count[i]++;
}
if (!current_raw && last_raw_state[i]){
ESP_LOGI("SENS", "RISING");
sensor_count[i]++;
}
last_raw_state[i] = current_raw;
ESP_LOGI("SENS", "RISING");
sensor_count[i]++;
}
last_raw_state[i] = current_raw;
}
// Handle safety sensor debouncing with asymmetric timing
bool safety_current = !gpio_get_level(sensor_pins[SENSOR_SAFETY]);
uint64_t now = esp_timer_get_time();
//now = esp_timer_get_time();
if (safety_current) {
// Safety sensor is LOW (active)
if (safety_low_start_time == 0) {
// First time going low, start timing
safety_low_start_time = now;
safety_high_start_time = 0;
ESP_LOGI(TAG, "Safety sensor went LOW, starting trip timer");
} else if (!safety_tripped && (now - safety_low_start_time >= SAFETY_TRIP_DEBOUNCE_US)) {
// Been low for 200ms, trip the safety
safety_tripped = true;
i2c_set_safety_status(false);
ESP_LOGW(TAG, "SAFETY TRIPPED - Relays disabled");
}
} else {
// Safety sensor is HIGH (inactive)
if (safety_high_start_time == 0) {
// First time going high, start timing
safety_high_start_time = now;
safety_low_start_time = 0;
ESP_LOGI(TAG, "Safety sensor went HIGH, starting un-trip timer");
} else if (safety_tripped && (now - safety_high_start_time >= SAFETY_UNTRIP_DEBOUNCE_US)) {
// Been high for 300ms, un-trip the safety
safety_tripped = false;
i2c_set_safety_status(true);
ESP_LOGI(TAG, "SAFETY CLEARED - Relays enabled");
}
}
/*// Wait for debounce period since last ISR
if (now - sensor_last_isr_time[i] >= (DEBOUNCE_TIME_US)) {
bool current_raw = !gpio_get_level(sensor_pins[i]);
// Only update if stable and different from last stable
if (current_raw != sensor_stable_state[i]) {
bool was_high = sensor_stable_state[i];
// Count rising OR falling edges
if (current_raw && !sensor_stable_state[i]){
ESP_LOGI("SENS", "FALLING");
sensor_count[i]++;
}
if (!current_raw && sensor_stable_state[i]){
ESP_LOGI("SENS", "RISING");
sensor_count[i]++;
}
sensor_stable_state[i] = current_raw;
last_raw_state[i] = current_raw;
last_processed_time[i] = now;
}
}*/
esp_task_wdt_reset();
}
}
@@ -160,6 +179,10 @@ bool get_sensor(sensor_t i) {
return sensor_stable_state[i];
}
bool get_safety_sensor(void) {
return !safety_tripped; // Returns true if safe, false if tripped
}
int32_t get_sensor_counter(sensor_t i) {
return sensor_count[i];
}

View File

@@ -26,8 +26,9 @@ void set_sensor_counter(sensor_t i, int32_t to);
int32_t get_sensor_counter(sensor_t i);
bool get_sensor(sensor_t i);
bool get_safety_sensor(void);
esp_err_t sensors_init();
esp_err_t sensors_stop();
#endif /* MAIN_SENSORS_H_ */
#endif /* MAIN_SENSORS_H_ */

View File

@@ -120,6 +120,7 @@ static inline uint8_t param_type_size(param_type_e type) {
PARAM_DEF(JACK_I_DOWN, f32, 8.0, "A") \
PARAM_DEF(V_SENS_K, f32, 0.00766666666, "V/mV") \
PARAM_DEF(BUILD_VERSION, str, "undefined", "") \
PARAM_DEF(SAFETY_BREAK_US, u32, 200000, "") \
// Generate enum for parameter indices

View File

@@ -1,329 +1,139 @@
#include "uart_comms.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "esp_task_wdt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "comms.h"
#include "cJSON.h"
#include "driver/uart.h"
#include "esp_log.h"
#include "esp_system.h"
#include "storage.h"
#include <errno.h>
#include <ctype.h>
#include "rf_433.h"
#include "esp_task_wdt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "rtc.h"
#include <stdio.h>
#include <string.h>
#define TAG "UART"
#define UART_NUM UART_NUM_0
#define BUF_SIZE (1024)
#define CMD_MAX_LEN (256)
#define CMD_MAX_LEN (2048)
static char cmd_buffer[CMD_MAX_LEN];
static int cmd_pos = 0;
static TaskHandle_t uart_task_handle = NULL;
// TODO: Set Time
// TODO: Pair Remote
// TODO: Command Move
// TODO: Show current sensor values
// Parse value as either decimal or hex (0x prefix)
static bool parse_uint64(const char *str, uint64_t *result) {
char *endptr;
if (str == NULL || result == NULL) {
return false;
}
// Check for hex prefix
if (strlen(str) > 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
*result = strtoull(str, &endptr, 16);
} else {
*result = strtoull(str, &endptr, 10);
}
// Check if conversion was successful (endptr should point to null terminator)
return (*endptr == '\0' && endptr != str);
}
// Format parameter value for display based on its type
static void print_param_value(param_idx_t id, param_value_t val) {
param_type_e type = get_param_type(id);
char sbuf[9] = {0};
switch (type) {
case PARAM_TYPE_u16:
printf("%u (0x%04X)\n",
val.u16, val.u16);
break;
case PARAM_TYPE_i16:
printf("%d (0x%04X)\n",
val.i16, (uint16_t)val.i16);
break;
case PARAM_TYPE_u32:
printf("%lu (0x%08lX)\n",
(unsigned long)val.u32, (unsigned long)val.u32);
break;
case PARAM_TYPE_i32:
printf("%ld (0x%08lX)\n",
(long)val.i32, (unsigned long)val.i32);
break;
case PARAM_TYPE_f32:
printf("%.6f (0x%08lX as bits)\n",
val.f32, (unsigned long)val.u32);
break;
case PARAM_TYPE_f64:
printf("%.6f (0x%016llX as bits)\n",
val.f64, (unsigned long long)val.f64);
break;
case PARAM_TYPE_str:
memcpy(val.str, sbuf, 8);
sbuf[8] = '\0';
printf("\"%s\"", sbuf);
break;
default:
printf("UNKNOWN TYPE\n");
break;
}
}
static esp_err_t parse_param_value(const char *orig_str, param_type_e type, param_value_t *val) {
const char *str = orig_str;
// Skip leading whitespace
while (isspace((unsigned char)*str)) str++;
// Check for negative sign on unsigned integer types
bool is_unsigned_int = (type == PARAM_TYPE_u16 || type == PARAM_TYPE_u32);
if (is_unsigned_int && *str == '-') {
return ESP_FAIL;
}
char *endptr;
errno = 0;
switch (type) {
case PARAM_TYPE_u16:
val->u16 = strtoull(str, &endptr, 0);
break;
case PARAM_TYPE_u32:
val->u32 = strtoull(str, &endptr, 0);
break;
case PARAM_TYPE_i16:
val->i16 = strtoll(str, &endptr, 0);
break;
case PARAM_TYPE_i32:
val->i32 = strtoll(str, &endptr, 0);
break;
case PARAM_TYPE_f32:
val->f32 = strtof(str, &endptr);
break;
case PARAM_TYPE_f64:
val->f64 = strtod(str, &endptr);
break;
default:
return ESP_FAIL;
}
if (errno == ERANGE || endptr == str || *endptr != '\0') {
return ESP_FAIL;
}
return ESP_OK;
}
// Process set parameter command: sp <id> <value>
static void cmd_set_param(char *args) {
char *id_str = strtok(args, " \t");
char *val_str = strtok(NULL, " \t");
if (id_str == NULL || val_str == NULL) {
printf("ERROR: Usage: sp <id> <value>\n");
return;
}
// Parse parameter ID
uint64_t id_u64;
if (!parse_uint64(id_str, &id_u64)) {
printf("ERROR: Invalid parameter ID\n");
return;
}
param_idx_t id = (param_idx_t)id_u64;
if (id >= NUM_PARAMS) {
printf("ERROR: Parameter ID %u out of range (max %d)\n",
id, NUM_PARAMS - 1);
return;
}
param_value_t param_val = {0};
param_type_e type = get_param_type(id);
esp_err_t parse_err = parse_param_value(val_str, type, &param_val);
if (parse_err != ESP_OK) {
printf("ERROR: Invalid value\n");
return;
}
esp_err_t err = set_param_value_t(id, param_val);
if (err == ESP_OK) {
printf("OK: Parameter %u (%s) set to ",
id, get_param_name(id));
print_param_value(id, param_val);
printf("(Not committed - use 'cp' to save)\n");
} else {
printf("ERROR: Failed to set parameter\n");
}
}
// Process get parameter command: gp <id>
static void cmd_get_param(char *args) {
char *id_str = strtok(args, " \t");
if (id_str == NULL) {
printf("ERROR: Usage: gp <id>\n");
return;
}
// Parse parameter ID
uint64_t id_u64;
if (!parse_uint64(id_str, &id_u64)) {
printf("ERROR: Invalid parameter ID\n");
return;
}
param_idx_t id = (param_idx_t)id_u64;
if (id >= NUM_PARAMS) {
printf("ERROR: Parameter ID %u out of range (max %d)\n",
id, NUM_PARAMS - 1);
return;
}
// Get parameter
param_value_t val = get_param_value_t(id);
printf("Parameter %u (%s) = ", id, get_param_name(id));
print_param_value(id, val);
}
// Process commit parameters command: cp
static void cmd_commit_params(char *args) {
(void)args; // Unused
printf("Committing parameters to flash...\n");
commit_params();
printf("OK: Parameters committed\n");
}
// Process reset parameter command: rp <id>
static void cmd_reset_param(char *args) {
char *id_str = strtok(args, " \t");
if (id_str == NULL) {
printf("ERROR: Usage: rp <id>\n");
return;
}
// Parse parameter ID
uint64_t id_u64;
if (!parse_uint64(id_str, &id_u64)) {
printf("ERROR: Invalid parameter ID\n");
return;
}
param_idx_t id = (param_idx_t)id_u64;
if (id >= NUM_PARAMS) {
printf("ERROR: Parameter ID %u out of range (max %d)\n",
id, NUM_PARAMS - 1);
return;
}
// Reset to default
param_value_t default_val = get_param_default(id);
esp_err_t err = set_param_value_t(id, default_val);
if (err == ESP_OK) {
printf("OK: Parameter %u (%s) reset to default: ",
id, get_param_name(id));
print_param_value(id, default_val);
printf("(Not committed - use 'cp' to save)\n");
} else {
printf("ERROR: Failed to reset parameter\n");
}
}
// Process list parameters command: lp
static void cmd_list_params(char *args) {
(void)args; // Unused
printf("\n=== Parameter List ===\n");
printf("ID | Name | Type | Value\n");
printf("----+-------------------+------+------------------\n");
const char* type_names[] = {
"u8 ", "i8 ", "u16", "i16", "u32", "i32", "u64", "i64", "f32", "f64"
};
for (int i = 0; i < NUM_PARAMS; i++) {
param_value_t val = get_param_value_t(i);
param_type_e type = get_param_type(i);
printf("%-3d | %-17s | %-4s | ", i, get_param_name(i), type_names[type]);
print_param_value(i, val);
}
// Process GET command
static void cmd_get(void) {
printf("\n");
cJSON *response = comms_handle_get();
if (response == NULL) {
printf("ERROR: Failed to generate GET response\n");
return;
}
// Pretty print the JSON
char *json_str = cJSON_Print(response);
cJSON_Delete(response);
if (json_str == NULL) {
printf("ERROR: Failed to serialize JSON\n");
return;
}
printf("%s\n", json_str);
free(json_str);
}
// Process POST command with JSON data
static void cmd_post(char *json_data) {
printf("\n");
// Parse the JSON input
cJSON *request = cJSON_Parse(json_data);
if (request == NULL) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr != NULL) {
printf("ERROR: JSON parse error before: %s\n", error_ptr);
} else {
printf("ERROR: Failed to parse JSON\n");
}
return;
}
// Call the unified POST handler
cJSON *response = NULL;
esp_err_t err = comms_handle_post(request, &response);
cJSON_Delete(request);
if (response == NULL) {
printf("ERROR: Failed to generate POST response\n");
return;
}
// Pretty print the response
char *json_str = cJSON_Print(response);
// Check for special response flags before deleting
cJSON *reboot_flag = cJSON_GetObjectItem(response, "reboot");
cJSON *sleep_flag = cJSON_GetObjectItem(response, "sleep");
bool should_reboot = cJSON_IsTrue(reboot_flag);
bool should_sleep = cJSON_IsTrue(sleep_flag);
cJSON_Delete(response);
if (json_str == NULL) {
printf("ERROR: Failed to serialize JSON\n");
return;
}
printf("%s\n", json_str);
free(json_str);
// Handle special actions
if (should_reboot) {
printf("\nRebooting in 2 seconds...\n");
fflush(stdout);
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
}
if (should_sleep) {
printf("\nEntering deep sleep in 2 seconds...\n");
fflush(stdout);
vTaskDelay(pdMS_TO_TICKS(2000));
rtc_enter_deep_sleep();
}
}
// Process help command
static void cmd_help(char *args) {
(void)args; // Unused
printf("\n=== Available Commands ===\n");
printf("sp <id> <value> - Set parameter (e.g., sp 0 42 or sp 14 0xDEADBEEF)\n");
printf("gp <id> - Get parameter (e.g., gp 0)\n");
printf("rp <id> - Reset parameter to default (e.g., rp 0)\n");
printf("cp - Commit parameters to flash\n");
printf("lp - List all parameters\n");
printf("help - Show this help\n");
printf("\nNotes:\n");
printf("- Values can be decimal (123) or hex (0xABC)\n");
printf("- Changes are not saved until you run 'cp'\n");
printf("- Parameter IDs range from 0 to %d\n\n", NUM_PARAMS - 1);
}
static void cmd_rf_learn(char *args) {
char *id_str = strtok(args, " \t");
if (id_str == NULL) {
rf_433_cancel_learn_keycode();
return;
}
// Parse parameter ID
uint64_t id_u64;
if (!parse_uint64(id_str, &id_u64)) {
printf("ERROR: Invalid parameter ID\n");
return;
}
param_idx_t id = (param_idx_t)id_u64;
if (id < 8) {
printf("Listening for keycode for slot %d\n", id);
rf_433_learn_keycode(id);
return;
}
printf("ERROR: Keycode slot index out of bounds.\n");
static void cmd_help(void) {
printf("\n=== UART JSON Interface ===\n");
printf("\nCommands:\n");
printf(" GET - Get complete system status (JSON)\n");
printf(" POST: {json} - Send command or update parameters (JSON)\n");
printf(" HELP - Show this help\n");
printf("\nPOST JSON Format:\n");
printf("{\n");
printf(" \"time\": 1234567, // Set RTC time (optional)\n");
printf(" \"remaining_dist\": 100, // Set remaining distance (optional)\n");
printf(" \"cmd\": \"start\", // Execute command (optional)\n");
printf(" \"parameters\": { // Update parameters (optional)\n");
printf(" \"PARAM_NAME\": value,\n");
printf(" \"PARAM_NAME2\": value\n");
printf(" }\n");
printf("}\n");
printf("\nAvailable Commands:\n");
printf(" start, stop, undo, fwd, rev, up, down, aux\n");
printf(" reboot, sleep\n");
printf(" rf_learn, rf_clear_temp, rf_disable, rf_enable, rf_status\n");
printf(" cal_jack_start, cal_jack_finish, cal_drive_start, cal_drive_finish, cal_get\n");
printf("\nExamples:\n");
printf(" GET\n");
printf(" POST: {\"cmd\":\"start\"}\n");
printf(" POST: {\"time\":1704067200}\n");
printf(" POST: {\"parameters\":{\"WIFI_SSID\":\"MyNetwork\",\"WIFI_CHANNEL\":6}}\n");
printf(" POST: {\"cmd\":\"rf_learn\",\"channel\":0}\n");
printf("\n");
}
// Parse and execute command
@@ -338,49 +148,47 @@ static void process_command(char *cmd) {
return;
}
// Extract command
char *space = strchr(cmd, ' ');
char *tab = strchr(cmd, '\t');
char *delim = NULL;
if (space && tab) {
delim = (space < tab) ? space : tab;
} else if (space) {
delim = space;
} else if (tab) {
delim = tab;
}
// Convert to uppercase for command matching
char command[16] = {0};
if (delim) {
int cmd_len = delim - cmd;
if (cmd_len >= sizeof(command)) {
cmd_len = sizeof(command) - 1;
}
strncpy(command, cmd, cmd_len);
cmd = delim + 1; // Point to arguments
} else {
strncpy(command, cmd, sizeof(command) - 1);
cmd = cmd + strlen(cmd); // Point to empty string
int i = 0;
while (cmd[i] && cmd[i] != ' ' && cmd[i] != '\t' && cmd[i] != ':' && i < 15) {
command[i] = (cmd[i] >= 'a' && cmd[i] <= 'z') ? (cmd[i] - 32) : cmd[i];
i++;
}
command[i] = '\0';
// Execute command
if (strcmp(command, "sp") == 0) {
cmd_set_param(cmd);
} else if (strcmp(command, "gp") == 0) {
cmd_get_param(cmd);
} else if (strcmp(command, "rp") == 0) {
cmd_reset_param(cmd);
} else if (strcmp(command, "cp") == 0) {
cmd_commit_params(cmd);
} else if (strcmp(command, "lp") == 0) {
cmd_list_params(cmd);
} else if (strcmp(command, "help") == 0) {
cmd_help(cmd);
} else if (strcmp(command, "rfl") == 0) {
cmd_rf_learn(cmd);
} else {
printf("ERROR: Unknown command '%s' (type 'help' for commands)\n", command);
if (strcmp(command, "GET") == 0) {
cmd_get();
}
else if (strcmp(command, "POST") == 0) {
// Find the start of JSON data (after ':')
char *json_start = strchr(cmd, ':');
if (json_start == NULL) {
printf("ERROR: POST command requires JSON data after ':'\n");
printf("Usage: POST: {json data}\n");
return;
}
json_start++; // Skip the ':'
// Trim whitespace
while (*json_start == ' ' || *json_start == '\t') {
json_start++;
}
if (*json_start == '\0') {
printf("ERROR: No JSON data provided\n");
return;
}
cmd_post(json_start);
}
else if (strcmp(command, "HELP") == 0) {
cmd_help();
}
else {
printf("ERROR: Unknown command '%s'\n", command);
printf("Type 'HELP' for available commands\n");
}
}
@@ -390,7 +198,7 @@ void uart_event_task(void *pvParameters) {
uint8_t data[BUF_SIZE];
while (1) {
esp_task_wdt_reset();
esp_task_wdt_reset();
int len = uart_read_bytes(UART_NUM, data, BUF_SIZE - 1, 20 / portTICK_PERIOD_MS);
if (len > 0) {
@@ -421,7 +229,7 @@ void uart_event_task(void *pvParameters) {
}
cmd_pos = 0;
printf("> ");
printf("\n> ");
fflush(stdout);
continue;
}
@@ -435,7 +243,7 @@ void uart_event_task(void *pvParameters) {
}
}
esp_err_t uart_init() {
esp_err_t uart_init(void) {
// Configure UART
uart_config_t uart_config = {
.baud_rate = 115200,
@@ -450,13 +258,14 @@ esp_err_t uart_init() {
ESP_ERROR_CHECK(uart_param_config(UART_NUM, &uart_config));
// Print startup message
/*printf("\n\n");
printf("=================================\n");
printf(" ESP32 Parameter Manager\n");
printf("=================================\n");
printf("Type 'help' for available commands\n\n");
printf("> ");
fflush(stdout);*/
printf("\n\n");
printf("========================================\n");
printf(" UART JSON Interface Ready\n");
printf("========================================\n");
printf("Type 'HELP' for available commands\n");
printf("Type 'GET' to see system status\n");
printf("\n> ");
fflush(stdout);
// Create UART task
xTaskCreate(uart_event_task, "uart_event_task", 4096, NULL, 12, &uart_task_handle);
@@ -465,7 +274,7 @@ esp_err_t uart_init() {
return ESP_OK;
}
esp_err_t uart_stop() {
esp_err_t uart_stop(void) {
if (uart_task_handle == NULL) {
ESP_LOGW(TAG, "UART task not running");
return ESP_OK;

View File

@@ -1,16 +1,21 @@
/*
* uart_comms.h
*
* Created on: Dec 13, 2025
* Author: Thad
*/
#ifndef MAIN_UART_COMMS_H_
#define MAIN_UART_COMMS_H_
#ifndef UART_COMMS_H
#define UART_COMMS_H
#include "esp_err.h"
esp_err_t uart_init();
esp_err_t uart_stop();
/**
* Initialize UART communication interface
* Provides JSON-based GET/POST commands over serial
*
* Commands:
* GET - Returns complete system status as pretty-printed JSON
* POST: {json data} - Sends JSON command/parameter update
*/
esp_err_t uart_init(void);
#endif /* MAIN_UART_COMMS_H_ */
/**
* Stop UART communication interface
*/
esp_err_t uart_stop(void);
#endif // UART_COMMS_H

File diff suppressed because one or more lines are too long

View File

@@ -333,6 +333,7 @@ black: #2f2f2f
let paramTableCreated = false; // Track if param table has been created
let pollInterval = null; // Store interval ID for polling
let modalResolve = null;
let modalDismissed = false; // Track if out-of-distance modal was dismissed
const ge = (id) => document.getElementById(id);
@@ -834,6 +835,11 @@ black: #2f2f2f
remainingDistInput.value = data.remaining_dist.toFixed(1);
}
// Check if we should show the "out of distance" modal
if (!modalDismissed && (data.remaining_dist === undefined || data.remaining_dist === null || data.remaining_dist <= 0)) {
showOutOfDistanceModal();
}
}
@@ -1282,6 +1288,34 @@ black: #2f2f2f
}
}
async function showOutOfDistanceModal() {
const distance = await modalPrompt(
'Remaining distance is 0 or less. Would you like to add more distance?',
'Add Distance (feet)'
);
if (distance === null || distance === '') {
// User clicked cancel or entered nothing - mark as dismissed
modalDismissed = true;
return;
}
const dist = parseFloat(distance);
if (isNaN(dist) || dist <= 0) {
await modalAlert('Invalid distance. Modal dismissed.');
modalDismissed = true;
return;
}
// Set the remaining distance input and commit
const remainingDistInput = ge('UX_REM_DIST');
if (remainingDistInput) {
remainingDistInput.value = dist.toFixed(1);
markChanged(remainingDistInput);
await commitParams();
}
}
// Start automatic polling
function startPolling() {
// Initial fetch

View File

@@ -138,7 +138,7 @@ def print_compression_stats(original_size, minified_size, gzip_size, brotli_size
def main():
input_file = "landingpage.html"
input_file = "webpage.html"
# Check if input file exists
if not Path(input_file).exists():

View File

@@ -4,7 +4,7 @@
#include <Arduino.h>
const unsigned char PROGMEM html_content_gz[] = {
0x1f, 0x8b, 0x08, 0x00, 0x38, 0xff, 0x5b, 0x69, 0x02, 0xff, 0xed, 0x3d, 0x6b, 0x57, 0xdb, 0x48,
0x1f, 0x8b, 0x08, 0x00, 0xdb, 0x47, 0x5c, 0x69, 0x02, 0xff, 0xed, 0x3d, 0x6b, 0x57, 0xdb, 0x48,
0xb2, 0x9f, 0x77, 0x7e, 0x45, 0xc3, 0x24, 0x8c, 0x94, 0x08, 0xd9, 0x06, 0x32, 0x0f, 0x1b, 0x99,
0x25, 0xe0, 0x4c, 0x32, 0x09, 0x8f, 0xc3, 0x23, 0x33, 0x7b, 0x59, 0x0e, 0x92, 0xad, 0x36, 0xd6,
0x20, 0x4b, 0x1a, 0x49, 0x86, 0x30, 0xc2, 0xff, 0x7d, 0xab, 0xfa, 0x21, 0xb5, 0x64, 0xd9, 0x98,

View File

@@ -8,6 +8,7 @@
*/
#include "cJSON.h"
#include "comms.h"
#include "control_fsm.h"
#include "endian.h"
#include "esp_ota_ops.h"
@@ -332,6 +333,9 @@ static esp_err_t log_handler(httpd_req_t *req) {
* ... other parameters as direct key-value pairs
* }
*/
/**
* Unified GET handler - returns complete system status
*/
static esp_err_t get_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "get_handler");
@@ -340,139 +344,43 @@ static esp_err_t get_handler(httpd_req_t *req) {
return ESP_FAIL;
}
rtc_reset_shutdown_timer();
int head = 0;
// Start JSON object
head += sprintf(httpBuffer+head, "{");
head += sprintf(httpBuffer+head, "\"build_version\":\"%s\",", FIRMWARE_VERSION);
head += sprintf(httpBuffer+head, "\"build_date\":\"%s\",", BUILD_DATE);
head += sprintf(httpBuffer+head, "\"time\":%lld,", (long long)rtc_get_s());
head += sprintf(httpBuffer+head, "\"rtc_set\":%s,", rtc_is_set() ? "true" : "false");
head += sprintf(httpBuffer+head, "\"state\":%d,", fsm_get_state());
head += sprintf(httpBuffer+head, "\"voltage\":%.3f,", get_battery_V());
head += sprintf(httpBuffer+head, "\"remaining_dist\":%.3f,", fsm_get_remaining_distance());
head += sprintf(httpBuffer+head, "\"next_alarm\":%lld,", rtc_get_next_alarm_s());
head += sprintf(httpBuffer+head, "\"msg\":\"");
switch(fsm_get_state()) {
case STATE_IDLE:
head += sprintf(httpBuffer+head, "IDLE");
break;
case STATE_UNDO_JACK:
case STATE_UNDO_JACK_START:
head += sprintf(httpBuffer+head, "CANCELLING MOVE");
break;
default:
head += sprintf(httpBuffer+head, "MOVING...");
break;
}
if (fsm_get_remaining_distance()<=0) {
head += sprintf(httpBuffer+head, " | DISTANCE LIMIT HIT");
}
if (efuse_is_tripped(BRIDGE_AUX)) head += sprintf(httpBuffer+head, " | AUX EFUSE TRIP");
if (efuse_is_tripped(BRIDGE_JACK)) head += sprintf(httpBuffer+head, " | JACK EFUSE TRIP");
if (efuse_is_tripped(BRIDGE_DRIVE)) head += sprintf(httpBuffer+head, " | DRIVE EFUSE TRIP");
if (!rtc_is_set()) {
head += sprintf(httpBuffer+head, " | CLOCK NOT SET");
}
// Add parameters metadata object
head += sprintf(httpBuffer+head, "\",\"parameters\":{");
// Values array
//head += sprintf(httpBuffer+head, "\"names\":[");
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (i > 0) {
head += sprintf(httpBuffer+head, ",");
}
head += sprintf(httpBuffer+head, "\"%s\":", get_param_name(i));
param_value_t value = get_param_value_t(i);
switch (get_param_type(i)) {
case PARAM_TYPE_f32:
head += sprintf(httpBuffer+head, "%.4f", value.f32);
break;
case PARAM_TYPE_f64:
head += sprintf(httpBuffer+head, "%.4f", value.f64);
break;
case PARAM_TYPE_i32:
head += sprintf(httpBuffer+head, "%ld", (long)value.i32);
break;
case PARAM_TYPE_i16:
head += sprintf(httpBuffer+head, "%d", value.i16);
break;
case PARAM_TYPE_u32:
head += sprintf(httpBuffer+head, "%lu", (long)value.u32);
break;
case PARAM_TYPE_u16:
head += sprintf(httpBuffer+head, "%u", value.u16);
break;
case PARAM_TYPE_str:
head += sprintf(httpBuffer+head, "\"%s\"", get_param_string(i));
break;
default:
head += sprintf(httpBuffer+head, "null");
break;
}
}
/*head += sprintf(httpBuffer+head, "],");
// Names array
head += sprintf(httpBuffer+head, "\"names\":[");
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (i > 0) {
head += sprintf(httpBuffer+head, ",");
}
head += sprintf(httpBuffer+head, "\"%s\"", get_param_name(i));
}
head += sprintf(httpBuffer+head, "],");
// Units array
head += sprintf(httpBuffer+head, "\"units\":[");
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (i > 0) {
head += sprintf(httpBuffer+head, ",");
}
head += sprintf(httpBuffer+head, "\"%s\"", get_param_unit(i));
}
head += sprintf(httpBuffer+head, "]");*/
// Close parameters object
head += sprintf(httpBuffer+head, "}");
// Close main JSON object
head += sprintf(httpBuffer+head, "}");
// Check if buffer might overflow
if (head >= (int)(sizeof(httpBuffer) - 100)) {
ESP_LOGE(TAG, "GET response buffer near overflow");
// Call unified GET handler
cJSON *response = comms_handle_get();
if (response == NULL) {
ESP_LOGE(TAG, "Failed to generate GET response");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Status data too large");
"Failed to generate response");
}
// Convert to string (not pretty printed for web - save bandwidth)
char *json_str = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
if (json_str == NULL) {
ESP_LOGE(TAG, "Failed to serialize JSON");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to serialize response");
}
// Send response
esp_err_t err = httpd_resp_set_type(req, "application/json");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set response type: %s", esp_err_to_name(err));
free(json_str);
return err;
}
err = httpd_resp_send(req, httpBuffer, head);
err = httpd_resp_send(req, json_str, strlen(json_str));
free(json_str);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send status response: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Failed to send 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
ESP_LOGW(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
}
return err;
@@ -492,6 +400,9 @@ static esp_err_t get_handler(httpd_req_t *req) {
* }
* }
*/
/**
* Unified POST handler - handles commands, parameter updates, time updates
*/
static esp_err_t post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "post_handler");
@@ -500,8 +411,6 @@ static esp_err_t post_handler(httpd_req_t *req) {
return ESP_FAIL;
}
rtc_reset_shutdown_timer();
// Receive POST data
int ret = httpd_req_recv(req, httpBuffer, sizeof(httpBuffer));
if (ret <= 0) {
@@ -532,390 +441,63 @@ static esp_err_t post_handler(httpd_req_t *req) {
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
}
bool cmd_executed = false;
bool sleep_requested = false;
bool reboot_requested = false;
const char *error_msg = NULL;
int params_updated = 0;
int params_failed = 0;
// Process time if present
cJSON *time = cJSON_GetObjectItem(root, "time");
if (cJSON_IsNumber(time)) {
int64_t new_time = (int64_t)cJSON_GetNumberValue(time);
ESP_LOGI(TAG, "Setting time to %lld", new_time);
rtc_set_s(new_time);
}
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 time to %lld", new_dist);
fsm_set_remaining_distance(new_dist);
}
// WE DO NOT PROCESS STATE. STATE IS A READ-ONLY.
// Process command if present
cJSON *cmd = cJSON_GetObjectItem(root, "cmd");
if (cJSON_IsString(cmd)) {
const char *cmd_str = cmd->valuestring;
ESP_LOGI(TAG, "Executing command: %s", cmd_str);
/*
// Claude made this, wtf?
if (strcmp(cmd_str, "trigger") == 0) {
// Trigger RF433 transmitter
rf_433_transmit();
cmd_executed = true;
} else */
if (strcmp(cmd_str, "start") == 0) {
// Start operation - transition FSM to running state
//fsm_set_state(FSM_STATE_RUNNING);
fsm_request(FSM_CMD_START);
cmd_executed = true;
}
else if (strcmp(cmd_str, "stop") == 0) {
// Stop operation - transition FSM to idle state
//fsm_set_state(FSM_STATE_IDLE);
fsm_request(FSM_CMD_STOP);
cmd_executed = true;
}
else if (strcmp(cmd_str, "undo") == 0) {
// Stop operation - transition FSM to idle state
//fsm_set_state(FSM_STATE_IDLE);
fsm_request(FSM_CMD_UNDO);
cmd_executed = true;
}
else if (strcmp(cmd_str, "fwd") == 0) {
pulseOverride(RELAY_A1); pulseOverride(RELAY_A3);
cmd_executed = true;
}
else if (strcmp(cmd_str, "rev") == 0) {
pulseOverride(RELAY_B1); pulseOverride(RELAY_A3);
cmd_executed = true;
}
else if (strcmp(cmd_str, "up") == 0) {
pulseOverride(RELAY_A2);
cmd_executed = true;
}
else if (strcmp(cmd_str, "down") == 0) {
pulseOverride(RELAY_B2);
cmd_executed = true;
}
else if (strcmp(cmd_str, "aux") == 0) {
pulseOverride(RELAY_A3);
cmd_executed = true;
}
else if (strcmp(cmd_str, "reboot") == 0) {
reboot_requested = true;
cmd_executed = true;
}
else if (strcmp(cmd_str, "sleep") == 0) {
sleep_requested = true;
cmd_executed = true;
}
/*else if (strcmp(cmd_str, "rfp") == 0) {
// RF programming command - get channel parameter
cJSON *channel = cJSON_GetObjectItem(root, "channel");
if (cJSON_IsNumber(channel)) {
int ch = (int)cJSON_GetNumberValue(channel);
ESP_LOGI(TAG, "RF programming channel: %d", ch);
rf_433_learn(ch);
cmd_executed = true;
} else {
ESP_LOGW(TAG, "rfp command missing or invalid channel parameter");
error_msg = "rfp requires channel parameter";
}
}*/
else if (strcmp(cmd_str, "rf_clear_temp") == 0) {
rf_433_clear_temp_keycodes();
cmd_executed = true;
}
else if (strcmp(cmd_str, "rf_disable") == 0) {
rf_433_disable_controls();
cmd_executed = true;
}
else if (strcmp(cmd_str, "rf_enable") == 0) {
rf_433_enable_controls();
cmd_executed = true;
}
else if (strcmp(cmd_str, "rf_learn") == 0) {
// Start learning for a specific channel
cJSON *channel = cJSON_GetObjectItem(root, "channel");
if (cJSON_IsNumber(channel)) {
int ch = (int)cJSON_GetNumberValue(channel);
if (ch >= 0 && ch < NUM_RF_BUTTONS) {
rf_433_learn_keycode(ch);
cmd_executed = true;
} else if (ch == -1) {
rf_433_cancel_learn_keycode();
cmd_executed = true;
} else {
error_msg = "Invalid channel number";
}
} else {
error_msg = "rf_learn requires channel parameter";
}
}
else if (strcmp(cmd_str, "rf_set_temp") == 0) {
cJSON *index = cJSON_GetObjectItem(root, "index");
cJSON *code = cJSON_GetObjectItem(root, "code");
if (cJSON_IsNumber(index) && cJSON_IsNumber(code)) {
int idx = (int)cJSON_GetNumberValue(index);
int32_t rf_code = (int32_t)cJSON_GetNumberValue(code);
rf_433_set_temp_keycode(idx, rf_code);
cmd_executed = true;
} else {
error_msg = "rf_set_temp requires index and code parameters";
}
}
else if (strcmp(cmd_str, "rf_status") == 0) {
// Return current temp keycodes
cJSON *response = cJSON_CreateObject();
cJSON *codes_array = cJSON_CreateArray();
for (int i = 0; i < 4; i++) { // Only return first 4 for web UI
int32_t code = rf_433_get_temp_keycode(i);
cJSON_AddItemToArray(codes_array, cJSON_CreateNumber(code));
}
cJSON_AddItemToObject(response, "codes", codes_array);
char *json_str = cJSON_Print(response);
cJSON_Delete(response);
cJSON_Delete(root);
esp_err_t err = httpd_resp_set_type(req, "application/json");
if (err == ESP_OK) {
err = httpd_resp_send(req, json_str, strlen(json_str));
}
free(json_str);
return err;
}
else if (strcmp(cmd_str, "cal_jack_start") == 0) {
fsm_request(FSM_CMD_CALIBRATE_JACK_PREP);
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP");
cmd_executed = true;
}
else if (strcmp(cmd_str, "cal_jack_finish") == 0) {
cJSON *i = cJSON_GetObjectItem(root, "amt");
if (cJSON_IsNumber(i) && i->valuedouble >= 0 && i->valuedouble < 8) {
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_FINISH");
fsm_set_cal_val(i->valuedouble);
fsm_request(FSM_CMD_CALIBRATE_JACK_FINISH);
cmd_executed = true;
} else {
error_msg = "cal_jack_finish requires amt parameter (0-8)";
}
}
else if (strcmp(cmd_str, "cal_drive_start") == 0) {
fsm_request(FSM_CMD_CALIBRATE_DRIVE_PREP);
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
cmd_executed = true;
}
else if (strcmp(cmd_str, "cal_drive_finish") == 0) {
cJSON *i = cJSON_GetObjectItem(root, "amt");
if (cJSON_IsNumber(i) && i->valuedouble >= 0 && i->valuedouble < 8) {
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_FINISH");
fsm_set_cal_val(i->valuedouble);
fsm_request(FSM_CMD_CALIBRATE_DRIVE_FINISH);
cmd_executed = true;
} else {
error_msg = "cal_drive_finish requires amt parameter (0-8)";
}
}
else if (strcmp(cmd_str, "cal_get") == 0) {
ESP_LOGI(TAG, "CAL_GET");
// Build JSON response with calibration values
cJSON *response = cJSON_CreateObject();
cJSON_AddItemToObject(response, "e", cJSON_CreateNumber(fsm_get_cal_e()));
cJSON_AddItemToObject(response, "t", cJSON_CreateNumber(fsm_get_cal_t()));
char *json_str = cJSON_Print(response);
cJSON_Delete(response);
cJSON_Delete(root);
esp_err_t err = httpd_resp_set_type(req, "application/json");
if (err == ESP_OK) {
err = httpd_resp_send(req, json_str, strlen(json_str));
}
free(json_str);
return err;
}
else {
ESP_LOGW(TAG, "Unknown command: %s", cmd_str);
error_msg = "Unknown command";
}
}
// Process batch parameter updates if present
cJSON *parameters = cJSON_GetObjectItem(root, "parameters");
if (cJSON_IsObject(parameters)) {
// Process individual parameter updates (direct key-value pairs)
cJSON *item = NULL;
cJSON_ArrayForEach(item, parameters) {
const char *key = item->string;
ESP_LOGW(TAG, "PARAMETER %s", key);
// Find parameter index by name
param_idx_t param_idx = NUM_PARAMS;
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (strcmp(get_param_name(i), key) == 0) {
param_idx = i;
break;
}
}
if (param_idx == NUM_PARAMS) {
ESP_LOGW(TAG, "UNKNOWN PARAMETER %s", key);
// Not a known parameter, skip silently
continue;
}
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->valueint});
params_updated++;
} else { ESP_LOGW(TAG, "PARAM TYPE MISMATCH FOR %s", key); }
break;
case PARAM_TYPE_f64:
if (cJSON_IsNumber(value_json)) {
set_param_value_t(param_idx,
(param_value_t){.f64=value_json->valueint});
params_updated++;
} else { ESP_LOGW(TAG, "PARAM TYPE MISMATCH FOR %s", key); }
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, "PARAM TYPE MISMATCH FOR %s", key); }
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, "PARAM TYPE MISMATCH FOR %s", key); }
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, "PARAM TYPE MISMATCH FOR %s", key); }
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, "PARAM TYPE MISMATCH FOR %s", key); }
break;
case PARAM_TYPE_str:
if (cJSON_IsString(value_json)) {
set_param_string(param_idx, value_json->valuestring);
params_updated++;
} else { ESP_LOGW(TAG, "PARAM TYPE MISMATCH FOR %s", key); }
break;
default:
break;
}
}
if (params_updated > 0) {
rtc_schedule_next_alarm();
commit_params();
}
}
// Call unified POST handler
cJSON *response = NULL;
esp_err_t err = comms_handle_post(root, &response);
cJSON_Delete(root);
if (response == NULL) {
ESP_LOGE(TAG, "Failed to generate POST response");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to generate response");
}
// Check for special response flags
cJSON *reboot_flag = cJSON_GetObjectItem(response, "reboot");
cJSON *sleep_flag = cJSON_GetObjectItem(response, "sleep");
bool should_reboot = cJSON_IsTrue(reboot_flag);
bool should_sleep = cJSON_IsTrue(sleep_flag);
// Convert response to string
char *json_str = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
if (json_str == NULL) {
ESP_LOGE(TAG, "Failed to serialize JSON");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to serialize response");
}
// Send response
esp_err_t err;
char response[256];
int response_len;
if (reboot_requested) {
// Special handling for reboot
response_len = snprintf(response, sizeof(response),
"{\"status\":\"ok\",\"message\":\"Rebooting...\"}");
if (response_len > 0 && response_len < (int)sizeof(response)) {
err = httpd_resp_set_type(req, "application/json");
if (err == ESP_OK) {
err = httpd_resp_send(req, response, response_len);
}
}
ESP_LOGI(TAG, "Rebooting in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
return ESP_OK; // Never reached
}
if (sleep_requested) {
// Special handling for sleep
response_len = snprintf(response, sizeof(response),
"{\"status\":\"ok\",\"message\":\"Sleeping...\"}");
if (response_len > 0 && response_len < (int)sizeof(response)) {
err = httpd_resp_set_type(req, "application/json");
if (err == ESP_OK) {
err = httpd_resp_send(req, response, response_len);
}
}
ESP_LOGI(TAG, "Sleeping in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000));
rtc_enter_deep_sleep();
return ESP_OK; // Never reached
}
if (error_msg != NULL) {
err = httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, error_msg);
} else {
response_len = snprintf(response, sizeof(response),
"{\"status\":\"ok\",\"params_updated\":%d,\"params_failed\":%d,\"cmd_executed\":%s}",
params_updated, params_failed, cmd_executed ? "true" : "false");
if (response_len < 0 || response_len >= (int)sizeof(response)) {
ESP_LOGE(TAG, "Failed to format response");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Internal error");
}
err = httpd_resp_set_type(req, "application/json");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set response type: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_send(req, response, response_len);
err = httpd_resp_set_type(req, "application/json");
if (err == ESP_OK) {
err = httpd_resp_send(req, json_str, strlen(json_str));
}
free(json_str);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send response: %s", esp_err_to_name(err));
return err;
}
// Handle special actions after response is sent
if (should_reboot) {
ESP_LOGI(TAG, "Rebooting in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
return ESP_OK; // Never reached
}
if (should_sleep) {
ESP_LOGI(TAG, "Sleeping in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000));
rtc_enter_deep_sleep();
return ESP_OK; // Never reached
}
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
ESP_LOGW(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
}
return err;
@@ -1412,6 +994,7 @@ static esp_err_t launchSoftAp(void) {
esp_err_t webserver_init(void) {
ESP_LOGI(TAG, "Initializing webserver...");
// Initialize comms module
esp_err_t err = launchSoftAp();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to launch SoftAP: %s", esp_err_to_name(err));