integrating web stuff into SC-F001

LFG
This commit is contained in:
Thaddeus Hughes
2025-12-27 00:01:59 -06:00
parent 426d84ee56
commit 81a8da24a0
24 changed files with 665 additions and 2081 deletions

View File

@@ -2,9 +2,24 @@
# 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.c rtc.c sensors.c solar.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 # 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
)
if(NOT CMAKE_BUILD_EARLY_EXPANSION)
add_custom_command(
OUTPUT ${COMPONENT_DIR}/webpage.h
COMMAND python ${COMPONENT_DIR}/webpage_compile.py
DEPENDS ${COMPONENT_DIR}/landingpage.html ${COMPONENT_DIR}/webpage_compile.py
WORKING_DIRECTORY ${COMPONENT_DIR}
COMMENT "Generating webpage.h from landingpage.html"
VERBATIM
)
add_custom_target(generate_webpage_h DEPENDS ${COMPONENT_DIR}/webpage.h)
add_dependencies(${COMPONENT_LIB} generate_webpage_h)
endif()

View File

@@ -80,7 +80,7 @@ static inline bool timer_done() { return current_time >= timer_end; }
void pulseOverride(relay_t relay) {
if (current_state == STATE_IDLE)
override_times[relay] = current_time + get_param(PARAM_RF_PULSE_LENGTH).u64;
override_times[relay] = current_time + get_param_value_t(PARAM_RF_PULSE_LENGTH).u64;
}
/*void fsm_begin_auto_move() {
@@ -120,9 +120,9 @@ int8_t fsm_get_current_progress(int8_t denominator) {
}
#define JACK_TIME get_param(PARAM_JACK_MSPI ).u32 * 1000 * get_param(PARAM_JACK_DIST ).u8
#define DRIVE_TIME get_param(PARAM_DRIVE_MSPF).u32 * 1000 * get_param(PARAM_DRIVE_DIST).u8
#define DRIVE_DIST get_param(PARAM_DRIVE_TPDF).u32 / 10 * get_param(PARAM_DRIVE_DIST).u8
#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
void control_task(void *param) {
esp_task_wdt_add(NULL);

View File

@@ -1,328 +0,0 @@
/*
* filemgmt.c
*
* Created on: Nov 20, 2025
* Author: Thad
*/
#include "filemgmt.h"
#include "endian.h"
#include "esp_log.h"
#include <stdio.h>
#include <inttypes.h>
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_mac.h" // for MACSTR
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_vfs.h"
#include "nvs_flash.h"
#include "esp_vfs_fat.h"
#include "esp_littlefs.h"
#include "ftp.h"
#include "control_fsm.h"
#include "power_mgmt.h"
#include "rtc.h"
#include "sensors.h"
const char* partition_label = "storage";
const char *TAG = "MAIN";
const char *mount_point = "/root";
RTC_DATA_ATTR bool find_new_filename = true;
void start_new_log_file() {
find_new_filename = true;
}
EventGroupHandle_t xEventTask;
int FTP_TASK_FINISH_BIT = BIT2;
esp_err_t start_filesystem() {
ESP_LOGI(TAG, "Initializing LittleFS on Builtin SPI Flash Memory");
esp_vfs_littlefs_conf_t conf = {
.base_path = mount_point,
.partition_label = partition_label,
.format_if_mount_failed = true,
.dont_mount = false,
};
// Use settings defined above to initialize and mount LittleFS filesystem.
// Note: esp_vfs_littlefs_register is an all-in-one convenience function.
esp_err_t ret = esp_vfs_littlefs_register(&conf);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount or format filesystem");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to find LittleFS partition");
} else {
ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
}
return ret;
}
size_t total = 0, used = 0;
ret = esp_littlefs_info(conf.partition_label, &total, &used);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get LittleFS partition information (%s)", esp_err_to_name(ret));
//esp_littlefs_format(conf.partition_label);
return ret;
}
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
ESP_LOGI(TAG, "Mount LittleFS on %s", mount_point);
return ret;
}
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", MAC2STR(event->mac), event->aid);
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d", MAC2STR(event->mac), event->aid);
}
}
extern void ftp_task (void *pvParameters);
// Start WiFi AP and FTP Server
// filemgmt.c
esp_err_t start_ftp_server(void)
{
ESP_LOGI("FTP", "START");
close_current_log();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
// THIS IS THE CORRECT WAY recreate the default AP netif every time
esp_netif_create_default_wifi_ap();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
NULL));
wifi_config_t wifi_config = {
.ap = {
.ssid = ESP_WIFI_AP_SSID,
.ssid_len = strlen(ESP_WIFI_AP_SSID),
.password = ESP_WIFI_AP_PASSWORD,
.max_connection = ESP_WIFI_AP_N_CONNECTIONS,
.authmode = strlen(ESP_WIFI_AP_PASSWORD) ? WIFI_AUTH_WPA_WPA2_PSK : WIFI_AUTH_OPEN
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "WiFi AP started IP 192.168.4.1");
xEventTask = xEventGroupCreate();
xTaskCreate(ftp_task, "FTP", 1024*6, NULL, 2, NULL);
return ESP_OK;
}
void stop_ftp_server(void)
{
ESP_LOGI("FTP", "OFF");
ftp_terminate();
vTaskDelay(pdMS_TO_TICKS(500)); // give task time to die
esp_wifi_stop();
esp_wifi_deinit();
// This single call does everything correctly:
// Destroy default Wi-Fi interface (handles AP)
esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
if (ap_netif) {
esp_netif_destroy_default_wifi(ap_netif); // Correct API for AP
}
esp_event_loop_delete_default();
esp_netif_deinit();
ESP_LOGI("FTP", "WiFi + FTP completely stopped safe to restart");
}
void stop_filesystem() {
esp_vfs_littlefs_unregister(partition_label);
ESP_LOGI(TAG, "LittleFS unmounted");
}
#define LOG_ENTRY_SIZE 32
#define MAX_LOG_FILES 999 // rotate after N files
#define ENTRIES_PER_FILE 8000 // ~256 KB per file → safe
static FILE* logfile = NULL;
RTC_DATA_ATTR char current_log_path[32] = "/root/log000.bin";
static uint32_t current_entry_count = 0;
// Helper: close current file safely
void close_current_log(void)
{
if (logfile) {
fflush(logfile);
fsync(fileno(logfile)); // CRITICAL: forces write to flash
fclose(logfile);
logfile = NULL;
current_entry_count = 0;
}
}
// Helper: open next log file (rotates 000→009)
bool open_next_log_file(void)
{
close_current_log();
if (find_new_filename) {
// Find next filename
for (int i = 0; i < MAX_LOG_FILES; i++) {
snprintf(current_log_path, sizeof(current_log_path), "/root/log%03d.bin", i);
struct stat st;
if (stat(current_log_path, &st) != 0) {
break; // file doesn't exist → use this one
}
if (i == MAX_LOG_FILES-1) {
ESP_LOGE("FILE", "RAN OUT OF FILES, GOING TO MAX");
// All files exist → overwrite oldest (log000.bin)
strcpy(current_log_path, "/root/log999.bin");
}
}
logfile = fopen(current_log_path, "ab"); // binary append
if (!logfile) {
ESP_LOGE("FILE", "Failed to open %s", current_log_path);
return false;
}
// Optional: truncate if too big (safety)
fseek(logfile, 0, SEEK_END);
if (ftell(logfile) > ENTRIES_PER_FILE * LOG_ENTRY_SIZE) {
fclose(logfile);
logfile = fopen(current_log_path, "wb"); // truncate
}
find_new_filename = false;
} else {
logfile = fopen(current_log_path, "ab");
}
ESP_LOGI("FILE", "Logging to %s", current_log_path);
return true;
}
void file_log() {
if (!logfile && !open_next_log_file()) {
return;
}
char entry[LOG_ENTRY_SIZE] = {0};
entry[0] = LOG_ENTRY_SIZE;
// pack 64-bit timestamp into bytes 1-8
uint64_t be_timestamp = htobe64(rtc_time_ms());
memcpy(&entry[1], &be_timestamp, 8);
// pack 32-bit voltages/currents into bytes 9-24
int32_t be_voltage = htobe32(get_battery_mV());
memcpy(&entry[9], &be_voltage, 4);
int32_t be_current1 = htobe32(get_bridge_mA(BRIDGE_DRIVE));
memcpy(&entry[13], &be_current1, 4);
int32_t be_current2 = htobe32(get_bridge_mA(BRIDGE_JACK));
memcpy(&entry[17], &be_current2, 4);
int32_t be_current3 = htobe32(get_bridge_mA(BRIDGE_AUX));
memcpy(&entry[21], &be_current3, 4);
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[31] = fsm_get_state();
ESP_LOGI("FILE", "LOGGING TO %s: 0x %02x %02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x %02x %02x", current_log_path,
entry[0],
entry[1],
entry[2],
entry[3],
entry[4],
entry[5],
entry[6],
entry[7],
entry[8],
entry[9],
entry[10],
entry[11],
entry[12],
entry[13],
entry[14],
entry[15],
entry[16],
entry[17],
entry[18],
entry[19],
entry[20],
entry[21],
entry[22],
entry[23],
entry[24],
entry[25],
entry[26],
entry[27],
entry[28],
entry[29],
entry[30],
entry[31]);
/* SEND TO FILE */
size_t written = fwrite(entry, 1, LOG_ENTRY_SIZE, logfile);
if (written != LOG_ENTRY_SIZE) {
ESP_LOGE("FILE", "Partial write! Closing file.");
close_current_log();
return;
}
current_entry_count++;
// Periodic flush (every 50100 entries) + full sync every 500
if (current_entry_count % 100 == 0) {
fflush(logfile);
}
if (current_entry_count % 500 == 0) {
fsync(fileno(logfile));
}
// Rotate if getting large
if (current_entry_count >= ENTRIES_PER_FILE) {
ESP_LOGI("FILE", "Rotating log file");
open_next_log_file();
}
}

