DNS, web ui nearly done, great log streaming, attempted https (abandoned that though)
This commit is contained in:
@@ -2,11 +2,14 @@
|
||||
# for more information about component CMakeLists.txt files.
|
||||
|
||||
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 # 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 # list the source files of this component
|
||||
INCLUDE_DIRS # optional, add here public include directories
|
||||
PRIV_INCLUDE_DIRS # optional, add here private include directories
|
||||
REQUIRES # optional, list the public requirements (component names)
|
||||
PRIV_REQUIRES # optional, list the private requirements
|
||||
|
||||
REQUIRES driver esp_http_server esp_netif lwip json esp_timer esp_adc app_update esp_wifi nvs_flash mdns # optional, list the public requirements (component names)
|
||||
# esp_https_server
|
||||
PRIV_REQUIRES # optional, list the private requirements
|
||||
#EMBED_TXTFILES servercert.pem prvtkey.pem
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "control_fsm.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esp_timer.h"
|
||||
#include "i2c.h"
|
||||
#include "power_mgmt.h"
|
||||
#include "rtc_wdt.h"
|
||||
#include "driver/gpio.h"
|
||||
@@ -18,6 +19,9 @@
|
||||
|
||||
#define TRANSITION_DELAY_US 1000000
|
||||
|
||||
#define CALIBRATE_JACK_MAX_TIME 3000000
|
||||
#define CALIBRATE_DRIVE_MAX_TIME 6000000
|
||||
|
||||
#define TAG "FSM"
|
||||
|
||||
static QueueHandle_t fsm_cmd_queue = NULL;
|
||||
@@ -37,8 +41,12 @@ bool relay_states[8] = {false};
|
||||
int64_t override_times[8] = {-1};
|
||||
bool enabled = false;
|
||||
|
||||
RTC_DATA_ATTR float remaining_distance = 0.0f;
|
||||
float fsm_get_remaining_distance(void) { return remaining_distance; }
|
||||
void fsm_set_remaining_distance(float x) { remaining_distance = x;}
|
||||
|
||||
|
||||
// Track the starting encoder count for the current move
|
||||
static int32_t move_start_encoder = 0;
|
||||
|
||||
volatile fsm_state_t current_state = STATE_IDLE;
|
||||
volatile int64_t current_time = 0;
|
||||
@@ -89,6 +97,12 @@ void pulseOverride(relay_t relay) {
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
}*/
|
||||
|
||||
int64_t fsm_cal_t, fsm_cal_e;
|
||||
float fsm_cal_val;
|
||||
void fsm_set_cal_val(float v) {fsm_cal_val = v;}
|
||||
int64_t fsm_get_cal_t(){return fsm_cal_t;}
|
||||
int64_t fsm_get_cal_e(){return fsm_cal_e;}
|
||||
|
||||
void fsm_request(fsm_cmd_t cmd)
|
||||
{
|
||||
if (fsm_cmd_queue != NULL)
|
||||
@@ -120,7 +134,7 @@ 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_TIME get_param_value_t(PARAM_JACK_KT).f32 * get_param_value_t(PARAM_JACK_DIST ).f32
|
||||
#define DRIVE_TIME get_param_value_t(PARAM_DRIVE_KT).f32 * get_param_value_t(PARAM_DRIVE_DIST).f32
|
||||
#define DRIVE_DIST get_param_value_t(PARAM_DRIVE_KE).f32 * get_param_value_t(PARAM_DRIVE_DIST).f32
|
||||
|
||||
@@ -141,8 +155,13 @@ void control_task(void *param) {
|
||||
switch (cmd) {
|
||||
case FSM_CMD_START:
|
||||
if (current_state == STATE_IDLE) {
|
||||
current_state = STATE_MOVE_START_DELAY;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
// Check if we have remaining distance before starting
|
||||
if (remaining_distance > 0.0f) {
|
||||
current_state = STATE_MOVE_START_DELAY;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Cannot start move: no remaining distance (%.2f)", remaining_distance);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FSM_CMD_STOP:
|
||||
@@ -158,6 +177,68 @@ void control_task(void *param) {
|
||||
case FSM_CMD_SHUTDOWN:
|
||||
enabled = false;
|
||||
break;
|
||||
|
||||
case FSM_CMD_CALIBRATE_JACK_PREP:
|
||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP");
|
||||
if (current_state == STATE_IDLE) {
|
||||
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) {
|
||||
current_state = STATE_CALIBRATE_JACK_MOVE;
|
||||
set_timer(CALIBRATE_JACK_MAX_TIME);
|
||||
}
|
||||
break;
|
||||
case FSM_CMD_CALIBRATE_JACK_END:
|
||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_END");
|
||||
if (current_state == STATE_CALIBRATE_JACK_MOVE) {
|
||||
fsm_cal_t = current_time - timer_start;
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
break;
|
||||
case FSM_CMD_CALIBRATE_JACK_FINISH:
|
||||
set_param_value_t(PARAM_JACK_KT,
|
||||
(param_value_t){.f32 = fsm_cal_t / fsm_cal_val});
|
||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_FINISH -> %f", get_param_value_t(PARAM_JACK_KT).f32);
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case FSM_CMD_CALIBRATE_DRIVE_PREP:
|
||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
|
||||
if (current_state == STATE_IDLE) {
|
||||
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) {
|
||||
current_state = STATE_CALIBRATE_DRIVE_MOVE;
|
||||
set_timer(CALIBRATE_DRIVE_MAX_TIME);
|
||||
set_sensor_counter(SENSOR_DRIVE, 0);
|
||||
}
|
||||
break;
|
||||
case FSM_CMD_CALIBRATE_DRIVE_END:
|
||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_END");
|
||||
if (current_state == STATE_CALIBRATE_DRIVE_MOVE) {
|
||||
fsm_cal_t = current_time - timer_start;
|
||||
fsm_cal_e = get_sensor_counter(SENSOR_DRIVE);
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
break;
|
||||
case FSM_CMD_CALIBRATE_DRIVE_FINISH:
|
||||
set_param_value_t(PARAM_DRIVE_KT,
|
||||
(param_value_t){.f32 = fsm_cal_t / fsm_cal_val});
|
||||
set_param_value_t(PARAM_DRIVE_KE,
|
||||
(param_value_t){.f32 = fsm_cal_e / fsm_cal_val});
|
||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_FINISH -> %f / %f",
|
||||
get_param_value_t(PARAM_DRIVE_KT).f32,
|
||||
get_param_value_t(PARAM_DRIVE_KE).f32);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,16 +269,58 @@ void control_task(void *param) {
|
||||
if (timer_done()) {
|
||||
current_state = STATE_DRIVE;
|
||||
set_timer(DRIVE_TIME);
|
||||
// Set the encoder counter to track remaining distance in this move
|
||||
set_sensor_counter(SENSOR_DRIVE, -DRIVE_DIST);
|
||||
// Record starting encoder position AFTER setting it
|
||||
move_start_encoder = get_sensor_counter(SENSOR_DRIVE);
|
||||
ESP_LOGI(TAG, "STATE_DRIVE starting: encoder=%ld, remaining_distance=%.2f, DRIVE_DIST=%.2f",
|
||||
(long)move_start_encoder, remaining_distance, DRIVE_DIST);
|
||||
}
|
||||
break;
|
||||
case STATE_DRIVE:
|
||||
if (timer_done() || get_sensor_counter(SENSOR_DRIVE) > 0) {
|
||||
current_state = STATE_DRIVE_END_DELAY;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
}
|
||||
if (efuse_is_tripped(BRIDGE_DRIVE)) {
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
{
|
||||
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;
|
||||
|
||||
ESP_LOGI(TAG, "STATE_DRIVE: current_encoder=%ld, move_start=%ld, ticks=%ld, ke=%.2f, dist_traveled=%.2f, remaining=%.2f",
|
||||
(long)current_encoder, (long)move_start_encoder, (long)ticks_traveled,
|
||||
ke, distance_traveled, remaining_distance);
|
||||
|
||||
// Check if we'll exceed remaining distance with a full move
|
||||
bool will_exceed = distance_traveled >= remaining_distance;
|
||||
|
||||
// Stop if timer expires OR encoder target reached OR we've used up remaining distance
|
||||
if (timer_done() || current_encoder > 0 || will_exceed) {
|
||||
ESP_LOGI(TAG, "Drive stopping: timer_done=%d, encoder>0=%d, will_exceed=%d",
|
||||
timer_done(), current_encoder > 0, will_exceed);
|
||||
|
||||
// Update remaining distance based on actual travel
|
||||
float old_remaining = remaining_distance;
|
||||
if (will_exceed) {
|
||||
ESP_LOGI(TAG, "Move stopped early - reached remaining distance limit (%.2f)", remaining_distance);
|
||||
remaining_distance = 0.0f;
|
||||
} else {
|
||||
remaining_distance -= distance_traveled;
|
||||
if (remaining_distance < 0.0f) remaining_distance = 0.0f;
|
||||
}
|
||||
ESP_LOGI(TAG, "Drive complete: traveled %.2f, old_remaining %.2f, new_remaining %.2f",
|
||||
distance_traveled, old_remaining, remaining_distance);
|
||||
|
||||
current_state = STATE_DRIVE_END_DELAY;
|
||||
set_timer(TRANSITION_DELAY_US);
|
||||
}
|
||||
|
||||
if (efuse_is_tripped(BRIDGE_DRIVE)) {
|
||||
float old_remaining = remaining_distance;
|
||||
// Update remaining distance even on fault
|
||||
remaining_distance -= distance_traveled;
|
||||
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);
|
||||
current_state = STATE_UNDO_JACK_START;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case STATE_DRIVE_END_DELAY:
|
||||
@@ -205,8 +328,9 @@ void control_task(void *param) {
|
||||
current_state = STATE_JACK_DOWN;
|
||||
set_timer(JACK_TIME);
|
||||
}
|
||||
break;
|
||||
case STATE_JACK_DOWN:
|
||||
if (timer_done() || get_sensor(SENSOR_JACK)) {
|
||||
if (timer_done()) { // || get_sensor(SENSOR_JACK)) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
|
||||
@@ -225,7 +349,7 @@ void control_task(void *param) {
|
||||
}
|
||||
break;
|
||||
case STATE_UNDO_JACK:
|
||||
if (timer_done() || get_sensor(SENSOR_JACK)) {
|
||||
if (timer_done()){ // || get_sensor(SENSOR_JACK)) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
|
||||
@@ -234,6 +358,32 @@ void control_task(void *param) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case STATE_CALIBRATE_JACK_DELAY:
|
||||
// no way out of this except a command
|
||||
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;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case STATE_CALIBRATE_DRIVE_DELAY:
|
||||
// no way out of this except a command
|
||||
break;
|
||||
case STATE_CALIBRATE_DRIVE_MOVE:
|
||||
if (timer_done()) {
|
||||
ESP_LOGI(TAG, "STATE_CALIBRATE_DRIVE_END");
|
||||
current_state = STATE_IDLE;
|
||||
fsm_cal_t = current_time - timer_start;
|
||||
fsm_cal_e = get_sensor_counter(SENSOR_DRIVE);
|
||||
}
|
||||
break;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -261,6 +411,7 @@ void control_task(void *param) {
|
||||
|
||||
}
|
||||
break;
|
||||
case STATE_CALIBRATE_JACK_MOVE:
|
||||
case STATE_JACK_UP:
|
||||
// jack up and fluff
|
||||
setRelay(RELAY_A1, false);
|
||||
@@ -272,6 +423,7 @@ void control_task(void *param) {
|
||||
setRelay(RELAY_A3, true);
|
||||
reset_shutdown_timer();
|
||||
break;
|
||||
case STATE_CALIBRATE_DRIVE_MOVE:
|
||||
case STATE_DRIVE:
|
||||
// drive and fluff
|
||||
setRelay(RELAY_A1, true);
|
||||
@@ -308,6 +460,7 @@ void control_task(void *param) {
|
||||
setRelay(RELAY_A3, true);
|
||||
reset_shutdown_timer();
|
||||
break;
|
||||
case STATE_CALIBRATE_JACK_DELAY:
|
||||
default:
|
||||
// invalid state; turn all relays off
|
||||
setRelay(RELAY_A1, false);
|
||||
|
||||
@@ -7,7 +7,22 @@
|
||||
#include "freertos/queue.h"
|
||||
|
||||
|
||||
typedef enum { FSM_CMD_START, FSM_CMD_STOP, FSM_CMD_UNDO, FSM_CMD_SHUTDOWN} fsm_cmd_t;
|
||||
typedef enum {
|
||||
FSM_CMD_START,
|
||||
FSM_CMD_STOP,
|
||||
FSM_CMD_UNDO,
|
||||
FSM_CMD_SHUTDOWN,
|
||||
|
||||
FSM_CMD_CALIBRATE_JACK_PREP,
|
||||
FSM_CMD_CALIBRATE_JACK_START,
|
||||
FSM_CMD_CALIBRATE_JACK_END,
|
||||
FSM_CMD_CALIBRATE_JACK_FINISH,
|
||||
|
||||
FSM_CMD_CALIBRATE_DRIVE_PREP,
|
||||
FSM_CMD_CALIBRATE_DRIVE_START,
|
||||
FSM_CMD_CALIBRATE_DRIVE_END,
|
||||
FSM_CMD_CALIBRATE_DRIVE_FINISH
|
||||
} fsm_cmd_t;
|
||||
|
||||
typedef enum {
|
||||
STATE_IDLE = 0,
|
||||
@@ -19,6 +34,12 @@ typedef enum {
|
||||
STATE_JACK_DOWN = 6,
|
||||
STATE_UNDO_JACK = 7,
|
||||
STATE_UNDO_JACK_START = 8,
|
||||
|
||||
STATE_CALIBRATE_JACK_DELAY,
|
||||
STATE_CALIBRATE_JACK_MOVE,
|
||||
|
||||
STATE_CALIBRATE_DRIVE_DELAY,
|
||||
STATE_CALIBRATE_DRIVE_MOVE
|
||||
} fsm_state_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -46,8 +67,15 @@ void pulseOverride(relay_t relay/*, int64_t pulse*/);
|
||||
esp_err_t fsm_init();
|
||||
esp_err_t fsm_stop();
|
||||
|
||||
void fsm_set_cal_val(float v);
|
||||
int64_t fsm_get_cal_t();
|
||||
int64_t fsm_get_cal_e();
|
||||
void fsm_request(fsm_cmd_t cmd);
|
||||
|
||||
|
||||
float fsm_get_remaining_distance(void);
|
||||
void fsm_set_remaining_distance(float x);
|
||||
|
||||
//void fsm_begin_auto_move();
|
||||
|
||||
int8_t fsm_get_current_progress(int8_t remainder);
|
||||
|
||||
37
main/i2c.c
37
main/i2c.c
@@ -24,9 +24,11 @@
|
||||
#define TCA_REG_CONFIG1 0x07
|
||||
|
||||
// Debounce & Repeat Settings
|
||||
#define DEBOUNCE_MS 50
|
||||
#define REPEAT_MS 200
|
||||
#define REPEAT_START_MS 700
|
||||
#define DEBOUNCE_US 50000
|
||||
#define REPEAT_US 200000
|
||||
#define REPEAT_START_US 700000
|
||||
|
||||
#define MAX_REPEATS 100
|
||||
|
||||
// Static Variables
|
||||
static bool i2c_initted = false;
|
||||
@@ -85,8 +87,8 @@ esp_err_t i2c_stop() {
|
||||
#define N_BTNS 2
|
||||
static bool debounced_state[N_BTNS] = {false};
|
||||
static bool last_known_state[N_BTNS] = {false};
|
||||
static uint64_t last_stable_time[N_BTNS] = {0};
|
||||
static uint64_t last_change_time[N_BTNS] = {0};
|
||||
static int64_t last_stable_time[N_BTNS] = {0};
|
||||
static int64_t last_change_time[N_BTNS] = {0};
|
||||
static uint8_t claimed_repeats[N_BTNS] = {0};
|
||||
esp_err_t i2c_poll_buttons() {
|
||||
for (uint8_t btn = 0; btn < N_BTNS; ++btn) {
|
||||
@@ -98,13 +100,13 @@ esp_err_t i2c_poll_buttons() {
|
||||
uint8_t raw_buttons = (uint8_t)(port_val & 0x0F);
|
||||
uint8_t raw_states = ~raw_buttons & 0x0F;
|
||||
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
int64_t now = esp_timer_get_time() / 1000;
|
||||
|
||||
for (uint8_t btn = 0; btn < N_BTNS; ++btn) {
|
||||
bool raw_pressed = (raw_states & (1 << btn)) != 0;
|
||||
|
||||
if (raw_pressed != debounced_state[btn]) {
|
||||
if (now - last_stable_time[btn] >= DEBOUNCE_MS) {
|
||||
if (now - last_stable_time[btn] >= DEBOUNCE_US) {
|
||||
debounced_state[btn] = raw_pressed;
|
||||
last_stable_time[btn] = now;
|
||||
last_change_time[btn] = now;
|
||||
@@ -131,9 +133,9 @@ bool i2c_get_button_state(uint8_t button) {
|
||||
|
||||
bool i2c_get_button_repeat(uint8_t btn) {
|
||||
if (btn >= N_BTNS || !debounced_state[btn]) return false;
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
|
||||
if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) {
|
||||
int64_t now = esp_timer_get_time();
|
||||
if (now + DEBOUNCE_US < last_change_time[btn]) return false;
|
||||
if ((now - last_change_time[btn]) > (REPEAT_START_US + REPEAT_US * claimed_repeats[btn])) {
|
||||
claimed_repeats[btn]++;
|
||||
return true;
|
||||
}
|
||||
@@ -145,18 +147,15 @@ int8_t i2c_get_button_repeats(uint8_t btn) {
|
||||
return 0;
|
||||
|
||||
if (btn >= N_BTNS || !debounced_state[btn]) return false;
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
if (now + DEBOUNCE_MS < last_change_time[btn]) return false;
|
||||
if ((now - last_change_time[btn]) > (REPEAT_START_MS + REPEAT_MS * claimed_repeats[btn])) {
|
||||
int64_t now = esp_timer_get_time();
|
||||
if (now + DEBOUNCE_US < last_change_time[btn]) return false;
|
||||
if ((now - last_change_time[btn]) > (REPEAT_START_US + REPEAT_US * 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);
|
||||
if (claimed_repeats[btn] > MAX_REPEATS)
|
||||
claimed_repeats[btn] = MAX_REPEATS;
|
||||
return claimed_repeats[btn]+1;
|
||||
}
|
||||
if (debounced_state[btn] && !last_known_state[btn]) {
|
||||
|
||||
ESP_LOGI("BTN", "FST %d", 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -167,6 +166,6 @@ int64_t i2c_get_button_ms(uint8_t btn) {
|
||||
if (!i2c_get_button_state(btn))
|
||||
return 0;
|
||||
|
||||
uint64_t now = esp_timer_get_time() / 1000;
|
||||
int64_t now = esp_timer_get_time();
|
||||
return now - last_change_time[btn];
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
## IDF Component Manager Manifest File
|
||||
dependencies:
|
||||
espressif/mdns: "*"
|
||||
joltwallet/littlefs: "==1.20.3"
|
||||
esp-idf-lib/tca95x5: "*"
|
||||
## Required IDF version
|
||||
|
||||
@@ -1,27 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
tan: #ba965b
|
||||
light tan: #efede9
|
||||
white: #ffffff
|
||||
green: #2a493d
|
||||
black: #2f2f2f
|
||||
-->
|
||||
<head>
|
||||
<title>Control Panel</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* { background-color: #111; color: #eee; font-family: sans-serif; }
|
||||
|
||||
#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;
|
||||
}
|
||||
input, button { width: 100%; }
|
||||
input, button { border: 1px solid #666; background-color: #333; font-family: monospace; text-align: right; box-sizing: border-box; }
|
||||
input, button { border: 1px solid #ba965b; border-radius: 5px; background-color: #efede9; text-align: right; box-sizing: border-box; }
|
||||
input[type="text"], input[type="number"] { font-family: monospace; }
|
||||
|
||||
button { text-align: center; }
|
||||
.changed { background-color: #3d3 !important; color: #111 !important; }
|
||||
#commit_btn { width: 100%; background-color: #3d3; color: #111; margin-top: 10px; padding: 10px; cursor: pointer; border: none; font-weight: bold; }
|
||||
#commit_btn[disabled] { background-color: #444; color: #888; cursor: not-allowed; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
td { padding: 8px; border-bottom: 1px solid #222; }
|
||||
tr:hover { background-color: #1a1a1a; }
|
||||
summary { font-weight: bold; text-align: left; color: #ccc; background-color: #723; padding: 0.3rem;}
|
||||
.changed, #commit_btn { background-color: #2a493d !important; color: #ffffff !important; }
|
||||
#commit_btn { width: 100%; margin-top: 10px; padding: 10px; cursor: pointer; border: none; font-weight: bold; }
|
||||
#commit_btn[disabled] { background-color: #444 !important; color: #888; cursor: not-allowed; }
|
||||
table { width: 100%; border-collapse: collapse; text-align: left; }
|
||||
td { padding: 8px; border-bottom: 1px solid #efede9; }
|
||||
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;}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 350px) {
|
||||
#content { max-width: 100%; padding: 0 5px; }
|
||||
table tr td { display: block; width: 100%; box-sizing: border-box; } /* Stack table cells vertically on mobile for better usability */
|
||||
table tr { display: block; margin-bottom: 10px; }
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button id="commit_btn" onclick="commit_params()" disabled>Save Changes</button>
|
||||
|
||||
<div id="wrapper">
|
||||
<div id="content">
|
||||
|
||||
<h1>ClusterCommand</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>
|
||||
|
||||
|
||||
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="3"><input readonly="" id="msg"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3"><input type="datetime-local" id="in_time" step="1" onchange="markChanged(this)"/>
|
||||
<button id="now_btn" onclick="setTimeToNow()">Sync Time</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Schedule Start</td>
|
||||
<td><input type="time" id="move_start" onchange="changeSchedule(this)"/></td>
|
||||
@@ -35,39 +89,26 @@
|
||||
<td><input type="number" min="0" id="num_moves" onchange="changeSchedule(this)"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Move Distance</td>
|
||||
<td>Remain. Distance (ft)</td>
|
||||
<td><input type="number" id="remaining_dist" onchange="markChanged(this)"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Move Distance (ft)</td>
|
||||
<td><input type="number" min="0" id="drive_dist" onchange="changeSchedule(this)"/></td>
|
||||
<td>ft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jack Height</td>
|
||||
<td>Jack Height (in)</td>
|
||||
<td><input type="number" min="0" id="jack_dist" onchange="changeSchedule(this)"/></td>
|
||||
<td>in</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Time</td>
|
||||
<td><input type="datetime-local" id="in_time" step="1" onchange="markChanged(this)"/></td>
|
||||
<td><button id="now_btn" onclick="setTimeToNow()">< NOW</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Battery</td>
|
||||
<td>Battery (V)</td>
|
||||
<td><input readonly="" id="voltage"/></td>
|
||||
<td>V</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><button onclick="cmd('start')">START MOVE</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><button onclick="cmd('undo')">UNDO MOVE</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><button onclick="cmd('stop')" style="background-color:#800">STOP MOVE</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<button id="commit_btn" onclick="commitParams()" disabled>Save Changes</button>
|
||||
|
||||
<br/>
|
||||
|
||||
<br/>
|
||||
<details>
|
||||
@@ -77,17 +118,13 @@
|
||||
<tr>
|
||||
<td>Program RF Remote</td>
|
||||
<td>
|
||||
<button onclick="programRF(0)" style="width:40%">Fwd</button>
|
||||
<button onclick="programRF(1)" style="width:40%">Rev</button>
|
||||
<button onclick="programRF(2)" style="width:40%">Up</button>
|
||||
<button onclick="programRF(3)" style="width:40%">Down</button>
|
||||
<button onclick="programRF(-1)">Cancel Learning</button>
|
||||
<button onclick="programRFSequence()">Program All Buttons</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Calibration</td>
|
||||
<td><button id="cal_jack_btn">Jack Calibration</button>
|
||||
<button id="cal_drive_btn">Drive Calibration</button></td>
|
||||
<td><button onclick="calibrate('jack')">Jack Calibration</button>
|
||||
<button onclick="calibrate('drive')">Drive Calibration</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Firmware</td>
|
||||
@@ -101,304 +138,485 @@
|
||||
</table>
|
||||
|
||||
<table id="table"></table>
|
||||
|
||||
|
||||
<td><button class="cmd" onclick="sendCommand('reboot')" style="background-color:#723; color: #fff">REBOOT</button></td>
|
||||
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
let param_values = [];
|
||||
let param_names = [];
|
||||
let param_units = [];
|
||||
let paramValues = [];
|
||||
let paramNames = [];
|
||||
let paramUnits = [];
|
||||
|
||||
function cmd(x) {
|
||||
if (x === 'start') {
|
||||
if(!confirm("Will begin moving - please confirm."))
|
||||
return;
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "./cmd", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(JSON.stringify({"cmd":x}));
|
||||
xhr.onload = function() {
|
||||
console.log(xhr);
|
||||
const ge = (id) => document.getElementById(id);
|
||||
|
||||
async function sendCommand(cmdName) {
|
||||
if (cmdName === 'start') {
|
||||
if (!confirm("Will begin moving - please confirm."))
|
||||
return;
|
||||
}
|
||||
if (cmdName === 'reboot') {
|
||||
if (!confirm("Device will reboot - clearing clock and distance. Are you sure?"))
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch('./cmd', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: cmdName})
|
||||
});
|
||||
}
|
||||
|
||||
function ge(x) { return document.getElementById(x); }
|
||||
async function calibrate(axis) {
|
||||
await fetch('./cmd', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: `cal_${axis}_start`})
|
||||
});
|
||||
|
||||
const amt = prompt(`Press button on mover. Press button again to stop it. Then, type in actual travelled distance here in inches:`);
|
||||
|
||||
if (!isNaN(parseFloat(amt))) {
|
||||
const response = await fetch('./cmd', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: 'cal_get'})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (axis === 'drive') {
|
||||
markChanged(ge('in_6')).value = data.e / amt;
|
||||
markChanged(ge('in_7')).value = data.t / amt * 1.2; // ok to have more
|
||||
} else {
|
||||
markChanged(ge('in_8')).value = data.t / amt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight changed inputs and enable the save button
|
||||
function markChanged(el) {
|
||||
el.classList.add("changed");
|
||||
ge('commit_btn').disabled = false;
|
||||
return el;
|
||||
}
|
||||
|
||||
function toFixed(input, n) {
|
||||
const num = parseFloat(input);
|
||||
if (isNaN(num)) {
|
||||
return null; // or throw error, or return 0
|
||||
}
|
||||
return Number(num.toFixed(n));
|
||||
}
|
||||
const num = parseFloat(input);
|
||||
if (isNaN(num)) {
|
||||
return null;
|
||||
}
|
||||
return Number(num.toFixed(n));
|
||||
}
|
||||
|
||||
// --- 1. GET DATA ---
|
||||
function fetchStatus() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "./status", true);
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
console.log(xhr.responseText);
|
||||
const data = JSON.parse(xhr.responseText);
|
||||
|
||||
// Update time field if available
|
||||
if(data.time) {
|
||||
const date = new Date(data.time * 1000).toISOString().slice(0, 19);
|
||||
ge('in_time').value = date;
|
||||
}
|
||||
|
||||
ge('voltage').value = toFixed(data.battery, 2);
|
||||
|
||||
|
||||
// Store values (default to empty array if missing)
|
||||
param_values = data.values || [];
|
||||
param_names = data.names || [];
|
||||
param_units = data.units || [];
|
||||
|
||||
if (!data.rtc_set) {
|
||||
ge('in_time').classList.add('error');
|
||||
if (confirm("Clock not set. Sync with this device's clock?")){
|
||||
setTimeToNow();
|
||||
commit_time();
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Error parsing JSON", e);
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
const response = await fetch('./status');
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
// Update time field if available
|
||||
if (data.time) {
|
||||
const date = new Date(data.time * 1000).toISOString().slice(0, 19);
|
||||
ge('in_time').value = date;
|
||||
}
|
||||
|
||||
ge('voltage').value = toFixed(data.battery, 2);
|
||||
ge('remaining_dist').value = toFixed(data.remaining_dist, 2);
|
||||
|
||||
ge('msg').value = data.msg;
|
||||
|
||||
// Store values (default to empty array if missing)
|
||||
paramValues = data.values || [];
|
||||
paramNames = data.names || [];
|
||||
paramUnits = data.units || [];
|
||||
|
||||
ge('commit_btn').disabled = true;
|
||||
|
||||
if (!data.rtc_set) {
|
||||
ge('in_time').classList.add('error');
|
||||
if (confirm("Clock not set. Sync with this device's clock?")) {
|
||||
setTimeToNow();
|
||||
commitTime();
|
||||
}
|
||||
}
|
||||
// Always render table even if request fails or data is empty
|
||||
renderTable();
|
||||
};
|
||||
xhr.onerror = function(e) {
|
||||
console.error("Network error", e);
|
||||
renderTable();
|
||||
};
|
||||
xhr.send();
|
||||
} catch(e) {
|
||||
console.error("Error parsing JSON", e);
|
||||
}
|
||||
// Always render table even if request fails or data is empty
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function s_to_hhmm(s) {
|
||||
const hh = String(Math.floor(s / 3600)).padStart(2, '0');
|
||||
const mm = String(Math.floor((s % 3600) / 60)).padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
function secondsToHHMM(seconds) {
|
||||
const hh = String(Math.floor(seconds / 3600)).padStart(2, '0');
|
||||
const mm = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const table = ge("table");
|
||||
// Clear existing parameter rows (rows between index 0 and the last row)
|
||||
// Clear existing parameter rows
|
||||
while(table.rows.length > 0) { table.deleteRow(0); }
|
||||
|
||||
// Loop through the NAMES array to ensure every input is shown
|
||||
param_names.forEach((name, i) => {
|
||||
paramNames.forEach((name, i) => {
|
||||
let row = table.insertRow(table.rows.length);
|
||||
|
||||
let pname =(param_names[i] !== undefined && param_names[i] !==null)
|
||||
? param_names[i]
|
||||
: "null";
|
||||
let pname = (paramNames[i] !== undefined && paramNames[i] !== null)
|
||||
? paramNames[i]
|
||||
: "null";
|
||||
|
||||
// If the server didn't send a value for this index, show "null"
|
||||
let pval = (param_values[i] !== undefined && param_values[i] !== null)
|
||||
? param_values[i]
|
||||
let pval = (paramValues[i] !== undefined && paramValues[i] !== null)
|
||||
? paramValues[i]
|
||||
: "null";
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${i}</td>
|
||||
<td>${pname}</td>
|
||||
<td><input type="number" id="in_${i}" value="${pval}" oninput="markChanged(this)"></td>
|
||||
<td>${param_units[i] || ""}</td>
|
||||
<td><input type="${typeof pval === 'number' ? 'number':'text'}" id="in_${i}" value="${pval}" oninput="markChanged(this)"></td>
|
||||
<td>${paramUnits[i] || ""}</td>
|
||||
`;
|
||||
});
|
||||
|
||||
ge('num_moves').value = param_values[1];
|
||||
ge('move_start').value = s_to_hhmm(param_values[2]);
|
||||
ge('move_end').value = s_to_hhmm(param_values[3]);
|
||||
for (const input of document.getElementsByTagName("input")) {
|
||||
input.addEventListener("click", (e) => {
|
||||
e.target.select();
|
||||
});
|
||||
}
|
||||
|
||||
ge('drive_dist').value = param_values[4];
|
||||
ge('jack_dist').value = param_values[5];
|
||||
ge('num_moves').value = paramValues[1];
|
||||
ge('move_start').value = secondsToHHMM(paramValues[2]);
|
||||
ge('move_end').value = secondsToHHMM(paramValues[3]);
|
||||
|
||||
ge('drive_dist').value = paramValues[4];
|
||||
ge('jack_dist').value = paramValues[5];
|
||||
}
|
||||
|
||||
function changeSchedule(e) {
|
||||
markChanged(e);
|
||||
if (e.id == "num_moves") {
|
||||
ge('in_1').value = e.value;
|
||||
markChanged(ge('in_1'));
|
||||
}
|
||||
if (e.id == "move_start") {
|
||||
const [hours, minutes] = e.value.split(':').map(Number);
|
||||
ge('in_2').value = hours*3600 + minutes*60;
|
||||
markChanged(ge('in_2'));
|
||||
}
|
||||
if (e.id == "move_end") {
|
||||
const [hours, minutes] = e.value.split(':').map(Number);
|
||||
ge('in_3').value = hours*3600 + minutes*60;
|
||||
markChanged(ge('in_3'));
|
||||
}
|
||||
if (e.id == "drive_dist") {
|
||||
ge('in_4').value = e.value;
|
||||
markChanged(ge('in_4'));
|
||||
}
|
||||
if (e.id == "jack_dist") {
|
||||
ge('in_5').value = e.value;
|
||||
markChanged(ge('in_5'));
|
||||
}
|
||||
markChanged(e);
|
||||
if (e.id === "num_moves") {
|
||||
ge('in_1').value = e.value;
|
||||
markChanged(ge('in_1'));
|
||||
}
|
||||
if (e.id === "move_start") {
|
||||
const [hours, minutes] = e.value.split(':').map(Number);
|
||||
ge('in_2').value = hours*3600 + minutes*60;
|
||||
markChanged(ge('in_2'));
|
||||
}
|
||||
if (e.id === "move_end") {
|
||||
const [hours, minutes] = e.value.split(':').map(Number);
|
||||
ge('in_3').value = hours*3600 + minutes*60;
|
||||
markChanged(ge('in_3'));
|
||||
}
|
||||
if (e.id === "drive_dist") {
|
||||
ge('in_4').value = e.value;
|
||||
markChanged(ge('in_4'));
|
||||
}
|
||||
if (e.id === "jack_dist") {
|
||||
ge('in_5').value = e.value;
|
||||
markChanged(ge('in_5'));
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeToNow() {
|
||||
e = ge('in_time');
|
||||
markChanged(e);
|
||||
const e = ge('in_time');
|
||||
markChanged(e);
|
||||
e.value = new Date().toLocaleString('sv-SE');
|
||||
}
|
||||
|
||||
function commit_time() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
async function commitTime() {
|
||||
// Time handling
|
||||
const datetimeStr = ge("in_time").value; // e.g., "2024-03-15T14:30:00"
|
||||
|
||||
// Parse the components
|
||||
const [datePart, timePart] = datetimeStr.split('T');
|
||||
const [year, month, day] = datePart.split('-').map(Number);
|
||||
const [hour, minute, second = 0] = timePart.split(':').map(Number);
|
||||
|
||||
// Create UTC timestamp (month is 0-indexed in Date.UTC)
|
||||
const epoch = Math.floor(Date.UTC(year, month - 1, day, hour, minute, second) / 1000);
|
||||
const datetimeStr = ge("in_time").value; // e.g., "2024-03-15T14:30:00"
|
||||
|
||||
// Parse the components
|
||||
const [datePart, timePart] = datetimeStr.split('T');
|
||||
const [year, month, day] = datePart.split('-').map(Number);
|
||||
const [hour, minute, second = 0] = timePart.split(':').map(Number);
|
||||
|
||||
// Create UTC timestamp (month is 0-indexed in Date.UTC)
|
||||
const epoch = Math.floor(Date.UTC(year, month - 1, day, hour, minute, second) / 1000);
|
||||
|
||||
xhr.open("POST", "./st", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
ge("in_time").classList.remove("changed");
|
||||
}
|
||||
|
||||
if (document.querySelectorAll('input.changed').length === 0)
|
||||
ge('commit_btn').disabled = true;
|
||||
else
|
||||
ge('commit_btn').disabled = false;
|
||||
}
|
||||
xhr.send(epoch.toString());
|
||||
const response = await fetch('./st', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: epoch.toString()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
ge("in_time").classList.remove("changed");
|
||||
}
|
||||
|
||||
if (document.querySelectorAll('input.changed').length === 0) {
|
||||
ge('commit_btn').disabled = true;
|
||||
} else {
|
||||
ge('commit_btn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. POST DATA ---
|
||||
function commit_params() {
|
||||
|
||||
async function commitParams() {
|
||||
ge('commit_btn').disabled = true;
|
||||
const changedInputs = document.querySelectorAll('input.changed');
|
||||
|
||||
sp = {}
|
||||
const sp = {};
|
||||
|
||||
changedInputs.forEach(input => {
|
||||
|
||||
for (const input of changedInputs) {
|
||||
if (input.id === "in_time") {
|
||||
commit_time();
|
||||
// we only really use the parameter table for inputs
|
||||
// the pretty inputs should just modify the param table
|
||||
} else if (input.id.startsWith('in_')) {
|
||||
// Parameter handling
|
||||
const id = input.id.split('_')[1];
|
||||
// If the user typed "null", we send null; otherwise parse as float
|
||||
const val = (input.value.toLowerCase() === "null") ? null : parseFloat(input.value);
|
||||
|
||||
sp[id] = val;
|
||||
await commitTime();
|
||||
}
|
||||
|
||||
else if (input.id === "remaining_dist") {
|
||||
const x = parseFloat(ge('remaining_dist').value);
|
||||
|
||||
const response = await fetch('./cmd', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: "remaining_dist", amt: x})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
ge('remaining_dist').classList.remove("changed");
|
||||
}
|
||||
}
|
||||
|
||||
else if (input.id.startsWith('in_')) {
|
||||
// Parameter handling
|
||||
const id = input.id.split('_')[1];
|
||||
// If the user typed "null", we send null; otherwise parse as float
|
||||
sp[id] = input.value;
|
||||
if (input.type === "number") {
|
||||
sp[id] = (input.value.toLowerCase() === "null") ? null : parseFloat(input.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(sp).length !== 0) {
|
||||
const xhr2 = new XMLHttpRequest();
|
||||
xhr2.open("POST", "./sp", true);
|
||||
xhr2.setRequestHeader("Content-Type", "application/json");
|
||||
xhr2.onload = function() {
|
||||
if (xhr2.status === 200) {
|
||||
changedInputs.forEach(input => {
|
||||
if (input.id !== "in_time")
|
||||
input.classList.remove("changed");
|
||||
});
|
||||
} else {
|
||||
ge('commit_btn').disabled = false;
|
||||
}
|
||||
};
|
||||
xhr2.send(JSON.stringify(sp));
|
||||
}
|
||||
|
||||
fetchStatus();
|
||||
if (Object.keys(sp).length !== 0) {
|
||||
const response = await fetch('./sp', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(sp)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
changedInputs.forEach(input => {
|
||||
if (input.id !== "in_time") {
|
||||
input.classList.remove("changed");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ge('commit_btn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchStatus();
|
||||
}
|
||||
|
||||
function uploadFirmware() {
|
||||
const fileInput = ge('firmware_file');
|
||||
if (!fileInput.files.length) {
|
||||
alert('No file selected');
|
||||
return;
|
||||
}
|
||||
const file = fileInput.files[0];
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "./ota", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
alert('Upload successful. Device may reboot.');
|
||||
} else {
|
||||
alert('Upload failed: ' + xhr.status);
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
alert('Network error during upload');
|
||||
};
|
||||
xhr.send(file);
|
||||
}
|
||||
|
||||
function programRF(i) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "./prf", true);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(i.toString());
|
||||
if (xhr.status === 200) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadLogFile() {
|
||||
try {
|
||||
const response = await fetch('./log');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const blob = await response.blob();
|
||||
|
||||
// Get current date and time
|
||||
const now = new Date();
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const monthNames = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
|
||||
const month = monthNames[now.getMonth()];
|
||||
const year = now.getFullYear();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
|
||||
const formattedDate = `${day}${month}${year}-${hours}${minutes}`;
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `storage-${formattedDate}.bin`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
}
|
||||
}
|
||||
async function uploadFirmware() {
|
||||
const fileInput = ge('firmware_file');
|
||||
if (!fileInput.files.length) {
|
||||
alert('No file selected');
|
||||
return;
|
||||
}
|
||||
const file = fileInput.files[0];
|
||||
|
||||
try {
|
||||
const response = await fetch('./ota', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/octet-stream'},
|
||||
body: file
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Upload successful. Device may reboot.');
|
||||
} else {
|
||||
alert('Upload failed: ' + response.status);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Network error during upload');
|
||||
}
|
||||
}
|
||||
|
||||
function programRF(i) {
|
||||
fetch('./cmd', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: 'rfp', channel: i})
|
||||
});
|
||||
}
|
||||
|
||||
async function programRFSequence() {
|
||||
const buttonNames = ["Forward", "Reverse", "Up", "Down"];
|
||||
const learnedCodes = [null, null, null, null];
|
||||
|
||||
if (!confirm("This will program all 4 RF remote buttons in sequence.\n\nPress OK to begin, then follow the prompts.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable RF controls during programming
|
||||
await fetch('./cmd', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: 'rf_disable'})
|
||||
});
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// Start learning for this button FIRST
|
||||
programRF(i);
|
||||
|
||||
// Give a moment for the learn flag to be set
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Ask user to press button or skip
|
||||
const shouldSkip = !confirm(`Button ${i+1}/4: ${buttonNames[i]}\n\nPress the ${buttonNames[i]} button on your remote now, then press OK.\n\nPress Cancel to skip this button.`);
|
||||
|
||||
if (shouldSkip) {
|
||||
// Cancel learning and set this button to -1 (disabled)
|
||||
programRF(-1);
|
||||
learnedCodes[i] = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wait for code to be learned (poll for a bit)
|
||||
let learned = false;
|
||||
const startCode = learnedCodes[i]; // Should be null at this point
|
||||
|
||||
for (let attempt = 0; attempt < 50; attempt++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Check if code was learned by polling RF status via /cmd
|
||||
try {
|
||||
const response = await fetch('./cmd', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: 'rf_status'})
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Check if this specific index changed from what we started with
|
||||
if (data.codes[i] !== -1 && data.codes[i] !== null && data.codes[i] !== startCode) {
|
||||
learnedCodes[i] = data.codes[i];
|
||||
learned = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error checking RF status:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!learned) {
|
||||
if (confirm(`Timeout waiting for ${buttonNames[i]} button.\n\nRetry this button?`)) {
|
||||
i--; // Retry this iteration
|
||||
continue;
|
||||
} else {
|
||||
// Skip this button
|
||||
learnedCodes[i] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update input fields for parameters 9-12 (PARAM_KEYCODE_0 through PARAM_KEYCODE_3)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const input = ge(`in_${9 + i}`);
|
||||
if (input) {
|
||||
input.value = learnedCodes[i];
|
||||
markChanged(input);
|
||||
}
|
||||
}
|
||||
|
||||
// Commit just the RF keycodes (params 9-12)
|
||||
const sp = {};
|
||||
for (let i = 0; i < 4; i++) {
|
||||
sp[9 + i] = learnedCodes[i];
|
||||
}
|
||||
|
||||
const response = await fetch('./sp', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(sp)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Unhighlight the keycode inputs
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const input = ge(`in_${9 + i}`);
|
||||
if (input) {
|
||||
input.classList.remove("changed");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if commit button should stay enabled
|
||||
if (document.querySelectorAll('input.changed').length === 0) {
|
||||
ge('commit_btn').disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable RF controls after programming
|
||||
await fetch('./cmd', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({cmd: 'rf_enable'})
|
||||
});
|
||||
|
||||
// Show summary
|
||||
let summary = "RF Remote Programming Complete!\n\n";
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (learnedCodes[i] === -1) {
|
||||
summary += `${buttonNames[i]}: Not programmed\n`;
|
||||
} else {
|
||||
summary += `${buttonNames[i]}: ${learnedCodes[i]}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
alert(summary);
|
||||
await fetchStatus();
|
||||
}
|
||||
|
||||
async function downloadLogFile() {
|
||||
try {
|
||||
const response = await fetch('./log');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const blob = await response.blob();
|
||||
|
||||
// Get current date and time
|
||||
const now = new Date();
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const monthNames = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
|
||||
const month = monthNames[now.getMonth()];
|
||||
const year = now.getFullYear();
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
|
||||
const formattedDate = `${day}${month}${year}-${hours}${minutes}`;
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `storage-${formattedDate}.bin`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial Load
|
||||
window.onload = fetchStatus;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
38
main/main.c
38
main/main.c
@@ -15,7 +15,6 @@
|
||||
|
||||
#define TAG "MAIN"
|
||||
|
||||
|
||||
int64_t last_log_time = 0;
|
||||
esp_err_t send_log() {
|
||||
|
||||
@@ -39,8 +38,8 @@ esp_err_t send_log() {
|
||||
int32_t be_counter = htobe32(get_sensor_counter(SENSOR_DRIVE));
|
||||
memcpy(&entry[25], &be_counter, 4);
|
||||
|
||||
entry[29] = get_sensor(SENSOR_DRIVE);
|
||||
entry[30] = get_sensor(SENSOR_JACK);
|
||||
entry[29] = get_sensor(SENSOR_SAFETY);
|
||||
entry[30] = get_sensor(SENSOR_DRIVE);
|
||||
entry[31] = fsm_get_state();
|
||||
|
||||
last_log_time = esp_timer_get_time();
|
||||
@@ -75,7 +74,7 @@ void driveLEDs(led_state_t state) {
|
||||
i2c_set_led1(patterns[state][(esp_timer_get_time()/100000) % 6]);
|
||||
break;
|
||||
case LED_STATE_ERROR:
|
||||
ESP_LOGE(TAG, "SOME SORT OF ERROR");
|
||||
//ESP_LOGE(TAG, "SOME SORT OF ERROR");
|
||||
i2c_set_led1(patterns[state][(esp_timer_get_time()/1000000) % 2]);
|
||||
break;
|
||||
case LED_STATE_AWAKE:
|
||||
@@ -147,7 +146,7 @@ void app_main(void) {
|
||||
}
|
||||
|
||||
/*** FULL BOOT ***/
|
||||
if (uart_init() != ESP_OK) ESP_LOGE(TAG, "UART FAILED");
|
||||
//if (uart_init() != ESP_OK) ESP_LOGE(TAG, "UART FAILED");
|
||||
if (power_init() != ESP_OK) ESP_LOGE(TAG, "POWER FAILED");
|
||||
if (rf_433_init() != ESP_OK) ESP_LOGE(TAG, "RF FAILED");
|
||||
if (fsm_init() != ESP_OK) ESP_LOGE(TAG, "FSM FAILED");
|
||||
@@ -186,10 +185,16 @@ void app_main(void) {
|
||||
} else{
|
||||
|
||||
|
||||
if (rtc_is_set())
|
||||
if (
|
||||
rtc_is_set() &&
|
||||
!efuse_is_tripped(BRIDGE_JACK) &&
|
||||
!efuse_is_tripped(BRIDGE_AUX) &&
|
||||
!efuse_is_tripped(BRIDGE_DRIVE)
|
||||
) {
|
||||
driveLEDs(LED_STATE_AWAKE);
|
||||
else
|
||||
} else {
|
||||
driveLEDs(LED_STATE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// when not actively moving we log at a low frequency
|
||||
@@ -210,6 +215,25 @@ void app_main(void) {
|
||||
}
|
||||
break;
|
||||
|
||||
case STATE_CALIBRATE_JACK_DELAY:
|
||||
if (i2c_get_button_tripped(0))
|
||||
fsm_request(FSM_CMD_CALIBRATE_JACK_START);
|
||||
break;
|
||||
case STATE_CALIBRATE_JACK_MOVE:
|
||||
if (i2c_get_button_tripped(0))
|
||||
fsm_request(FSM_CMD_CALIBRATE_JACK_END);
|
||||
break;
|
||||
|
||||
|
||||
case STATE_CALIBRATE_DRIVE_DELAY:
|
||||
if (i2c_get_button_tripped(0))
|
||||
fsm_request(FSM_CMD_CALIBRATE_DRIVE_START);
|
||||
break;
|
||||
case STATE_CALIBRATE_DRIVE_MOVE:
|
||||
if (i2c_get_button_tripped(0))
|
||||
fsm_request(FSM_CMD_CALIBRATE_DRIVE_END);
|
||||
break;
|
||||
|
||||
default:
|
||||
// it's running in every other case
|
||||
send_log();
|
||||
|
||||
28
main/prvtkey.pem
Normal file
28
main/prvtkey.pem
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc4kQulPmDvj52
|
||||
HrvgQPMrGrMsSQdtOKFOFBT0o7uTXXWK2y48TMdy3RKKNVaIu0nhKpsFSBfekyAe
|
||||
iUJH4N/1ih+byJtUZJ3iMyfDrPoElmAPBQJYkjwvM8V94ug648GV5YF20C9hNOyi
|
||||
7Z9BIdRvd3DiNexuYz+3C2vRGW/lHu9r7HdK1u4nv9SF10jZhjfa0DidotKaDrul
|
||||
558QoHskrQkxOQa5hKRcB4KmceRihs5bU9qngrgdSlax0LUBhpBBBsgUxsKdvht3
|
||||
C4R55MmAkDWuYYvqa8cAX8+XWylILRjlvktZhDrWFOlXbTxShD9ax3zUfuJRj8Jx
|
||||
t0/SLamZAgMBAAECggEAA/dwk+DuYhdYSvJB+8yImWlmaFM8XdWVtnypfvn4zyQI
|
||||
ycb650llrZDLXDU3B+P8XTYPj1WgTN9Za4w64chcwT+Jxw2OZ9bXaxWyBq+D7sPC
|
||||
j/6nbYfc/7CGaMVo67xAc8LGwDNJT2LgLMpnQWVSkrLpZr7ISI432S/vvOywuJFC
|
||||
gQVdslNvVDAwC0KU+0Y0P4CTq2gQYeThODMMstRm0Ui/L/nENUNWCNmNd+WQYx4E
|
||||
8DQfCCLh2u9W7whpYFbaOSQxUzDMRcgZMTzVjU48/oZTagNMEFfYmv5pSQN6Lhf2
|
||||
PvBbYRip3h0sdj1gmFuyGOEl0Mw4AzZyeBohxMNSUQKBgQDXy5A+Cd/8dPM8mL88
|
||||
RtKrV3+Dy+otML1bK7eDN1jJ6XDCmvOAtWIwH2v1quls7uWX54lDDYLs6F2w5+Hz
|
||||
dRnfkMO/JtWInYjLqh1pu5iVPbP+6HVSX6wslMvg8K+M5DOsG/q6J62j6e643o9G
|
||||
fCiNR8rX8V/BdaCvtCBiCtNPjQKBgQC6HOd3302D6PKQSWLJss7QYdEQ5sYesoIu
|
||||
GW6KfJEh6FafJ/fwJoDOH+9klvN9EBJLlXZa/9NM23DqovI/Ca+ABfFjjzea70+U
|
||||
f9UbzaAMl6ENKU7vmAyM2WwHdZPJ4PCQ9J7nBkODzKAm++eQh12iOglcIMKdUPiL
|
||||
72Ygx1bJPQKBgA9Ae/lmiUY2ndpykVGZT9p8XK7cArke8MM428eSadwbe7TFbuBx
|
||||
8XalQeQjKExitid+Xd03X0GPSs/uE7I5XJLIkI06GW2GdNywMVP/xlEGA2rI00H3
|
||||
MRwViDNlz4KNvnkzoQz3MPac2hqG4GmC7PrPUC7jCHmL7sd8W62SRk0hAoGAEwEI
|
||||
kbD3lVSgECOuNrJPc+/JDVTDPjc0G8j1BKcbmr7CuZW3N4p29JVGOJtBWa/ebmFg
|
||||
qIIe7WYq7Yqd+dnfVc9FiskBAI0XLy6ucBxbD24cP9/L86MvBOLeqRRUdvTFG8ge
|
||||
wbBeDINEhzaJurRX10zdz854kN/HwWI8p3QzZHECgYBeDTRyYbi9c7eo6Uyo+pjz
|
||||
bFWyVtAjzjChHa9ifI6GCgs1qakaNBrwb4yOlheEkuakscZY1myzHwHRH1QTQK2c
|
||||
4du0I3/JFkk1JmYXw1VLo/BKs2S6RGBx+4jE5bkSXrxnQmT0EtbexkfIJHEAlIqU
|
||||
DMmi7yrQo4dydJ20IHEF6A==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -38,6 +38,7 @@ typedef struct {
|
||||
} rf_code_t;
|
||||
|
||||
int learn_flag = -1;
|
||||
bool controls_enabled = true;
|
||||
|
||||
// For rmt_rx_register_event_callbacks
|
||||
static bool rfrx_done(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *udata) {
|
||||
@@ -129,16 +130,16 @@ static void rf_433_receiver_task(void* param) {
|
||||
|
||||
// If we got a valid code, process it
|
||||
if (code) {
|
||||
int64_t encoded = ((int64_t)len << 56) | code;
|
||||
|
||||
ESP_LOGI(TAG, "GOT KEYCODE 0x%lx [%d]", (long) code, len);
|
||||
|
||||
if (learn_flag >= 0) {
|
||||
// Store just the 32-bit code
|
||||
set_param_value_t(PARAM_KEYCODE_0 + learn_flag,
|
||||
(param_value_t){.i64 = encoded});
|
||||
(param_value_t){.i32 = (int32_t)code});
|
||||
ESP_LOGI(TAG, "LEARNED KEYCODE");
|
||||
learn_flag = -1;
|
||||
} else {
|
||||
} else if (controls_enabled) {
|
||||
// Only process RF commands if controls are enabled
|
||||
rf_code_t rf_msg = {
|
||||
.code = code,
|
||||
.high_avg = high / 24,
|
||||
@@ -148,8 +149,9 @@ static void rf_433_receiver_task(void* param) {
|
||||
};
|
||||
|
||||
for (uint8_t i = 0; i < NUM_RF_BUTTONS; i++) {
|
||||
int64_t match = get_param_value_t(PARAM_KEYCODE_0+i).i64;
|
||||
if (encoded == match) {
|
||||
int32_t match = get_param_value_t(PARAM_KEYCODE_0+i).i32;
|
||||
// Compare just the code (lower 32 bits)
|
||||
if ((uint32_t)match == code) {
|
||||
switch (i) {
|
||||
case 0: pulseOverride(RELAY_A1); pulseOverride(RELAY_A3); break;
|
||||
case 1: pulseOverride(RELAY_B1); pulseOverride(RELAY_A3); break;
|
||||
@@ -223,4 +225,12 @@ void rf_433_learn_keycode(uint8_t index) {
|
||||
|
||||
void rf_433_cancel_learn_keycode() {
|
||||
learn_flag = -1;
|
||||
}
|
||||
|
||||
void rf_433_disable_controls() {
|
||||
controls_enabled = false;
|
||||
}
|
||||
|
||||
void rf_433_enable_controls() {
|
||||
controls_enabled = true;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
#ifndef RF_H
|
||||
#define RF_H
|
||||
|
||||
@@ -27,4 +26,7 @@ int64_t rf_433_get_raw_keycode();
|
||||
void rf_433_learn_keycode(uint8_t index);
|
||||
void rf_433_cancel_learn_keycode();
|
||||
|
||||
void rf_433_disable_controls();
|
||||
void rf_433_enable_controls();
|
||||
|
||||
#endif
|
||||
@@ -16,9 +16,9 @@
|
||||
#define SENSOR_DEBOUNCE_US 500 // Reduced to 0.5 ms for responsiveness
|
||||
|
||||
typedef enum {
|
||||
SENSOR_DRIVE = 0,
|
||||
SENSOR_JACK = 1,
|
||||
N_SENSORS = 2
|
||||
SENSOR_SAFETY = 0,
|
||||
SENSOR_DRIVE = 1,
|
||||
N_SENSORS = 2
|
||||
} sensor_t;
|
||||
|
||||
void reset_sensor_counter(sensor_t i);
|
||||
|
||||
19
main/servercert.pem
Normal file
19
main/servercert.pem
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDIjCCAgqgAwIBAgIUDpXEqSZ2cdhTBSkwUpC6uSIsfvEwDQYJKoZIhvcNAQEL
|
||||
BQAwEzERMA8GA1UEAwwIc2MubG9jYWwwHhcNMjUxMjMwMTcyMDMwWhcNMzUxMjI4
|
||||
MTcyMDMwWjATMREwDwYDVQQDDAhzYy5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBAJziRC6U+YO+PnYeu+BA8ysasyxJB204oU4UFPSju5NddYrb
|
||||
LjxMx3LdEoo1Voi7SeEqmwVIF96TIB6JQkfg3/WKH5vIm1RkneIzJ8Os+gSWYA8F
|
||||
AliSPC8zxX3i6DrjwZXlgXbQL2E07KLtn0Eh1G93cOI17G5jP7cLa9EZb+Ue72vs
|
||||
d0rW7ie/1IXXSNmGN9rQOJ2i0poOu6XnnxCgeyStCTE5BrmEpFwHgqZx5GKGzltT
|
||||
2qeCuB1KVrHQtQGGkEEGyBTGwp2+G3cLhHnkyYCQNa5hi+prxwBfz5dbKUgtGOW+
|
||||
S1mEOtYU6VdtPFKEP1rHfNR+4lGPwnG3T9ItqZkCAwEAAaNuMGwwHQYDVR0OBBYE
|
||||
FICBmIRVzS0CQHS5OZmXSLQcTOz6MB8GA1UdIwQYMBaAFICBmIRVzS0CQHS5OZmX
|
||||
SLQcTOz6MA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0RBBIwEIIIc2MubG9jYWyHBMCo
|
||||
BAEwDQYJKoZIhvcNAQELBQADggEBAIn1zeotOH72CcRUq5e+nsKLRCZSUZV1Gvip
|
||||
nTADfFI8MpMGe0ikPJqetSYmP1HVIzGWQlWamqGhXckBeR9uq1e2k1Jsjf80w6o6
|
||||
ild+fr1oga7n1xwXIMumDbSFjtsWe0fO9xI5NVGP3h6ikj1SN0o6T5EtTyjZ+vGw
|
||||
u73tFmD7c/YBKkvLlP1UxVbAvJFEHL+O7e3QiIzeyryWFDEWRjOHPuPLCV84J5Q6
|
||||
55mzfuUFkf7piLpLUXZ6lJ3MYa+iylll7Z9jQ8W5LWPpZhbTwyK4j7L6IiJoluzL
|
||||
eV0ZuqoHda9Igd+Gigih1YOH4raMc98KVrNIUjNFoWlOx4rvWkY=
|
||||
-----END CERTIFICATE-----
|
||||
143
main/simple_dns_server.c
Normal file
143
main/simple_dns_server.c
Normal file
@@ -0,0 +1,143 @@
|
||||
#include "simple_dns_server.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "lwip/netdb.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "DNS_SERVER";
|
||||
static int dns_socket = -1;
|
||||
static TaskHandle_t dns_task_handle = NULL;
|
||||
static char dns_ip[16] = {0};
|
||||
|
||||
// DNS header structure
|
||||
typedef struct {
|
||||
uint16_t id;
|
||||
uint16_t flags;
|
||||
uint16_t qdcount;
|
||||
uint16_t ancount;
|
||||
uint16_t nscount;
|
||||
uint16_t arcount;
|
||||
} dns_header_t;
|
||||
|
||||
static void dns_server_task(void *pvParameters) {
|
||||
char rx_buffer[512];
|
||||
char tx_buffer[512];
|
||||
struct sockaddr_in dest_addr;
|
||||
|
||||
dest_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
dest_addr.sin_family = AF_INET;
|
||||
dest_addr.sin_port = htons(53);
|
||||
|
||||
dns_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
if (dns_socket < 0) {
|
||||
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
int err = bind(dns_socket, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Socket bind failed: errno %d", errno);
|
||||
close(dns_socket);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "DNS server started on port 53");
|
||||
|
||||
while (1) {
|
||||
struct sockaddr_in source_addr;
|
||||
socklen_t socklen = sizeof(source_addr);
|
||||
|
||||
int len = recvfrom(dns_socket, rx_buffer, sizeof(rx_buffer) - 1, 0,
|
||||
(struct sockaddr *)&source_addr, &socklen);
|
||||
|
||||
if (len < 0) {
|
||||
ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
|
||||
break;
|
||||
}
|
||||
|
||||
if (len < sizeof(dns_header_t)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse DNS query
|
||||
dns_header_t *header = (dns_header_t *)rx_buffer;
|
||||
|
||||
// Build DNS response
|
||||
memcpy(tx_buffer, rx_buffer, len);
|
||||
dns_header_t *response = (dns_header_t *)tx_buffer;
|
||||
|
||||
// Set response flags
|
||||
response->flags = htons(0x8180); // Standard query response, no error
|
||||
response->ancount = htons(1); // One answer
|
||||
|
||||
// Add answer section (Type A record)
|
||||
int pos = len;
|
||||
|
||||
// Name pointer to question
|
||||
tx_buffer[pos++] = 0xC0;
|
||||
tx_buffer[pos++] = 0x0C;
|
||||
|
||||
// Type A
|
||||
tx_buffer[pos++] = 0x00;
|
||||
tx_buffer[pos++] = 0x01;
|
||||
|
||||
// Class IN
|
||||
tx_buffer[pos++] = 0x00;
|
||||
tx_buffer[pos++] = 0x01;
|
||||
|
||||
// TTL (60 seconds)
|
||||
tx_buffer[pos++] = 0x00;
|
||||
tx_buffer[pos++] = 0x00;
|
||||
tx_buffer[pos++] = 0x00;
|
||||
tx_buffer[pos++] = 0x3C;
|
||||
|
||||
// Data length (4 bytes for IPv4)
|
||||
tx_buffer[pos++] = 0x00;
|
||||
tx_buffer[pos++] = 0x04;
|
||||
|
||||
// IP address
|
||||
int a, b, c, d;
|
||||
sscanf(dns_ip, "%d.%d.%d.%d", &a, &b, &c, &d);
|
||||
tx_buffer[pos++] = a;
|
||||
tx_buffer[pos++] = b;
|
||||
tx_buffer[pos++] = c;
|
||||
tx_buffer[pos++] = d;
|
||||
|
||||
// Send response
|
||||
sendto(dns_socket, tx_buffer, pos, 0,
|
||||
(struct sockaddr *)&source_addr, sizeof(source_addr));
|
||||
}
|
||||
|
||||
close(dns_socket);
|
||||
dns_socket = -1;
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t simple_dns_server_start(const char *ap_ip) {
|
||||
if (dns_task_handle != NULL) {
|
||||
ESP_LOGW(TAG, "DNS server already running");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
strncpy(dns_ip, ap_ip, sizeof(dns_ip) - 1);
|
||||
|
||||
xTaskCreate(dns_server_task, "dns_server", 4096, NULL, 5, &dns_task_handle);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void simple_dns_server_stop(void) {
|
||||
if (dns_task_handle != NULL) {
|
||||
if (dns_socket >= 0) {
|
||||
close(dns_socket);
|
||||
dns_socket = -1;
|
||||
}
|
||||
vTaskDelete(dns_task_handle);
|
||||
dns_task_handle = NULL;
|
||||
ESP_LOGI(TAG, "DNS server stopped");
|
||||
}
|
||||
}
|
||||
19
main/simple_dns_server.h
Normal file
19
main/simple_dns_server.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef SIMPLE_DNS_SERVER_H
|
||||
#define SIMPLE_DNS_SERVER_H
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/**
|
||||
* @brief Start a simple DNS server that redirects all queries to the AP IP
|
||||
*
|
||||
* @param ap_ip The IP address to return for all DNS queries (e.g., "192.168.4.1")
|
||||
* @return esp_err_t ESP_OK on success
|
||||
*/
|
||||
esp_err_t simple_dns_server_start(const char *ap_ip);
|
||||
|
||||
/**
|
||||
* @brief Stop the DNS server
|
||||
*/
|
||||
void simple_dns_server_stop(void);
|
||||
|
||||
#endif // SIMPLE_DNS_SERVER_H
|
||||
431
main/storage.c
431
main/storage.c
@@ -1,9 +1,12 @@
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include "esp_partition.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_crc.h"
|
||||
#include "storage.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#define TAG "STORAGE"
|
||||
|
||||
@@ -51,19 +54,34 @@ const char parameter_units[NUM_PARAMS][8] = {
|
||||
};
|
||||
#undef PARAM_DEF
|
||||
|
||||
|
||||
// Partition pointer
|
||||
static const esp_partition_t *storage_partition = NULL;
|
||||
|
||||
|
||||
// Log head tracking
|
||||
// Log head tracking with mutex protection
|
||||
static uint32_t log_head_index = 0;
|
||||
static uint32_t log_tail_index = 0;
|
||||
uint32_t get_log_head() { return LOG_START_OFFSET + (log_head_index * LOG_ENTRY_SIZE); }
|
||||
uint32_t get_log_tail() { return LOG_START_OFFSET + (log_tail_index * LOG_ENTRY_SIZE); }
|
||||
uint32_t get_log_offset() { return LOG_START_OFFSET; }
|
||||
static SemaphoreHandle_t log_mutex = NULL;
|
||||
static bool log_initialized = false;
|
||||
|
||||
uint32_t get_log_head(void) {
|
||||
uint32_t head;
|
||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||
head = LOG_START_OFFSET + (log_head_index * LOG_ENTRY_SIZE);
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
return head;
|
||||
}
|
||||
|
||||
uint32_t get_log_tail(void) {
|
||||
uint32_t tail;
|
||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||
tail = LOG_START_OFFSET + (log_tail_index * LOG_ENTRY_SIZE);
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
return tail;
|
||||
}
|
||||
|
||||
uint32_t get_log_offset(void) {
|
||||
return LOG_START_OFFSET;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PARAMETER FUNCTIONS
|
||||
@@ -88,6 +106,43 @@ esp_err_t set_param_value_t(param_idx_t id, param_value_t val) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t set_param_string(param_idx_t id, const char* str) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (parameter_types[id] != PARAM_TYPE_str) {
|
||||
ESP_LOGE(TAG, "Parameter %d (%s) is not a string type", id, parameter_names[id]);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
if (str == NULL) {
|
||||
parameter_table[id].str[0] = '\0';
|
||||
} else {
|
||||
strncpy(parameter_table[id].str, str, 15);
|
||||
parameter_table[id].str[15] = '\0'; // Ensure null termination
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "String parameter %d (%s) set to '%s' (not committed)",
|
||||
id, parameter_names[id], parameter_table[id].str);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char* get_param_string(param_idx_t id) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
|
||||
return "";
|
||||
}
|
||||
|
||||
if (parameter_types[id] != PARAM_TYPE_str) {
|
||||
ESP_LOGE(TAG, "Parameter %d (%s) is not a string type", id, parameter_names[id]);
|
||||
return "";
|
||||
}
|
||||
|
||||
return parameter_table[id].str;
|
||||
}
|
||||
|
||||
param_type_e get_param_type(param_idx_t id) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
return PARAM_TYPE_u64; // Default fallback
|
||||
@@ -95,6 +150,63 @@ param_type_e get_param_type(param_idx_t id) {
|
||||
return parameter_types[id];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSON-FRIENDLY STRING CONVERSION
|
||||
// ============================================================================
|
||||
const char* get_param_json_string(param_idx_t id, char* buffer, size_t buf_size) {
|
||||
if (id >= NUM_PARAMS || buffer == NULL || buf_size == 0) {
|
||||
if (buffer && buf_size > 0) buffer[0] = '\0';
|
||||
return "";
|
||||
}
|
||||
|
||||
param_type_e type = parameter_types[id];
|
||||
param_value_t val = parameter_table[id];
|
||||
|
||||
switch(type) {
|
||||
case PARAM_TYPE_u16:
|
||||
snprintf(buffer, buf_size, "%u", val.u16);
|
||||
break;
|
||||
case PARAM_TYPE_i16:
|
||||
snprintf(buffer, buf_size, "%d", val.i16);
|
||||
break;
|
||||
case PARAM_TYPE_u32:
|
||||
snprintf(buffer, buf_size, "%lu", (unsigned long)val.u32);
|
||||
break;
|
||||
case PARAM_TYPE_i32:
|
||||
snprintf(buffer, buf_size, "%ld", (long)val.i32);
|
||||
break;
|
||||
case PARAM_TYPE_u64:
|
||||
snprintf(buffer, buf_size, "%llu", (unsigned long long)val.u64);
|
||||
break;
|
||||
case PARAM_TYPE_i64:
|
||||
snprintf(buffer, buf_size, "%lld", (long long)val.i64);
|
||||
break;
|
||||
case PARAM_TYPE_f32:
|
||||
if (isnan(val.f32) || isinf(val.f32)) {
|
||||
snprintf(buffer, buf_size, "null");
|
||||
} else {
|
||||
snprintf(buffer, buf_size, "%.6g", val.f32);
|
||||
}
|
||||
break;
|
||||
case PARAM_TYPE_f64:
|
||||
if (isnan(val.f64) || isinf(val.f64)) {
|
||||
snprintf(buffer, buf_size, "null");
|
||||
} else {
|
||||
snprintf(buffer, buf_size, "%.15g", val.f64);
|
||||
}
|
||||
break;
|
||||
case PARAM_TYPE_str:
|
||||
// Escape quotes and backslashes for JSON string
|
||||
snprintf(buffer, buf_size, "\"%s\"", val.str);
|
||||
break;
|
||||
default:
|
||||
snprintf(buffer, buf_size, "null");
|
||||
break;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const char* get_param_name(param_idx_t id) {
|
||||
if (id >= NUM_PARAMS) {
|
||||
return "INVALID";
|
||||
@@ -117,33 +229,125 @@ const char* get_param_unit(param_idx_t id) {
|
||||
return parameter_units[id];
|
||||
}
|
||||
|
||||
esp_err_t commit_params() {
|
||||
// ============================================================================
|
||||
// STORAGE HELPER: Pack parameter value into buffer
|
||||
// ============================================================================
|
||||
static void pack_param(uint8_t *dest, param_idx_t id) {
|
||||
param_type_e type = parameter_types[id];
|
||||
|
||||
switch(type) {
|
||||
case PARAM_TYPE_u16:
|
||||
memcpy(dest, ¶meter_table[id].u16, 2);
|
||||
break;
|
||||
case PARAM_TYPE_i16:
|
||||
memcpy(dest, ¶meter_table[id].i16, 2);
|
||||
break;
|
||||
case PARAM_TYPE_u32:
|
||||
memcpy(dest, ¶meter_table[id].u32, 4);
|
||||
break;
|
||||
case PARAM_TYPE_i32:
|
||||
memcpy(dest, ¶meter_table[id].i32, 4);
|
||||
break;
|
||||
case PARAM_TYPE_f32:
|
||||
memcpy(dest, ¶meter_table[id].f32, 4);
|
||||
break;
|
||||
case PARAM_TYPE_u64:
|
||||
memcpy(dest, ¶meter_table[id].u64, 8);
|
||||
break;
|
||||
case PARAM_TYPE_i64:
|
||||
memcpy(dest, ¶meter_table[id].i64, 8);
|
||||
break;
|
||||
case PARAM_TYPE_f64:
|
||||
memcpy(dest, ¶meter_table[id].f64, 8);
|
||||
break;
|
||||
case PARAM_TYPE_str:
|
||||
memcpy(dest, parameter_table[id].str, 16);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STORAGE HELPER: Unpack parameter value from buffer
|
||||
// ============================================================================
|
||||
static void unpack_param(const uint8_t *src, param_idx_t id) {
|
||||
param_type_e type = parameter_types[id];
|
||||
|
||||
switch(type) {
|
||||
case PARAM_TYPE_u16:
|
||||
memcpy(¶meter_table[id].u16, src, 2);
|
||||
break;
|
||||
case PARAM_TYPE_i16:
|
||||
memcpy(¶meter_table[id].i16, src, 2);
|
||||
break;
|
||||
case PARAM_TYPE_u32:
|
||||
memcpy(¶meter_table[id].u32, src, 4);
|
||||
break;
|
||||
case PARAM_TYPE_i32:
|
||||
memcpy(¶meter_table[id].i32, src, 4);
|
||||
break;
|
||||
case PARAM_TYPE_f32:
|
||||
memcpy(¶meter_table[id].f32, src, 4);
|
||||
break;
|
||||
case PARAM_TYPE_u64:
|
||||
memcpy(¶meter_table[id].u64, src, 8);
|
||||
break;
|
||||
case PARAM_TYPE_i64:
|
||||
memcpy(¶meter_table[id].i64, src, 8);
|
||||
break;
|
||||
case PARAM_TYPE_f64:
|
||||
memcpy(¶meter_table[id].f64, src, 8);
|
||||
break;
|
||||
case PARAM_TYPE_str:
|
||||
memcpy(parameter_table[id].str, src, 16);
|
||||
parameter_table[id].str[15] = '\0'; // Ensure null termination
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMMIT PARAMETERS TO FLASH
|
||||
// ============================================================================
|
||||
esp_err_t commit_params(void) {
|
||||
if (storage_partition == NULL) {
|
||||
ESP_LOGE(TAG, "Storage partition not initialized");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Prepare storage buffer with parameters and CRCs
|
||||
param_stored_t params_to_store[NUM_PARAMS];
|
||||
// Calculate flash offset for each parameter
|
||||
uint32_t flash_offset = PARAMS_OFFSET;
|
||||
|
||||
// Erase the parameter sectors
|
||||
esp_err_t err = esp_partition_erase_range(storage_partition, PARAMS_OFFSET,
|
||||
LOG_START_OFFSET);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase parameter sectors: %s", esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Write each parameter with its CRC
|
||||
for (int i = 0; i < NUM_PARAMS; i++) {
|
||||
params_to_store[i].val = parameter_table[i];
|
||||
// Calculate CRC32 for each parameter value
|
||||
params_to_store[i].crc = esp_crc32_le(0, (uint8_t*)¶meter_table[i], sizeof(param_value_t));
|
||||
}
|
||||
|
||||
// Erase the sectors for parameter storage
|
||||
esp_err_t err = esp_partition_erase_range(storage_partition, PARAMS_OFFSET, LOG_START_OFFSET);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase parameter sector: %s", esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Write parameters to flash
|
||||
err = esp_partition_write(storage_partition, PARAMS_OFFSET, params_to_store, PARAMS_TOTAL_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write parameters: %s", esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
param_stored_t stored;
|
||||
memset(&stored, 0, sizeof(stored));
|
||||
|
||||
// Pack the parameter value
|
||||
uint8_t size = param_type_size(parameter_types[i]);
|
||||
pack_param(stored.data, i);
|
||||
|
||||
// Calculate CRC over the actual data size used
|
||||
uint32_t crc_input = PARAM_CRC_SALT;
|
||||
//uint32_t crc = esp_crc32_le(0, (uint8_t*)&crc_input, sizeof(crc_input));
|
||||
stored.crc = esp_crc32_le(crc_input, stored.data, size);
|
||||
|
||||
// Write to flash
|
||||
err = esp_partition_write(storage_partition, flash_offset,
|
||||
&stored, sizeof(param_stored_t));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write parameter %d (%s): %s",
|
||||
i, parameter_names[i], esp_err_to_name(err));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
flash_offset += sizeof(param_stored_t);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Parameters committed to flash successfully");
|
||||
@@ -165,31 +369,44 @@ esp_err_t storage_init(void) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Storage partition found: size=%lu bytes", (unsigned long)storage_partition->size);
|
||||
ESP_LOGI(TAG, "Storage partition found: size=%lu bytes",
|
||||
(unsigned long)storage_partition->size);
|
||||
|
||||
// Load parameters from flash
|
||||
param_stored_t params_stored[NUM_PARAMS];
|
||||
esp_err_t err = esp_partition_read(storage_partition, PARAMS_OFFSET,
|
||||
params_stored, PARAMS_TOTAL_SIZE);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to read parameters, using defaults");
|
||||
return err;
|
||||
}
|
||||
|
||||
// Validate and load each parameter
|
||||
uint32_t flash_offset = PARAMS_OFFSET;
|
||||
bool all_valid = true;
|
||||
|
||||
for (int i = 0; i < NUM_PARAMS; i++) {
|
||||
uint32_t calculated_crc = esp_crc32_le(0, (uint8_t*)¶ms_stored[i].val,
|
||||
sizeof(param_value_t));
|
||||
param_stored_t stored;
|
||||
|
||||
if (calculated_crc == params_stored[i].crc) {
|
||||
parameter_table[i] = params_stored[i].val;
|
||||
esp_err_t err = esp_partition_read(storage_partition, flash_offset,
|
||||
&stored, sizeof(param_stored_t));
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to read parameter %d (%s), using default",
|
||||
i, parameter_names[i]);
|
||||
memcpy(¶meter_table[i], ¶meter_defaults[i], sizeof(param_value_t)); // SET DEFAULT HERE
|
||||
all_valid = false;
|
||||
flash_offset += sizeof(param_stored_t);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate CRC over actual data size
|
||||
uint8_t size = param_type_size(parameter_types[i]);
|
||||
uint32_t crc_input = PARAM_CRC_SALT;
|
||||
//uint32_t crc = esp_crc32_le(0, (uint8_t*)&crc_input, sizeof(crc_input));
|
||||
uint32_t calculated_crc = esp_crc32_le(crc_input, stored.data, size);
|
||||
|
||||
if (calculated_crc == stored.crc) {
|
||||
unpack_param(stored.data, i);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Parameter %d (%s) failed CRC check, using default",
|
||||
i, parameter_names[i]);
|
||||
memcpy(¶meter_table[i], ¶meter_defaults[i], sizeof(param_value_t)); // SET DEFAULT HERE
|
||||
all_valid = false;
|
||||
}
|
||||
|
||||
flash_offset += sizeof(param_stored_t);
|
||||
}
|
||||
|
||||
if (all_valid) {
|
||||
@@ -202,7 +419,7 @@ esp_err_t storage_init(void) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LOGGING FUNCTIONS
|
||||
// LOGGING FUNCTIONS (unchanged from original)
|
||||
// ============================================================================
|
||||
|
||||
static esp_err_t find_log_head(void) {
|
||||
@@ -210,16 +427,13 @@ static esp_err_t find_log_head(void) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// Calculate total log area size
|
||||
uint32_t log_area_size = storage_partition->size - LOG_START_OFFSET;
|
||||
uint32_t max_entries = log_area_size / LOG_ENTRY_SIZE;
|
||||
|
||||
// Read through entries to find first uninitialized (all 0xFF)
|
||||
uint8_t entry[LOG_ENTRY_SIZE];
|
||||
uint8_t empty_entry[LOG_ENTRY_SIZE];
|
||||
memset(empty_entry, 0xFF, LOG_ENTRY_SIZE);
|
||||
|
||||
// Binary search would be faster, but linear is safer for circular buffer
|
||||
for (uint32_t i = 0; i < max_entries; i++) {
|
||||
uint32_t offset = LOG_START_OFFSET + (i * LOG_ENTRY_SIZE);
|
||||
|
||||
@@ -229,7 +443,6 @@ static esp_err_t find_log_head(void) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Check if this entry is uninitialized
|
||||
if (memcmp(entry, empty_entry, LOG_ENTRY_SIZE) == 0) {
|
||||
log_head_index = i;
|
||||
ESP_LOGI(TAG, "Log head found at index %lu", (unsigned long)log_head_index);
|
||||
@@ -237,11 +450,9 @@ static esp_err_t find_log_head(void) {
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all entries are full - wrap to beginning
|
||||
log_head_index = 0;
|
||||
ESP_LOGI(TAG, "Log is full, wrapping to beginning");
|
||||
|
||||
// Erase the first log sector to start fresh
|
||||
esp_err_t err = esp_partition_erase_range(storage_partition, LOG_START_OFFSET,
|
||||
FLASH_SECTOR_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
@@ -258,8 +469,16 @@ esp_err_t log_init(void) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
log_mutex = xSemaphoreCreateMutex();
|
||||
if (log_mutex == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to create log mutex");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
esp_err_t err = find_log_head();
|
||||
if (err != ESP_OK) {
|
||||
vSemaphoreDelete(log_mutex);
|
||||
log_mutex = NULL;
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -273,111 +492,121 @@ esp_err_t write_log(char* entry) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||
|
||||
uint32_t log_area_end = storage_partition->size;
|
||||
uint32_t max_entries = (log_area_end - LOG_START_OFFSET) / LOG_ENTRY_SIZE;
|
||||
//uint32_t max_sectors = max_entries / FLASH_SECTOR_SIZE;
|
||||
|
||||
// Calculate current offset
|
||||
uint32_t current_offset = LOG_START_OFFSET + (log_head_index * LOG_ENTRY_SIZE);
|
||||
|
||||
// Check if we need to erase the next sector
|
||||
uint32_t current_sector = current_offset / FLASH_SECTOR_SIZE;
|
||||
uint32_t next_offset = current_offset + LOG_ENTRY_SIZE;
|
||||
if (next_offset >= log_area_end)
|
||||
next_offset = LOG_START_OFFSET;
|
||||
if (next_offset >= log_area_end) {
|
||||
next_offset = LOG_START_OFFSET;
|
||||
}
|
||||
|
||||
uint32_t current_sector = current_offset / FLASH_SECTOR_SIZE;
|
||||
uint32_t next_sector = next_offset / FLASH_SECTOR_SIZE;
|
||||
|
||||
// If we're crossing into a new sector, check if it needs erasing
|
||||
if (next_sector != current_sector) {
|
||||
// Check if next sector is uninitialized
|
||||
uint8_t check_byte;
|
||||
esp_err_t err = esp_partition_read(storage_partition, next_sector * FLASH_SECTOR_SIZE,
|
||||
&check_byte, 1);
|
||||
|
||||
if (err == ESP_OK && check_byte != 0xFF) {
|
||||
// Sector needs erasing
|
||||
ESP_LOGI(TAG, "Erasing sector %lu for log", (unsigned long)next_sector);
|
||||
err = esp_partition_erase_range(storage_partition,
|
||||
next_sector * FLASH_SECTOR_SIZE,
|
||||
FLASH_SECTOR_SIZE);
|
||||
|
||||
log_tail_index = (next_sector)*FLASH_SECTOR_SIZE/LOG_ENTRY_SIZE;
|
||||
if (log_tail_index >= max_entries)
|
||||
log_tail_index = 0;
|
||||
|
||||
|
||||
ESP_LOGI(TAG, "Tail/Head are now %ld/%ld", (long)log_tail_index, (long)log_head_index);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase sector: %s", esp_err_to_name(err));
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
} else if (err == ESP_OK) {
|
||||
ESP_LOGI(TAG, "Next sector %ld clear, no erasing needed", (unsigned long)next_sector);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error checking byte (sector %ld)", (unsigned long)next_sector);
|
||||
}
|
||||
|
||||
uint32_t tail_offset = next_sector * FLASH_SECTOR_SIZE;
|
||||
if (tail_offset < LOG_START_OFFSET) {
|
||||
tail_offset = LOG_START_OFFSET;
|
||||
}
|
||||
log_tail_index = (tail_offset - LOG_START_OFFSET) / LOG_ENTRY_SIZE;
|
||||
|
||||
if (log_tail_index >= max_entries) {
|
||||
log_tail_index = 0;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Tail/Head are now %ld/%ld", (long)log_tail_index, (long)log_head_index);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the log entry
|
||||
esp_err_t err = esp_partition_write(storage_partition, current_offset, entry, LOG_ENTRY_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write log entry: %s", esp_err_to_name(err));
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
//ESP_LOGI(TAG, "Log @ sector %lu / index %lu / offset %lu", (unsigned long) current_sector, (unsigned long) log_head_index, (unsigned long)current_offset);
|
||||
|
||||
|
||||
log_head_index++;
|
||||
if (log_head_index >= max_entries) {
|
||||
log_head_index = 0;
|
||||
ESP_LOGI(TAG, "Log wrapped to beginning");
|
||||
}
|
||||
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t write_dummy_log_1() {
|
||||
log_head_index = 0;
|
||||
log_tail_index = 0;
|
||||
esp_err_t write_dummy_log_1(void) {
|
||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||
log_head_index = 0;
|
||||
log_tail_index = 0;
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
|
||||
uint32_t log_area_end = storage_partition->size;
|
||||
uint32_t max_entries = (log_area_end - LOG_START_OFFSET) / LOG_ENTRY_SIZE;
|
||||
for (uint32_t i=0; i<max_entries*3/2; i++) {
|
||||
ESP_LOGI(TAG, "log[%ld]", (long)i);
|
||||
char entry[32] = {32, i>>24,i>>16,i>>8,i>>0};
|
||||
write_log(entry);
|
||||
}
|
||||
return ESP_OK;
|
||||
for (uint32_t i=0; i<max_entries*3/2; i++) {
|
||||
ESP_LOGI(TAG, "log[%ld]", (long)i);
|
||||
char entry[32] = {32, i>>24,i>>16,i>>8,i>>0};
|
||||
write_log(entry);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t write_dummy_log_2() {
|
||||
log_head_index = 56;
|
||||
log_tail_index = 105;
|
||||
esp_err_t write_dummy_log_2(void) {
|
||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||
log_head_index = 56;
|
||||
log_tail_index = 105;
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
|
||||
uint32_t log_area_end = storage_partition->size;
|
||||
uint32_t max_entries = (log_area_end - LOG_START_OFFSET) / LOG_ENTRY_SIZE;
|
||||
for (uint32_t i=0; i<max_entries*3/2; i++) {
|
||||
ESP_LOGI(TAG, "log[%ld]", (long)i);
|
||||
char entry[32] = {32, i>>24,i>>16,i>>8,i>>0};
|
||||
write_log(entry);
|
||||
}
|
||||
return ESP_OK;
|
||||
for (uint32_t i=0; i<max_entries*3/2; i++) {
|
||||
ESP_LOGI(TAG, "log[%ld]", (long)i);
|
||||
char entry[32] = {32, i>>24,i>>16,i>>8,i>>0};
|
||||
write_log(entry);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t write_dummy_log_3() {
|
||||
log_head_index = 105;
|
||||
log_tail_index = 34;
|
||||
esp_err_t write_dummy_log_3(void) {
|
||||
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
||||
log_head_index = 105;
|
||||
log_tail_index = 34;
|
||||
if (log_mutex) xSemaphoreGive(log_mutex);
|
||||
|
||||
uint32_t log_area_end = storage_partition->size;
|
||||
uint32_t max_entries = (log_area_end - LOG_START_OFFSET) / LOG_ENTRY_SIZE;
|
||||
for (uint32_t i=0; i<max_entries*3/2; i++) {
|
||||
ESP_LOGI(TAG, "log[%ld]", (long)i);
|
||||
char entry[32] = {32, i>>24,i>>16,i>>8,i>>0};
|
||||
write_log(entry);
|
||||
}
|
||||
return ESP_OK;
|
||||
for (uint32_t i=0; i<max_entries*3/2; i++) {
|
||||
ESP_LOGI(TAG, "log[%ld]", (long)i);
|
||||
char entry[32] = {32, i>>24,i>>16,i>>8,i>>0};
|
||||
write_log(entry);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void storage_deinit(void) {
|
||||
storage_partition = NULL;
|
||||
log_initialized = false;
|
||||
if (log_mutex) {
|
||||
vSemaphoreDelete(log_mutex);
|
||||
log_mutex = NULL;
|
||||
}
|
||||
}
|
||||
135
main/storage.h
135
main/storage.h
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* storage.h
|
||||
* storage.h - Simple variable-size parameter storage with per-param CRC
|
||||
*
|
||||
* Created on: Nov 5, 2025
|
||||
* Author: Thad
|
||||
@@ -17,9 +17,10 @@
|
||||
#include <stdarg.h>
|
||||
#include "i2c.h"
|
||||
|
||||
#define PARAM_CRC_SALT 0xDEADBEEF // Salt to prevent all-zero CRC collision
|
||||
|
||||
// Union for parameter values - now sized appropriately
|
||||
typedef union {
|
||||
uint8_t u8;
|
||||
int8_t i8;
|
||||
uint16_t u16;
|
||||
int16_t i16;
|
||||
uint32_t u32;
|
||||
@@ -28,25 +29,43 @@ typedef union {
|
||||
int64_t i64;
|
||||
float f32;
|
||||
double f64;
|
||||
char str[16]; // 15 chars + null terminator
|
||||
} param_value_t;
|
||||
|
||||
typedef struct {
|
||||
param_value_t val;
|
||||
uint32_t crc;
|
||||
// Enum for parameter types
|
||||
typedef enum {
|
||||
PARAM_TYPE_u16 = 0,
|
||||
PARAM_TYPE_i16 = 1,
|
||||
PARAM_TYPE_u32 = 2,
|
||||
PARAM_TYPE_i32 = 3,
|
||||
PARAM_TYPE_u64 = 4,
|
||||
PARAM_TYPE_i64 = 5,
|
||||
PARAM_TYPE_f32 = 6,
|
||||
PARAM_TYPE_f64 = 7,
|
||||
PARAM_TYPE_str = 8
|
||||
} param_type_e;
|
||||
|
||||
// Storage format: each param stored as [data][crc32]
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t data[16]; // Max size needed (for strings)
|
||||
uint32_t crc; // CRC of actual data bytes used
|
||||
} param_stored_t;
|
||||
|
||||
typedef enum {
|
||||
PARAM_TYPE_u8,
|
||||
PARAM_TYPE_i8,
|
||||
PARAM_TYPE_u16,
|
||||
PARAM_TYPE_i16,
|
||||
PARAM_TYPE_u32,
|
||||
PARAM_TYPE_i32,
|
||||
PARAM_TYPE_u64,
|
||||
PARAM_TYPE_i64,
|
||||
PARAM_TYPE_f32,
|
||||
PARAM_TYPE_f64
|
||||
} param_type_e;
|
||||
// Get storage size for a given type (data only, not including CRC)
|
||||
static inline uint8_t param_type_size(param_type_e type) {
|
||||
switch(type) {
|
||||
case PARAM_TYPE_u16:
|
||||
case PARAM_TYPE_i16: return 2;
|
||||
case PARAM_TYPE_u32:
|
||||
case PARAM_TYPE_i32:
|
||||
case PARAM_TYPE_f32: return 4;
|
||||
case PARAM_TYPE_u64:
|
||||
case PARAM_TYPE_i64:
|
||||
case PARAM_TYPE_f64: return 8;
|
||||
case PARAM_TYPE_str: return 16;
|
||||
default: return 8; // Fallback
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PARAMETER DEFINITION MACRO
|
||||
@@ -56,9 +75,9 @@ typedef enum {
|
||||
// Examples:
|
||||
// PARAM_DEF(NUM_MOVES, u32, 0, "")
|
||||
// PARAM_DEF(EFUSE_1_AS, u16, 2400, "mA")
|
||||
// PARAM_DEF(JACK_DIST, u8, 5, "mm")
|
||||
// PARAM_DEF(KEYCODE_0, i64, -1, "")
|
||||
// PARAM_DEF(TEMPERATURE, f32, 25.5, "C")
|
||||
// PARAM_DEF(DEVICE_NAME, str, "ESP32", "")
|
||||
// ============================================================================
|
||||
// REMEMBER: ORDER IS IMPERATIVE! PARAMETERS ARE ENTERED IN THE TABLE BY INDEX!
|
||||
// ============================================================================
|
||||
@@ -68,39 +87,40 @@ typedef enum {
|
||||
PARAM_DEF(NUM_MOVES, u32, 0, "") \
|
||||
PARAM_DEF(MOVE_START, u32, 0, "s") \
|
||||
PARAM_DEF(MOVE_END, u32, 0, "s") \
|
||||
PARAM_DEF(DRIVE_DIST, u16, 10, "ft") /*4*/\
|
||||
PARAM_DEF(JACK_DIST, u8, 5, "in") \
|
||||
PARAM_DEF(DRIVE_DIST, f32, 10, "ft") \
|
||||
PARAM_DEF(JACK_DIST, f32, 5, "in") \
|
||||
PARAM_DEF(DRIVE_KE, f32, 4000, "n/ft") \
|
||||
PARAM_DEF(DRIVE_KT, f32, 600, "ms/ft") \
|
||||
PARAM_DEF(JACK_KT, f32, 600, "ms/in") /*8*/\
|
||||
PARAM_DEF(KEYCODE_0, i64, 0x19000000005D0C61, "") \
|
||||
PARAM_DEF(KEYCODE_1, i64, 0x19000000005D0C62, "") \
|
||||
PARAM_DEF(KEYCODE_2, i64, 0x19000000005D0C64, "") \
|
||||
PARAM_DEF(KEYCODE_3, i64, 0x19000000005D0C68, "") /*12*/\
|
||||
PARAM_DEF(KEYCODE_4, i64, -1, "") \
|
||||
PARAM_DEF(KEYCODE_5, i64, -1, "") \
|
||||
PARAM_DEF(KEYCODE_6, i64, -1, "") \
|
||||
PARAM_DEF(KEYCODE_7, i64, -1, "") /*16*/\
|
||||
PARAM_DEF(JACK_KT, f32, 600, "ms/in") \
|
||||
PARAM_DEF(KEYCODE_0, i32, -1, "") \
|
||||
PARAM_DEF(KEYCODE_1, i32, -1, "") \
|
||||
PARAM_DEF(KEYCODE_2, i32, -1, "") \
|
||||
PARAM_DEF(KEYCODE_3, i32, -1, "") \
|
||||
PARAM_DEF(KEYCODE_4, i32, -1, "") \
|
||||
PARAM_DEF(KEYCODE_5, i32, -1, "") \
|
||||
PARAM_DEF(KEYCODE_6, i32, -1, "") \
|
||||
PARAM_DEF(KEYCODE_7, i32, -1, "") \
|
||||
PARAM_DEF(ADC_ALPHA_BATTERY, f32, 0.02, "-") \
|
||||
PARAM_DEF(ADC_ALPHA_ISENS, f32, 0.02, "-") \
|
||||
PARAM_DEF(ADC_ALPHA_IAZ, f32, 0.005, "-") \
|
||||
PARAM_DEF(ADC_DB_IAZ, f32, 5.0, "A") /*20*/\
|
||||
PARAM_DEF(ADC_DB_IAZ, f32, 5.0, "A") \
|
||||
PARAM_DEF(EFUSE_INOM_1, f32, 40.0, "A") \
|
||||
PARAM_DEF(EFUSE_INOM_2, f32, 6.0, "A") \
|
||||
PARAM_DEF(EFUSE_INOM_3, f32, 2.0, "A") \
|
||||
PARAM_DEF(EFUSE_HEAT_THRESH, f32, 60.0, "i/i^2-s") /*24*/\
|
||||
PARAM_DEF(EFUSE_HEAT_THRESH, f32, 60.0, "i/i^2-s") \
|
||||
PARAM_DEF(EFUSE_KINST, f32, 4.0, "i/i") \
|
||||
PARAM_DEF(EFUSE_TAUCOOL, f32, 0.2, "i") \
|
||||
PARAM_DEF(EFUSE_TCOOL, i64, 5000000, "us") \
|
||||
PARAM_DEF(LOW_PROTECTION_V, f32, 10.0, "V") /*28*/\
|
||||
PARAM_DEF(LOW_PROTECTION_V, f32, 10.0, "V") \
|
||||
PARAM_DEF(LOW_PROTECTION_S, i64, 10, "s") \
|
||||
PARAM_DEF(CHG_LOW_V, f32, 5.0, "V") \
|
||||
PARAM_DEF(CHG_LOW_S, i64, 5.0, "s") \
|
||||
PARAM_DEF(CHG_BULK_S, i64, 20, "s") /*32*/\
|
||||
PARAM_DEF(CHG_LOW_S, i64, 5, "s") \
|
||||
PARAM_DEF(CHG_BULK_S, i64, 20, "s") \
|
||||
PARAM_DEF(RF_PULSE_LENGTH, u64, 350000, "us") \
|
||||
PARAM_DEF(V_SENS_OFFSET, f32, 0.4, "V") \
|
||||
|
||||
|
||||
PARAM_DEF(WIFI_CHANNEL, i16, 6, "") \
|
||||
PARAM_DEF(WIFI_SSID, str, "sc.local", "") \
|
||||
PARAM_DEF(WIFI_PASS, str, "password", "") \
|
||||
|
||||
|
||||
// Generate enum for parameter indices
|
||||
@@ -111,9 +131,10 @@ typedef enum {
|
||||
} param_idx_t;
|
||||
#undef PARAM_DEF
|
||||
|
||||
#define PARAMS_SIZE sizeof(param_stored_t)
|
||||
#define FLASH_SECTOR_SIZE 4096
|
||||
#define PARAMS_OFFSET 0
|
||||
#define PARAMS_TOTAL_SIZE (NUM_PARAMS * PARAMS_SIZE)
|
||||
#define PARAMETER_NUM_SECTORS 4
|
||||
#define LOG_START_OFFSET (FLASH_SECTOR_SIZE * PARAMETER_NUM_SECTORS)
|
||||
|
||||
// External declarations
|
||||
extern param_value_t parameter_table[NUM_PARAMS];
|
||||
@@ -122,36 +143,38 @@ extern const param_type_e parameter_types[NUM_PARAMS];
|
||||
extern const char* parameter_names[NUM_PARAMS];
|
||||
extern const char parameter_units[NUM_PARAMS][8];
|
||||
|
||||
esp_err_t storage_init();
|
||||
esp_err_t log_init();
|
||||
// Core functions
|
||||
esp_err_t storage_init(void);
|
||||
esp_err_t log_init(void);
|
||||
void storage_deinit(void);
|
||||
|
||||
// Parameter access functions
|
||||
param_value_t get_param_value_t(param_idx_t id);
|
||||
esp_err_t set_param_value_t(param_idx_t id, param_value_t val);
|
||||
param_type_e get_param_type(param_idx_t id);
|
||||
const char* get_param_name(param_idx_t id);
|
||||
param_value_t get_param_default(param_idx_t id);
|
||||
const char* get_param_unit(param_idx_t id);
|
||||
const char* get_param_json_string(param_idx_t id, char* buffer, size_t buf_size);
|
||||
|
||||
esp_err_t commit_params();
|
||||
|
||||
uint32_t get_log_head();
|
||||
uint32_t get_log_tail();
|
||||
uint32_t get_log_offset();
|
||||
|
||||
esp_err_t write_dummy_log_1();
|
||||
esp_err_t write_dummy_log_2();
|
||||
esp_err_t write_dummy_log_3();
|
||||
// Helper functions for string parameters
|
||||
esp_err_t set_param_string(param_idx_t id, const char* str);
|
||||
char* get_param_string(param_idx_t id);
|
||||
|
||||
// Storage operations
|
||||
esp_err_t commit_params(void);
|
||||
|
||||
// Log functions
|
||||
#define LOG_ENTRY_SIZE 32
|
||||
#define FLASH_SECTOR_SIZE 4096
|
||||
|
||||
// Calculate offset for log area (after parameters sector)
|
||||
#define PARARMETER_NUM_SECTORS 4
|
||||
#define LOG_START_OFFSET FLASH_SECTOR_SIZE*PARARMETER_NUM_SECTORS
|
||||
|
||||
uint32_t get_log_head(void);
|
||||
uint32_t get_log_tail(void);
|
||||
uint32_t get_log_offset(void);
|
||||
esp_err_t write_log(char* entry);
|
||||
|
||||
void storage_deinit();
|
||||
// Test functions
|
||||
esp_err_t write_dummy_log_1(void);
|
||||
esp_err_t write_dummy_log_2(void);
|
||||
esp_err_t write_dummy_log_3(void);
|
||||
|
||||
#endif /* MAIN_STORAGE_H_ */
|
||||
@@ -51,16 +51,9 @@ static bool parse_uint64(const char *str, uint64_t *result) {
|
||||
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_u8:
|
||||
printf("%u (0x%02X)\n", val.u8, val.u8);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_i8:
|
||||
printf("%d (0x%02X)\n",
|
||||
val.i8, (uint8_t)val.i8);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_u16:
|
||||
printf("%u (0x%04X)\n",
|
||||
val.u16, val.u16);
|
||||
@@ -102,6 +95,12 @@ static void print_param_value(param_idx_t id, param_value_t val) {
|
||||
val.f64, (unsigned long long)val.u64);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_str:
|
||||
memcpy(val.str, sbuf, 8);
|
||||
sbuf[8] = '\0';
|
||||
printf("\"%s\"", sbuf);
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("UNKNOWN TYPE\n");
|
||||
break;
|
||||
@@ -114,7 +113,7 @@ static esp_err_t parse_param_value(const char *orig_str, param_type_e type, para
|
||||
while (isspace((unsigned char)*str)) str++;
|
||||
|
||||
// Check for negative sign on unsigned integer types
|
||||
bool is_unsigned_int = (type == PARAM_TYPE_u8 || type == PARAM_TYPE_u16 || type == PARAM_TYPE_u32 || type == PARAM_TYPE_u64);
|
||||
bool is_unsigned_int = (type == PARAM_TYPE_u16 || type == PARAM_TYPE_u32 || type == PARAM_TYPE_u64);
|
||||
if (is_unsigned_int && *str == '-') {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -123,14 +122,12 @@ static esp_err_t parse_param_value(const char *orig_str, param_type_e type, para
|
||||
errno = 0;
|
||||
|
||||
switch (type) {
|
||||
case PARAM_TYPE_u8:
|
||||
case PARAM_TYPE_u16:
|
||||
case PARAM_TYPE_u32:
|
||||
case PARAM_TYPE_u64:
|
||||
val->u64 = strtoull(str, &endptr, 0);
|
||||
break;
|
||||
|
||||
case PARAM_TYPE_i8:
|
||||
case PARAM_TYPE_i16:
|
||||
case PARAM_TYPE_i32:
|
||||
case PARAM_TYPE_i64:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,22 +1,52 @@
|
||||
<!doctype html><title>Control Panel</title><style>*{color:#eee;background-color:#111;font-family:sans-serif}input,button{text-align:right;box-sizing:border-box;background-color:#333;border:1px solid #666;width:100%;font-family:monospace}button{text-align:center}.changed{color:#111!important;background-color:#3d3!important}#commit_btn{color:#111;cursor:pointer;background-color:#3d3;border:none;width:100%;margin-top:10px;padding:10px;font-weight:700}#commit_btn[disabled]{color:#888;cursor:not-allowed;background-color:#444}table{border-collapse:collapse;width:100%}td{border-bottom:1px solid #222;padding:8px}tr:hover{background-color:#1a1a1a}summary{text-align:left;color:#ccc;background-color:#723;padding:.3rem;font-weight:700}</style></head><body><button disabled id=commit_btn onclick=commit_params()>Save Changes</button><table><tr><td>Schedule Start</td><td><input id=move_start onchange=changeSchedule(this) type=time></td></tr><tr><td>Schedule End</td><td><input id=move_end onchange=changeSchedule(this) type=time></td></tr><tr><td># Moves/Day</td><td><input id=num_moves min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Move Distance</td><td><input id=drive_dist min=0 onchange=changeSchedule(this) type=number></td><td>ft</td></tr><tr><td>Jack Height</td><td><input id=jack_dist min=0 onchange=changeSchedule(this) type=number></td><td>in</td></tr><tr><td>Time</td><td><input id=in_time onchange=markChanged(this) step=1 type=datetime-local></td><td><button id=now_btn onclick=setTimeToNow()>< NOW</button></td></tr><tr><td>Battery</td><td><input id=voltage readonly></td><td>V</td></tr><tr><td></td><td><button onclick="cmd('start')">START MOVE</button></td></tr><tr><td></td><td><button onclick="cmd('undo')">UNDO MOVE</button></td></tr><tr><td></td><td><button onclick="cmd('stop')" style=background-color:#800>STOP MOVE</button></td></tr></table><br><details><summary>DANGER ZONE</summary> <table><tr><td>Program RF Remote</td><td><button onclick=programRF(0) style=width:40%>Fwd</button> <button onclick=programRF(1) style=width:40%>Rev</button> <button onclick=programRF(2) style=width:40%>Up</button> <button onclick=programRF(3) style=width:40%>Down</button> <button onclick=programRF(-1)>Cancel Learning</button></td></tr><tr><td>Calibration</td><td><button id=cal_jack_btn>Jack Calibration</button> <button id=cal_drive_btn>Drive Calibration</button></td></tr><tr><td>Firmware</td><td><input accept=.bin id=firmware_file type=file> <button id=upload_btn onclick=uploadFirmware()>Upload Firmware</button></td></tr><tr><td>Log File</td><td><button id=log_btn onclick=downloadLogFile()>Download Log</button></td></tr></table> <table id=table></table></details><script>let param_values=[],param_names=[],param_units=[];function cmd(x){if(x===`start`&&!confirm(`Will begin moving - please confirm.`))return;let xhr=new XMLHttpRequest;xhr.open(`POST`,`./cmd`,!0),xhr.setRequestHeader(`Content-Type`,`application/json`),xhr.send(JSON.stringify({cmd:x})),xhr.onload=function(){console.log(xhr)}}function ge(x){return document.getElementById(x)}
|
||||
<!doctype html><title>Control Panel</title><meta content="width=device-width,initial-scale=1.0" name=viewport><style>#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}*{color:#2f2f2f;background-color:#fff;font-family:Noto Sans,Verdana,sans-serif;font-size:1.2rem}input,button{text-align:right;box-sizing:border-box;background-color:#efede9;border:1px solid #ba965b;border-radius:5px;width:100%}input[type=text],input[type=number]{font-family:monospace}button{text-align:center}.changed,#commit_btn{color:#fff!important;background-color:#2a493d!important}#commit_btn{cursor:pointer;border:none;width:100%;margin-top:10px;padding:10px;font-weight:700}#commit_btn[disabled]{color:#888;cursor:not-allowed;background-color:#444!important}table{border-collapse:collapse;text-align:left;width:100%}td{border-bottom:1px solid #efede9;padding:8px}summary{text-align:left;color:#fff;background-color:#723;border-radius:5px;padding:.3rem;font-weight:700}.cmd{border:none;font-size:1.5rem}#msg{text-align:center}h1{font-size:2.5rem}@media screen and (width<=350px){#content{max-width:100%;padding:0 5px}table tr td{box-sizing:border-box;width:100%;display:block}table tr{margin-bottom:10px;display:block}}</style></head><body><div id=wrapper><div id=content><h1>ClusterCommand</h1><table><tr><td><button onclick="sendCommand('start')" class=cmd>START</button></td><td><button onclick="sendCommand('stop')" class=cmd style=color:#fff;background-color:#723>STOP</button></td><td><button onclick="sendCommand('undo')" class=cmd>UNDO</button></td></tr></table><table><tr><td colspan=3><input id=msg readonly></td></tr><tr><td colspan=3><input id=in_time onchange=markChanged(this) step=1 type=datetime-local> <button id=now_btn onclick=setTimeToNow()>Sync Time</button></td></tr><tr><td>Schedule Start</td><td><input id=move_start onchange=changeSchedule(this) type=time></td></tr><tr><td>Schedule End</td><td><input id=move_end onchange=changeSchedule(this) type=time></td></tr><tr><td># Moves/Day</td><td><input id=num_moves min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Remain. Distance (ft)</td><td><input id=remaining_dist onchange=markChanged(this) type=number></td></tr><tr><td>Move Distance (ft)</td><td><input id=drive_dist min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Jack Height (in)</td><td><input id=jack_dist min=0 onchange=changeSchedule(this) type=number></td></tr><tr><td>Battery (V)</td><td><input id=voltage readonly></td></tr></table><button disabled id=commit_btn onclick=commitParams()>Save Changes</button><br><br><details><summary>DANGER ZONE</summary> <table><tr><td>Program RF Remote</td><td><button onclick=programRFSequence()>Program All Buttons</button></td></tr><tr><td>Calibration</td><td><button onclick="calibrate('jack')">Jack Calibration</button> <button onclick="calibrate('drive')">Drive Calibration</button></td></tr><tr><td>Firmware</td><td><input accept=.bin id=firmware_file type=file> <button id=upload_btn onclick=uploadFirmware()>Upload Firmware</button></td></tr><tr><td>Log File</td><td><button id=log_btn onclick=downloadLogFile()>Download Log</button></td></tr></table> <table id=table></table> <td><button onclick="sendCommand('reboot')" class=cmd style=color:#fff;background-color:#723>REBOOT</button></td></details></div></div><script>let paramValues=[],paramNames=[],paramUnits=[];const ge=id=>document.getElementById(id);async function sendCommand(cmdName){cmdName===`start`&&!confirm(`Will begin moving - please confirm.`)||cmdName===`reboot`&&!confirm(`Device will reboot - clearing clock and distance. Are you sure?`)||await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:cmdName})})}async function calibrate(axis){await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`cal_${axis}_start`})});let amt=prompt(`Press button on mover. Press button again to stop it. Then, type in actual travelled distance here in inches:`);if(!isNaN(parseFloat(amt))){let response=await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`cal_get`})});if(response.ok){let data=await response.json();axis===`drive`?(markChanged(ge(`in_6`)).value=data.e/amt,markChanged(ge(`in_7`)).value=data.t/amt*1.2):markChanged(ge(`in_8`)).value=data.t/amt}}}
|
||||
// Highlight changed inputs and enable the save button
|
||||
function markChanged(el){el.classList.add(`changed`),ge(`commit_btn`).disabled=!1}function toFixed(input,n){let num=parseFloat(input);return isNaN(num)?null:Number(num.toFixed(n))}
|
||||
function markChanged(el){return el.classList.add(`changed`),ge(`commit_btn`).disabled=!1,el}function toFixed(input,n){let num=parseFloat(input);return isNaN(num)?null:Number(num.toFixed(n))}
|
||||
// --- 1. GET DATA ---
|
||||
function fetchStatus(){let xhr=new XMLHttpRequest;xhr.open(`GET`,`./status`,!0),xhr.onload=function(){if(xhr.status===200)try{console.log(xhr.responseText);let data=JSON.parse(xhr.responseText);
|
||||
async function fetchStatus(){try{let response=await fetch(`./status`);if(!response.ok)return;let data=await response.json();
|
||||
// Update time field if available
|
||||
if(data.time){let date=(/* @__PURE__ */ new Date(data.time*1e3)).toISOString().slice(0,19);ge(`in_time`).value=date}ge(`voltage`).value=toFixed(data.battery,2),param_values=data.values||[],param_names=data.names||[],param_units=data.units||[],data.rtc_set||(ge(`in_time`).classList.add(`error`),confirm(`Clock not set. Sync with this device's clock?`)&&(setTimeToNow(),commit_time()))}catch(e){console.error(`Error parsing JSON`,e)}
|
||||
if(console.log(data),data.time){let date=(/* @__PURE__ */ new Date(data.time*1e3)).toISOString().slice(0,19);ge(`in_time`).value=date}ge(`voltage`).value=toFixed(data.battery,2),ge(`remaining_dist`).value=toFixed(data.remaining_dist,2),ge(`msg`).value=data.msg,paramValues=data.values||[],paramNames=data.names||[],paramUnits=data.units||[],ge(`commit_btn`).disabled=!0,data.rtc_set||(ge(`in_time`).classList.add(`error`),confirm(`Clock not set. Sync with this device's clock?`)&&(setTimeToNow(),commitTime()))}catch(e){console.error(`Error parsing JSON`,e)}
|
||||
// Always render table even if request fails or data is empty
|
||||
renderTable()},xhr.onerror=function(e){console.error(`Network error`,e),renderTable()},xhr.send()}function s_to_hhmm(s){return`${String(Math.floor(s/3600)).padStart(2,`0`)}:${String(Math.floor(s%3600/60)).padStart(2,`0`)}`}function renderTable(){let table=ge(`table`);
|
||||
// Clear existing parameter rows (rows between index 0 and the last row)
|
||||
for(;table.rows.length>0;)table.deleteRow(0);param_names.forEach((name,i)=>{let row=table.insertRow(table.rows.length);row.innerHTML=`
|
||||
renderTable()}function secondsToHHMM(seconds){return`${String(Math.floor(seconds/3600)).padStart(2,`0`)}:${String(Math.floor(seconds%3600/60)).padStart(2,`0`)}`}function renderTable(){let table=ge(`table`);
|
||||
// Clear existing parameter rows
|
||||
for(;table.rows.length>0;)table.deleteRow(0);
|
||||
// Loop through the NAMES array to ensure every input is shown
|
||||
paramNames.forEach((name,i)=>{let row=table.insertRow(table.rows.length),pname=paramNames[i]!==void 0&¶mNames[i]!==null?paramNames[i]:`null`,pval=paramValues[i]!==void 0&¶mValues[i]!==null?paramValues[i]:`null`;row.innerHTML=`
|
||||
<td>${i}</td>
|
||||
<td>${param_names[i]!==void 0&¶m_names[i]!==null?param_names[i]:`null`}</td>
|
||||
<td><input type="number" id="in_${i}" value="${param_values[i]!==void 0&¶m_values[i]!==null?param_values[i]:`null`}" oninput="markChanged(this)"></td>
|
||||
<td>${param_units[i]||``}</td>
|
||||
`}),ge(`num_moves`).value=param_values[1],ge(`move_start`).value=s_to_hhmm(param_values[2]),ge(`move_end`).value=s_to_hhmm(param_values[3]),ge(`drive_dist`).value=param_values[4],ge(`jack_dist`).value=param_values[5]}function changeSchedule(e){if(markChanged(e),e.id==`num_moves`&&(ge(`in_1`).value=e.value,markChanged(ge(`in_1`))),e.id==`move_start`){let[hours,minutes]=e.value.split(`:`).map(Number);ge(`in_2`).value=hours*3600+minutes*60,markChanged(ge(`in_2`))}if(e.id==`move_end`){let[hours,minutes]=e.value.split(`:`).map(Number);ge(`in_3`).value=hours*3600+minutes*60,markChanged(ge(`in_3`))}e.id==`drive_dist`&&(ge(`in_4`).value=e.value,markChanged(ge(`in_4`))),e.id==`jack_dist`&&(ge(`in_5`).value=e.value,markChanged(ge(`in_5`)))}function setTimeToNow(){e=ge(`in_time`),markChanged(e),e.value=(/* @__PURE__ */ new Date()).toLocaleString(`sv-SE`)}function commit_time(){let xhr=new XMLHttpRequest,[datePart,timePart]=ge(`in_time`).value.split(`T`),[year,month,day]=datePart.split(`-`).map(Number),[hour,minute,second=0]=timePart.split(`:`).map(Number),epoch=Math.floor(Date.UTC(year,month-1,day,hour,minute,second)/1e3);xhr.open(`POST`,`./st`,!0),xhr.setRequestHeader(`Content-Type`,`application/json`),xhr.onload=function(){xhr.status===200&&ge(`in_time`).classList.remove(`changed`),document.querySelectorAll(`input.changed`).length===0?ge(`commit_btn`).disabled=!0:ge(`commit_btn`).disabled=!1},xhr.send(epoch.toString())}
|
||||
<td>${pname}</td>
|
||||
<td><input type="${typeof pval==`number`?`number`:`text`}" id="in_${i}" value="${pval}" oninput="markChanged(this)"></td>
|
||||
<td>${paramUnits[i]||``}</td>
|
||||
`});for(let input of document.getElementsByTagName(`input`))input.addEventListener(`click`,e=>{e.target.select()});ge(`num_moves`).value=paramValues[1],ge(`move_start`).value=secondsToHHMM(paramValues[2]),ge(`move_end`).value=secondsToHHMM(paramValues[3]),ge(`drive_dist`).value=paramValues[4],ge(`jack_dist`).value=paramValues[5]}function changeSchedule(e){if(markChanged(e),e.id===`num_moves`&&(ge(`in_1`).value=e.value,markChanged(ge(`in_1`))),e.id===`move_start`){let[hours,minutes]=e.value.split(`:`).map(Number);ge(`in_2`).value=hours*3600+minutes*60,markChanged(ge(`in_2`))}if(e.id===`move_end`){let[hours,minutes]=e.value.split(`:`).map(Number);ge(`in_3`).value=hours*3600+minutes*60,markChanged(ge(`in_3`))}e.id===`drive_dist`&&(ge(`in_4`).value=e.value,markChanged(ge(`in_4`))),e.id===`jack_dist`&&(ge(`in_5`).value=e.value,markChanged(ge(`in_5`)))}function setTimeToNow(){let e=ge(`in_time`);markChanged(e),e.value=(/* @__PURE__ */ new Date()).toLocaleString(`sv-SE`)}async function commitTime(){
|
||||
// Parse the components
|
||||
let[datePart,timePart]=ge(`in_time`).value.split(`T`),[year,month,day]=datePart.split(`-`).map(Number),[hour,minute,second=0]=timePart.split(`:`).map(Number),epoch=Math.floor(Date.UTC(year,month-1,day,hour,minute,second)/1e3);(await fetch(`./st`,{method:`POST`,headers:{"Content-Type":`application/json`},body:epoch.toString()})).ok&&ge(`in_time`).classList.remove(`changed`),document.querySelectorAll(`input.changed`).length===0?ge(`commit_btn`).disabled=!0:ge(`commit_btn`).disabled=!1}
|
||||
// --- 2. POST DATA ---
|
||||
function commit_params(){ge(`commit_btn`).disabled=!0;let changedInputs=document.querySelectorAll(`input.changed`);if(sp={},changedInputs.forEach(input=>{if(input.id===`in_time`)commit_time();else if(input.id.startsWith(`in_`)){
|
||||
async function commitParams(){ge(`commit_btn`).disabled=!0;let changedInputs=document.querySelectorAll(`input.changed`),sp={};for(let input of changedInputs)if(input.id===`in_time`)await commitTime();else if(input.id===`remaining_dist`){let x=parseFloat(ge(`remaining_dist`).value);(await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`remaining_dist`,amt:x})})).ok&&ge(`remaining_dist`).classList.remove(`changed`)}else if(input.id.startsWith(`in_`)){
|
||||
// Parameter handling
|
||||
let id=input.id.split(`_`)[1],val=input.value.toLowerCase()===`null`?null:parseFloat(input.value);sp[id]=val}}),Object.keys(sp).length!==0){let xhr2=new XMLHttpRequest;xhr2.open(`POST`,`./sp`,!0),xhr2.setRequestHeader(`Content-Type`,`application/json`),xhr2.onload=function(){xhr2.status===200?changedInputs.forEach(input=>{input.id!==`in_time`&&input.classList.remove(`changed`)}):ge(`commit_btn`).disabled=!1},xhr2.send(JSON.stringify(sp))}fetchStatus()}function uploadFirmware(){let fileInput=ge(`firmware_file`);if(!fileInput.files.length){alert(`No file selected`);return}let file=fileInput.files[0],xhr=new XMLHttpRequest;xhr.open(`POST`,`./ota`,!0),xhr.setRequestHeader(`Content-Type`,`application/octet-stream`),xhr.onload=function(){xhr.status===200?alert(`Upload successful. Device may reboot.`):alert(`Upload failed: `+xhr.status)},xhr.onerror=function(){alert(`Network error during upload`)},xhr.send(file)}function programRF(i){let xhr=new XMLHttpRequest;xhr.open(`POST`,`./prf`,!0),xhr.setRequestHeader(`Content-Type`,`application/json`),xhr.send(i.toString()),xhr.status}async function downloadLogFile(){try{let response=await fetch(`./log`);if(!response.ok)throw Error(`Network response was not ok`);let blob=await response.blob(),now=/* @__PURE__ */ new Date,formattedDate=`${String(now.getDate()).padStart(2,`0`)}${[`JAN`,`FEB`,`MAR`,`APR`,`MAY`,`JUN`,`JUL`,`AUG`,`SEP`,`OCT`,`NOV`,`DEC`][now.getMonth()]}${now.getFullYear()}-${String(now.getHours()).padStart(2,`0`)}${String(now.getMinutes()).padStart(2,`0`)}`,url=URL.createObjectURL(blob),a=document.createElement(`a`);a.href=url,a.download=`storage-${formattedDate}.bin`,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(url)}catch(error){console.error(`Download failed:`,error)}}
|
||||
let id=input.id.split(`_`)[1];sp[id]=input.value,input.type===`number`&&(sp[id]=input.value.toLowerCase()===`null`?null:parseFloat(input.value))}Object.keys(sp).length!==0&&((await fetch(`./sp`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(sp)})).ok?changedInputs.forEach(input=>{input.id!==`in_time`&&input.classList.remove(`changed`)}):ge(`commit_btn`).disabled=!1),await fetchStatus()}async function uploadFirmware(){let fileInput=ge(`firmware_file`);if(!fileInput.files.length){alert(`No file selected`);return}let file=fileInput.files[0];try{let response=await fetch(`./ota`,{method:`POST`,headers:{"Content-Type":`application/octet-stream`},body:file});response.ok?alert(`Upload successful. Device may reboot.`):alert(`Upload failed: `+response.status)}catch{alert(`Network error during upload`)}}function programRF(i){fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rfp`,channel:i})})}async function programRFSequence(){let buttonNames=[`Forward`,`Reverse`,`Up`,`Down`],learnedCodes=[null,null,null,null];if(!confirm(`This will program all 4 RF remote buttons in sequence.
|
||||
|
||||
Press OK to begin, then follow the prompts.`))return;
|
||||
// Disable RF controls during programming
|
||||
await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_disable`})});for(let i=0;i<4;i++){if(programRF(i),await new Promise(resolve=>setTimeout(resolve,100)),!confirm(`Button ${i+1}/4: ${buttonNames[i]}\n\nPress the ${buttonNames[i]} button on your remote now, then press OK.\n\nPress Cancel to skip this button.`)){programRF(-1),learnedCodes[i]=-1;continue}
|
||||
// Wait for code to be learned (poll for a bit)
|
||||
let learned=!1,startCode=learnedCodes[i];for(let attempt=0;attempt<50;attempt++){await new Promise(resolve=>setTimeout(resolve,100));
|
||||
// Check if code was learned by polling RF status via /cmd
|
||||
try{let data=await(await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_status`})})).json();
|
||||
// Check if this specific index changed from what we started with
|
||||
if(data.codes[i]!==-1&&data.codes[i]!==null&&data.codes[i]!==startCode){learnedCodes[i]=data.codes[i],learned=!0;break}}catch(e){console.error(`Error checking RF status:`,e)}}if(!learned)if(confirm(`Timeout waiting for ${buttonNames[i]} button.\n\nRetry this button?`)){i--;continue}else
|
||||
// Skip this button
|
||||
learnedCodes[i]=-1}
|
||||
// Update input fields for parameters 9-12 (PARAM_KEYCODE_0 through PARAM_KEYCODE_3)
|
||||
for(let i=0;i<4;i++){let input=ge(`in_${9+i}`);input&&(input.value=learnedCodes[i],markChanged(input))}
|
||||
// Commit just the RF keycodes (params 9-12)
|
||||
let sp={};for(let i=0;i<4;i++)sp[9+i]=learnedCodes[i];if((await fetch(`./sp`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(sp)})).ok){
|
||||
// Unhighlight the keycode inputs
|
||||
for(let i=0;i<4;i++){let input=ge(`in_${9+i}`);input&&input.classList.remove(`changed`)}
|
||||
// Check if commit button should stay enabled
|
||||
document.querySelectorAll(`input.changed`).length===0&&(ge(`commit_btn`).disabled=!0)}
|
||||
// Re-enable RF controls after programming
|
||||
await fetch(`./cmd`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({cmd:`rf_enable`})});
|
||||
// Show summary
|
||||
let summary=`RF Remote Programming Complete!
|
||||
|
||||
`;for(let i=0;i<4;i++)learnedCodes[i]===-1?summary+=`${buttonNames[i]}: Not programmed\n`:summary+=`${buttonNames[i]}: ${learnedCodes[i]}\n`;alert(summary),await fetchStatus()}async function downloadLogFile(){try{let response=await fetch(`./log`);if(!response.ok)throw Error(`Network response was not ok`);let blob=await response.blob(),now=/* @__PURE__ */ new Date,formattedDate=`${String(now.getDate()).padStart(2,`0`)}${[`JAN`,`FEB`,`MAR`,`APR`,`MAY`,`JUN`,`JUL`,`AUG`,`SEP`,`OCT`,`NOV`,`DEC`][now.getMonth()]}${now.getFullYear()}-${String(now.getHours()).padStart(2,`0`)}${String(now.getMinutes()).padStart(2,`0`)}`,url=URL.createObjectURL(blob),a=document.createElement(`a`);a.href=url,a.download=`storage-${formattedDate}.bin`,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(url)}catch(error){console.error(`Download failed:`,error)}}
|
||||
// Initial Load
|
||||
window.onload=fetchStatus;</script></body></html>
|
||||
604
main/webserver.c
604
main/webserver.c
@@ -15,6 +15,7 @@
|
||||
#include "power_mgmt.h"
|
||||
#include "rf_433.h"
|
||||
#include "rtc.h"
|
||||
#include "simple_dns_server.h"
|
||||
#include "string.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
@@ -24,6 +25,7 @@
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_http_server.h"
|
||||
//#include "esp_https_server.h"
|
||||
#include "esp_netif.h"
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
@@ -31,7 +33,8 @@
|
||||
#include <time.h>
|
||||
#include "stdio.h"
|
||||
#include "storage.h"
|
||||
//#include "mdns.h"
|
||||
#include "mdns.h"
|
||||
|
||||
#include "webpage.h"
|
||||
|
||||
#include "esp_partition.h"
|
||||
@@ -44,10 +47,17 @@
|
||||
|
||||
static const char *TAG = "WEBSERVER";
|
||||
|
||||
// HTTPS
|
||||
/*
|
||||
extern const uint8_t servercert_pem_start[] asm("_binary_servercert_pem_start");
|
||||
extern const uint8_t servercert_pem_end[] asm("_binary_servercert_pem_end");
|
||||
extern const uint8_t prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");
|
||||
extern const uint8_t prvtkey_pem_end[] asm("_binary_prvtkey_pem_end");
|
||||
*/
|
||||
|
||||
static httpd_handle_t httpServerInstance = NULL;
|
||||
|
||||
char httpBuffer[1024];
|
||||
char httpBuffer[4096];
|
||||
|
||||
/* Handler to serve the HTML page */
|
||||
static esp_err_t root_get_handler(httpd_req_t *req) {
|
||||
@@ -55,131 +65,174 @@ static esp_err_t root_get_handler(httpd_req_t *req) {
|
||||
// Send the HTML response
|
||||
|
||||
httpd_resp_set_type(req, "text/html"); // Original MIME type
|
||||
httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); // Tell browser it's gzipped
|
||||
return httpd_resp_send(req, (const char *)html_content, html_content_len);
|
||||
httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); // Tell browser it's gzipped
|
||||
return httpd_resp_send(req, (const char *)html_content, html_content_len);
|
||||
}
|
||||
|
||||
// Cache the storage partition pointer to avoid repeated lookups
|
||||
static const esp_partition_t *cached_storage_partition = NULL;
|
||||
|
||||
static esp_err_t log_handler(httpd_req_t *req) {
|
||||
ESP_LOGI(TAG, "log_handler");
|
||||
|
||||
|
||||
int32_t tail = -1;
|
||||
int32_t tail = -1;
|
||||
|
||||
if (req -> method == HTTP_GET) {
|
||||
// give the whole log
|
||||
}
|
||||
if (req -> method == HTTP_POST) {
|
||||
int ret = httpd_req_recv(req, httpBuffer, sizeof(httpBuffer));
|
||||
if (ret <= 0) {
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
httpd_resp_send_408(req);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
httpBuffer[ret] = '\0'; // Null-terminate the string
|
||||
|
||||
ESP_LOGI(TAG, "ST POST %.*s", ret, httpBuffer);
|
||||
|
||||
if(sscanf(httpBuffer, "%ld", (long*)&tail) != 1) {
|
||||
// if malformed, just send the whole log.
|
||||
//httpd_resp_send_err(req, 400, "INVALID TAIL POINTER");
|
||||
}
|
||||
}
|
||||
// give the whole log
|
||||
}
|
||||
if (req -> method == HTTP_POST) {
|
||||
int ret = httpd_req_recv(req, httpBuffer, sizeof(httpBuffer));
|
||||
if (ret <= 0) {
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
httpd_resp_send_408(req);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
httpBuffer[ret] = '\0'; // Null-terminate the string
|
||||
|
||||
//ESP_LOGI(TAG, "LOG POST %.*s", ret, httpBuffer);
|
||||
|
||||
if(sscanf(httpBuffer, "%ld", (long*)&tail) != 1) {
|
||||
// if malformed, just send the whole log.
|
||||
tail = -1;
|
||||
}
|
||||
}
|
||||
|
||||
const esp_partition_t *storage_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage");
|
||||
// Use cached partition pointer instead of looking it up each time
|
||||
const esp_partition_t *storage_partition = cached_storage_partition;
|
||||
if (storage_partition == NULL) {
|
||||
ESP_LOGE(TAG, "Storage partition not found");
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Storage partition not found");
|
||||
// Fall back to lookup if cache is empty (shouldn't happen in normal operation)
|
||||
storage_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
||||
ESP_PARTITION_SUBTYPE_ANY,
|
||||
"storage");
|
||||
if (storage_partition == NULL) {
|
||||
ESP_LOGE(TAG, "Storage partition not found");
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Storage partition not found");
|
||||
}
|
||||
cached_storage_partition = storage_partition;
|
||||
}
|
||||
|
||||
// Figure out the bounds of data
|
||||
if (tail < 0)
|
||||
tail = get_log_tail();
|
||||
// Get head and tail atomically and store in local variables
|
||||
// This releases the mutex before we do any partition operations
|
||||
int32_t head = get_log_head();
|
||||
int32_t total_size = head - tail + 8; // 8 bytes for the head/tail pointers
|
||||
int32_t offset = tail;
|
||||
int32_t sent = 0;
|
||||
int32_t log_start = get_log_offset();
|
||||
|
||||
if (tail >= head) {
|
||||
total_size = storage_partition->size - tail + head - get_log_offset() + 8;
|
||||
if (tail < 0) {
|
||||
tail = get_log_tail();
|
||||
} else {
|
||||
// Validate tail is within log area bounds
|
||||
if (tail < log_start || tail >= (int32_t)storage_partition->size) {
|
||||
ESP_LOGW(TAG, "Invalid tail pointer %ld, using current tail", (long)tail);
|
||||
tail = get_log_tail();
|
||||
}
|
||||
// Also validate tail is aligned to LOG_ENTRY_SIZE
|
||||
if ((tail - log_start) % LOG_ENTRY_SIZE != 0) {
|
||||
ESP_LOGW(TAG, "Tail pointer %ld not aligned to entry size, using current tail",
|
||||
(long)tail);
|
||||
tail = get_log_tail();
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total size to send
|
||||
int32_t total_size;
|
||||
if (tail == head) {
|
||||
// Empty log - just send pointers
|
||||
total_size = 8;
|
||||
} else if (tail < head) {
|
||||
// Normal case: tail before head
|
||||
total_size = head - tail + 8; // +8 for head/tail pointers
|
||||
} else {
|
||||
// Wrapped case: tail after head
|
||||
total_size = (storage_partition->size - tail) + (head - log_start) + 8;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "start/end: %ld/%ld -> %ld", (long)tail, (long)head, (long)total_size);
|
||||
//ESP_LOGI(TAG, "Log bounds: tail=%ld, head=%ld, total_size=%ld",
|
||||
// (long)tail, (long)head, (long)total_size);
|
||||
|
||||
// Send header
|
||||
// Send HTTP headers
|
||||
char len_str[16];
|
||||
sprintf(len_str, "%u", (unsigned)total_size);
|
||||
httpd_resp_set_type(req, "application/octet-stream");
|
||||
httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"sc_storage.bin\"");
|
||||
httpd_resp_set_hdr(req, "Content-Length", len_str);
|
||||
|
||||
|
||||
int32_t htail = htobe32(tail);
|
||||
int32_t hhead = htobe32(head);
|
||||
// Send head/tail pointers
|
||||
// Send head/tail pointers in big-endian format
|
||||
int32_t htail = htobe32(tail);
|
||||
int32_t hhead = htobe32(head);
|
||||
memcpy(&httpBuffer[0], &(htail), 4);
|
||||
memcpy(&httpBuffer[4], &(hhead), 4);
|
||||
if (httpd_resp_send_chunk(req, (const char *)httpBuffer, 8) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send chunk");
|
||||
ESP_LOGE(TAG, "Failed to send head/tail chunk");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
sent += 8;
|
||||
|
||||
int32_t sent = 8;
|
||||
int32_t offset = tail;
|
||||
|
||||
// Only send data if there's something to send
|
||||
if (tail != head) {
|
||||
// Send data between tail and end (if wrapped)
|
||||
if (tail >= head) {
|
||||
ESP_LOGI(TAG, "STARTING wrapped section (tail=%ld, head=%ld)", (long)tail, (long)head);
|
||||
while (offset < storage_partition->size) {
|
||||
// FIXED: Don't limit by head in this section - read to end of partition
|
||||
size_t to_read = MIN(sizeof(httpBuffer), storage_partition->size - offset);
|
||||
esp_err_t err = esp_partition_read(storage_partition, offset, httpBuffer, to_read);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read partition: %s", esp_err_to_name(err));
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read storage");
|
||||
}
|
||||
ESP_LOGI(TAG, "Sending wrapped chunk: offset=%ld size=%d", (long)offset, to_read);
|
||||
if (httpd_resp_send_chunk(req, (const char *)httpBuffer, to_read) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send chunk");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
sent += to_read;
|
||||
offset += to_read;
|
||||
}
|
||||
// Loop back to the beginning
|
||||
offset = get_log_offset();
|
||||
ESP_LOGI(TAG, "Wrapped to beginning, offset now=%ld", (long)offset);
|
||||
}
|
||||
|
||||
// Send data between start (or tail) and head
|
||||
ESP_LOGI(TAG, "FINISHING final section (offset=%ld, head=%ld)", (long)offset, (long)head);
|
||||
while (offset < head) {
|
||||
size_t to_read = MIN(sizeof(httpBuffer), head - offset);
|
||||
esp_err_t err = esp_partition_read(storage_partition, offset, httpBuffer, to_read);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read partition: %s", esp_err_to_name(err));
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read storage");
|
||||
}
|
||||
ESP_LOGI(TAG, "Sending final chunk: offset=%ld size=%d", (long)offset, to_read);
|
||||
if (httpd_resp_send_chunk(req, (const char *)httpBuffer, to_read) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send chunk");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
sent += to_read;
|
||||
offset += to_read;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Transfer complete: sent %ld bytes (expected %ld)", (long)sent, (long)total_size);
|
||||
|
||||
|
||||
// Handle wrapped case: send from tail to end of partition first
|
||||
if (tail > head) {
|
||||
//ESP_LOGI(TAG, "Wrapped log: sending tail=%ld to partition_end=%lu",
|
||||
// (long)tail, (unsigned long)storage_partition->size);
|
||||
|
||||
while (offset < (int32_t)storage_partition->size) {
|
||||
size_t to_read = MIN(sizeof(httpBuffer), storage_partition->size - offset);
|
||||
esp_err_t err = esp_partition_read(storage_partition, offset, httpBuffer, to_read);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
|
||||
(long)offset, esp_err_to_name(err));
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to read storage");
|
||||
}
|
||||
|
||||
if (httpd_resp_send_chunk(req, (const char *)httpBuffer, to_read) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send chunk at offset %ld", (long)offset);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
sent += to_read;
|
||||
offset += to_read;
|
||||
}
|
||||
|
||||
// Wrap to beginning of log area
|
||||
offset = log_start;
|
||||
//ESP_LOGI(TAG, "Wrapped to log start, offset=%ld", (long)offset);
|
||||
}
|
||||
|
||||
// Send from current offset to head
|
||||
//ESP_LOGI(TAG, "Sending final section: offset=%ld to head=%ld", (long)offset, (long)head);
|
||||
|
||||
while (offset < head) {
|
||||
size_t to_read = MIN(sizeof(httpBuffer), head - offset);
|
||||
esp_err_t err = esp_partition_read(storage_partition, offset, httpBuffer, to_read);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
|
||||
(long)offset, esp_err_to_name(err));
|
||||
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Failed to read storage");
|
||||
}
|
||||
|
||||
if (httpd_resp_send_chunk(req, (const char *)httpBuffer, to_read) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send chunk at offset %ld", (long)offset);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
sent += to_read;
|
||||
offset += to_read;
|
||||
}
|
||||
}
|
||||
|
||||
//ESP_LOGI(TAG, "Transfer complete: sent %ld bytes (expected %ld)", (long)sent, (long)total_size);
|
||||
|
||||
// End chunked transfer
|
||||
if (httpd_resp_send_chunk(req, NULL, 0) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send final empty chunk");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Final empty chunk sent successfully");
|
||||
//ESP_LOGI(TAG, "Final empty chunk sent successfully");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -188,7 +241,7 @@ static esp_err_t st_post_handler(httpd_req_t *req) {
|
||||
ESP_LOGI(TAG, "st_post_handler");
|
||||
// Send the HTML response
|
||||
|
||||
int ret = httpd_req_recv(req, httpBuffer, sizeof(httpBuffer));
|
||||
int ret = httpd_req_recv(req, httpBuffer, sizeof(httpBuffer));
|
||||
if (ret <= 0) {
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
httpd_resp_send_408(req);
|
||||
@@ -196,18 +249,17 @@ static esp_err_t st_post_handler(httpd_req_t *req) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
httpBuffer[ret] = '\0'; // Null-terminate the string
|
||||
|
||||
ESP_LOGI(TAG, "ST POST %.*s", ret, httpBuffer);
|
||||
int64_t tv = -1;
|
||||
|
||||
/*if(sscanf(httpBuffer, "%d-%d-%dT%d:%d", &tv) == 1) {
|
||||
system_rtc_set_raw_time(tv);
|
||||
}*/
|
||||
if(sscanf(httpBuffer, "%lld", &tv) == 1) {
|
||||
system_rtc_set_raw_time(tv);
|
||||
}
|
||||
|
||||
|
||||
|
||||
ESP_LOGI(TAG, "ST POST %.*s", ret, httpBuffer);
|
||||
int64_t tv = -1;
|
||||
|
||||
/*if(sscanf(httpBuffer, "%d-%d-%dT%d:%d", &tv) == 1) {
|
||||
system_rtc_set_raw_time(tv);
|
||||
}*/
|
||||
if(sscanf(httpBuffer, "%lld", &tv) == 1) {
|
||||
system_rtc_set_raw_time(tv);
|
||||
}
|
||||
|
||||
return httpd_resp_send(req, "200 OK", HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
@@ -274,53 +326,50 @@ static esp_err_t sp_post_handler(httpd_req_t *req) {
|
||||
}
|
||||
|
||||
// Get the value
|
||||
if (!cJSON_IsNumber(item)) {
|
||||
ESP_LOGW(TAG, "Parameter %s has non-numeric value", key);
|
||||
params_failed++;
|
||||
continue;
|
||||
}
|
||||
if (cJSON_IsNumber(item)) {
|
||||
|
||||
double param_val = item->valuedouble;
|
||||
|
||||
ESP_LOGI(TAG, "Updating Param '%s' (ID: %d) to Value: %.2f", key, param_id, param_val);
|
||||
|
||||
// Set the parameter based on its type
|
||||
switch(get_param_type(param_id)) {
|
||||
case PARAM_TYPE_u8:
|
||||
set_param_value_t(param_id, (param_value_t){.u8 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_i8:
|
||||
set_param_value_t(param_id, (param_value_t){.i8 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_u16:
|
||||
set_param_value_t(param_id, (param_value_t){.u16 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_i16:
|
||||
set_param_value_t(param_id, (param_value_t){.i16 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_u32:
|
||||
set_param_value_t(param_id, (param_value_t){.u32 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_i32:
|
||||
set_param_value_t(param_id, (param_value_t){.i32 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_u64:
|
||||
set_param_value_t(param_id, (param_value_t){.u64 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_i64:
|
||||
set_param_value_t(param_id, (param_value_t){.i64 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_f32:
|
||||
set_param_value_t(param_id, (param_value_t){.f32 = param_val});
|
||||
break;
|
||||
case PARAM_TYPE_f64:
|
||||
set_param_value_t(param_id, (param_value_t){.f64 = param_val});
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown parameter type for ID %d", param_id);
|
||||
params_failed++;
|
||||
continue;
|
||||
}
|
||||
double param_val = item->valuedouble;
|
||||
|
||||
ESP_LOGI(TAG, "Updating Param '%s' (ID: %d) to Value: %.2f", key, param_id, param_val);
|
||||
|
||||
// Set the parameter based on its type
|
||||
switch(get_param_type(param_id)) {
|
||||
case PARAM_TYPE_u16:
|
||||
set_param_value_t(param_id, (param_value_t){.u16 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_i16:
|
||||
set_param_value_t(param_id, (param_value_t){.i16 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_u32:
|
||||
set_param_value_t(param_id, (param_value_t){.u32 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_i32:
|
||||
set_param_value_t(param_id, (param_value_t){.i32 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_u64:
|
||||
set_param_value_t(param_id, (param_value_t){.u64 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_i64:
|
||||
set_param_value_t(param_id, (param_value_t){.i64 = round(param_val)});
|
||||
break;
|
||||
case PARAM_TYPE_f32:
|
||||
set_param_value_t(param_id, (param_value_t){.f32 = param_val});
|
||||
break;
|
||||
case PARAM_TYPE_f64:
|
||||
set_param_value_t(param_id, (param_value_t){.f64 = param_val});
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown parameter type for ID %d", param_id);
|
||||
params_failed++;
|
||||
continue;
|
||||
}
|
||||
} else if (cJSON_IsString(item)) {
|
||||
set_param_string(param_id, item->valuestring);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Parameter bad type: %s", key);
|
||||
params_failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
params_updated++;
|
||||
}
|
||||
@@ -334,11 +383,10 @@ static esp_err_t sp_post_handler(httpd_req_t *req) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
static esp_err_t cmd_post_handler(httpd_req_t *req) {
|
||||
ESP_LOGI(TAG, "cmd_post_handler");
|
||||
// Send the HTML response
|
||||
int ret = httpd_req_recv(req, httpBuffer, sizeof(httpBuffer));
|
||||
int ret = httpd_req_recv(req, httpBuffer, sizeof(httpBuffer));
|
||||
if (ret <= 0) {
|
||||
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
httpd_resp_send_408(req);
|
||||
@@ -346,8 +394,8 @@ static esp_err_t cmd_post_handler(httpd_req_t *req) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
httpBuffer[ret] = '\0'; // Null-terminate the string
|
||||
|
||||
cJSON *root = cJSON_Parse(httpBuffer);
|
||||
|
||||
cJSON *root = cJSON_Parse(httpBuffer);
|
||||
if (root == NULL) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON: %s", httpBuffer);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
@@ -356,30 +404,98 @@ static esp_err_t cmd_post_handler(httpd_req_t *req) {
|
||||
|
||||
cJSON *cmd = cJSON_GetObjectItem(root, "cmd");
|
||||
|
||||
if (!cJSON_IsString(cmd) || cmd->valuestring == NULL)
|
||||
return httpd_resp_send(req, "400 Bad Request", HTTPD_RESP_USE_STRLEN);
|
||||
|
||||
if (strcmp(cmd->valuestring, "stop") == 0) {
|
||||
fsm_request(FSM_CMD_STOP);
|
||||
ESP_LOGI(TAG, "FSM_CMD_STOP");
|
||||
} else if (strcmp(cmd->valuestring, "undo") == 0) {
|
||||
fsm_request(FSM_CMD_UNDO);
|
||||
ESP_LOGI(TAG, "FSM_CMD_UNDO");
|
||||
} else if (strcmp(cmd->valuestring, "start") == 0) {
|
||||
fsm_request(FSM_CMD_START);
|
||||
ESP_LOGI(TAG, "FSM_CMD_START");
|
||||
} else if (strcmp(cmd->valuestring, "rfp") == 0) {
|
||||
cJSON *i = cJSON_GetObjectItem(root, "channel");
|
||||
if (cJSON_IsNumber(i) && i->valueint >= 0 && i->valueint < 8) {
|
||||
rf_433_learn_keycode(i->valueint);
|
||||
} else {
|
||||
rf_433_cancel_learn_keycode();
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Command not valid: %s", httpBuffer);
|
||||
return httpd_resp_send(req, "400 Bad Request", HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
if (!cJSON_IsString(cmd) || cmd->valuestring == NULL)
|
||||
return httpd_resp_send(req, "400 Bad Request", HTTPD_RESP_USE_STRLEN);
|
||||
|
||||
if (strcmp(cmd->valuestring, "stop") == 0) {
|
||||
fsm_request(FSM_CMD_STOP);
|
||||
ESP_LOGI(TAG, "FSM_CMD_STOP");
|
||||
} else if (strcmp(cmd->valuestring, "undo") == 0) {
|
||||
fsm_request(FSM_CMD_UNDO);
|
||||
ESP_LOGI(TAG, "FSM_CMD_UNDO");
|
||||
} else if (strcmp(cmd->valuestring, "reboot") == 0) {
|
||||
// Send response FIRST
|
||||
httpd_resp_send(req, "Rebooting...", HTTPD_RESP_USE_STRLEN);
|
||||
|
||||
set_param_value_t(PARAM_BOOT_TIME, (param_value_t){.i64 = system_rtc_get_raw_time()});
|
||||
|
||||
// THEN delay and reboot
|
||||
vTaskDelay(pdMS_TO_TICKS(2000)); // Give time for TCP to close properly
|
||||
esp_restart();
|
||||
} else if (strcmp(cmd->valuestring, "start") == 0) {
|
||||
fsm_request(FSM_CMD_START);
|
||||
ESP_LOGI(TAG, "FSM_CMD_START");
|
||||
} else if (strcmp(cmd->valuestring, "cal_jack_start") == 0) {
|
||||
fsm_request(FSM_CMD_CALIBRATE_JACK_PREP);
|
||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_JACK_PREP");
|
||||
} else if (strcmp(cmd->valuestring, "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);
|
||||
}
|
||||
} else if (strcmp(cmd->valuestring, "cal_drive_start") == 0) {
|
||||
fsm_request(FSM_CMD_CALIBRATE_DRIVE_PREP);
|
||||
ESP_LOGI(TAG, "FSM_CMD_CALIBRATE_DRIVE_PREP");
|
||||
} else if (strcmp(cmd->valuestring, "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);
|
||||
}
|
||||
} else if (strcmp(cmd->valuestring, "cal_get") == 0) {
|
||||
ESP_LOGI(TAG, "CAL_GET");
|
||||
sprintf(httpBuffer, "{\"e\":%lld,\"t\":%lld}", fsm_get_cal_e(), fsm_get_cal_t());
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
return httpd_resp_send(req, httpBuffer, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
else if (strcmp(cmd->valuestring, "remaining_dist") == 0) {
|
||||
cJSON *i = cJSON_GetObjectItem(root, "amt");
|
||||
if (cJSON_IsNumber(i)) {
|
||||
ESP_LOGI(TAG, "remaining_dist");
|
||||
fsm_set_remaining_distance(i->valuedouble);
|
||||
}
|
||||
}
|
||||
|
||||
else if (strcmp(cmd->valuestring, "rfp") == 0) {
|
||||
cJSON *i = cJSON_GetObjectItem(root, "channel");
|
||||
if (cJSON_IsNumber(i) && i->valueint >= 0 && i->valueint < 8) {
|
||||
rf_433_learn_keycode(i->valueint);
|
||||
} else {
|
||||
rf_433_cancel_learn_keycode();
|
||||
}
|
||||
} else if (strcmp(cmd->valuestring, "rf_status") == 0) {
|
||||
// Return current RF button codes (just the 32-bit values)
|
||||
int head = 0;
|
||||
head += sprintf(httpBuffer, "{\"codes\":[");
|
||||
|
||||
for (uint8_t i = 0; i < NUM_RF_BUTTONS; i++) {
|
||||
if (i > 0) head += sprintf(httpBuffer+head, ",");
|
||||
|
||||
int64_t code = get_param_value_t(PARAM_KEYCODE_0 + i).i64;
|
||||
// Return just the code as uint32
|
||||
head += sprintf(httpBuffer+head, "%lu", (unsigned long)(uint32_t)code);
|
||||
}
|
||||
|
||||
head += sprintf(httpBuffer+head, "]}");
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
return httpd_resp_send(req, httpBuffer, head);
|
||||
} else if (strcmp(cmd->valuestring, "rf_disable") == 0) {
|
||||
rf_433_disable_controls();
|
||||
ESP_LOGI(TAG, "RF controls disabled");
|
||||
} else if (strcmp(cmd->valuestring, "rf_enable") == 0) {
|
||||
rf_433_enable_controls();
|
||||
ESP_LOGI(TAG, "RF controls enabled");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Command not valid: %s", httpBuffer);
|
||||
return httpd_resp_send(req, "400 Bad Request", HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "text/html");
|
||||
return httpd_resp_send(req, "200 OK", HTTPD_RESP_USE_STRLEN);
|
||||
|
||||
@@ -394,11 +510,50 @@ static esp_err_t status_get_handler(httpd_req_t *req) {
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
|
||||
// Start building the JSON string with time
|
||||
head += sprintf(httpBuffer+head, "{\"time\":%lld,\"battery\":%f,\"rtc_set\":%s,\"values\":[",
|
||||
system_rtc_get_raw_time(),
|
||||
get_battery_V(),
|
||||
rtc_is_set()?"true":"false"
|
||||
);
|
||||
head += sprintf(httpBuffer+head, "{\"time\":%lld,\"rtc_set\":%s,",
|
||||
system_rtc_get_raw_time(),
|
||||
rtc_is_set()?"true":"false");
|
||||
|
||||
float vbat = get_battery_V();
|
||||
if (fpclassify(vbat) == FP_NORMAL) {
|
||||
head += sprintf(httpBuffer+head, "\"battery\":%f,",
|
||||
vbat);
|
||||
} else {
|
||||
head += sprintf(httpBuffer+head, "\"battery\":null,");
|
||||
}
|
||||
|
||||
float d = fsm_get_remaining_distance();
|
||||
if (fpclassify(d) == FP_NORMAL) {
|
||||
head += sprintf(httpBuffer+head, "\"remaining_dist\":%f,\"msg\":\"",d);
|
||||
} else {
|
||||
head += sprintf(httpBuffer+head, "\"remaining_dist\":null,\"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 (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, " | RTC NOT SET");
|
||||
}
|
||||
|
||||
|
||||
head += sprintf(httpBuffer+head, "\",\"values\":[");
|
||||
|
||||
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
|
||||
if (i > 0) {
|
||||
@@ -410,16 +565,27 @@ static esp_err_t status_get_handler(httpd_req_t *req) {
|
||||
|
||||
// Append the parameter value based on its type
|
||||
switch (get_param_type(i)) {
|
||||
case PARAM_TYPE_u8: head+=sprintf(httpBuffer+head, "%u", param.u8); break;
|
||||
case PARAM_TYPE_i8: head+=sprintf(httpBuffer+head, "%d", param.i8); break;
|
||||
case PARAM_TYPE_u16: head+=sprintf(httpBuffer+head, "%u", param.u16); break;
|
||||
case PARAM_TYPE_i16: head+=sprintf(httpBuffer+head, "%d", param.i16); break;
|
||||
case PARAM_TYPE_u32: head+=sprintf(httpBuffer+head, "%lu", (unsigned long)param.u32); break;
|
||||
case PARAM_TYPE_i32: head+=sprintf(httpBuffer+head, "%ld", (long)param.i32); break;
|
||||
case PARAM_TYPE_u64: head+=sprintf(httpBuffer+head, "%llu", param.u64); break;
|
||||
case PARAM_TYPE_i64: head+=sprintf(httpBuffer+head, "%lld", param.i64); break;
|
||||
case PARAM_TYPE_f32: head+=sprintf(httpBuffer+head, "%.8f", param.f32); break;
|
||||
case PARAM_TYPE_f64: head+=sprintf(httpBuffer+head, "%.8f", param.f64); break;
|
||||
case PARAM_TYPE_f32:
|
||||
if (fpclassify(param.f32) == FP_NORMAL)
|
||||
head+=sprintf(httpBuffer+head, "%.8f", param.f32);
|
||||
else
|
||||
head+=sprintf(httpBuffer+head, "null");
|
||||
break;
|
||||
case PARAM_TYPE_f64:
|
||||
if (fpclassify(param.f32) == FP_NORMAL)
|
||||
head+=sprintf(httpBuffer+head, "%.8f", param.f64);
|
||||
else
|
||||
head+=sprintf(httpBuffer+head, "null");
|
||||
break;
|
||||
case PARAM_TYPE_str:
|
||||
head+=sprintf(httpBuffer+head, "\"%s\"", param.str);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,14 +719,11 @@ httpd_uri_t uris[] = {{
|
||||
static void startHttpServer(void) {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = SERVER_PORT;
|
||||
|
||||
ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
|
||||
|
||||
if (httpd_start(&httpServerInstance, &config) == ESP_OK) {
|
||||
for (uint8_t i=0; i<(sizeof(uris)/sizeof(httpd_uri_t)); i++) {
|
||||
httpd_register_uri_handler(httpServerInstance, &uris[i]);
|
||||
|
||||
}
|
||||
}
|
||||
for (uint8_t i=0; i<(sizeof(uris)/sizeof(httpd_uri_t)); i++)
|
||||
httpd_register_uri_handler(httpServerInstance, &uris[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Event handler for WiFi events */
|
||||
@@ -579,18 +742,15 @@ void launchSoftAp() {
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
// Create default WiFi AP and get the netif handle
|
||||
esp_netif_t *ap_netif = esp_netif_create_default_wifi_ap();
|
||||
assert(ap_netif); // Optional: check for NULL
|
||||
|
||||
// Set your custom hostname here (max 32 chars, no spaces/special chars recommended)
|
||||
esp_netif_t *ap_netif = esp_netif_create_default_wifi_ap();
|
||||
assert(ap_netif);
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_set_hostname(ap_netif, HOSTNAME));
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
// Register the event handler
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
|
||||
ESP_EVENT_ANY_ID,
|
||||
&wifi_event_handler,
|
||||
@@ -599,29 +759,51 @@ void launchSoftAp() {
|
||||
|
||||
wifi_config_t wifi_config = {
|
||||
.ap = {
|
||||
.channel = 8,
|
||||
.channel = 6,
|
||||
.ssid = SOFT_AP_SSID,
|
||||
.ssid_len = strlen(SOFT_AP_SSID),
|
||||
.password = SOFT_AP_PASSWORD,
|
||||
.max_connection = 4,
|
||||
.authmode = WIFI_AUTH_WPA2_PSK
|
||||
},
|
||||
};
|
||||
|
||||
// Get the strings from your parameter system
|
||||
char* ssid_str = get_param_string(PARAM_WIFI_SSID);
|
||||
char* password_str = get_param_string(PARAM_WIFI_PASS);
|
||||
|
||||
// Allocate and set the SSID and password
|
||||
memcpy(wifi_config.ap.ssid, ssid_str, 16);
|
||||
memcpy(wifi_config.ap.password, password_str, 16);
|
||||
|
||||
// password minimum length of 8
|
||||
if (strlen(password_str) < 8) {
|
||||
wifi_config.ap.password[0] = '\0';
|
||||
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||
}
|
||||
|
||||
// Set the length of SSID
|
||||
wifi_config.ap.ssid_len = strlen(ssid_str);
|
||||
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
//dns_server_config_t dns_config = DNS_SERVER_CONFIG_SINGLE(HOSTNAME, "192.168.4.1");
|
||||
//ESP_ERROR_CHECK(dns_server_start(&dns_config));
|
||||
|
||||
//ESP_ERROR_CHECK(mdns_init());
|
||||
//ESP_ERROR_CHECK(mdns_hostname_set(HOSTNAME)); // Matches the netif hostname
|
||||
//ESP_ERROR_CHECK(mdns_instance_name_set("My ESP32 Device")); // Optional friendly name
|
||||
// After mdns_init() and hostname set
|
||||
//ESP_ERROR_CHECK(mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0));
|
||||
|
||||
ESP_LOGI(TAG, "SoftAP set up. SSID:%s password:%s", SOFT_AP_SSID, SOFT_AP_PASSWORD);
|
||||
// Start DNS server with your specific hostname
|
||||
// Option 1: Only respond to your hostname
|
||||
ESP_ERROR_CHECK(simple_dns_server_start("192.168.4.1"));
|
||||
|
||||
// Option 2: Respond to ALL domains (captive portal style)
|
||||
// ESP_ERROR_CHECK(simple_dns_server_start(HOSTNAME, "192.168.4.1", true));
|
||||
|
||||
// Start mDNS for .local domain
|
||||
ESP_ERROR_CHECK(mdns_init());
|
||||
ESP_ERROR_CHECK(mdns_hostname_set(HOSTNAME));
|
||||
ESP_ERROR_CHECK(mdns_instance_name_set("ClusterCommand"));
|
||||
ESP_ERROR_CHECK(mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0));
|
||||
|
||||
ESP_LOGI(TAG, "SoftAP ready. Access at: http://%s or http://%s.local or http://192.168.4.1",
|
||||
HOSTNAME, HOSTNAME);
|
||||
}
|
||||
|
||||
esp_err_t webserver_init(void) {
|
||||
|
||||
Reference in New Issue
Block a user