Ironed out tons of stuff on the webserver

Logging, time sync, collapsible menus, oh my!
This commit is contained in:
Thaddeus Hughes
2025-12-29 22:21:43 -06:00
parent 2ac5d30490
commit 012d28ae14
12 changed files with 620 additions and 817 deletions

View File

@@ -120,9 +120,9 @@ int8_t fsm_get_current_progress(int8_t denominator) {
}
#define JACK_TIME get_param_value_t(PARAM_JACK_MSPI ).u32 * 1000 * get_param_value_t(PARAM_JACK_DIST ).u8
#define DRIVE_TIME get_param_value_t(PARAM_DRIVE_MSPF).u32 * 1000 * get_param_value_t(PARAM_DRIVE_DIST).u8
#define DRIVE_DIST get_param_value_t(PARAM_DRIVE_TPDF).u32 / 10 * get_param_value_t(PARAM_DRIVE_DIST).u8
#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
void control_task(void *param) {
esp_task_wdt_add(NULL);
@@ -239,8 +239,8 @@ void control_task(void *param) {
int64_t elapsed_t = (current_time-timer_start);
int64_t total_t = (timer_end-timer_start);
int32_t ticks = get_sensor_counter(SENSOR_DRIVE);
int64_t total_t = (timer_end-timer_start);
int32_t ticks = get_sensor_counter(SENSOR_DRIVE);
//ESP_LOGI("FSM", "[%d] %lld / %lld ms, %ld ticks", current_state, (long long) elapsed_t, (long long) total_t, (long) ticks);
// Output control

View File

@@ -4,55 +4,129 @@
<title>Control Panel</title>
<style>
* { background-color: #111; color: #eee; font-family: sans-serif; }
input { border: 1px solid #666; background-color: #333; font-family: monospace; text-align: right; width: 100%; box-sizing: border-box; }
input, button { width: 100%; }
input, button { border: 1px solid #666; background-color: #333; font-family: monospace; text-align: right; box-sizing: border-box; }
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;}
</style>
</head>
<body>
<button id="commit_btn" onclick="commit_params()" disabled>Save Changes</button>
<table id="table">
<table>
<tr>
<td>-</td>
<td>System Time</td>
<td>Schedule Start</td>
<td><input type="time" id="move_start" onchange="changeSchedule(this)"/></td>
</tr>
<tr>
<td>Schedule End</td>
<td><input type="time" id="move_end" onchange="changeSchedule(this)"/></td>
</tr>
<tr>
<td># Moves/Day</td>
<td><input type="number" min="0" id="num_moves" onchange="changeSchedule(this)"/></td>
</tr>
<tr>
<td>Move Distance</td>
<td><input type="number" min="0" id="drive_dist" onchange="changeSchedule(this)"/></td>
<td>ft</td>
</tr>
<tr>
<td>Jack Height</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><input readonly="" id="voltage"/></td>
<td>V</td>
</tr>
<tr>
<td></td>
<td><button onclick="cmd('start')">START MOVE</button></td>
</tr>
<tr><td colspan="4">
<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>-</td>
<td>Battery Voltage</td>
<td> <input readonly="" id="voltage"/> </td>
<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>
<button id="commit_btn" onclick="commit_params()" disabled>Save Changes</button>
</td></tr>
</table>
<table id="table2">
<tr>
<td>Firmware</td>
<td><input type="file" id="firmware_file" accept=".bin"></td>
<td><button id="upload_btn" onclick="uploadFirmware()">Upload Firmware</button></td>
<td></td>
<td><input type="file" id="firmware_file" accept=".bin">
<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>
<td></td>
</tr>
</table>
<table id="table"></table>
</details>
<script>
let param_values = [];
let param_names = [];
const param_units = ["ms", "Per Day", "time", "time", "feet", "inches", "", "", "", "", "","","","","","","","","Amps","Amps","Amps","Amps","Amps","Seconds","","","","Volts","Seconds","Volts","Seconds","Seconds","uSeconds","Volts"];
let param_units = [];
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);
}
}
function ge(x) { return document.getElementById(x); }
// Highlight changed inputs and enable the save button
@@ -60,11 +134,19 @@
el.classList.add("changed");
ge('commit_btn').disabled = false;
}
function toFixed(input, n) {
const num = parseFloat(input);
if (isNaN(num)) {
return null; // or throw error, or return 0
}
return Number(num.toFixed(n));
}
// --- 1. GET DATA ---
function fetchStatus() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://192.168.4.1/status", true);
xhr.open("GET", "./status", true);
xhr.onload = function() {
if (xhr.status === 200) {
try {
@@ -77,12 +159,21 @@
ge('in_time').value = date;
}
ge('voltage').value = data.battery;
ge('voltage').value = toFixed(data.battery, 2);
// Store values (default to empty array if missing)
param_values = data.values || [];
param_names = data.names || [];
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);
}
@@ -96,65 +187,149 @@
};
xhr.send();
}
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 renderTable() {
const table = ge("table");
// Clear existing parameter rows (rows between index 0 and the last row)
while(table.rows.length > 2) { table.deleteRow(1); }
while(table.rows.length > 0) { table.deleteRow(0); }
// Loop through the NAMES array to ensure every input is shown
param_names.forEach((name, i) => {
let row = table.insertRow(table.rows.length - 1);
let row = table.insertRow(table.rows.length);
let name =(param_names[i] !== undefined && param_names[i] !==null)
let pname =(param_names[i] !== undefined && param_names[i] !==null)
? param_names[i]
: "null";
// If the server didn't send a value for this index, show "null"
let val = (param_values[i] !== undefined && param_values[i] !== null)
? param_values[i]
let pval = (param_values[i] !== undefined && param_values[i] !== null)
? param_values[i]
: "null";
row.innerHTML = `
<td>${i}</td>
<td>${name}</td>
<td><input type="text" id="in_${i}" value="${val}" oninput="markChanged(this)"></td>
<td>${pname}</td>
<td><input type="number" id="in_${i}" value="${pval}" 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) {
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);
e.value = new Date().toLocaleString('sv-SE');
}
function commit_time() {
const xhr = new XMLHttpRequest();
// 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);
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());
}
// --- 2. POST DATA ---
function commit_params() {
ge('commit_btn').disabled = true;
const changedInputs = document.querySelectorAll('input.changed');
sp = {}
changedInputs.forEach(input => {
const xhr = new XMLHttpRequest();
if (input.id === "in_time") {
// Time handling
const epoch = Math.floor(new Date(input.value).getTime() / 1000);
xhr.open("POST", "http://192.168.4.1/st", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ time: epoch }));
input.classList.remove("changed");
} else {
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);
xhr.open("POST", "http://192.168.4.1/sp", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = function() {
if (xhr.status === 200) {
input.classList.remove("changed");
}
};
xhr.send(JSON.stringify({ id: parseInt(id), value: val }));
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;
}
});
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();
}
function uploadFirmware() {
@@ -165,7 +340,7 @@
}
const file = fileInput.files[0];
const xhr = new XMLHttpRequest();
xhr.open("POST", "http://192.168.4.1/ota", true);
xhr.open("POST", "./ota", true);
xhr.setRequestHeader("Content-Type", "application/octet-stream");
xhr.onload = function() {
if (xhr.status === 200) {
@@ -180,9 +355,19 @@
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');
const response = await fetch('./log');
if (!response.ok) {
throw new Error('Network response was not ok');
}

View File

@@ -54,6 +54,7 @@ typedef enum {
LED_STATE_ERROR,
LED_STATE_AWAKE,
LED_STATE_CANCELLING,
LED_STATE_ERRORED,
LED_STATE_START1,
LED_STATE_START2,
LED_STATE_START3,
@@ -62,17 +63,19 @@ typedef enum {
} led_state_t;
void driveLEDs(led_state_t state) {
uint8_t patterns[4][12] = {
uint8_t patterns[5][12] = {
{1,3,7,6,4,0},
{7,0},
{0b101,0b001},
{1,1,1,1,1,1, 1,1,1,3},
{4,2}
{4,2},
{0b001, 0b101},
};
switch(state) {
case LED_STATE_DRIVING:
i2c_set_led1(patterns[state][(esp_timer_get_time()/100000) % 6]);
break;
case LED_STATE_ERROR:
ESP_LOGE(TAG, "SOME SORT OF ERROR");
i2c_set_led1(patterns[state][(esp_timer_get_time()/1000000) % 2]);
break;
case LED_STATE_AWAKE:
@@ -82,6 +85,9 @@ void driveLEDs(led_state_t state) {
i2c_set_led1(patterns[state][(esp_timer_get_time()/200000) % 2]);
break;
case LED_STATE_ERRORED:
i2c_set_led1(patterns[state][(esp_timer_get_time()/200000) % 2]);
case LED_STATE_BOOTING:
i2c_set_led1(0b001);
break;
@@ -152,11 +158,11 @@ void app_main(void) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(100);
while(true) {
/*while(true) {
ESP_LOGI(TAG, "TICK");
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(1000));
esp_task_wdt_reset();
}
}*/
while(true) {
vTaskDelayUntil(&xLastWakeTime, xFrequency);
@@ -178,7 +184,12 @@ void app_main(void) {
} else if (i2c_get_button_ms(0) > 100){
driveLEDs(LED_STATE_START1);
} else{
driveLEDs(LED_STATE_AWAKE);
if (rtc_is_set())
driveLEDs(LED_STATE_AWAKE);
else
driveLEDs(LED_STATE_ERROR);
}
// when not actively moving we log at a low frequency

View File

@@ -112,7 +112,7 @@ float get_raw_battery_voltage(void) {
!= ESP_OK) { return NAN; }
// Voltage divider: 150kohm to 1Mohm -> gain = 1.15 -> scale = 1150/150
return voltage_mv * 0.00766666666; // same as / 1000.0 * 1150.0 / 150.0;
return voltage_mv * 0.00766666666 + get_param_value_t(PARAM_V_SENS_OFFSET).f32; // same as / 1000.0 * 1150.0 / 150.0;
}
esp_err_t process_battery_voltage(void)

View File

@@ -1,546 +0,0 @@
/*
* power_mgmt.c
*
* 1 kHz power-management task:
* • Samples all three H-bridge current sensors (DRIVE, AUX, JACK)
* • Samples battery voltage (BAT)
* • Applies EMA filtering on every channel
* • Updates shared volatile globals for the control FSM
* • Handles over-current spike protection
*
* Updated to modern ESP-IDF ADC API (line fitting)
* All variables now defined locally
*
* Created on: Nov 10, 2025
*/
#include <math.h>
#include <stdint.h>
#include <stdbool.h>
#include "driver/rtc_io.h"
#include "esp_log.h"
#include "esp_task_wdt.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "control_fsm.h"
#include "soc/rtc_io_reg.h"
#include "power_mgmt.h"
#include "storage.h"
#include "rtc.h"
// === GPIO Pin Definitions ===
#define PIN_V_ISENS1 ADC_CHANNEL_0 // GPIO36 / VP
#define PIN_V_ISENS2 ADC_CHANNEL_6 // GPIO34
#define PIN_V_ISENS3 ADC_CHANNEL_7 // GPIO35
#define PIN_V_BATTERY ADC_CHANNEL_3 // GPIO39 / VN
#define PIN_V_SENS_BAT PIN_V_BATTERY
#define PIN_CHG_BULK GPIO_NUM_26
#define AUTOZERO_THRESH 2000.0f // mA
typedef enum {
CHG_T_LOWBAT = 0,
CHG_T_BULK = 1,
CHG_T_STEADY = 2,
} charge_timer_t;
#define N_CHG_TIMERS 3
RTC_DATA_ATTR charge_state_t current_charge_state = CHG_STATE_BULK;
RTC_DATA_ATTR int64_t charge_timers[N_CHG_TIMERS] = {-1};
int64_t now;
charge_state_t get_charging_state() { return current_charge_state; }
void setTimerN(charge_timer_t i, int64_t sec) {
// set the timer for <sec> in the future if it's currently less than now
if (charge_timers[i] < now) {
charge_timers[i] = now + sec;
ESP_LOGI("BAT", "Set timer[%d] +%lld", i, (long long)sec);
}
}
void resetTimerN(charge_timer_t i) {
charge_timers[i] = -1;
}
void resetBatTimers() {
for (uint8_t i=0; i<N_CHG_TIMERS; i++)
resetTimerN(i);
}
bool getTimerN(charge_timer_t i) {
if (charge_timers[i] < 0) return false;
return system_rtc_get_raw_time() > charge_timers[i];
}
#define BULK_CHARGE_S 20 //2*60*60
#define FLOAT_STEADY_S 10 //30*60
#define LOW_DETECT_S 10 //5*60
#define STEADY_MV 13000
#define LOW_MV 12800
void run_charge_fsm() {
now = system_rtc_get_raw_time();
//ESP_LOGI("BAT", "FSM STATE %d", current_charge_state);
if (rtc_is_set()) {
switch(current_charge_state) {
case CHG_STATE_BULK:
// turn off bulk charging and go to float when time is up
if (getTimerN(CHG_T_BULK)) {
ESP_LOGI("BAT", "BULK -> FLOAT");
current_charge_state = CHG_STATE_FLOAT;
}
break;
case CHG_STATE_FLOAT:
if (getTimerN(CHG_T_STEADY)) {
ESP_LOGI("BAT", "FLOAT -> OFF");
current_charge_state = CHG_STATE_OFF;
}
if (get_battery_mV() > STEADY_MV) {
setTimerN(CHG_T_STEADY, FLOAT_STEADY_S);
} else {
resetTimerN(CHG_T_STEADY);
}
// NO break; !! float should also kick into bulk with same triggers
case CHG_STATE_OFF:
// after 5 minutes of low-ish battery go into bulk charge
if (getTimerN(CHG_T_LOWBAT)) {
ESP_LOGI("BAT", " -> BULK");
current_charge_state = CHG_STATE_BULK;
setTimerN(CHG_T_BULK, BULK_CHARGE_S);
}
if (get_battery_mV() < LOW_MV) {
setTimerN(CHG_T_LOWBAT, LOW_DETECT_S);
} else {
resetTimerN(CHG_T_LOWBAT);
}
break;
}
} else {
//ESP_LOGI("BAT", " -> BULK");
current_charge_state = CHG_STATE_BULK;
}
//rtc_gpio_hold_dis(PIN_CHG_BULK);
//rtc_gpio_hold_dis(PIN_CHG_DISABLE);
switch(current_charge_state) {
case CHG_STATE_BULK:
gpio_set_level(PIN_CHG_BULK, 1);
//ESP_LOGI("BAT", "BULK");
break;
case CHG_STATE_FLOAT:
gpio_set_level(PIN_CHG_BULK, 0);
//ESP_LOGI("BAT", "FLOAT");
break;
case CHG_STATE_OFF:
gpio_set_level(PIN_CHG_BULK, 0);
//ESP_LOGI("BAT", "OFF");
break;
}
//rtc_gpio_hold_en(PIN_CHG_BULK);
//rtc_gpio_hold_en(PIN_CHG_DISABLE);
}
typedef struct {
bool enabled; // Auto-zero active for this channel
float threshold_ma; // Max current to consider "zero" (mA)
float learned_offset_mv; // Accumulated zero offset (mV)
bool initialized; // First valid zero established
} autozero_t;
static autozero_t autozero[N_BRIDGES] = {0};
// === E-Fuse (Software Breaker) Configuration ===
static const char* currentLimits_A[N_BRIDGES] = {
[BRIDGE_DRIVE] = "efuse_drive_A", //40000,
[BRIDGE_AUX] = "efuse_aux_A", // 5000,
[BRIDGE_JACK] = "efuse_jack_A" // 10000
};
static const float i2t_thresholds[N_BRIDGES] = { // A^2*s (tunable per bridge if needed)
[BRIDGE_DRIVE] = 6.0f,
[BRIDGE_AUX] = 6.0f,
[BRIDGE_JACK] = 6.0f
};
static const float i_instant[N_BRIDGES] = { // Instant trip multiplier of I_rated
[BRIDGE_DRIVE] = 15.0f,
[BRIDGE_AUX] = 15.0f,
[BRIDGE_JACK] = 15.0f
};
static const float cool_rate[N_BRIDGES] = { // Cooling constant (1/s)
[BRIDGE_DRIVE] = 0.008f,
[BRIDGE_AUX] = 0.008f,
[BRIDGE_JACK] = 0.008f
};
static const int32_t cooldown_ms[N_BRIDGES] = { // Auto-reset delay after trip
[BRIDGE_DRIVE] = 5000,
[BRIDGE_AUX] = 5000,
[BRIDGE_JACK] = 5000
};
static float efuse_heat[N_BRIDGES] = {0};
static uint64_t efuse_trip_time[N_BRIDGES] = {0}; // Timestamp when tripped
static bool efuse_tripped[N_BRIDGES] = {false};
// === ADC Handles ===
static adc_oneshot_unit_handle_t adc1_handle = NULL;
static adc_cali_handle_t adc_cali_handle = NULL;
// === EMA Filter State ===
#define EMA_ALPHA_CURRENT 0.5f
#define EMA_ALPHA_BATTERY 0.05f
static float ema_current[N_BRIDGES] = {0};
static bool ema_init[N_BRIDGES] = {false};
static float ema_battery = 0.0f;
static bool ema_battery_init = false;
// === Shared Volatile Outputs ===
volatile int32_t bridgeCurrents_mA[N_BRIDGES] = {0};
volatile int32_t batteryVoltage_mV = 0;
// === ADC Initialization ===
static esp_err_t adc_init(void) {
if (adc1_handle != NULL) {
return ESP_OK; // Already initialized
}
// ADC1 oneshot mode
adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = ADC_UNIT_1,
};
ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_cfg, &adc1_handle));
// Configure all channels
adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_11,
.bitwidth = ADC_BITWIDTH_12,
};
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, PIN_V_ISENS1, &chan_cfg));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, PIN_V_ISENS2, &chan_cfg));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, PIN_V_ISENS3, &chan_cfg));
ESP_ERROR_CHECK(adc_oneshot_config_channel(adc1_handle, PIN_V_SENS_BAT, &chan_cfg));
// Line fitting calibration (modern scheme)
adc_cali_line_fitting_config_t cali_cfg = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN_DB_11,
.bitwidth = ADC_BITWIDTH_12,
};
ESP_ERROR_CHECK(adc_cali_create_scheme_line_fitting(&cali_cfg, &adc_cali_handle));
return ESP_OK;
}
void autozero_enable(bridge_t bridge, bool enable) {
if (bridge >= N_BRIDGES) return;
autozero[bridge].enabled = enable;
if (!enable) {
autozero[bridge].learned_offset_mv = 0.0f;
autozero[bridge].initialized = false;
}
}
void autozero_set_threshold(bridge_t bridge, float threshold_ma) {
if (bridge >= N_BRIDGES) return;
autozero[bridge].threshold_ma = fmaxf(0.0f, threshold_ma);
}
float autozero_get_offset_mv(bridge_t bridge) {
if (bridge >= N_BRIDGES) return 0.0f;
return autozero[bridge].learned_offset_mv;
}
void autozero_reset(bridge_t bridge) {
if (bridge >= N_BRIDGES) return;
autozero[bridge].learned_offset_mv = 0.0f;
autozero[bridge].initialized = false;
}
void autozero_reset_all(void) {
for (uint8_t i = 0; i < N_BRIDGES; i++) {
autozero_reset((bridge_t)i);
}
}
// === Raw Current Reading (mA) ===
static int32_t read_bridge_current_raw(bridge_t bridge) {
int adc_raw = 0;
int voltage_mv = 0;
adc_channel_t pin;
switch(bridge) {
case BRIDGE_DRIVE: pin = PIN_V_ISENS1; break;
case BRIDGE_AUX: pin = PIN_V_ISENS3; break;
case BRIDGE_JACK: pin = PIN_V_ISENS2; break;
default: return -42069; // lol
}
if (adc_oneshot_read(adc1_handle, pin, &adc_raw) != ESP_OK) {
return 0;
}
if (adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv) != ESP_OK) {
return 0;
}
float current_sense_mv = (float)voltage_mv;
autozero_t *az = &autozero[bridge];
// === AUTO-ZERO LEARNING PHASE ===
if (az->enabled && get_bridge_state(bridge)==0) {
float raw_current_ma = 0.0f;
switch (bridge) {
case BRIDGE_JACK:
case BRIDGE_AUX:
// ACS37042KLHBLT-030B3 is 30A capable and 44 mV/A
raw_current_ma = (current_sense_mv - 1650.0f) * 1000.0f / 44.0f;
break;
case BRIDGE_DRIVE:
// ACS37220LEZATR-100B3 is 100A capable and 13.2 mV/A
raw_current_ma = (current_sense_mv - 1650.0f) * 1000.0f / 13.20f;
break;
}
if (fabsf(raw_current_ma) <= az->threshold_ma) {
// Valid zero sample
if (!az->initialized) {
az->learned_offset_mv = current_sense_mv - 1650.0f;
az->initialized = true;
} else {
// EMA on offset (slow adaptation)
float alpha = 0.1f;
az->learned_offset_mv = alpha * (current_sense_mv - 1650.0f) +
(1.0f - alpha) * az->learned_offset_mv;
}
}
}
// === APPLY AUTO-ZERO OFFSET ===
float corrected_mv = current_sense_mv - az->learned_offset_mv;
int32_t offset_mv = (int32_t)(corrected_mv - 1650.0f);
int32_t current_ma = 0;
switch (bridge) {
case BRIDGE_JACK:
case BRIDGE_AUX:
current_ma = offset_mv * 1000 / 44; // 44 mV/A
break;
case BRIDGE_DRIVE:
current_ma = offset_mv * 10000 / 132; // 13.2 mV/A
break;
}
return current_ma;
}
// === Raw Battery Voltage Reading (mV) ===
static int32_t read_battery_voltage_raw(void)
{
int adc_raw = 0;
int voltage_mv = 0;
if (adc_oneshot_read(adc1_handle, PIN_V_SENS_BAT, &adc_raw) != ESP_OK) {
return 0;
}
if (adc_cali_raw_to_voltage(adc_cali_handle, adc_raw, &voltage_mv) != ESP_OK) {
return 0;
}
// Voltage divider: 150kΩ to 1MΩ → gain = 1.15 → scale = 1150/150
return (int32_t)voltage_mv * 1150 / 150;
}
// === EMA Filter Update ===
static void apply_ema(float *state, bool *init, float alpha, int32_t raw, volatile int32_t *out)
{
if (!*init) {
*state = (float)raw;
*init = true;
} else {
*state = alpha * (float)raw + (1.0f - alpha) * *state;
}
*out = (int32_t)(*state + 0.5f);
}
// === Public Accessors ===
int32_t get_bridge_mA(uint8_t bridge)
{
if (bridge >= N_BRIDGES) return -1;
return (int32_t)bridgeCurrents_mA[bridge];
}
int32_t get_battery_mV(void)
{
return (int32_t)batteryVoltage_mV;
}
// === E-Fuse: Trip Logic (called every cycle) ===
static void efuse_update(uint8_t bridge, float I, float dt, uint64_t now)
{
float I_rated = (float)get_param_i8(currentLimits_A[bridge]);
float I_norm = I / I_rated;
// Instant trip on extreme overcurrent
if (I_norm >= i_instant[bridge]) {
efuse_tripped[bridge] = true;
efuse_trip_time[bridge] = now;
return;
}
// Cooling when below threshold
if (I_norm < 1.1f) {
efuse_heat[bridge] -= efuse_heat[bridge] * cool_rate[bridge] * dt;
efuse_heat[bridge] = fmaxf(0.0f, efuse_heat[bridge]);
efuse_tripped[bridge] = false; // Auto-clear if cooled
return;
}
// Accumulate heat (I²t)
efuse_heat[bridge] += (I_norm * I_norm) * dt;
if (efuse_heat[bridge] >= i2t_thresholds[bridge]) {
efuse_tripped[bridge] = true;
efuse_trip_time[bridge] = now;
}
}
// === E-Fuse: Auto-Reset After Cooldown ===
static void efuse_cooldown_check(uint64_t now)
{
for (uint8_t i = 0; i < N_BRIDGES; i++) {
if (efuse_tripped[i] &&
(now - efuse_trip_time[i]) >= (cooldown_ms[i] * 1000ULL)) {
efuse_heat[i] = 0.0f;
efuse_tripped[i] = false;
}
}
}
// === Public E-Fuse Controls ===
void efuse_reset_all(void)
{
for (uint8_t i = 0; i < N_BRIDGES; i++) {
efuse_heat[i] = 0.0f;
efuse_tripped[i] = false;
}
}
bool efuse_is_tripped(uint8_t bridge)
{
if (bridge >= N_BRIDGES) return false;
return efuse_tripped[bridge];
}
// === Power Management Task ===
void power_mgmt_task(void *param) {
esp_task_wdt_add(NULL);
/*gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << PIN_CHG_DISABLE) | (1ULL << PIN_CHG_BULK),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);*/
/*// Enable RTC GPIO domain (required for hold)
rtc_gpio_init(PIN_CHG_DISABLE);
rtc_gpio_init(PIN_CHG_BULK);
// Set as output
rtc_gpio_set_direction(PIN_CHG_DISABLE, RTC_GPIO_MODE_OUTPUT_ONLY);
rtc_gpio_set_direction(PIN_CHG_BULK, RTC_GPIO_MODE_OUTPUT_ONLY);
// Optional: set initial level (will be held)
//rtc_gpio_set_level(PIN_CHG_DISABLE, 1); // e.g., start disabled
//rtc_gpio_set_level(PIN_CHG_BULK, 0);
// **Critical: Enable hold function**
rtc_gpio_hold_en(PIN_CHG_DISABLE);
rtc_gpio_hold_en(PIN_CHG_BULK);*/
ESP_ERROR_CHECK(adc_init());
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(20);
// Optional: Enable auto-zero with default threshold
autozero_enable(BRIDGE_DRIVE, true);
autozero_enable(BRIDGE_AUX, true);
autozero_enable(BRIDGE_JACK, true);
autozero_set_threshold(BRIDGE_DRIVE, AUTOZERO_THRESH);
autozero_set_threshold(BRIDGE_AUX, AUTOZERO_THRESH);
autozero_set_threshold(BRIDGE_JACK, AUTOZERO_THRESH);
//uint64_t last_wake_time = esp_timer_get_time();
//const uint64_t period = 5000; // 100 us => 10kHz
while (1) {
vTaskDelayUntil(&xLastWakeTime, xFrequency);
uint64_t now_us = esp_timer_get_time();
/*if (now - last_wake_time < period) {
uint32_t delay_us = (period - (now - last_wake_time)) / 1000;
if (delay_us > 0) vTaskDelay(pdMS_TO_TICKS(delay_us));
continue;
}
last_wake_time = now;*/
// Sample currents
for (uint8_t i = 0; i < N_BRIDGES; i++) {
int32_t raw_ma = read_bridge_current_raw((bridge_t)i);
apply_ema(&ema_current[i], &ema_init[i], EMA_ALPHA_CURRENT,
raw_ma, &bridgeCurrents_mA[i]);
// Reset spike timer if under limit
/*if (bridgeCurrents_mA[i] < currentLimits_mA[i]) {
currentSpikeSafeTimes[i] = now + CURRENT_SPIKE_TIME_US;
}*/
// === E-FUSE UPDATE ===
float I = (float)bridgeCurrents_mA[i] / 1000.0f;
float dt = 0.020f; // 20 ms task period
efuse_update(i, I, dt, now_us);
}
/*ESP_LOGI("PWR", "[ %6ld | %6ld | %6ld mA ] { %6ld mV }",
(long)bridgeCurrents_mA[BRIDGE_DRIVE],
(long)bridgeCurrents_mA[BRIDGE_JACK],
(long)bridgeCurrents_mA[BRIDGE_AUX],
(long)batteryVoltage_mV);*/
// Sample battery
int32_t raw_bat = read_battery_voltage_raw();
apply_ema(&ema_battery, &ema_battery_init, EMA_ALPHA_BATTERY,
raw_bat, &batteryVoltage_mV);
//run_charge_fsm();
efuse_cooldown_check(now_us);
esp_task_wdt_reset();
}
}
void start_power() {
xTaskCreate(power_mgmt_task, "PWR", 4096, NULL, 5, NULL);
}
void shutdown_power() {
}

View File

@@ -103,6 +103,13 @@ int64_t system_rtc_get_raw_time(void)
return (int64_t)tv.tv_sec;
}
void system_rtc_set_raw_time(int64_t tv_sec)
{
rtc_set = true;
settimeofday(&(struct timeval){.tv_sec = tv_sec, .tv_usec=0}, NULL);
}
uint64_t rtc_time_ms(void)
{
struct timeval tv;

View File

@@ -42,6 +42,7 @@ void adjust_rtc_min(char *key, int8_t dir);
void rtc_get_time(struct tm * timeinfo);
int64_t system_rtc_get_raw_time(void);
void system_rtc_set_raw_time(int64_t);
bool alarm_tripped();

View File

@@ -17,41 +17,45 @@
#define PARAM_NAME_STR(name) #name
// Generate parameter table with live values (initialized to defaults)
#define PARAM_DEF(name, type, default_val) PARAM_VALUE_INIT(type, default_val),
#define PARAM_DEF(name, type, default_val, unit) PARAM_VALUE_INIT(type, default_val),
param_value_t parameter_table[NUM_PARAMS] = {
PARAM_LIST
};
#undef PARAM_DEF
// Generate default values array
#define PARAM_DEF(name, type, default_val) PARAM_VALUE_INIT(type, default_val),
#define PARAM_DEF(name, type, default_val, unit) PARAM_VALUE_INIT(type, default_val),
const param_value_t parameter_defaults[NUM_PARAMS] = {
PARAM_LIST
};
#undef PARAM_DEF
// Generate parameter types array
#define PARAM_DEF(name, type, default_val) PARAM_TYPE_ENUM(type),
#define PARAM_DEF(name, type, default_val, unit) PARAM_TYPE_ENUM(type),
const param_type_e parameter_types[NUM_PARAMS] = {
PARAM_LIST
};
#undef PARAM_DEF
// Generate parameter names array
#define PARAM_DEF(name, type, default_val) PARAM_NAME_STR(name),
#define PARAM_DEF(name, type, default_val, unit) PARAM_NAME_STR(name),
const char* parameter_names[NUM_PARAMS] = {
PARAM_LIST
};
#undef PARAM_DEF
// Generate parameter units array (8 chars max per unit)
#define PARAM_DEF(name, type, default_val, unit) unit,
const char parameter_units[NUM_PARAMS][8] = {
PARAM_LIST
};
#undef PARAM_DEF
// Partition pointer
static const esp_partition_t *storage_partition = NULL;
// Calculate offset for log area (after parameters sector)
#define LOG_START_OFFSET FLASH_SECTOR_SIZE
// Log head tracking
static uint32_t log_head_index = 0;
static uint32_t log_tail_index = 0;
@@ -106,6 +110,13 @@ param_value_t get_param_default(param_idx_t id) {
return parameter_defaults[id];
}
const char* get_param_unit(param_idx_t id) {
if (id >= NUM_PARAMS) {
return "";
}
return parameter_units[id];
}
esp_err_t commit_params() {
if (storage_partition == NULL) {
ESP_LOGE(TAG, "Storage partition not initialized");
@@ -121,8 +132,8 @@ esp_err_t commit_params() {
params_to_store[i].crc = esp_crc32_le(0, (uint8_t*)&parameter_table[i], sizeof(param_value_t));
}
// Erase the first sector (4096 bytes)
esp_err_t err = esp_partition_erase_range(storage_partition, PARAMS_OFFSET, FLASH_SECTOR_SIZE);
// 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;

View File

@@ -51,59 +51,60 @@ typedef enum {
// ============================================================================
// PARAMETER DEFINITION MACRO
// ============================================================================
// Usage: PARAM_DEF(NAME, TYPE, DEFAULT_VALUE)
// Usage: PARAM_DEF(NAME, TYPE, DEFAULT_VALUE, UNIT)
//
// Examples:
// PARAM_DEF(NUM_MOVES, u32, 0)
// PARAM_DEF(EFUSE_1_AS, u16, 2400)
// PARAM_DEF(JACK_DIST, u8, 5)
// PARAM_DEF(KEYCODE_0, i64, -1)
// PARAM_DEF(TEMPERATURE, f32, 25.5)
// 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")
// ============================================================================
// REMEMBER: ORDER IS IMPERATIVE! PARAMETERS ARE ENTERED IN THE TABLE BY INDEX!
// ============================================================================
#define PARAM_LIST \
PARAM_DEF(BOOT_TIME, i64, 0) \
PARAM_DEF(NUM_MOVES, u32, 0) \
PARAM_DEF(MOVE_START, u32, 0) \
PARAM_DEF(MOVE_END, u32, 0) \
PARAM_DEF(DRIVE_DIST, u16, 10) /*4*/\
PARAM_DEF(JACK_DIST, u8, 5) \
PARAM_DEF(DRIVE_TPDF, u16, 4000) \
PARAM_DEF(DRIVE_MSPF, u16, 600) \
PARAM_DEF(JACK_MSPI, u16, 600) /*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(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) /*20*/\
PARAM_DEF(EFUSE_INOM_1, f32, 40.0) \
PARAM_DEF(EFUSE_INOM_2, f32, 6.0) \
PARAM_DEF(EFUSE_INOM_3, f32, 2.0) \
PARAM_DEF(EFUSE_HEAT_THRESH, f32, 60.0) /*24*/\
PARAM_DEF(EFUSE_KINST, f32, 4.0) \
PARAM_DEF(EFUSE_TAUCOOL, f32, 0.2) \
PARAM_DEF(EFUSE_TCOOL, i64, 5000000) \
PARAM_DEF(LOW_PROTECTION_V, f32, 10.0) /*28*/\
PARAM_DEF(LOW_PROTECTION_S, i64, 10) \
PARAM_DEF(CHG_LOW_V, f32, 5.0) \
PARAM_DEF(CHG_LOW_S, i64, 5.0) \
PARAM_DEF(CHG_BULK_S, i64, 20) /*32*/\
PARAM_DEF(RF_PULSE_LENGTH, u64, 350000) \
PARAM_DEF(BOOT_TIME, i64, 0, "us") \
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_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(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(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_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_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(RF_PULSE_LENGTH, u64, 350000, "us") \
PARAM_DEF(V_SENS_OFFSET, f32, 0.4, "V") \
// Generate enum for parameter indices
#define PARAM_DEF(name, type, default_val) PARAM_##name,
#define PARAM_DEF(name, type, default_val, unit) PARAM_##name,
typedef enum {
PARAM_LIST
NUM_PARAMS
@@ -119,6 +120,7 @@ extern param_value_t parameter_table[NUM_PARAMS];
extern const param_value_t parameter_defaults[NUM_PARAMS];
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();
@@ -128,6 +130,7 @@ 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);
esp_err_t commit_params();
@@ -141,9 +144,12 @@ esp_err_t write_dummy_log_3();
#define LOG_ENTRY_SIZE 32
#define LOG_NUM_ENTRIES 512
#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
esp_err_t write_log(char* entry);
void storage_deinit();

File diff suppressed because one or more lines are too long

View File

@@ -1,28 +1,22 @@
<!doctype html><title>Control Panel</title><style>*{color:#eee;background-color:#111;font-family:sans-serif}input{text-align:right;box-sizing:border-box;background-color:#333;border:1px solid #666;width:100%;font-family:monospace}.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}</style></head><body><table id=table><tr><td>-</td><td>System Time</td><td><input id=in_time onchange=markChanged(this) step=1 type=datetime-local></td><td></td></tr><tr><td colspan=4><button disabled id=commit_btn onclick=commit_params()>Save Changes</button></td></tr></table><table id=table2><tr><td>Firmware</td><td><input accept=.bin id=firmware_file type=file></td><td><button id=upload_btn onclick=uploadFirmware()>Upload Firmware</button></td><td></td></tr><tr><td>Log File</td><td><button id=log_btn onclick=downloadLogFile()>Download Log</button></td><td></td></tr></table><script>let param_values=[];const param_names=[`Drive Distance`,`TPDF`,`Efuse Amt`,`Gain`,`Offset`],param_units=[`in`,`ft`,`in`,`V`,`ms`];function ge(x){return document.getElementById(x)}
<!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)}
// Highlight changed inputs and enable the save button
function markChanged(el){el.classList.add(`changed`),ge(`commit_btn`).disabled=!1}
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))}
// --- 1. GET DATA ---
function fetchStatus(){let xhr=new XMLHttpRequest;xhr.open(`GET`,`http://192.168.4.1/status`,!0),xhr.onload=function(){if(xhr.status===200)try{console.log(xhr.responseText);let data=JSON.parse(xhr.responseText);
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);
// 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}
// Store values (default to empty array if missing)
param_values=data.params||[]}catch(e){console.error(`Error parsing JSON`,e)}
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)}
// 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 renderTable(){let table=ge(`table`);
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>2;)table.deleteRow(1);
// Loop through the NAMES array to ensure every input is shown
param_names.forEach((name,i)=>{let row=table.insertRow(table.rows.length-1);row.innerHTML=`
for(;table.rows.length>0;)table.deleteRow(0);param_names.forEach((name,i)=>{let row=table.insertRow(table.rows.length);row.innerHTML=`
<td>${i}</td>
<td>${name}</td>
<td><input type="text" id="in_${i}" value="${param_values[i]!==void 0&&param_values[i]!==null?param_values[i]:`null`}" oninput="markChanged(this)"></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())}
// --- 2. POST DATA ---
function commit_params(){ge(`commit_btn`).disabled=!0,document.querySelectorAll(`input.changed`).forEach(input=>{let xhr=new XMLHttpRequest;if(input.id===`in_time`){
// Time handling
let epoch=Math.floor(new Date(input.value).getTime()/1e3);xhr.open(`POST`,`http://192.168.4.1/st`,!0),xhr.setRequestHeader(`Content-Type`,`application/json`),xhr.send(JSON.stringify({time:epoch})),input.classList.remove(`changed`)}else{
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_`)){
// Parameter handling
let id=input.id.split(`_`)[1],val=input.value.toLowerCase()===`null`?null:parseFloat(input.value);xhr.open(`POST`,`/sp`,!0),xhr.setRequestHeader(`Content-Type`,`application/json`),xhr.onload=function(){xhr.status===200&&input.classList.remove(`changed`)},xhr.send(JSON.stringify({id:parseInt(id),value:val}))}})}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`,`http://192.168.4.1/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)}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],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)}}
// Initial Load
window.onload=fetchStatus;</script></body></html>

View File

@@ -8,9 +8,12 @@
*/
#include "cJSON.h"
#include "control_fsm.h"
#include "endian.h"
#include "esp_ota_ops.h"
#include "esp_timer.h"
#include "power_mgmt.h"
#include "rf_433.h"
#include "rtc.h"
#include "string.h"
#include "freertos/FreeRTOS.h"
@@ -56,75 +59,127 @@ static esp_err_t root_get_handler(httpd_req_t *req) {
return httpd_resp_send(req, (const char *)html_content, html_content_len);
}
static esp_err_t log_get_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "log_get_handler");
static esp_err_t log_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "log_handler");
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");
}
}
const esp_partition_t *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");
}
int32_t start = get_log_tail();
int32_t end = get_log_head();
int32_t total_size = end - start;
int32_t offset = start;
if (start >= end) {
total_size = storage_partition->size - start + end - get_log_offset();
}
ESP_LOGI(TAG, "start/end: %ld/%ld -> %ld", (long)start, (long)end, (long)total_size);
//size_t total_size = storage_partition->size;
// Figure out the bounds of data
if (tail < 0)
tail = get_log_tail();
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;
if (tail >= head) {
total_size = storage_partition->size - tail + head - get_log_offset() + 8;
}
ESP_LOGI(TAG, "start/end: %ld/%ld -> %ld", (long)tail, (long)head, (long)total_size);
// Send header
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
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");
return ESP_FAIL;
}
sent += 8;
if (start >= end) {
ESP_LOGI(TAG, "STARTING");
while (offset < storage_partition->size) {
// if wrapped around, just go from the start all the way to the end of storage
// then set start = get_log_offset();
// and continue
size_t to_read = MIN(sizeof(httpBuffer), storage_partition->size - offset);
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 %ld x %d", (long) offset, to_read);
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;
}
offset = get_log_offset();
}
}
while (offset < end) {
ESP_LOGI(TAG, "FINISHING");
// if wrapped around, just go from the start all the way to the end of storage
// then set start = get_log_offset();
// and continue
size_t to_read = MIN(sizeof(httpBuffer), end - 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 %ld x %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;
}
offset += to_read;
}
ESP_LOGI(TAG, "Transfer complete: sent %ld bytes (expected %ld)", (long)sent, (long)total_size);
}
// End chunked transfer
httpd_resp_send_chunk(req, NULL, 0);
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");
return ESP_OK;
}
@@ -133,25 +188,33 @@ static esp_err_t st_post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "st_post_handler");
// Send the HTML response
int ret=0;
int remaining = req -> content_len;
while (remaining > 0) {
if ((ret = httpd_req_recv(req, httpBuffer, MIN(remaining, sizeof(httpBuffer))))<= 0) {
if(ret == HTTPD_SOCK_ERR_TIMEOUT){
continue;
}
return ESP_FAIL;
}
}
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);
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);
}
// set parameters id & value
// set parameters - accepts multiple parameters with mixed key types
static esp_err_t sp_post_handler(httpd_req_t *req) {
char content[128]; // Buffer for incoming JSON
char content[512]; // Increased buffer for multiple parameters
size_t recv_size = (req->content_len < sizeof(content)) ? req->content_len : sizeof(content) - 1;
// 1. Receive the data
@@ -172,92 +235,156 @@ static esp_err_t sp_post_handler(httpd_req_t *req) {
return ESP_FAIL;
}
// 3. Extract the ID and Value
cJSON *id_item = cJSON_GetObjectItem(root, "id");
cJSON *val_item = cJSON_GetObjectItem(root, "value");
int params_updated = 0;
int params_failed = 0;
int param_id = -1;
//char param_idstring[32] = "";
if (cJSON_IsNumber(id_item)) {
param_id = id_item->valueint;
}
if (cJSON_IsString(id_item)){
//param_idstring = id_item->valuestring;
for (uint8_t i=0; i<NUM_PARAMS; i++) {
if (strcmp(id_item->valuestring, get_param_name(i))) {
param_id = i;
break;
}
}
}
double param_val = cJSON_IsNumber(val_item) ? val_item->valuedouble : 0.0f;
ESP_LOGI(TAG, "Updating Param ID: %d to Value: %.2f", param_id, param_val);
// 3. Iterate through all items in the JSON object
cJSON *item = NULL;
cJSON_ArrayForEach(item, root) {
int param_id = -1;
const char *key = item->string;
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);
break;
}
if (key == NULL) {
ESP_LOGW(TAG, "Skipping item with null key");
params_failed++;
continue;
}
// Try to parse key as a parameter ID (numeric string like "4")
char *endptr;
long parsed_id = strtol(key, &endptr, 10);
if (*endptr == '\0' && parsed_id >= 0 && parsed_id < NUM_PARAMS) {
// Key is a valid numeric string
param_id = (int)parsed_id;
} else {
// Key is a string name, search for matching parameter
for (uint8_t i = 0; i < NUM_PARAMS; i++) {
if (strcmp(key, get_param_name(i)) == 0) {
param_id = i;
break;
}
}
}
// Check if we found a valid parameter
if (param_id < 0 || param_id >= NUM_PARAMS) {
ESP_LOGW(TAG, "Unknown parameter key: %s", key);
params_failed++;
continue;
}
// Get the value
if (!cJSON_IsNumber(item)) {
ESP_LOGW(TAG, "Parameter %s has non-numeric value", key);
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_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;
}
params_updated++;
}
commit_params();
cJSON_Delete(root);
// 5. Send Success Response
httpd_resp_set_type(req, "application/json");
return httpd_resp_send(req, "{\"status\":\"ok\"}", HTTPD_RESP_USE_STRLEN);
return httpd_resp_send(req, "200 OK", HTTPD_RESP_USE_STRLEN);
}
static esp_err_t move_post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "move_post_handler");
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));
if (ret <= 0) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_408(req);
}
return ESP_FAIL;
}
httpBuffer[ret] = '\0'; // Null-terminate the string
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");
return ESP_FAIL;
}
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);
}
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, "/move NOT IMPLEMENTED", HTTPD_RESP_USE_STRLEN);
return httpd_resp_send(req, "200 OK", HTTPD_RESP_USE_STRLEN);
}
static esp_err_t stop_post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "stop_post_handler");
// Send the HTML response
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, "/stop NOT IMPLEMENTED", HTTPD_RESP_USE_STRLEN);
}
/* Handler for Status GET request*/
static esp_err_t status_get_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "status_get_handler");
@@ -267,7 +394,11 @@ 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,\" values\":[", system_rtc_get_raw_time(), get_battery_V());
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"
);
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (i > 0) {
@@ -299,6 +430,14 @@ static esp_err_t status_get_handler(httpd_req_t *req) {
}
head += sprintf(httpBuffer+head, "\"%s\"", get_param_name(i));
}
head += sprintf(httpBuffer+head, "], \"units\":[");
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (i > 0) {
head += sprintf(httpBuffer+head, ",");
}
head += sprintf(httpBuffer+head, "\"%s\"", get_param_unit(i));
}
// Close the JSON array and object
head += sprintf(httpBuffer+head, "]}");
@@ -390,8 +529,8 @@ httpd_uri_t uris[] = {{
.user_ctx = NULL
},{
.uri = "/log",
.method = HTTP_GET,
.handler = log_get_handler,
.method = HTTP_ANY,
.handler = log_handler,
.user_ctx = NULL
},{
.uri = "/sp",
@@ -399,14 +538,9 @@ httpd_uri_t uris[] = {{
.handler = sp_post_handler,
.user_ctx = NULL
},{
.uri = "/move",
.uri = "/cmd",
.method = HTTP_POST,
.handler = move_post_handler,
.user_ctx = NULL
},{
.uri = "/stop",
.method = HTTP_POST,
.handler = stop_post_handler,
.handler = cmd_post_handler,
.user_ctx = NULL
},{
.uri = "/ota",