Files
SC-F001/main/webserver.c
2026-03-11 12:07:18 -05:00

1164 lines
40 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 "comms.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 "simple_dns_server.h"
#include "string.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_task_wdt.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 "version.h"
#include "webpage.h"
#include "webserver.h"
#include "esp_partition.h"
#define HOSTNAME "sc.local"
#define SERVER_PORT 80
static const char *TAG = "WEBSERVER";
// HTTPS
/*
#include "esp_https_server.h"
extern const uint8_t servercert_pem_start[] asm("_binary_servercert_pem_start");
extern const uint8_t servercert_pem_end[] asm("_binary_servercert_pem_end");
extern const uint8_t prvtkey_pem_start[] asm("_binary_prvtkey_pem_start");
extern const uint8_t prvtkey_pem_end[] asm("_binary_prvtkey_pem_end");
*/
static httpd_handle_t http_server_instance = NULL;
char http_buffer[4096];
/* Handler to serve the HTML page */
static esp_err_t root_get_handler(httpd_req_t *req) {
//ESP_LOGI(TAG, "root_get_handler");
if (req == NULL) {
ESP_LOGE(TAG, "Null request pointer");
return ESP_FAIL;
}
// Send the HTML response
esp_err_t err = httpd_resp_set_type(req, "text/html");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set response type: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set content encoding header: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_send(req, (const char *)html_content, html_content_len);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send HTML response: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_set_hdr(req, "Connection", "close");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
// Continue anyway
}
return err;
}
// Cache the storage partition pointer to avoid repeated lookups
static const esp_partition_t *cached_storage_partition = NULL;
// In webserver.c - Replace the log_handler function
static esp_err_t log_handler(httpd_req_t *req) {
if (req == NULL) {
ESP_LOGE(TAG, "Null request pointer");
return ESP_FAIL;
}
rtc_reset_shutdown_timer();
int32_t tail = -1;
if (req->method == HTTP_GET) {
// GET without parameters - return JSON + full log
}
else if (req->method == HTTP_POST) {
int ret = httpd_req_recv(req, http_buffer, sizeof(http_buffer));
if (ret <= 0) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Socket timeout during receive");
return httpd_resp_send_408(req);
}
ESP_LOGE(TAG, "Failed to receive POST data: %d", ret);
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to receive data");
}
if (ret >= (int)sizeof(http_buffer)) {
ESP_LOGE(TAG, "POST data exceeds buffer size");
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Request too large");
}
http_buffer[ret] = '\0';
if(sscanf(http_buffer, "%ld", (long*)&tail) != 1) {
ESP_LOGW(TAG, "Malformed tail parameter, using default");
tail = -1;
}
}
else {
ESP_LOGE(TAG, "Unsupported HTTP method: %d", req->method);
return httpd_resp_send_err(req, HTTPD_405_METHOD_NOT_ALLOWED, "Method not allowed");
}
const esp_partition_t *storage_partition = cached_storage_partition;
if (storage_partition == NULL) {
storage_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_ANY,
"storage");
if (storage_partition == NULL) {
ESP_LOGE(TAG, "Storage partition not found");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Storage partition not found");
}
cached_storage_partition = storage_partition;
}
int32_t head = log_get_head();
int32_t log_start = log_get_offset();
if (tail < 0) {
tail = log_get_tail();
} else {
if (tail < log_start || tail >= (int32_t)storage_partition->size) {
ESP_LOGW(TAG, "Invalid tail pointer %ld, using current tail", (long)tail);
tail = log_get_tail();
}
}
// Calculate log data size
int32_t log_data_size;
if (tail == head) {
log_data_size = 0;
} else if (tail < head) {
log_data_size = head - tail;
} else {
log_data_size = (storage_partition->size - tail) + (head - log_start);
}
// Generate JSON header (same as /get endpoint)
cJSON *json_response = comms_handle_get();
if (json_response == NULL) {
ESP_LOGE(TAG, "Failed to generate JSON response");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to generate response");
}
char *json_str = cJSON_PrintUnformatted(json_response);
cJSON_Delete(json_response);
if (json_str == NULL) {
ESP_LOGE(TAG, "Failed to serialize JSON");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to serialize response");
}
uint32_t json_len = strlen(json_str);
// Total size: 4 (json length) + json + 8 (head/tail) + log_data
uint32_t total_size = 4 + json_len + 8 + log_data_size;
//ESP_LOGI(TAG, "Log request: tail=%ld, head=%ld, json_len=%lu, log_size=%ld, total=%lu",
// (long)tail, (long)head, (unsigned long)json_len, (long)log_data_size,
// (unsigned long)total_size);
// Send HTTP headers
char len_str[16];
snprintf(len_str, sizeof(len_str), "%u", (unsigned)total_size);
esp_err_t err = httpd_resp_set_type(req, "application/octet-stream");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set response type: %s", esp_err_to_name(err));
free(json_str);
return err;
}
err = httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"sc_log.bin\"");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set content disposition: %s", esp_err_to_name(err));
free(json_str);
return err;
}
err = httpd_resp_set_hdr(req, "Content-Length", len_str);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set content length: %s", esp_err_to_name(err));
free(json_str);
return err;
}
// Send JSON length (4 bytes, big-endian)
uint32_t json_len_be = htobe32(json_len);
memcpy(&http_buffer[0], &json_len_be, 4);
err = httpd_resp_send_chunk(req, (const char *)http_buffer, 4);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send JSON length: %s", esp_err_to_name(err));
free(json_str);
return err;
}
// Send JSON string
err = httpd_resp_send_chunk(req, json_str, json_len);
free(json_str);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send JSON: %s", esp_err_to_name(err));
return err;
}
// Send head/tail pointers (8 bytes, big-endian)
int32_t htail = htobe32(tail);
int32_t hhead = htobe32(head);
memcpy(&http_buffer[0], &htail, 4);
memcpy(&http_buffer[4], &hhead, 4);
err = httpd_resp_send_chunk(req, (const char *)http_buffer, 8);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send head/tail chunk: %s", esp_err_to_name(err));
return err;
}
// Send log data (same as before)
int32_t offset = tail;
if (tail == head) {
// Empty log, nothing more to send
}
else if (tail < head) {
// Normal case: tail before head
while (offset < head) {
size_t to_read = MIN(sizeof(http_buffer), head - offset);
err = esp_partition_read(storage_partition, offset, http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to read storage");
}
err = httpd_resp_send_chunk(req, (const char *)http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return err;
}
offset += to_read;
}
}
else {
// Wrapped case: tail after head, read from tail to end, then start to head
while (offset < (int32_t)storage_partition->size) {
size_t to_read = MIN(sizeof(http_buffer), storage_partition->size - offset);
err = esp_partition_read(storage_partition, offset, http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to read storage");
}
err = httpd_resp_send_chunk(req, (const char *)http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return err;
}
offset += to_read;
}
// Now read from start to head
offset = log_start;
while (offset < head) {
size_t to_read = MIN(sizeof(http_buffer), head - offset);
err = esp_partition_read(storage_partition, offset, http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read partition at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to read storage");
}
err = httpd_resp_send_chunk(req, (const char *)http_buffer, to_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send chunk at offset %ld: %s",
(long)offset, esp_err_to_name(err));
return err;
}
offset += to_read;
}
}
// Send empty chunk to signal end
err = httpd_resp_send_chunk(req, NULL, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send final chunk: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_set_hdr(req, "Connection", "close");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
}
return err;
}
/**
* Unified GET handler - returns complete system status
* Response format:
* {
* "time": 1234567,
* "rtc_valid": true,
* "state": 2,
* "voltage": 12.45,
* "remaining_dist": 12.0,
* "msg": "IDLE",
* "parameters": {
* "values": [12, 45, -3, 45.6, ...],
* "names": ["param1", "param2", ...],
* "units": ["s", "ms", "inches", ...]
* },
* ... other parameters as direct key-value pairs
* }
*/
/**
* Unified GET handler - returns complete system status
*/
static esp_err_t get_handler(httpd_req_t *req) {
//ESP_LOGI(TAG, "get_handler");
if (req == NULL) {
ESP_LOGE(TAG, "Null request pointer");
return ESP_FAIL;
}
// Call unified GET handler
cJSON *response = comms_handle_get();
if (response == NULL) {
ESP_LOGE(TAG, "Failed to generate GET response");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to generate response");
}
// Convert to string (not pretty printed for web - save bandwidth)
char *json_str = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
if (json_str == NULL) {
ESP_LOGE(TAG, "Failed to serialize JSON");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to serialize response");
}
// Send response
esp_err_t err = httpd_resp_set_type(req, "application/json");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set response type: %s", esp_err_to_name(err));
free(json_str);
return err;
}
err = httpd_resp_send(req, json_str, strlen(json_str));
free(json_str);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send response: %s", esp_err_to_name(err));
return err;
}
err = httpd_resp_set_hdr(req, "Connection", "close");
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
}
return err;
}
/**
* Unified POST handler - handles commands, parameter updates, time updates
* Request format (all fields optional):
* {
* "time": 1234567, // Update RTC time
* "state": 2, // Update FSM state
* "cmd": "start", // Execute command
* "voltage": 12.45, // Update individual parameter
* "parameters": { // Batch update parameters
* "PARAM_NAME": -15,
* "PARAM_NAME2": 12.0
* }
* }
*/
static void soft_idle_enter_cb(void *arg) { soft_idle_enter(); }
static void webserver_restart_wifi_cb(void *arg) { webserver_restart_wifi(); }
/**
* Unified POST handler - handles commands, parameter updates, time updates
*/
static esp_err_t post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "post_handler");
if (req == NULL) {
ESP_LOGE(TAG, "Null request pointer");
return ESP_FAIL;
}
// Receive POST data
int ret = httpd_req_recv(req, http_buffer, sizeof(http_buffer));
if (ret <= 0) {
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Socket timeout during receive");
return httpd_resp_send_408(req);
}
ESP_LOGE(TAG, "Failed to receive POST data: %d", ret);
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to receive data");
}
if (ret >= (int)sizeof(http_buffer)) {
ESP_LOGE(TAG, "POST data exceeds buffer size");
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Request too large");
}
http_buffer[ret] = '\0';
ESP_LOGI(TAG, "POST: %.*s", ret, http_buffer);
// Parse JSON
cJSON *root = cJSON_Parse(http_buffer);
if (root == NULL) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr != NULL) {
ESP_LOGE(TAG, "JSON parse error before: %s", error_ptr);
}
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
}
// Call unified POST handler
cJSON *response = NULL;
esp_err_t err = comms_handle_post(root, &response);
cJSON_Delete(root);
if (response == NULL) {
ESP_LOGE(TAG, "Failed to generate POST response");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to generate response");
}
// Check for special response flags
cJSON *reboot_flag = cJSON_GetObjectItem(response, "reboot");
cJSON *sleep_flag = cJSON_GetObjectItem(response, "sleep");
cJSON *wifi_restart_flag = cJSON_GetObjectItem(response, "wifi_restart");
bool should_reboot = cJSON_IsTrue(reboot_flag);
bool should_sleep = cJSON_IsTrue(sleep_flag);
bool should_restart_wifi = cJSON_IsTrue(wifi_restart_flag);
// Convert response to string
char *json_str = cJSON_PrintUnformatted(response);
cJSON_Delete(response);
if (json_str == NULL) {
ESP_LOGE(TAG, "Failed to serialize JSON");
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to serialize response");
}
// Send response
err = httpd_resp_set_type(req, "application/json");
if (err == ESP_OK) {
err = httpd_resp_send(req, json_str, strlen(json_str));
}
free(json_str);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send response: %s", esp_err_to_name(err));
return err;
}
// Handle special actions after response is sent
if (should_reboot) {
ESP_LOGI(TAG, "Rebooting in 2 seconds...");
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
return ESP_OK; // Never reached
}
if (should_restart_wifi) {
/* Same deadlock risk as should_sleep — httpd_stop() inside
* webserver_restart_wifi() cannot be called from within a handler. */
static esp_timer_handle_t s_wifi_restart_timer = NULL;
if (s_wifi_restart_timer == NULL) {
esp_timer_create_args_t ta = {
.callback = webserver_restart_wifi_cb,
.name = "wifi_restart",
};
esp_timer_create(&ta, &s_wifi_restart_timer);
}
if (s_wifi_restart_timer != NULL) {
esp_timer_start_once(s_wifi_restart_timer, 500 * 1000); /* 500 ms in µs */
}
return ESP_OK;
}
if (should_sleep) {
ESP_LOGI(TAG, "Entering soft idle in 2 seconds...");
/* Cannot call soft_idle_enter() (→ httpd_stop()) from within an httpd
* handler — httpd_stop() waits for all handlers to finish, causing a
* deadlock. Schedule via a one-shot timer so this handler returns
* first and the httpd task is free. */
static esp_timer_handle_t s_sleep_timer = NULL;
if (s_sleep_timer == NULL) {
esp_timer_create_args_t ta = {
.callback = soft_idle_enter_cb,
.name = "soft_idle",
};
esp_timer_create(&ta, &s_sleep_timer);
}
if (s_sleep_timer != NULL) {
esp_timer_start_once(s_sleep_timer, 2000 * 1000); /* 2 s in µs */
}
return ESP_OK;
}
err = httpd_resp_set_hdr(req, "Connection", "close");
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to set connection header: %s", esp_err_to_name(err));
}
return err;
}
static esp_err_t ota_post_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "OTA POST request received");
if (req == NULL) {
ESP_LOGE(TAG, "Null request pointer");
return ESP_FAIL;
}
rtc_reset_shutdown_timer();
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 available");
}
ESP_LOGI(TAG, "Starting OTA update on partition: %s", update_partition->label);
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");
}
int recv_len;
int total_len = req->content_len;
int remaining = total_len;
int received = 0;
if (total_len <= 0) {
ESP_LOGE(TAG, "Invalid content length: %d", total_len);
esp_ota_abort(update_handle);
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"Invalid or missing content length");
}
ESP_LOGI(TAG, "Expected OTA size: %d bytes", total_len);
int timeout_count = 0;
const int MAX_TIMEOUTS = 3;
while (remaining > 0) {
recv_len = httpd_req_recv(req, http_buffer, MIN(remaining, sizeof(http_buffer)));
if (recv_len < 0) {
if (recv_len == HTTPD_SOCK_ERR_TIMEOUT) {
timeout_count++;
ESP_LOGW(TAG, "Socket timeout (%d/%d), retrying...",
timeout_count, MAX_TIMEOUTS);
if (timeout_count < MAX_TIMEOUTS) {
continue;
} else {
ESP_LOGE(TAG, "Too many timeouts, aborting OTA");
esp_ota_abort(update_handle);
return httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT,
"Request timeout");
}
} else if (recv_len == HTTPD_SOCK_ERR_FAIL) {
ESP_LOGE(TAG, "Socket error during receive");
esp_ota_abort(update_handle);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Socket error during OTA");
} else {
ESP_LOGE(TAG, "Unexpected error during receive: %d", recv_len);
esp_ota_abort(update_handle);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"OTA receive failed");
}
}
if (recv_len == 0) {
ESP_LOGE(TAG, "Connection closed prematurely. Received %d of %d bytes",
received, total_len);
esp_ota_abort(update_handle);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Connection closed during OTA");
}
// Reset timeout counter on successful receive
timeout_count = 0;
err = esp_ota_write(update_handle, (const void *)http_buffer, recv_len);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_write failed (%s)", esp_err_to_name(err));
esp_ota_abort(update_handle);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"OTA write failed");
}
remaining -= recv_len;
received += recv_len;
// Log progress every 10%
if (total_len > 0 && (received % (total_len / 10)) < recv_len) {
ESP_LOGI(TAG, "OTA progress: %d%%", (received * 100) / total_len);
}
}
ESP_LOGI(TAG, "OTA data received completely. Total: %d bytes", received);
if (received != total_len) {
ESP_LOGE(TAG, "Size mismatch: received %d, expected %d", received, total_len);
esp_ota_abort(update_handle);
return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"OTA size mismatch");
}
err = esp_ota_end(update_handle);
if (err != ESP_OK) {
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
ESP_LOGE(TAG, "Image validation failed");
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
"OTA image validation failed");
} else {
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,
"Failed to set boot partition");
}
ESP_LOGI(TAG, "OTA update successful. Rebooting in 2 seconds...");
// Send success response FIRST
err = httpd_resp_set_type(req, "application/json");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set response type: %s", esp_err_to_name(err));
// Continue anyway, try to send response
}
err = httpd_resp_send(req, "{\"status\":\"ok\",\"message\":\"OTA update successful, rebooting...\"}",
HTTPD_RESP_USE_STRLEN);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send response: %s", esp_err_to_name(err));
// Continue with reboot anyway
}
// Update boot time parameter
// set_param_value_t(PARAM_BOOT_TIME, (param_value_t){.i32 = 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; // Never reached
}
static esp_err_t catchall_handler(httpd_req_t *req) {
//ESP_LOGI(TAG, "catchall_handler; %s", req->uri);
const char *uri = req->uri;
// Windows NCSI
if (strcmp(uri, "/connecttest.txt") == 0) {
httpd_resp_set_type(req, "text/plain");
httpd_resp_sendstr(req, "Microsoft Connect Test");
httpd_resp_set_hdr(req, "Connection", "close");
return ESP_OK;
}
if (strncmp(uri, "/success.txt", 12) == 0) { // Handles query params too
httpd_resp_set_type(req, "text/plain");
httpd_resp_sendstr(req, "Success");
httpd_resp_set_hdr(req, "Connection", "close");
return ESP_OK;
}
if (strcmp(uri, "/canonical.html") == 0) {
httpd_resp_set_type(req, "text/html");
httpd_resp_sendstr(req, "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>");
httpd_resp_set_hdr(req, "Connection", "close");
return ESP_OK;
}
// Android
if (strcmp(uri, "/generate_204") == 0 || strcmp(uri, "/gen_204") == 0) {
httpd_resp_set_status(req, "204 No Content");
httpd_resp_send(req, NULL, 0);
httpd_resp_set_hdr(req, "Connection", "close");
return ESP_OK;
}
// iOS/macOS
if (strcmp(uri, "/hotspot-detect.html") == 0 ||
strcmp(uri, "/library/test/success.html") == 0) {
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "/");
httpd_resp_send(req, NULL, 0);
httpd_resp_set_hdr(req, "Connection", "close");
return ESP_OK;
}
// Default 404
httpd_resp_send_404(req);
httpd_resp_set_hdr(req, "Connection", "close");
return ESP_OK;
}
/************************************************
**************** URI HANDLER MAP ****************
************************************************/
httpd_uri_t uris[] = {{
.uri = "/",
.method = HTTP_GET,
.handler = root_get_handler,
.user_ctx = NULL
},{
.uri = "/get",
.method = HTTP_GET,
.handler = get_handler,
.user_ctx = NULL
},{
.uri = "/post",
.method = HTTP_POST,
.handler = post_handler,
.user_ctx = NULL
},{
.uri = "/log",
.method = HTTP_ANY,
.handler = log_handler,
.user_ctx = NULL
},{
.uri = "/ota",
.method = HTTP_POST,
.handler = ota_post_handler,
.user_ctx = NULL
},{
.uri = "/*",
.method = HTTP_GET,
.handler = catchall_handler,
.user_ctx = NULL
}};
/**********************************************************
**************** WIFI + WEB SERVER RUNNERS ****************
**********************************************************/
bool server_running = false;
static bool s_wifi_running = false;
static esp_netif_t *s_ap_netif = NULL;
static esp_netif_t *s_sta_netif = NULL;
static bool s_wifi_initted = false;
static SemaphoreHandle_t s_sta_sem = NULL;
static bool s_sta_connected = false;
static esp_err_t start_http_server(void) {
if (server_running) return ESP_OK;
ESP_LOGI(TAG, "STARTING HTTP");
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = 80;
config.max_open_sockets = 7;
config.lru_purge_enable = true;
config.uri_match_fn = httpd_uri_match_wildcard; // enable wildcarding
esp_err_t err = httpd_start(&http_server_instance, &config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "HTTP server started successfully");
// Register URI handlers
for (uint8_t i = 0; i < (sizeof(uris)/sizeof(httpd_uri_t)); i++) {
err = httpd_register_uri_handler(http_server_instance, &uris[i]);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register URI handler for %s: %s",
uris[i].uri, esp_err_to_name(err));
// Continue registering other handlers even if one fails
} else {
ESP_LOGI(TAG, "Registered URI handler: %s", uris[i].uri);
}
}
server_running = true;
return ESP_OK;
}
static esp_err_t stop_http_server(void) {
if (!server_running) return ESP_OK;
ESP_LOGI(TAG, "STOPPING HTTP");
if (http_server_instance == NULL) {
ESP_LOGW(TAG, "HTTP server not running");
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = httpd_stop(http_server_instance);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to stop HTTP server: %s", esp_err_to_name(err));
return err;
}
http_server_instance = NULL;
ESP_LOGI(TAG, "HTTP server stopped");
server_running = false;
return ESP_OK;
}
/* Event handler for WiFi + IP events */
int n_connected = 0;
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data) {
if (event_base == WIFI_EVENT) {
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
ESP_LOGI(TAG, "Station connected, AID=%d", event->aid);
rtc_reset_shutdown_timer();
n_connected++;
if (n_connected > 0) start_http_server();
} 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 disconnected, AID=%d", event->aid);
n_connected--;
if (n_connected <= 0) stop_http_server();
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
s_sta_connected = false;
if (s_sta_sem) xSemaphoreGive(s_sta_sem);
}
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *e = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "STA connected, IP: " IPSTR, IP2STR(&e->ip_info.ip));
s_sta_connected = true;
if (s_sta_sem) xSemaphoreGive(s_sta_sem);
}
}
/* One-time WiFi driver + event system init (guarded by s_wifi_initted) */
static esp_err_t wifi_common_init(void) {
if (s_wifi_initted) return ESP_OK;
esp_err_t err = esp_netif_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err));
return err;
}
err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err));
return err;
}
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
err = esp_wifi_init(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err));
return err;
}
err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL, NULL);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register WIFI_EVENT handler: %s", esp_err_to_name(err));
return err;
}
err = esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL, NULL);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register IP_EVENT handler: %s", esp_err_to_name(err));
return err;
}
s_wifi_initted = true;
return ESP_OK;
}
/* Attempt STA connection; blocks up to 10 s. Returns ESP_OK on GOT_IP. */
static esp_err_t try_connect_sta(const char *ssid, const char *pass, bool reset_wdt) {
if (s_sta_netif == NULL) {
s_sta_netif = esp_netif_create_default_wifi_sta();
if (s_sta_netif == NULL) {
ESP_LOGE(TAG, "Failed to create STA netif");
return ESP_FAIL;
}
esp_netif_set_hostname(s_sta_netif, HOSTNAME);
}
esp_err_t err = wifi_common_init();
if (err != ESP_OK) return err;
wifi_config_t sta_cfg = {};
strlcpy((char *)sta_cfg.sta.ssid, ssid, sizeof(sta_cfg.sta.ssid));
strlcpy((char *)sta_cfg.sta.password, pass ? pass : "", sizeof(sta_cfg.sta.password));
err = esp_wifi_set_mode(WIFI_MODE_STA);
if (err != ESP_OK) { ESP_LOGE(TAG, "set_mode STA: %s", esp_err_to_name(err)); return err; }
err = esp_wifi_set_config(WIFI_IF_STA, &sta_cfg);
if (err != ESP_OK) { ESP_LOGE(TAG, "set_config STA: %s", esp_err_to_name(err)); return err; }
s_sta_connected = false;
if (s_sta_sem == NULL) {
s_sta_sem = xSemaphoreCreateBinary();
} else {
xSemaphoreTake(s_sta_sem, 0); // drain any stale token
}
err = esp_wifi_start();
if (err != ESP_OK) { ESP_LOGE(TAG, "wifi_start: %s", esp_err_to_name(err)); return err; }
/* Yield so the event loop (priority 20) can process WIFI_EVENT_STA_START
* and esp_netif can finish initialising the STA interface before we call
* esp_wifi_connect(). The esp_timer task runs at priority 22, so without
* this yield it would call connect before STA_START is handled — the
* driver accepts the call (returns ESP_OK) but silently discards it when
* it finishes its own internal start sequence. */
vTaskDelay(pdMS_TO_TICKS(10));
err = esp_wifi_connect();
if (err != ESP_OK) {
ESP_LOGE(TAG, "wifi_connect: %s", esp_err_to_name(err));
esp_wifi_stop();
return err;
}
/* Poll in 100 ms slices so the WDT gets reset when needed (init path). */
for (int i = 0; i < 100 && !s_sta_connected; i++) {
xSemaphoreTake(s_sta_sem, pdMS_TO_TICKS(100));
if (reset_wdt) esp_task_wdt_reset();
}
if (!s_sta_connected) {
ESP_LOGW(TAG, "STA connection timed out or rejected");
esp_wifi_stop();
return ESP_FAIL;
}
s_wifi_running = true;
return ESP_OK;
}
static esp_err_t launch_soft_ap(void) {
ESP_LOGI(TAG, "AP LAUNCHING");
if (s_ap_netif == NULL) {
s_ap_netif = esp_netif_create_default_wifi_ap();
if (s_ap_netif == NULL) {
ESP_LOGE(TAG, "Failed to create default WiFi AP interface");
return ESP_FAIL;
}
esp_netif_set_hostname(s_ap_netif, HOSTNAME);
}
esp_err_t err = wifi_common_init();
if (err != ESP_OK) return err;
wifi_config_t wifi_config = {
.ap = {
.channel = get_param_value_t(PARAM_WIFI_CHANNEL).u16,
.max_connection = 4,
.authmode = WIFI_AUTH_WPA2_PSK,
},
};
char *ssid_str = get_param_string(PARAM_WIFI_SSID);
char *password_str = get_param_string(PARAM_WIFI_PASS);
if (ssid_str == NULL || password_str == NULL) {
ESP_LOGE(TAG, "Failed to get WiFi credentials from parameters");
return ESP_FAIL;
}
memcpy(wifi_config.ap.ssid, ssid_str, MIN(strlen(ssid_str), sizeof(wifi_config.ap.ssid)));
memcpy(wifi_config.ap.password, password_str, MIN(strlen(password_str), sizeof(wifi_config.ap.password)));
if (strlen(password_str) < 8) {
ESP_LOGW(TAG, "Password too short, using open authentication");
wifi_config.ap.password[0] = '\0';
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
}
if (wifi_config.ap.channel > 11 || wifi_config.ap.channel < 1) {
ESP_LOGW(TAG, "Invalid WiFi channel %d, using default channel 6", wifi_config.ap.channel);
wifi_config.ap.channel = 6;
}
wifi_config.ap.ssid_len = strlen(ssid_str);
err = esp_wifi_set_mode(WIFI_MODE_AP);
if (err != ESP_OK) { ESP_LOGE(TAG, "set_mode AP: %s", esp_err_to_name(err)); return err; }
err = esp_wifi_set_config(WIFI_IF_AP, &wifi_config);
if (err != ESP_OK) { ESP_LOGE(TAG, "set_config AP: %s", esp_err_to_name(err)); return err; }
err = esp_wifi_start();
if (err != ESP_OK) { ESP_LOGE(TAG, "wifi_start: %s", esp_err_to_name(err)); return err; }
s_wifi_running = true;
err = simple_dns_server_start("192.168.4.1");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start DNS server: %s", esp_err_to_name(err));
// Non-critical, continue
}
err = mdns_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize mDNS: %s", esp_err_to_name(err));
// Non-critical, continue
} else {
mdns_hostname_set(HOSTNAME);
mdns_instance_name_set("ClusterCommand");
mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
}
uint8_t *placeholder = (wifi_config.ap.authmode == WIFI_AUTH_OPEN)
? (uint8_t *)"<open network>"
: wifi_config.ap.password;
ESP_LOGI(TAG, "SoftAP ready. SSID: %s, Channel: %d, Password: %s",
wifi_config.ap.ssid, wifi_config.ap.channel, placeholder);
ESP_LOGI(TAG, "Access at: http://%s.local or http://192.168.4.1", HOSTNAME);
return ESP_OK;
}
/* STA-first startup: try NET_SSID, fall back to softAP on failure/empty. */
static esp_err_t start_wifi(bool reset_wdt) {
char *net_ssid = get_param_string(PARAM_NET_SSID);
if (net_ssid && strlen(net_ssid) > 0) {
char *net_pass = get_param_string(PARAM_NET_PASS);
ESP_LOGI(TAG, "Trying STA connection to '%s'...", net_ssid);
if (try_connect_sta(net_ssid, net_pass, reset_wdt) == ESP_OK) {
ESP_LOGI(TAG, "STA connected — HTTP server running");
return ESP_OK;
}
ESP_LOGW(TAG, "STA failed — falling back to softAP");
/* try_connect_sta already called esp_wifi_stop() on failure */
}
return launch_soft_ap();
}
esp_err_t webserver_stop(void) {
stop_http_server();
if (s_wifi_running) {
esp_wifi_stop();
s_wifi_running = false;
}
return ESP_OK;
}
esp_err_t webserver_restart_wifi(void) {
ESP_LOGI(TAG, "Restarting WiFi with updated params...");
stop_http_server();
if (s_wifi_running) {
esp_wifi_stop();
s_wifi_running = false;
/* Allow the event loop to drain the WIFI_EVENT_STA_DISCONNECTED (or
* AP stop) event that esp_wifi_stop() queues asynchronously. Without
* this delay, the stale disconnect event is processed after the new
* esp_wifi_connect() call, which resets the driver's internal
* connection state machine and silently kills the new attempt. */
vTaskDelay(pdMS_TO_TICKS(200));
}
esp_err_t err = start_wifi(false); // called from esp_timer task, not subscribed to WDT
if (err != ESP_OK) return err;
start_http_server(); // no-op if STA path already started it
return ESP_OK;
}
esp_err_t webserver_init(void) {
ESP_LOGI(TAG, "Initializing webserver...");
esp_err_t err = start_wifi(true); // called from app_main, which is subscribed to WDT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start WiFi: %s", esp_err_to_name(err));
return err;
}
start_http_server(); // no-op if STA path already started it
ESP_LOGI(TAG, "Webserver initialization complete");
return ESP_OK;
}