461 lines
19 KiB
C++
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, ¤tState, 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);
|
|
}
|
|
}
|
|
} |