Files
waterlevel-software/src/networking/webserver.cpp
T
2026-05-24 16:22:07 +02:00

461 lines
19 KiB
C++

#include "webserver.h"
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
#include <Elog.h>
#include <AsyncTCP.h>
#include <Preferences.h>
#include "tools/log.h"
#include "AsyncJson.h"
#include <ArduinoJson.h>
#include <SPIFFS.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/queue.h>
#include "../global_data/global_data.h"
#include "../tools/tools.h"
#include "../tools/readers_writer_lock.h"
#include "../tools/ota.h"
#include "../global_data/defines.h"
#include "json_builder.h"
AsyncWebServer server(80);
// Queue handles (initialized in webserver_task)
static QueueHandle_t g_dataQueue = NULL;
static QueueHandle_t g_prefsQueue = NULL;
WaterData local_water_data;
SensorData local_sensor_data;
NetworkData local_wifi_data;
NetworkData local_ethernet_data;
DeviceTelemetry local_telemetry;
OTAStatus local_ota_status;
// Local preference storage for template processing
float local_level_sensor_range = -1.0f;
float local_water_level_min = 0.0f;
float local_water_level_max = 200.0f;
float local_water_volume = 10000.0f;
char local_ssid[33] = "";
char local_wifi_password[65] = "";
ReadersWriterLock waterDataLock;
ReadersWriterLock sensorDataLock;
ReadersWriterLock networkDataLock;
ReadersWriterLock telemetryDataLock;
ReadersWriterLock otaStatusLock;
// ======================
// Template Processor
// ======================
/**
* @brief Template processor for substituting variables in HTML/JavaScript templates.
*
* Replaces template placeholders with values from preferences.
* Used by AsyncWebServer when serving template files.
*
* @param var The template variable name to replace
* @return String The value to substitute, or empty string if not found
*/
String processor(const String& var)
{
if (var == level_sensor_range_key) {
return String(local_level_sensor_range);
} else if (var == water_level_min_key) {
return String(local_water_level_min);
} else if (var == water_level_max_key) {
return String(local_water_level_max);
} else if (var == water_volume_key) {
return String(local_water_volume);
} else if (var == ssid_key) {
return String(local_ssid);
}
return String("");
}
// ======================
// Webserver Setup
// ======================
/**
* @brief Sets up all routes for the webserver.
*
* Configures routes for serving HTML pages, handling form submissions,
* REST API endpoints and static assets.
*
* @note Calls setup_api_endpoints() to configure API-specific routes.
*/
void setup_routes() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(LittleFS, "/status.html", "text/html", false, processor); });
server.on("/settings", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(LittleFS, "/settings.html", "text/html", false, processor); });
server.on("/export", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(LittleFS, "/data_export.html", "text/html", false); });
server.on("/logic.js", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(LittleFS, "/logic.js", "application/javascript", false, processor); });
server.on("/update", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(LittleFS, "/update_progress.html", "text/html", false, processor); });
// API stuff - internal
server.on("/update_wifi_credentials", HTTP_POST, [](AsyncWebServerRequest* request) {
handle_update_wifi_credentials(request);
});
server.on("/update_sensor_settings", HTTP_POST, [](AsyncWebServerRequest* request) {
handle_update_sensor_settings(request);
});
setup_api_endpoints();
server.on("/chota.css", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(LittleFS, "/chota.css", "text/css", false); });
server.on("/gauge.js", HTTP_GET, [](AsyncWebServerRequest* request) { request->send(LittleFS, "/gauge.js", "application/javascript", false); });
}
/**
* @brief Configures REST API endpoints for data retrieval.
*
* Sets up endpoints to fetch sensor data, water data, telemetry,
* network information, and OTA update status in JSON format.
*
* @note These endpoints are used by the frontend to dynamically
* display device status and configuration.
*/
void setup_api_endpoints(){
server.on("/sensor_data", HTTP_GET, [](AsyncWebServerRequest* request) {
rwLockAcquireRead(&sensorDataLock);
String output;
build_shunt_data_json(local_sensor_data, output);
rwLockReleaseRead(&sensorDataLock);
request->send(200, "application/json", output); });
server.on("/water_data", HTTP_GET, [](AsyncWebServerRequest* request) {
rwLockAcquireRead(&waterDataLock);
String output;
build_water_data_json(local_water_data, output);
rwLockReleaseRead(&waterDataLock);
request->send(200, "application/json", output);
});
server.on("/raw_percent", HTTP_GET, [](AsyncWebServerRequest* request) {
rwLockAcquireRead(&waterDataLock);
String output = String(local_water_data.percentage);
rwLockReleaseRead(&waterDataLock);
request->send(200, "text/raw", output);
});
server.on("/raw_level", HTTP_GET, [](AsyncWebServerRequest* request) {
rwLockAcquireRead(&waterDataLock);
String output = String(local_water_data.level);
rwLockReleaseRead(&waterDataLock);
request->send(200, "text/raw", output);
});
server.on("/telemetry", HTTP_GET, [](AsyncWebServerRequest* request) {
rwLockAcquireRead(&telemetryDataLock);
String output;
build_telemetry_json(local_telemetry, output);
rwLockReleaseRead(&telemetryDataLock);
request->send(200, "application/json", output); });
server.on("/network_info", HTTP_GET, [](AsyncWebServerRequest* request) {
rwLockAcquireRead(&networkDataLock);
String output;
build_network_json(local_ethernet_data, local_wifi_data, output);
rwLockReleaseRead(&networkDataLock);
request->send(200, "application/json", output);
});
server.on("/ota_update_status", HTTP_GET, [](AsyncWebServerRequest* request) {
rwLockAcquireRead(&otaStatusLock);
String output;
build_ota_json(local_ota_status, output);
rwLockReleaseRead(&otaStatusLock);
request->send(200, "application/json", output); });
server.on("/run_ota_update", HTTP_GET, [](AsyncWebServerRequest* request) {
if (local_ota_status.update_progress > -1) {
request->send(200, "text/plain", "OTA Update already in progress");
return;
} else if (!local_ota_status.update_available) {
request->send(200, "text/plain", "No update available");
return;
}
static TaskArgs_t args = {
.ota_status = local_ota_status
};
xTaskCreate(run_ota_update_task, "RunOTAUpdate", 1024 * 8, (void *)&args, 1, NULL);
request->send(LittleFS, "/update_progress.html", "text/html", false, processor);
});
}
// ======================
// Helper Functions
// ======================
/**
* @brief Handles the update of WiFi credentials.
*
* Validates and sends SSID and password update request to main task.
* Main task will write to preferences and distribute to tasks.
*
* @param request The web server request object.
*/
void handle_update_wifi_credentials(AsyncWebServerRequest* request) {
if (request->hasParam(ssid_key, true) && request->hasParam(wifi_password_key, true)) {
LOG(ELOG_LEVEL_DEBUG, "Updating SSID config");
const AsyncWebParameter* ssid_param = request->getParam(ssid_key, true);
const AsyncWebParameter* password_param = request->getParam(wifi_password_key, true);
// Send preference update to main task
DataMessage prefsMessage;
prefsMessage.type = DATA_TYPE_PREFERENCES;
prefsMessage.data.preferencesData.level_sensor_range = local_level_sensor_range; // Keep existing values
prefsMessage.data.preferencesData.water_level_min = local_water_level_min;
prefsMessage.data.preferencesData.water_level_max = local_water_level_max;
prefsMessage.data.preferencesData.water_volume = local_water_volume;
strncpy(prefsMessage.data.preferencesData.ssid, ssid_param->value().c_str(), sizeof(prefsMessage.data.preferencesData.ssid) - 1);
prefsMessage.data.preferencesData.ssid[sizeof(prefsMessage.data.preferencesData.ssid) - 1] = '\0';
strncpy(prefsMessage.data.preferencesData.wifi_password, password_param->value().c_str(), sizeof(prefsMessage.data.preferencesData.wifi_password) - 1);
prefsMessage.data.preferencesData.wifi_password[sizeof(prefsMessage.data.preferencesData.wifi_password) - 1] = '\0';
if (g_dataQueue != NULL && xQueueSend(g_dataQueue, &prefsMessage, 100 / portTICK_PERIOD_MS) != pdTRUE) {
LOG(ELOG_LEVEL_ERROR, "Failed to send WiFi credentials update to main");
}
} else {
request->send(400, "text/plain", "Missing parameters");
return;
}
request->send(LittleFS, "/settings.html", "text/html", false, processor);
}
/**
* @brief Handles the update of sensor settings.
*
* Validates and updates sensor configuration parameters in preferences.
*
* @param request The web server request object.
*/
void handle_update_sensor_settings(AsyncWebServerRequest* request) {
int params = request->params();
if (request->hasParam(level_sensor_range_key, true) && request->hasParam(water_level_min_key, true) &&
request->hasParam(water_level_max_key, true) && request->hasParam(water_volume_key, true)) {
LOG(ELOG_LEVEL_DEBUG, "Updating Sensor config");
const AsyncWebParameter* range_param = request->getParam(level_sensor_range_key, true);
const AsyncWebParameter* level_min_param = request->getParam(water_level_min_key, true);
const AsyncWebParameter* level_max_param = request->getParam(water_level_max_key, true);
const AsyncWebParameter* liters_param = request->getParam(water_volume_key, true);
String range_str = range_param->value();
String level_min_str = level_min_param->value();
String level_max_str = level_max_param->value();
String liters_str = liters_param->value();
float range_float = range_str.toFloat();
float level_min_float = level_min_str.toFloat();
float level_max_float = level_max_str.toFloat();
float liters_float = liters_str.toFloat();
LOG(ELOG_LEVEL_DEBUG, "range_float:%D:", range_float);
// Send preference update to main task
DataMessage prefsMessage;
prefsMessage.type = DATA_TYPE_PREFERENCES;
prefsMessage.data.preferencesData.level_sensor_range = range_float;
prefsMessage.data.preferencesData.water_level_min = level_min_float;
prefsMessage.data.preferencesData.water_level_max = level_max_float;
prefsMessage.data.preferencesData.water_volume = liters_float;
strncpy(prefsMessage.data.preferencesData.ssid, local_ssid, sizeof(prefsMessage.data.preferencesData.ssid) - 1);
prefsMessage.data.preferencesData.ssid[sizeof(prefsMessage.data.preferencesData.ssid) - 1] = '\0';
strncpy(prefsMessage.data.preferencesData.wifi_password, local_wifi_password, sizeof(prefsMessage.data.preferencesData.wifi_password) - 1);
prefsMessage.data.preferencesData.wifi_password[sizeof(prefsMessage.data.preferencesData.wifi_password) - 1] = '\0';
if (g_dataQueue != NULL && xQueueSend(g_dataQueue, &prefsMessage, 100 / portTICK_PERIOD_MS) != pdTRUE) {
LOG(ELOG_LEVEL_ERROR, "Failed to send sensor settings update to main");
}
LOG(ELOG_LEVEL_DEBUG, "Sent sensor settings update to main");
} else {
LOG(ELOG_LEVEL_DEBUG, "!!!! FAIL lo");
for (int i = 0; i < params; i++) {
const AsyncWebParameter* p = request->getParam(i);
if (p->isFile()) {
LOG(ELOG_LEVEL_DEBUG, "POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
} else if (p->isPost()) {
LOG(ELOG_LEVEL_DEBUG, "POST[%s]: %s\n", p->name().c_str(), p->value().c_str());
} else {
LOG(ELOG_LEVEL_DEBUG, "GET[%s]: %s\n", p->name().c_str(), p->value().c_str());
}
}
request->send(400, "text/plain", "Missing parameters");
return;
}
request->send(LittleFS, "/settings.html", "text/html", false);
}
/**
* @brief Main task for the webserver.
*
* Initializes all routes, starts the webserver, and sets up local water data.
* Receives the current state from the main task via a queue and updates local_water_data.
* Runs indefinitely to keep the server active.
*
* @param pvParameters Task parameters (expected to be a WebserverQueues* containing stateQueue, dataQueue, and prefsQueue).
*/
void webserver_task(void *pvParameters) {
// Extract the queue handles from the task parameters
WebserverQueues* queues = (WebserverQueues*)pvParameters;
if (queues == NULL || queues->stateQueue == NULL) {
LOG(ELOG_LEVEL_ERROR, "Queue parameters are NULL");
vTaskDelete(NULL);
return;
}
// Store queue handles in file-scoped variables
g_dataQueue = queues->dataQueue;
g_prefsQueue = queues->prefsQueue;
QueueHandle_t stateQueue = queues->stateQueue;
LOG(ELOG_LEVEL_DEBUG, "Setting up routes");
setup_routes();
// Initialize the readers-writer locks
if (!rwLockInit(&waterDataLock)) {
LOG(ELOG_LEVEL_ERROR, "Failed to initialize water data readers-writer lock");
vTaskDelete(NULL);
return;
}
if (!rwLockInit(&sensorDataLock)) {
LOG(ELOG_LEVEL_ERROR, "Failed to initialize sensor data readers-writer lock");
vTaskDelete(NULL);
return;
}
if (!rwLockInit(&networkDataLock)) {
LOG(ELOG_LEVEL_ERROR, "Failed to initialize network data readers-writer lock");
vTaskDelete(NULL);
return;
}
if (!rwLockInit(&telemetryDataLock)) {
LOG(ELOG_LEVEL_ERROR, "Failed to initialize telemetry data readers-writer lock");
vTaskDelete(NULL);
return;
}
if (!rwLockInit(&otaStatusLock)) {
LOG(ELOG_LEVEL_ERROR, "Failed to initialize OTA status readers-writer lock");
vTaskDelete(NULL);
return;
}
// Initialize local water data with static values
rwLockAcquireWrite(&waterDataLock);
local_water_data.level = -1.0;
local_water_data.liters = -1.0;
local_water_data.percentage = -1.0;
rwLockReleaseWrite(&waterDataLock);
// Initialize local sensor data with static values
rwLockAcquireWrite(&sensorDataLock);
local_sensor_data.bus_voltage = -1.0;
local_sensor_data.shunt_voltage = -1.0;
local_sensor_data.shunt_current = -1.0;
rwLockReleaseWrite(&sensorDataLock);
// Initialize local network data with static values
rwLockAcquireWrite(&networkDataLock);
strcpy(local_wifi_data.ip_address, "0.0.0.0");
local_wifi_data.link = false;
local_wifi_data.rssi = -1.0;
strcpy(local_wifi_data.network_name, "UNKNOWN");
strcpy(local_ethernet_data.ip_address, "0.0.0.0");
local_ethernet_data.link = false;
local_ethernet_data.rssi = -1.0;
strcpy(local_ethernet_data.network_name, "UNKNOWN");
rwLockReleaseWrite(&networkDataLock);
// Initialize local telemetry data with static values
rwLockAcquireWrite(&telemetryDataLock);
local_telemetry.heap_used_percent = -1.0;
local_telemetry.uptime_seconds = -1;
local_telemetry.temperature = -1.0;
rwLockReleaseWrite(&telemetryDataLock);
// Initialize local OTA status with static values
rwLockAcquireWrite(&otaStatusLock);
local_ota_status.update_available = false;
local_ota_status.current_version = {0, 0, 0};
local_ota_status.latest_version = {0, 0, 0};
local_ota_status.update_progress = -1;
strncpy(local_ota_status.update_url, "UNKNOWN", sizeof(local_ota_status.update_url) - 1);
local_ota_status.update_url[sizeof(local_ota_status.update_url) - 1] = '\0';
rwLockReleaseWrite(&otaStatusLock);
LOG(ELOG_LEVEL_DEBUG, "Starting webserver");
server.begin();
while (1) {
// Check if there is new preference data in the queue
DataMessage prefsMessage;
if (g_prefsQueue != NULL && xQueueReceive(g_prefsQueue, &prefsMessage, 0) == pdTRUE) {
if (prefsMessage.type == DATA_TYPE_PREFERENCES) {
// Update local preference values
local_level_sensor_range = prefsMessage.data.preferencesData.level_sensor_range;
local_water_level_min = prefsMessage.data.preferencesData.water_level_min;
local_water_level_max = prefsMessage.data.preferencesData.water_level_max;
local_water_volume = prefsMessage.data.preferencesData.water_volume;
strncpy(local_ssid, prefsMessage.data.preferencesData.ssid, sizeof(local_ssid) - 1);
local_ssid[sizeof(local_ssid) - 1] = '\0';
strncpy(local_wifi_password, prefsMessage.data.preferencesData.wifi_password, sizeof(local_wifi_password) - 1);
local_wifi_password[sizeof(local_wifi_password) - 1] = '\0';
LOG(ELOG_LEVEL_DEBUG, "Updated preferences: range=%F, min=%F, max=%F, volume=%F, ssid=%s",
local_level_sensor_range, local_water_level_min, local_water_level_max,
local_water_volume, local_ssid);
}
}
// Check if there is new state data in the queue
CurrentState currentState;
if (xQueueReceive(stateQueue, &currentState, portMAX_DELAY) == pdTRUE) {
// Update local_water_data with the new data from the queue
rwLockAcquireWrite(&waterDataLock);
local_water_data = currentState.waterData;
rwLockReleaseWrite(&waterDataLock);
// Update local_sensor_data with the new data from the queue
rwLockAcquireWrite(&sensorDataLock);
local_sensor_data = currentState.sensorData;
rwLockReleaseWrite(&sensorDataLock);
// Update local network data with the new data from the queue
rwLockAcquireWrite(&networkDataLock);
local_wifi_data = currentState.wifiData;
local_ethernet_data = currentState.ethernetData;
rwLockReleaseWrite(&networkDataLock);
// Update local telemetry data with the new data from the queue
rwLockAcquireWrite(&telemetryDataLock);
local_telemetry = currentState.telemetryData;
rwLockReleaseWrite(&telemetryDataLock);
// Update local OTA status with the new data from the queue
rwLockAcquireWrite(&otaStatusLock);
local_ota_status = currentState.otaStatus;
rwLockReleaseWrite(&otaStatusLock);
}
}
}