#include "webserver.h" #include #include #include #include #include #include #include "tools/log.h" #include "AsyncJson.h" #include #include #include #include #include #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); } } }