View File

@@ -1,33 +0,0 @@
/*
* filemgmt.h
*
* Created on: Nov 20, 2025
* Author: Thad
*/
#ifndef MAIN_FILEMGMT_H_
#define MAIN_FILEMGMT_H_
#include "esp_err.h"
#define ESP_WIFI_AP_SSID "stockcropper"
#define ESP_WIFI_AP_PASSWORD ""
#define ESP_WIFI_AP_N_CONNECTIONS 4
// Open the filesystem (e.g. for logging)
esp_err_t start_filesystem();
// Start WiFi AP and FTP Server
esp_err_t start_ftp_server();
void stop_filesystem();
void stop_ftp_server();
void file_log();
void close_current_log();
void start_new_log_file();
#endif /* MAIN_FILEMGMT_H_ */

1413
main/ftp.c

File diff suppressed because it is too large Load Diff

View File

@@ -1,214 +0,0 @@
/*
* This file is part of the MicroPython ESP32 project, https://github.com/loboris/MicroPython_ESP32_psRAM_LoBo
*
* The MIT License (MIT)
*
* Copyright (c) 2018 LoBo (https://github.com/loboris)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/*
* This file is based on 'ftp' from Pycom Limited.
*
* Author: LoBo, loboris@gmail.com
* Copyright (c) 2017, LoBo
*/
#ifndef FTP_H_
#define FTP_H_
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "sys/dirent.h"
/******************************************************************************
DEFINE PRIVATE CONSTANTS
******************************************************************************/
#define FTP_CMD_PORT 21
#define FTP_ACTIVE_DATA_PORT 20
#define FTP_PASIVE_DATA_PORT 2024
#define FTP_CMD_SIZE_MAX 6
#define FTP_CMD_CLIENTS_MAX 1
#define FTP_DATA_CLIENTS_MAX 1
#define FTP_MAX_PARAM_SIZE (MICROPY_ALLOC_PATH_MAX + 1)
#define FTP_UNIX_SECONDS_180_DAYS 15552000
#define FTP_DATA_TIMEOUT_MS 10000 // 10 seconds
#define FTP_SOCKETFIFO_ELEMENTS_MAX 4
typedef enum {
E_FTP_STE_DISABLED = 0,
E_FTP_STE_START,
E_FTP_STE_READY,
E_FTP_STE_END_TRANSFER,
E_FTP_STE_CONTINUE_LISTING,
E_FTP_STE_CONTINUE_FILE_TX,
E_FTP_STE_CONTINUE_FILE_RX,
E_FTP_STE_CONNECTED
} ftp_state_t;
typedef enum {
E_FTP_STE_SUB_DISCONNECTED = 0,
E_FTP_STE_SUB_LISTEN_FOR_DATA,
E_FTP_STE_SUB_DATA_CONNECTED
} ftp_substate_t;
/******************************************************************************
DEFINE PRIVATE TYPES
******************************************************************************/
typedef enum {
E_FTP_RESULT_OK = 0,
E_FTP_RESULT_CONTINUE,
E_FTP_RESULT_FAILED
} ftp_result_t;
typedef struct {
bool uservalid : 1;
bool passvalid : 1;
} ftp_loggin_t;
typedef enum {
E_FTP_NOTHING_OPEN = 0,
E_FTP_FILE_OPEN,
E_FTP_DIR_OPEN
} ftp_e_open_t;
typedef enum {
E_FTP_CLOSE_NONE = 0,
E_FTP_CLOSE_DATA,
E_FTP_CLOSE_CMD_AND_DATA,
} ftp_e_closesocket_t;
typedef struct {
uint8_t *dBuffer;
uint32_t ctimeout;
union {
DIR *dp;
FILE *fp;
};
int32_t lc_sd;
int32_t ld_sd;
int32_t c_sd;
int32_t d_sd;
int32_t dtimeout;
uint32_t ip_addr;
uint8_t state;
uint8_t substate;
uint8_t txRetries;
uint8_t logginRetries;
ftp_loggin_t loggin;
uint8_t e_open;
bool closechild;
bool enabled;
bool listroot;
uint32_t total;
uint32_t time;
} ftp_data_t;
typedef struct {
char * cmd;
} ftp_cmd_t;
typedef enum {
E_FTP_CMD_NOT_SUPPORTED = -1,
E_FTP_CMD_FEAT = 0,
E_FTP_CMD_SYST,
E_FTP_CMD_CDUP,
E_FTP_CMD_CWD,
E_FTP_CMD_PWD,
E_FTP_CMD_XPWD,
E_FTP_CMD_SIZE,
E_FTP_CMD_MDTM,
E_FTP_CMD_TYPE,
E_FTP_CMD_USER,
E_FTP_CMD_PASS,
E_FTP_CMD_PASV,
E_FTP_CMD_LIST,
E_FTP_CMD_RETR,
E_FTP_CMD_STOR,
E_FTP_CMD_DELE,
E_FTP_CMD_RMD,
E_FTP_CMD_MKD,
E_FTP_CMD_RNFR,
E_FTP_CMD_RNTO,
E_FTP_CMD_NOOP,
E_FTP_CMD_QUIT,
E_FTP_CMD_APPE,
E_FTP_CMD_NLST,
E_FTP_CMD_AUTH,
E_FTP_NUM_FTP_CMDS
} ftp_cmd_index_t;
#define FTP_USER_PASS_LEN_MAX 32
#define FTP_DEF_USER "micro"
#define FTP_DEF_PASS "python"
#define FTP_MUTEX_TIMEOUT_MS 1000
#define FTP_CMD_TIMEOUT_MS (CONFIG_MICROPY_FTPSERVER_TIMEOUT*1000)
#define CONFIG_MICROPY_FTPSERVER_BUFFER_SIZE 1024
#define CONFIG_MICROPY_FTPSERVER_TIMEOUT 300
#define ONFIG_MICROPY_FILESYSTEM_TYPE 0
#define MICROPY_ALLOC_PATH_MAX (512)
// #define MAX_ACTIVE_INTERFACES 3
//tcpip_adapter_if_t tcpip_if[MAX_ACTIVE_INTERFACES] = {TCPIP_ADAPTER_IF_MAX};
#define VFS_NATIVE_MOUNT_POINT "/_#!#_spiffs"
#define VFS_NATIVE_SDCARD_MOUNT_POINT "/_#!#_sdcard"
#define VFS_NATIVE_INTERNAL_PART_LABEL "internalfs"
#define VFS_NATIVE_INTERNAL_MP "/flash"
#define VFS_NATIVE_EXTERNAL_MP "/sd"
#define VFS_NATIVE_TYPE_SPIFLASH 0
#define VFS_NATIVE_TYPE_SDCARD 1
#ifndef MIN
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#endif
#ifndef MAX
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#endif
#if 0
extern const char *FTP_TAG;
extern char ftp_user[FTP_USER_PASS_LEN_MAX + 1];
extern char ftp_pass[FTP_USER_PASS_LEN_MAX + 1];
extern uint32_t ftp_stack_size;
extern QueueHandle_t ftp_mutex;
extern int ftp_buff_size;
extern int ftp_timeout;
#endif
bool ftp_init (void);
void ftp_deinit (void);
int ftp_run (uint32_t elapsed);
bool ftp_enable (void);
bool ftp_isenabled (void);
bool ftp_disable (void);
bool ftp_reset (void);
int ftp_getstate();
bool ftp_terminate (void);
bool ftp_stop_requested();
int32_t ftp_get_maxstack (void);
uint64_t mp_hal_ticks_ms(void);
#endif /* FTP_H_ */

