Files
SC-F001/main/webserver.c
Thaddeus Hughes 012d28ae14 Ironed out tons of stuff on the webserver
Logging, time sync, collapsible menus, oh my!
2025-12-29 22:21:43 -06:00

631 lines
21 KiB
C

/* 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 "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"
#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"
#include "esp_partition.h"
#define HOSTNAME "sc.local"
#define SOFT_AP_SSID "sc_main"
#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_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");
}
// 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 (tail != head) {
// Send data between tail and end (if wrapped)
if (tail >= head) {
ESP_LOGI(TAG, "STARTING wrapped section (tail=%ld, head=%ld)", (long)tail, (long)head);
while (offset < storage_partition->size) {
// FIXED: Don't limit by head in this section - read to end of partition
size_t to_read = MIN(sizeof(httpBuffer), storage_partition->size - offset);
esp_err_t err = esp_partition_read(storage_partition, offset, httpBuffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition: %s", esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read storage");
}
ESP_LOGI(TAG, "Sending wrapped chunk: offset=%ld size=%d", (long)offset, to_read);
if (httpd_resp_send_chunk(req, (const char *)httpBuffer, to_read) != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk");
return ESP_FAIL;
}
sent += to_read;
offset += to_read;
}
// Loop back to the beginning
offset = get_log_offset();
ESP_LOGI(TAG, "Wrapped to beginning, offset now=%ld", (long)offset);
}
// Send data between start (or tail) and head
ESP_LOGI(TAG, "FINISHING final section (offset=%ld, head=%ld)", (long)offset, (long)head);
while (offset < head) {
size_t to_read = MIN(sizeof(httpBuffer), head - offset);
esp_err_t err = esp_partition_read(storage_partition, offset, httpBuffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition: %s", esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read storage");
}
ESP_LOGI(TAG, "Sending final chunk: offset=%ld size=%d", (long)offset, to_read);
if (httpd_resp_send_chunk(req, (const char *)httpBuffer, to_read) != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk");
return ESP_FAIL;
}
sent += to_read;
offset += to_read;
}
ESP_LOGI(TAG, "Transfer complete: sent %ld bytes (expected %ld)", (long)sent, (long)total_size);
}
// End chunked transfer
if (httpd_resp_send_chunk(req, NULL, 0) != ESP_OK) {
ESP_LOGE(TAG, "Failed to send final empty chunk");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Final empty chunk sent successfully");
return ESP_OK;
}
// 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 = 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);
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[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
int ret = httpd_req_recv(req, content, recv_size);
if (ret <= 0) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_408(req);
}
return ESP_FAIL;
}
content[ret] = '\0'; // Null-terminate the string
// 2. Parse the JSON
cJSON *root = cJSON_Parse(content);
if (root == NULL) {
ESP_LOGE(TAG, "Failed to parse JSON: %s", content);
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
return ESP_FAIL;
}
int params_updated = 0;
int params_failed = 0;
// 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;
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
return httpd_resp_send(req, "200 OK", HTTPD_RESP_USE_STRLEN);
}
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, "200 OK", 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,\"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) {
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;
}
}
head += sprintf(httpBuffer+head, "], \"names\":[");
for (param_idx_t i = 0; i < NUM_PARAMS; i++) {
if (i > 0) {
head += sprintf(httpBuffer+head, ",");
}
head += sprintf(httpBuffer+head, "\"%s\"", get_param_name(i));
}
head += sprintf(httpBuffer+head, "], \"units\":[");
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, "]}");
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);
set_param_value_t(PARAM_BOOT_TIME, (param_value_t){.i64 = system_rtc_get_raw_time()});
// THEN delay and reboot
vTaskDelay(pdMS_TO_TICKS(2000)); // Give time for TCP to close properly
esp_restart();
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_ANY,
.handler = log_handler,
.user_ctx = NULL
},{
.uri = "/sp",
.method = HTTP_POST,
.handler = sp_post_handler,
.user_ctx = NULL
},{
.uri = "/cmd",
.method = HTTP_POST,
.handler = cmd_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 = {
.channel = 8,
.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;
}