DNS, web ui nearly done, great log streaming, attempted https (abandoned that though)

This commit is contained in:
Thaddeus Hughes
2025-12-30 18:51:11 -06:00
parent 012d28ae14
commit d46cb252fb
134 changed files with 19226 additions and 740 deletions

View File

@@ -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
)

View File

@@ -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);

View File

@@ -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);

View File

@@ -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];
}

View File

@@ -1,5 +1,6 @@
## IDF Component Manager Manifest File
dependencies:
espressif/mdns: "*"
joltwallet/littlefs: "==1.20.3"
esp-idf-lib/tca95x5: "*"
## Required IDF version

View File

@@ -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>

View File

@@ -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
View 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-----

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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, &parameter_table[id].u16, 2);
break;
case PARAM_TYPE_i16:
memcpy(dest, &parameter_table[id].i16, 2);
break;
case PARAM_TYPE_u32:
memcpy(dest, &parameter_table[id].u32, 4);
break;
case PARAM_TYPE_i32:
memcpy(dest, &parameter_table[id].i32, 4);
break;
case PARAM_TYPE_f32:
memcpy(dest, &parameter_table[id].f32, 4);
break;
case PARAM_TYPE_u64:
memcpy(dest, &parameter_table[id].u64, 8);
break;
case PARAM_TYPE_i64:
memcpy(dest, &parameter_table[id].i64, 8);
break;
case PARAM_TYPE_f64:
memcpy(dest, &parameter_table[id].f64, 8);
break;
case PARAM_TYPE_str:
memcpy(dest, parameter_table[id].str, 16);
break;
}
}
// ============================================================================
// 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(&parameter_table[id].u16, src, 2);
break;
case PARAM_TYPE_i16:
memcpy(&parameter_table[id].i16, src, 2);
break;
case PARAM_TYPE_u32:
memcpy(&parameter_table[id].u32, src, 4);
break;
case PARAM_TYPE_i32:
memcpy(&parameter_table[id].i32, src, 4);
break;
case PARAM_TYPE_f32:
memcpy(&parameter_table[id].f32, src, 4);
break;
case PARAM_TYPE_u64:
memcpy(&parameter_table[id].u64, src, 8);
break;
case PARAM_TYPE_i64:
memcpy(&parameter_table[id].i64, src, 8);
break;
case PARAM_TYPE_f64:
memcpy(&parameter_table[id].f64, src, 8);
break;
case PARAM_TYPE_str:
memcpy(parameter_table[id].str, src, 16);
parameter_table[id].str[15] = '\0'; // Ensure null termination
break;
}
}
// ============================================================================
// 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*)&parameter_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*)&params_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(&parameter_table[i], &parameter_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(&parameter_table[i], &parameter_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;
}
}

View File

@@ -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_ */

View File

@@ -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

View File

@@ -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()>&lt; 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&&paramNames[i]!==null?paramNames[i]:`null`,pval=paramValues[i]!==void 0&&paramValues[i]!==null?paramValues[i]:`null`;row.innerHTML=`
<td>${i}</td>
<td>${param_names[i]!==void 0&&param_names[i]!==null?param_names[i]:`null`}</td>
<td><input type="number" id="in_${i}" value="${param_values[i]!==void 0&&param_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>

View File

@@ -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) {