159
main/landingpage.html Normal file
View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html>
<head>
<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; }
.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; }
</style>
</head>
<body>
<table id="table">
<tr>
<td>-</td>
<td>System Time</td>
<td><input type="datetime-local" id="in_time" step="1" onchange="markChanged(this)"/></td>
<td></td>
</tr>
<tr><td colspan="4"><button id="commit_btn" onclick="commit_params()" disabled>Save Changes</button></td></tr>
</table>
<input type="file" id="firmware_file" accept=".bin">
<button id="upload_btn" onclick="uploadFirmware()">Upload Firmware</button>
<script>
let param_values = [];
const param_names = ["Drive Distance", "TPDF", "Efuse Amt", "Gain", "Offset"];
const param_units = ["in", "ft", "in", "V", "ms"];
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 = false;
}
// --- 1. GET DATA ---
function fetchStatus() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://192.168.4.1/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;
}
// Store values (default to empty array if missing)
param_values = data.params || [];
} 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() {
const table = ge("table");
// Clear existing parameter rows (rows between index 0 and the last row)
while(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);
// 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]
: "null";
row.innerHTML = `
<td>${i}</td>
<td>${name}</td>
<td><input type="text" id="in_${i}" value="${val}" oninput="markChanged(this)"></td>
<td>${param_units[i] || ""}</td>
`;
});
}
// --- 2. POST DATA ---
function commit_params() {
ge('commit_btn').disabled = true;
const changedInputs = document.querySelectorAll('input.changed');
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 {
// 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", "/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 }));
}
});
}
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", "http://192.168.4.1/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);
}
// Initial Load
window.onload = fetchStatus;
</script>
</body>
</html>

View File

@@ -10,7 +10,8 @@
#include "rtc.h"
#include "sensors.h"
#include "solar.h"
#include "rf.h"
#include "rf_433.h"
#include "webserver.h"
#define TAG "MAIN"
@@ -140,9 +141,10 @@ void app_main(void) {
/*** FULL BOOT ***/
if (uart_init() != ESP_OK) ESP_LOGE(TAG, "UART FAILED");
if (power_init() != ESP_OK) ESP_LOGE(TAG, "POWER FAILED");
if (rf_init() != ESP_OK) ESP_LOGE(TAG, "RF FAILED");
if (rf_433_init() != ESP_OK) ESP_LOGE(TAG, "RF FAILED");
if (fsm_init() != ESP_OK) ESP_LOGE(TAG, "FSM FAILED");
if (sensors_init() != ESP_OK) ESP_LOGE(TAG, "SENSORS FAILED");
if (webserver_init() != ESP_OK) ESP_LOGE(TAG, "WEBSERVER FAILED");
/*** MAIN LOOP ***/

View File

@@ -123,7 +123,7 @@ esp_err_t process_battery_voltage(void)
ema_battery = (float)raw;
ema_battery_init = true;
} else {
float alpha = get_param(PARAM_ADC_ALPHA_BATTERY).f32;
float alpha = get_param_value_t(PARAM_ADC_ALPHA_BATTERY).f32;
if (isnan(raw)) {
ESP_LOGI(TAG, "RAW BATTERY IS NAN");
} else {
@@ -182,7 +182,7 @@ esp_err_t process_bridge_current(bridge_t bridge) {
channel->ema_current = (float)raw_a;
channel->ema_init = true;
} else {
float alpha = get_param(PARAM_ADC_ALPHA_ISENS).f32;
float alpha = get_param_value_t(PARAM_ADC_ALPHA_ISENS).f32;
if (isnan(raw_a)) {
ESP_LOGI(TAG, "RAW BATTERY IS NAN");
channel->ema_current = NAN;
@@ -197,14 +197,14 @@ esp_err_t process_bridge_current(bridge_t bridge) {
// === AUTO-ZERO LEARNING PHASE ===
if (now > channel->az_enable_time) {
//ESP_LOGI(TAG, "AZING %d", bridge);
float db = get_param(PARAM_ADC_DB_IAZ).f32;
float db = get_param_value_t(PARAM_ADC_DB_IAZ).f32;
if (isnan(db) || fabsf(channel->ema_current) <= db) {
// Valid zero sample
if (!channel->az_initialized) {
channel->az_offset = channel->ema_current;
channel->az_initialized = true;
} else {
float alpha = get_param(PARAM_ADC_ALPHA_IAZ).f32;
float alpha = get_param_value_t(PARAM_ADC_ALPHA_IAZ).f32;
if (isnan(raw_a)) {
ESP_LOGI(TAG, "RAW BATTERY IS NAN");
} else {
@@ -231,13 +231,13 @@ esp_err_t process_bridge_current(bridge_t bridge) {
float I_nominal = NAN;
switch(bridge) {
case BRIDGE_DRIVE:
I_nominal = get_param(PARAM_EFUSE_INOM_1).f32;
I_nominal = get_param_value_t(PARAM_EFUSE_INOM_1).f32;
break;
case BRIDGE_JACK:
I_nominal = get_param(PARAM_EFUSE_INOM_2).f32;
I_nominal = get_param_value_t(PARAM_EFUSE_INOM_2).f32;
break;
case BRIDGE_AUX:
I_nominal = get_param(PARAM_EFUSE_INOM_3).f32;
I_nominal = get_param_value_t(PARAM_EFUSE_INOM_3).f32;
break;
}
@@ -245,7 +245,7 @@ esp_err_t process_bridge_current(bridge_t bridge) {
float I_norm = fabsf(channel->current / I_nominal);
// Instant trip on extreme overcurrent
if (I_norm >= get_param(PARAM_EFUSE_KINST).f32) {
if (I_norm >= get_param_value_t(PARAM_EFUSE_KINST).f32) {
channel->tripped = true;
channel->trip_time = now;
ESP_LOGI(TAG, "FUSE TRIP: Inom: %+.5f HEAT:%+2.5f", I_norm, channel->heat);
@@ -259,7 +259,7 @@ esp_err_t process_bridge_current(bridge_t bridge) {
if (I_norm < 1.0f) {
// if we are hot we radiate more heat
// (I^2/I^2*t) * (1/t) * t = I^2/I^2*t
channel->heat -= channel->heat * get_param(PARAM_EFUSE_TAUCOOL).f32 * UPDATE_S;
channel->heat -= channel->heat * get_param_value_t(PARAM_EFUSE_TAUCOOL).f32 * UPDATE_S;
channel->heat = fmaxf(0.0f, channel->heat); // keep it from going negative
// channel.tripped = false; // Auto-clear if cooled (WTF why this is insane)
}
@@ -267,7 +267,7 @@ esp_err_t process_bridge_current(bridge_t bridge) {
// If built-up heat exceeds the time limit, trip
// Recall units of heat are (current_actual^2/current_nominal^2)*time
// Ergo, heat is measured in seconds
if (channel->heat > get_param(PARAM_EFUSE_HEAT_THRESH).f32) {
if (channel->heat > get_param_value_t(PARAM_EFUSE_HEAT_THRESH).f32) {
channel->tripped = true;
channel->trip_time = now;
@@ -275,7 +275,7 @@ esp_err_t process_bridge_current(bridge_t bridge) {
// And enough time has passed
// Go ahead and reset the e-fuse
} else if (channel->tripped &&
(now - channel->trip_time) > get_param(PARAM_EFUSE_TCOOL).i64) {
(now - channel->trip_time) > get_param_value_t(PARAM_EFUSE_TCOOL).i64) {
channel->tripped = false;
// channel.heat = 0.0f // I think we should wait for the e-fuse to catch up
}

View File

@@ -1,30 +0,0 @@
#ifndef RF_H
#define RF_H
#include <inttypes.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#define NUM_RF_BUTTONS 8
int64_t recieveKeycode();
esp_err_t rf_init();
esp_err_t rf_stop();
void rf_set_keycode(uint8_t index, int64_t code);
int8_t rf_get_keycode();
int64_t rf_get_raw_keycode();
void rf_clear_queue();
void rf_learn_keycode(uint8_t index);
void rf_cancel_learn_keycode();
#endif

View File

@@ -15,7 +15,7 @@
#include "driver/rmt_rx.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "rf.h"
#include "rf_433.h"
#include "storage.h"
@@ -51,7 +51,7 @@ static bool rfrx_done(rmt_channel_handle_t channel, const rmt_rx_done_event_data
}
// Task that receives and decodes RF signals
static void rf_receiver_task(void* param) {
static void rf_433_receiver_task(void* param) {
esp_task_wdt_add(NULL);
esp_log_level_set("rmt", ESP_LOG_NONE); // disable rmt messages about hw buffer too small
const uint16_t tlow = (P_HIGH - P_LOW - (2 * P_MARGIN));
@@ -137,7 +137,7 @@ static void rf_receiver_task(void* param) {
ESP_LOGI(TAG, "GOT KEYCODE 0x%lx [%d]", (long) code, len);
if (learn_flag >= 0) {
set_param(PARAM_KEYCODE_0 + learn_flag,
set_param_value_t(PARAM_KEYCODE_0 + learn_flag,
(param_value_t){.i64 = encoded});
ESP_LOGI(TAG, "LEARNED KEYCODE");
learn_flag = -1;
@@ -156,7 +156,7 @@ static void rf_receiver_task(void* param) {
for (uint8_t i = 0; i < NUM_RF_BUTTONS; i++) {
int64_t match = get_param(PARAM_KEYCODE_0+i).i64;
int64_t match = get_param_value_t(PARAM_KEYCODE_0+i).i64;
if (encoded == match) {
switch (i) {
case 0: pulseOverride(RELAY_A1); pulseOverride(RELAY_A3); break;
@@ -216,36 +216,36 @@ static void rf_receiver_task(void* param) {
vTaskDelete(NULL);
}
esp_err_t rf_init() {
esp_err_t rf_433_init() {
g_code_queue = xQueueCreate(5, sizeof(rf_code_t));
assert(g_code_queue);
xTaskCreate(rf_receiver_task, TAG, 4096, NULL, 10, NULL);
xTaskCreate(rf_433_receiver_task, TAG, 4096, NULL, 10, NULL);
return ESP_OK;
}
esp_err_t rf_stop() { return ESP_OK; }
esp_err_t rf_433_stop() { return ESP_OK; }
void rf_set_keycode(uint8_t index, int64_t code) {
set_param(PARAM_KEYCODE_0+index, (param_value_t){.i64=code});
void rf_433_set_keycode(uint8_t index, int64_t code) {
set_param_value_t(PARAM_KEYCODE_0+index, (param_value_t){.i64=code});
}
void rf_learn_keycode(uint8_t index) {
void rf_433_learn_keycode(uint8_t index) {
if (index >= 8) return;
learn_flag = index;
}
void rf_cancel_learn_keycode() {
void rf_433_cancel_learn_keycode() {
learn_flag = -1;
}
int8_t rf_get_keycode() {
/*int8_t rf_433_get_keycode() {
rf_code_t received_code;
if (xQueueReceive(g_code_queue, &received_code, 0) == pdPASS) {
int64_t newcode = ((int64_t)received_code.num_symbols << 56) | received_code.code;
for (uint8_t i = 0; i < NUM_RF_BUTTONS; i++) {
if (newcode == get_param(PARAM_KEYCODE_0+i).i64)
if (newcode == get_param_value_t(PARAM_KEYCODE_0+i).i64)
return i;
}
ESP_LOGI("RF", "Received unknown code 0x%08lx (%d) [0x%16llx]", (unsigned long)received_code.code, received_code.num_symbols, (unsigned long long) newcode);
@@ -253,7 +253,7 @@ int8_t rf_get_keycode() {
return -1;
}
int64_t rf_get_raw_keycode() {
int64_t rf_433_get_raw_keycode() {
rf_code_t received_code;
int64_t code = -1;
if (xQueueReceive(g_code_queue, &received_code, 0) == pdPASS) {
@@ -261,8 +261,8 @@ int64_t rf_get_raw_keycode() {
//ESP_LOGI("RF", "Raw Code 0x%08lx (%d) [0x%16llx]", (unsigned long)received_code.code, received_code.num_symbols, (unsigned long long) code);
}
return code;
}
}*/
void rf_clear_queue() {
void rf_433_clear_queue() {
xQueueReset(g_code_queue);
}

30
main/rf_433.h Normal file
View File

@@ -0,0 +1,30 @@
#ifndef RF_H
#define RF_H
#include <inttypes.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#define NUM_RF_BUTTONS 8
int64_t recieveKeycode();
esp_err_t rf_433_init();
esp_err_t rf_433_stop();
void rf_433_set_keycode(uint8_t index, int64_t code);
/*
int8_t rf_433_get_keycode();
int64_t rf_433_get_raw_keycode();
*/
void rf_433_learn_keycode(uint8_t index);
void rf_433_cancel_learn_keycode();
#endif

View File

@@ -24,7 +24,6 @@
#include "freertos/task.h"
#include "i2c.h"
#include "rtc_wdt.h"
#include "filemgmt.h"
//#include "esp32/rtc_clk.h" // For RTC_SLOW_FREQ_32K_XTAL enum and rtc_clk_slow_freq_set()
#include "driver/rtc_io.h" // For RTC I/O handling (optional but recommended for pin configuration)
@@ -85,7 +84,6 @@ void enter_deep_sleep(void)
void rtc_set_time(struct tm *tm) {
rtc_set = true;
start_new_log_file();
struct timeval tv = { .tv_sec = mktime(tm), .tv_usec = 0 };
settimeofday(&tv, NULL);
reset_solar_fsm();

View File

@@ -50,18 +50,18 @@ esp_err_t run_solar_fsm() {
//if (rtc_is_set()) {
switch(current_charge_state) {
case CHG_STATE_BULK:
if (now > timer+get_param(PARAM_CHG_BULK_S).i64) {
if (now > timer+get_param_value_t(PARAM_CHG_BULK_S).i64) {
current_charge_state = CHG_STATE_FLOAT;
}
break;
case CHG_STATE_FLOAT:
// if we have sufficient voltage, reset the timer
if (vbat > get_param(PARAM_CHG_LOW_V).f32) {
if (vbat > get_param_value_t(PARAM_CHG_LOW_V).f32) {
timer = now;
}
if (now > timer+get_param(PARAM_CHG_LOW_S).i64) {
if (now > timer+get_param_value_t(PARAM_CHG_LOW_S).i64) {
timer = now;
current_charge_state = CHG_STATE_BULK;
}

View File

@@ -59,7 +59,7 @@ static bool log_initialized = false;
// PARAMETER FUNCTIONS
// ============================================================================
param_value_t get_param(param_idx_t id) {
param_value_t get_param_value_t(param_idx_t id) {
if (id >= NUM_PARAMS) {
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
param_value_t err = {0};
@@ -68,7 +68,7 @@ param_value_t get_param(param_idx_t id) {
return parameter_table[id];
}
esp_err_t set_param(param_idx_t id, param_value_t val) {
esp_err_t set_param_value_t(param_idx_t id, param_value_t val) {
if (id >= NUM_PARAMS) {
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
return ESP_ERR_INVALID_ARG;

View File

@@ -122,8 +122,8 @@ extern const char* parameter_names[NUM_PARAMS];
esp_err_t storage_init();
esp_err_t log_init();
param_value_t get_param(param_idx_t id);
esp_err_t set_param(param_idx_t id, param_value_t val);
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);

View File

@@ -11,7 +11,7 @@
#include "storage.h"
#include <errno.h>
#include <ctype.h>
#include "rf.h"
#include "rf_433.h"
#define TAG "UART"
@@ -188,7 +188,7 @@ static void cmd_set_param(char *args) {
return;
}
esp_err_t err = set_param(id, param_val);
esp_err_t err = set_param_value_t(id, param_val);
if (err == ESP_OK) {
printf("OK: Parameter %u (%s) set to ",
id, get_param_name(id));
@@ -223,7 +223,7 @@ static void cmd_get_param(char *args) {
}
// Get parameter
param_value_t val = get_param(id);
param_value_t val = get_param_value_t(id);
printf("Parameter %u (%s) = ", id, get_param_name(id));
print_param_value(id, val);
}
@@ -262,7 +262,7 @@ static void cmd_reset_param(char *args) {
// Reset to default
param_value_t default_val = get_param_default(id);
esp_err_t err = set_param(id, default_val);
esp_err_t err = set_param_value_t(id, default_val);
if (err == ESP_OK) {
printf("OK: Parameter %u (%s) reset to default: ",
@@ -287,7 +287,7 @@ static void cmd_list_params(char *args) {
};
for (int i = 0; i < NUM_PARAMS; i++) {
param_value_t val = get_param(i);
param_value_t val = get_param_value_t(i);
param_type_e type = get_param_type(i);
printf("%-3d | %-17s | %-4s | ", i, get_param_name(i), type_names[type]);
@@ -317,7 +317,7 @@ static void cmd_rf_learn(char *args) {
char *id_str = strtok(args, " \t");
if (id_str == NULL) {
rf_cancel_learn_keycode();
rf_433_cancel_learn_keycode();
return;
}
@@ -331,7 +331,7 @@ static void cmd_rf_learn(char *args) {
param_idx_t id = (param_idx_t)id_u64;
if (id < 8) {
printf("Listening for keycode for slot %d\n", id);
rf_learn_keycode(id);
rf_433_learn_keycode(id);
return;
}
printf("ERROR: Keycode slot index out of bounds.\n");

3
main/webpage.h Normal file

File diff suppressed because one or more lines are too long

31
main/webpage_compile.py Normal file
View File

@@ -0,0 +1,31 @@
import minify_html
import gzip
with open("landingpage.html", "r", encoding="utf-8") as fin:
original_html = fin.read()
minified_html = minify_html.minify(
original_html,
minify_js=True,
minify_css=True,
keep_comments=False, # Remove comments
keep_html_and_head_opening_tags=False, # Remove <html> and <head> if possible
keep_closing_tags=True, # Keep closing tags (safer; set False for more aggression if you test thoroughly)
remove_processing_instructions=True, # Remove <?xml ...> etc.
#keep_comments=False,
remove_bangs=False # Keep <!DOCTYPE> bang (removing saves bytes but can break some edge cases)
)
with open("webpage_minified.html", "w") as fout:
fout.write(minified_html)
minified_bytes = minified_html.encode('utf-8')
gzipped_bytes = gzip.compress(minified_bytes, compresslevel=9)
with open("webpage.h", "w") as fout:
fout.write("const char html_content[] = {")
fout.write(','.join(f'0x{byte:02x}' for byte in gzipped_bytes))
fout.write("};\n\n")
fout.write(f"const unsigned int html_content_len = {len(gzipped_bytes)};\n")

View File

@@ -0,0 +1,28 @@
<!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><input accept=.bin id=firmware_file type=file><button id=upload_btn onclick=uploadFirmware()>Upload Firmware</button><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)}
// Highlight changed inputs and enable the save button
function markChanged(el){el.classList.add(`changed`),ge(`commit_btn`).disabled=!1}
// --- 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);
// 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)}
// 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`);
// 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=`
<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_units[i]||``}</td>
`})}
// --- 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{
// 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)}
// Initial Load
window.onload=fetchStatus;</script></body></html>

333
main/webserver.c Normal file
View File

@@ -0,0 +1,333 @@
/* WiFi softAP Example
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include "esp_ota_ops.h"
#include "esp_timer.h"
#include "rtc.h"
#include "string.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_http_server.h"
#include "esp_netif.h"
#include <math.h>
#include <stdint.h>
#include <sys/param.h>
#include <time.h>
#include "stdio.h"
#include "storage.h"
//#include "mdns.h"
#include "webpage.h"
#define HOSTNAME "sc.local"
#define SOFT_AP_SSID "stockcropper"
#define SOFT_AP_PASSWORD "stockcropper"
#define SERVER_PORT 80
static const char *TAG = "WEBSERVER";
static httpd_handle_t httpServerInstance = NULL;
char httpBuffer[1024];
/* Handler to serve the HTML page */
static esp_err_t root_get_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "root_get_handler");
// 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);
}
static esp_err_t log_get_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "log_get_handler");
// Send the HTML response
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, html_content, HTTPD_RESP_USE_STRLEN);
}
// set time: timestamp (unix epoch, seconds)
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;
}
}
ESP_LOGI(TAG, "ST POST %.*s", ret, httpBuffer);
return httpd_resp_send(req, "200 OK", HTTPD_RESP_USE_STRLEN);
}
// set parameters id & value
static esp_err_t sp_post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "sp_post_handler");
// Send the HTML response
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, "/sp NOT IMPLEMENTED", HTTPD_RESP_USE_STRLEN);
}
static esp_err_t move_post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "move_post_handler");
// Send the HTML response
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, "/move NOT IMPLEMENTED", 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");
size_t head = 0;
// Set the response type to JSON
httpd_resp_set_type(req, "application/json");
// Start building the JSON string with time
head += sprintf(httpBuffer+head, "{\"time\":%lld,\"params\":[", system_rtc_get_raw_time());
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (i > 0) {
head += sprintf(httpBuffer+head, ",");
}
// Retrieve the parameter; assuming get_param(i) returns a param_t struct with union
param_value_t param = get_param_value_t(i);
// 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;
}
}
// Close the JSON array and object
head += sprintf(httpBuffer+head, "]}");
return httpd_resp_send(req, httpBuffer, head);
}
static esp_err_t ota_post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "OTA POST request received");
esp_ota_handle_t update_handle = 0;
const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);
if (update_partition == NULL) {
ESP_LOGE(TAG, "No OTA partition found");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No OTA partition");
}
esp_err_t err = esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &update_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
}
char buf[1024];
int recv_len;
int total_len = req->content_len;
int remaining = total_len;
while (remaining > 0) {
recv_len = httpd_req_recv(req, buf, MIN(remaining, sizeof(buf)));
if (recv_len <= 0) {
if (recv_len == HTTPD_SOCK_ERR_TIMEOUT) {
continue;
}
esp_ota_abort(update_handle);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Receive failed");
}
err = esp_ota_write(update_handle, (const void *)buf, recv_len);
if (err != ESP_OK) {
esp_ota_abort(update_handle);
ESP_LOGE(TAG, "esp_ota_write failed (%s)", esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
}
remaining -= recv_len;
}
err = esp_ota_end(update_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_end failed (%s)", esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA end failed");
}
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)", esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Set boot partition failed");
}
ESP_LOGI(TAG, "OTA update successful. Rebooting in 2 seconds...");
// Send response FIRST
httpd_resp_send(req, "OTA update successful, rebooting...", HTTPD_RESP_USE_STRLEN);
// THEN delay and reboot
vTaskDelay(pdMS_TO_TICKS(2000)); // Give time for TCP to close properly
esp_restart();
return ESP_OK;
}
httpd_uri_t uris[] = {{
.uri = "/status",
.method = HTTP_GET,
.handler = status_get_handler,
.user_ctx = NULL
},{
.uri = "/",
.method = HTTP_GET,
.handler = root_get_handler,
.user_ctx = NULL
},{
.uri = "/st",
.method = HTTP_ANY,
.handler = st_post_handler,
.user_ctx = NULL
},{
.uri = "/log",
.method = HTTP_GET,
.handler = log_get_handler,
.user_ctx = NULL
},{
.uri = "/sp",
.method = HTTP_POST,
.handler = sp_post_handler,
.user_ctx = NULL
},{
.uri = "/move",
.method = HTTP_POST,
.handler = move_post_handler,
.user_ctx = NULL
},{
.uri = "/stop",
.method = HTTP_POST,
.handler = stop_post_handler,
.user_ctx = NULL
},{
.uri = "/ota",
.method = HTTP_POST,
.handler = ota_post_handler,
.user_ctx = NULL
}};
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]);
}
}
}
/* Event handler for WiFi events */
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data) {
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
ESP_LOGI(TAG, "Station connected.");
//startHttpServer();
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
ESP_LOGI(TAG, "Station disconnected.");
//stopHttpServer();
}
}
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_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,
NULL,
NULL));
wifi_config_t wifi_config = {
.ap = {
.ssid = SOFT_AP_SSID,
.ssid_len = strlen(SOFT_AP_SSID),
.password = SOFT_AP_PASSWORD,
.max_connection = 4,
.authmode = WIFI_AUTH_WPA2_PSK
},
};
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);
}
esp_err_t webserver_init(void) {
launchSoftAp();
startHttpServer();
return ESP_OK;
}

3
main/webserver.h Normal file
View File

@@ -0,0 +1,3 @@
#include "esp_err.h"
esp_err_t webserver_init(void);

View File

@@ -353,14 +353,14 @@ CONFIG_ESPTOOLPY_FLASHFREQ_40M=y
# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set
CONFIG_ESPTOOLPY_FLASHFREQ="40m"
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE="2MB"
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
# CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE is not set
CONFIG_ESPTOOLPY_BEFORE_RESET=y
# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set
@@ -374,12 +374,12 @@ CONFIG_ESPTOOLPY_MONITOR_BAUD=115200
#
# Partition Table
#
CONFIG_PARTITION_TABLE_SINGLE_APP=y
# CONFIG_PARTITION_TABLE_SINGLE_APP is not set
# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set
# CONFIG_PARTITION_TABLE_TWO_OTA is not set
# CONFIG_PARTITION_TABLE_CUSTOM is not set
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_OFFSET=0x8000
CONFIG_PARTITION_TABLE_MD5=y
# end of Partition Table
@@ -733,7 +733,7 @@ CONFIG_HTTPD_PURGE_BUF_LEN=32
# ESP HTTPS OTA
#
# CONFIG_ESP_HTTPS_OTA_DECRYPT_CB is not set
# CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP is not set
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y
# end of ESP HTTPS OTA
#
@@ -1880,7 +1880,7 @@ CONFIG_POST_EVENTS_FROM_ISR=y
CONFIG_POST_EVENTS_FROM_IRAM_ISR=y
CONFIG_GDBSTUB_SUPPORT_TASKS=y
CONFIG_GDBSTUB_MAX_TASKS=32
# CONFIG_OTA_ALLOW_HTTP is not set
CONFIG_OTA_ALLOW_HTTP=y
# CONFIG_TWO_UNIVERSAL_MAC_ADDRESS is not set
CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y
CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4

View File

@@ -353,14 +353,14 @@ CONFIG_ESPTOOLPY_FLASHFREQ_40M=y
# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set
CONFIG_ESPTOOLPY_FLASHFREQ="40m"
# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_2MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set
# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
CONFIG_ESPTOOLPY_FLASHSIZE="2MB"
# CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE is not set
CONFIG_ESPTOOLPY_BEFORE_RESET=y
# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set
@@ -733,7 +733,7 @@ CONFIG_HTTPD_PURGE_BUF_LEN=32
# ESP HTTPS OTA
#
# CONFIG_ESP_HTTPS_OTA_DECRYPT_CB is not set
# CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP is not set
CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y
# end of ESP HTTPS OTA
#
@@ -1880,7 +1880,7 @@ CONFIG_POST_EVENTS_FROM_ISR=y
CONFIG_POST_EVENTS_FROM_IRAM_ISR=y
CONFIG_GDBSTUB_SUPPORT_TASKS=y
CONFIG_GDBSTUB_MAX_TASKS=32
# CONFIG_OTA_ALLOW_HTTP is not set
CONFIG_OTA_ALLOW_HTTP=y
# CONFIG_TWO_UNIVERSAL_MAC_ADDRESS is not set
CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y
CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4