/** * @file webui.cpp * * @brief Implementation file for web-based user interface (WebUI) functions in the ChainLube application. * * This file contains the implementation of functions related to the initialization and processing of the * web-based user interface (WebUI). It includes the setup of LittleFS, handling of firmware version checks, * initialization of mDNS, setup of web server routes, and handling of various HTTP events. * * @author Marcel Peterkau * @date 09.01.2024 */ #include "webui.h" #include "common.h" #include // std::unique_ptr #include // strlen, strncpy, memcpy #include // std::clamp AsyncWebServer webServer(80); const char *PARAM_MESSAGE = "message"; SpeedSource_t speedsourcePreselect; /**< Preselect Memory for change SourceAdress */ struct WsInitBurst { uint32_t client_id = 0; uint8_t step = 0; // 0..N uint32_t next_due_ms = 0; bool active = false; } g_wsBurst; static const char kMappingStatus[] = "MAPPING_STATUS:systemstatus;tankremain;odometer;"; static const char kMappingStatic[] = "MAPPING_STATIC:" "lubedistancenormal;lubedistancerain;washdistance;washinterval;" "tankcap;pumppulse;tankwarn;pulserev;tirewidth;tireratio;tiredia;" "speedsource;speedsource-options;" "gpsbaud;gpsbaud-options;" "cansource;cansource-options;" "ledmodeflash;ledmaxbrightness;ledminbrightness;" "showimpulse;showgps;showcan;bleedingpulses;wifi-ssid;wifi-password;" "fw-version;req-flash-version;git-rev;flash-version"; String processor(const String &var); void WebserverNotFound_Callback(AsyncWebServerRequest *request); void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final); void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final); void WebServerEEJSON_Callback(AsyncWebServerRequest *request); void GetFlashVersion(char *buff, size_t buff_size); AsyncWebSocket webSocket("/ws"); void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len); void Websocket_RefreshClientData_DTCs(uint32_t client_id); void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping = false); void Websocket_RefreshClientData_Static(uint32_t client_id, bool send_mapping = false); void Websocket_HandleButtons(uint8_t *data); void Websocket_HandleSettings(uint8_t *data); void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierBufferSize, char *valueBuffer, size_t valueBufferSize); int findIndexByString(const char *searchString, const char *const *array, int arraySize); // ---------- small helpers (safety) ---------- static inline const char *nz(const char *p) { return p ? p : ""; } static inline String tableStr(const char *const *tbl, int idx, int size) { if (idx < 0 || idx >= size) return String(); const char *p = tbl[idx]; return p ? String(p) : String(); } static void appendCsv(String &out, const char *const *arr, size_t n) { for (size_t i = 0; i < n; ++i) { if (!arr[i]) continue; out += arr[i]; if (i + 1 < n) out += ","; } } static inline bool validIndex(int idx, int size) { return idx >= 0 && idx < size; } /** * @brief Initializes the web-based user interface (WebUI) for the ChainLube application. * * This function sets up the necessary components for the WebUI, including mounting LittleFS, * performing flash version checks, initializing mDNS, and configuring the web server with * routes and event handlers. If any errors occur during setup, appropriate diagnostic messages * are pushed to the debugging system, and potential error conditions are recorded as Diagnostic * Trouble Codes (DTCs). * * @note This function should be called during the initialization phase of the application. */ void initWebUI() { // Attempt to mount LittleFS if (!LittleFS.begin()) { Debug_pushMessage("An Error has occurred while mounting LittleFS\n"); MaintainDTC(DTC_FLASHFS_ERROR, true); return; } // Retrieve the flash version GetFlashVersion(globals.FlashVersion, sizeof(globals.FlashVersion)); // Compare the flash version with the required version char buffer[6]; snprintf(buffer, sizeof(buffer), "%d.%02d", constants.Required_Flash_Version_major, constants.Required_Flash_Version_minor); if (strcmp(globals.FlashVersion, buffer)) { MaintainDTC(DTC_FLASHFS_VERSION_ERROR, true); } // Initialize mDNS and add service MDNS.begin(globals.DeviceName); MDNS.addService("http", "tcp", 80); // Set up WebSocket event handler and attach to web server webSocket.onEvent(WebsocketEvent_Callback); webServer.addHandler(&webSocket); // Serve static files and define routes webServer.serveStatic("/static/", LittleFS, "/static/").setCacheControl("max-age=360000"); webServer.serveStatic("/index.htm", LittleFS, "/index.htm").setCacheControl("max-age=360000"); webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->redirect("/index.htm"); }); webServer.onNotFound(WebserverNotFound_Callback); webServer.on("/eejson", HTTP_GET, WebServerEEJSON_Callback); webServer.on( "/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback); webServer.on( "/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback); // Start the web server webServer.begin(); } /** * @brief Processes the web server functionality for the ChainLube application. * * This function performs periodic processing tasks for the web server, including cleaning up * WebSocket clients and refreshing client data when WebSocket connections are active. It ensures * that WebSocket client data related to Diagnostic Trouble Codes (DTCs) and system status is * updated at regular intervals. * * @note This function should be called in the main loop of the application. */ void Webserver_Process() { static uint32_t previousMillis = 0; webSocket.cleanupClients(); if ((webSocket.count() > 0) && (millis() - previousMillis >= 10000)) { Websocket_RefreshClientData_DTCs(0); Websocket_RefreshClientData_Status(0); previousMillis = millis(); } // Gestaffelter Initial-Burst nach Verbindungsaufbau if (g_wsBurst.active && millis() >= g_wsBurst.next_due_ms) { if (!webSocket.hasClient(g_wsBurst.client_id)) { g_wsBurst.active = false; // Client ist schon wieder weg } else if (!webSocket.availableForWrite(g_wsBurst.client_id)) { g_wsBurst.next_due_ms = millis() + 10; // kurz warten, Puffer voll } else { switch (g_wsBurst.step) { case 0: // Mapping Status webSocket.text(g_wsBurst.client_id, kMappingStatus); g_wsBurst.step++; g_wsBurst.next_due_ms = millis() + 25; break; case 1: // Status-Daten (send_mapping=false!) Websocket_RefreshClientData_Status(g_wsBurst.client_id, false); g_wsBurst.step++; g_wsBurst.next_due_ms = millis() + 25; break; case 2: // Mapping Static (achte auf 'wifi-password') webSocket.text(g_wsBurst.client_id, kMappingStatic); g_wsBurst.step++; g_wsBurst.next_due_ms = millis() + 25; break; case 3: // Static-Daten (send_mapping=false!) Websocket_RefreshClientData_Static(g_wsBurst.client_id, false); g_wsBurst.step++; [[fallthrough]]; default: g_wsBurst.active = false; break; } } } } /** * @brief Shuts down the web server functionality for the ChainLube application. * * This function closes all WebSocket connections and terminates the web server. It is intended * to be called when the application is being shut down or when there is a need to deactivate the * web server. * * @details This function ensures a graceful shutdown of the web server by closing all active * WebSocket connections and ending the web server instance. * * @note This function should be called before shutting down the application to properly * deactivate the web server. */ void Webserver_Shutdown() { if (webSocket.count() > 0) webSocket.closeAll(); webServer.end(); } /** * @brief Callback function for handling HTTP 404 (Not Found) errors on the web server. * * This function is invoked when an HTTP request results in a 404 error (Not Found). It sends * a simple "Not found" text response with an HTTP status code of 404. * * @param request Pointer to the AsyncWebServerRequest object representing the HTTP request. */ void WebserverNotFound_Callback(AsyncWebServerRequest *request) { request->send(404, "text/html", "Not found"); } /** * @brief Reads the flash version information from a file in LittleFS. * * This function reads the flash version information stored in a file named "version" in the * LittleFS filesystem. It opens the file, reads the content until a carriage return ('\r') is * encountered, and stores the result in the provided buffer. The buffer is null-terminated. * * @param buff Pointer to the buffer where the flash version information will be stored. * @param buff_size Size of the buffer. */ void GetFlashVersion(char *buff, size_t buff_size) { File this_file = LittleFS.open("version", "r"); if (!this_file) { // failed to open the file, return empty result buff[0] = '\0'; return; } if (this_file.available()) { int bytes_read = this_file.readBytesUntil('\r', buff, buff_size - 1); if (bytes_read < 0) bytes_read = 0; buff[bytes_read] = '\0'; } this_file.close(); } /** * @brief Callback function for handling firmware updates via the web server. * * This function is invoked during the firmware update process when a new firmware file * is received. It handles the update process using the ESPAsyncHTTPUpdate library. The update * process involves checking the firmware type, initializing the update, writing data, and finalizing * the update. If the update is successful, it triggers a system shutdown. * * @param request Pointer to the AsyncWebServerRequest object. * @param filename The name of the file being updated. * @param index The index of the file being updated. * @param data Pointer to the data buffer. * @param len The length of the data buffer. * @param final Boolean indicating if this is the final chunk of data. */ void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { if (!index) { Debug_pushMessage("Update\n"); size_t content_len = request->contentLength(); int cmd = (filename.indexOf(".fs") > -1) ? U_FS : U_FLASH; Update.runAsync(true); if (!Update.begin(content_len, cmd)) { Update.printError(Serial); } } if (Update.write(data, len) != len) { Update.printError(Serial); } else { Debug_pushMessage("Progress: %d%%\n", (Update.progress() * 100) / Update.size()); } if (final) { AsyncWebServerResponse *response = request->beginResponse(302, "text/plain", "Please wait while the device reboots"); response->addHeader("Refresh", "20"); response->addHeader("Location", "/"); request->send(response); if (!Update.end(true)) { Update.printError(Serial); } else { Debug_pushMessage("Update complete\n"); globals.systemStatus = sysStat_Shutdown; } } } void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { constexpr size_t kBufCap = 1536; bool ee_done = false; static bool validext = false; static char *buffer = nullptr; static uint32_t read_ptr = 0; DeserializationError error; // kleines Helferlein zum sicheren Kopieren & Terminieren auto safe_copy = [](char *dst, size_t dst_sz, const char *src) { if (!dst || dst_sz == 0) return; if (!src) { dst[0] = '\0'; return; } strncpy(dst, src, dst_sz - 1); dst[dst_sz - 1] = '\0'; }; // Grenzen/Hilfen für Enum-Ranges (Sentinel bevorzugt, sonst *_Elements) const int maxSpeedSrc = static_cast(SPEEDSOURCE_COUNT); const int maxGPSBaud = static_cast(GPSBAUDRATE_COUNT); const int maxCANSrc = static_cast(CANSOURCE_COUNT); if (!index) { validext = (filename.indexOf(".ee.json") > -1); if (validext) { buffer = (char *)malloc(kBufCap); read_ptr = 0; if (!buffer) { Debug_pushMessage("malloc() failed for EEPROM-Restore\n"); } } } // Chunked receive mit Cap/Trunkierungsschutz if (buffer && len > 0) { size_t remain = (read_ptr < kBufCap) ? (kBufCap - read_ptr) : 0; size_t to_copy = (len <= remain) ? len : remain; if (to_copy > 0) { memcpy(buffer + read_ptr, data, to_copy); read_ptr += to_copy; } else { Debug_pushMessage("EEPROM-Restore input exceeds buffer, truncating\n"); } } if (final) { if (buffer) { // Null-terminieren if (read_ptr == kBufCap) read_ptr = kBufCap - 1; buffer[read_ptr] = '\0'; // Parse JsonDocument json; // entspricht deinem bisherigen Stil error = deserializeJson(json, buffer); if (error) { Debug_pushMessage("deserializeJson() failed: %s\n", error.f_str()); } else if (validext) { // ---- Konfiguration sicher in RAM übernehmen ---- // clamp-Helfer passend zu deinen Sanity-Grenzen auto clamp_u32 = [](uint32_t v, uint32_t lo, uint32_t hi) { return (v < lo) ? lo : (v > hi ? hi : v); }; auto clamp_u16 = [](uint16_t v, uint16_t lo, uint16_t hi) { return (v < lo) ? lo : (v > hi ? hi : v); }; auto clamp_u8 = [](uint8_t v, uint8_t lo, uint8_t hi) { return (v < lo) ? lo : (v > hi ? hi : v); }; // config.* LubeConfig.DistancePerLube_Default = clamp_u32(json["config"]["DistancePerLube_Default"].as(), 0, 50000); LubeConfig.DistancePerLube_Rain = clamp_u32(json["config"]["DistancePerLube_Rain"].as(), 0, 50000); LubeConfig.tankCapacity_ml = clamp_u32(json["config"]["tankCapacity_ml"].as(), 0, 5000); LubeConfig.amountPerDose_microL = clamp_u32(json["config"]["amountPerDose_microL"].as(), 0, 100); LubeConfig.TankRemindAtPercentage = clamp_u8(json["config"]["TankRemindAtPercentage"].as(), 0, 100); LubeConfig.PulsePerRevolution = clamp_u8(json["config"]["PulsePerRevolution"].as(), 0, 255); LubeConfig.TireWidth_mm = clamp_u32(json["config"]["TireWidth_mm"].as(), 0, 500); LubeConfig.TireWidthHeight_Ratio = clamp_u32(json["config"]["TireWidthHeight_Ratio"].as(), 0, 150); LubeConfig.RimDiameter_Inch = clamp_u32(json["config"]["RimDiameter_Inch"].as(), 0, 30); LubeConfig.DistancePerRevolution_mm = clamp_u32(json["config"]["DistancePerRevolution_mm"].as(), 0, 10000); LubeConfig.BleedingPulses = clamp_u16(json["config"]["BleedingPulses"].as(), 0, 1000); LubeConfig.WashMode_Distance = json["config"]["WashMode_Distance"].as(); // ggf. Grenzen anpassen LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as(); // ggf. Grenzen anpassen LubeConfig.LED_Mode_Flash = json["config"]["LED_Mode_Flash"].as(); LubeConfig.LED_Max_Brightness = json["config"]["LED_Max_Brightness"].as(); LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as(); // Enums nur nach Range-Check übernehmen { int v = json["config"]["SpeedSource"].as(); if (v >= 0 && v < maxSpeedSrc) LubeConfig.SpeedSource = (SpeedSource_t)v; else Debug_pushMessage("Restore: invalid SpeedSource=%d\n", v); } { int v = json["config"]["GPSBaudRate"].as(); if (v >= 0 && v < maxGPSBaud) LubeConfig.GPSBaudRate = (GPSBaudRate_t)v; else Debug_pushMessage("Restore: invalid GPSBaudRate=%d\n", v); } { int v = json["config"]["CANSource"].as(); if (v >= 0 && v < maxCANSrc) LubeConfig.CANSource = (CANSource_t)v; else Debug_pushMessage("Restore: invalid CANSource=%d\n", v); } // Strings sicher kopieren (0-terminiert) safe_copy(LubeConfig.wifi_ap_ssid, sizeof(LubeConfig.wifi_ap_ssid), json["config"]["wifi_ap_ssid"]); safe_copy(LubeConfig.wifi_ap_password, sizeof(LubeConfig.wifi_ap_password), json["config"]["wifi_ap_password"]); safe_copy(LubeConfig.wifi_client_ssid, sizeof(LubeConfig.wifi_client_ssid), json["config"]["wifi_client_ssid"]); safe_copy(LubeConfig.wifi_client_password, sizeof(LubeConfig.wifi_client_password), json["config"]["wifi_client_password"]); // persis.* PersistenceData.writeCycleCounter = json["persis"]["writeCycleCounter"].as(); PersistenceData.tankRemain_microL = json["persis"]["tankRemain_microL"].as(); PersistenceData.TravelDistance_highRes_mm = json["persis"]["TravelDistance_highRes_mm"].as(); PersistenceData.odometer_mm = json["persis"]["odometer_mm"].as(); PersistenceData.odometer = json["persis"]["odometer"].as(); PersistenceData.checksum = json["persis"]["checksum"].as(); // Optional: Sanity-Autokorrektur im RAM (keine EEPROM-Writes hier!) { uint32_t sanity = ConfigSanityCheck(true); if (sanity > 0) { MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity); Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity); } } ee_done = true; } free(buffer); buffer = nullptr; } // Browser zurückleiten & ggf. Shutdown AsyncWebServerResponse *response = request->beginResponse(302, "text/plain", "Please wait while the device reboots"); response->addHeader("Refresh", "20"); response->addHeader("Location", "/"); request->send(response); if (ee_done) { Debug_pushMessage("EEPROM restore complete\n"); globals.systemStatus = sysStat_Shutdown; } } } /** * @brief Callback function for handling EEPROM JSON request via the web server. * * This function is invoked when a request for EEPROM JSON data is received. It constructs a JSON * response containing information about the firmware, configuration, and persistence data. * * @param request Pointer to the AsyncWebServerRequest object. */ void WebServerEEJSON_Callback(AsyncWebServerRequest *request) { AsyncResponseStream *response = request->beginResponseStream("application/json"); JsonDocument json; JsonObject info = json["info"].to(); char buffer[16]; info["DeviceName"] = nz(globals.DeviceName); sprintf(buffer, "%d.%02d", constants.Required_Flash_Version_major, constants.Required_Flash_Version_minor); info["FW-Version"] = buffer; info["FS-Version"] = nz(globals.FlashVersion); snprintf_P(buffer, sizeof(buffer), "%s", constants.GitHash); info["Git-Hash"] = buffer; JsonObject config = json["config"].to(); generateJsonObject_LubeConfig(config); JsonObject persis = json["persis"].to(); generateJsonObject_PersistenceData(persis); JsonObject eepart = json["eepart"].to(); sprintf(buffer, "0x%04X", globals.eePersistenceAddress); eepart["PersistenceAddress"] = buffer; serializeJsonPretty(json, *response); response->addHeader("Content-disposition", "attachment; filename=backup.ee.json"); request->send(response); } /** * @brief Callback function for handling WebSocket events. * * This function is invoked when events occur in the WebSocket communication, such as client connection, * disconnection, reception of data, and others. It dispatches the events to the appropriate handlers. * * @param server Pointer to the AsyncWebSocket object. * @param client Pointer to the AsyncWebSocketClient object representing the WebSocket client. * @param type Type of WebSocket event. * @param arg Event-specific argument. * @param data Pointer to the received data (if applicable). * @param len Length of the received data. */ void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { switch (type) { case WS_EVT_CONNECT: { Debug_pushMessage("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); // NICHT direkt senden! Nur Burst triggern: g_wsBurst.client_id = client->id(); g_wsBurst.step = 0; g_wsBurst.next_due_ms = millis(); // sofort startbereit g_wsBurst.active = true; break; } case WS_EVT_DISCONNECT: Debug_pushMessage("WebSocket client #%u disconnected\n", client->id()); break; case WS_EVT_DATA: Websocket_HandleMessage(arg, data, len); break; case WS_EVT_PONG: case WS_EVT_ERROR: break; } } /** * @brief Handles WebSocket messages received from clients. * * This function processes WebSocket messages, such as starting or stopping debugging, * and provides appropriate responses. * * @param arg Pointer to the WebSocket frame information. * @param data Pointer to the received data. * @param len Length of the received data. */ void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len) { AwsFrameInfo *info = (AwsFrameInfo *)arg; if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { // Create a safe, null-terminated local copy std::unique_ptr buf(new char[len + 1]); memcpy(buf.get(), data, len); buf[len] = '\0'; Debug_pushMessage("Websocket-Message (len: %d): %s\n", (int)len, buf.get()); if (strncmp(buf.get(), "btn-", 4) == 0) { Websocket_HandleButtons((uint8_t *)buf.get() + 4); } else if (strncmp(buf.get(), "set-", 4) == 0) { Websocket_HandleSettings((uint8_t *)buf.get() + 4); } else { Debug_pushMessage("Got unknown Websocket-Message '%s' from client\n", buf.get()); } } } /** * @brief Handle button commands received via WebSocket. * * This function parses a WebSocket string representing button commands, extracts * the identifier and value components, and performs corresponding actions based on * the received commands. * * @param data The WebSocket data containing button commands. */ void Websocket_HandleButtons(uint8_t *data) { char identifier[32]; char value[32]; parseWebsocketString((char *)data, identifier, sizeof(identifier), value, sizeof(value)); if (strcmp(identifier, "debugstart") == 0) { SetDebugportStatus(dbg_Webui, enabled); } else if (strcmp(identifier, "debugstop") == 0) { SetDebugportStatus(dbg_Webui, disabled); } else if (strcmp(identifier, "measurereset") == 0) { globals.measuredPulses = 0; } else if (strcmp(identifier, "measurestartstop") == 0) { globals.measurementActive = !globals.measurementActive; } else if (strcmp(identifier, "purgenow") == 0) { globals.systemStatus = sysStat_Purge; } else if (strcmp(identifier, "sourcesave") == 0) { LubeConfig.SpeedSource = speedsourcePreselect; globals.requestEEAction = EE_CFG_SAVE; globals.systemStatus = sysStat_Shutdown; } else if (strcmp(identifier, "settingssave") == 0) { globals.requestEEAction = EE_CFG_SAVE; } else if (strcmp(identifier, "reboot") == 0) { globals.systemStatus = sysStat_Shutdown; } else if (strcmp(identifier, "resettank") == 0) { PersistenceData.tankRemain_microL = LubeConfig.tankCapacity_ml * 1000; globals.requestEEAction = EE_PDS_SAVE; } else { Debug_pushMessage("Got unknown Button-id '%s' from ws-client\n", identifier); } } /** * @brief Handle settings commands received via WebSocket. * * This function parses a WebSocket string representing settings commands, extracts * the identifier and value components, and updates the system settings accordingly. * * @param data The WebSocket data containing settings commands. */ void Websocket_HandleSettings(uint8_t *data) { char identifier[32]; char value[63]; parseWebsocketString((char *)data, identifier, sizeof(identifier), value, sizeof(value)); if (strcmp(identifier, "bleedingpulses") == 0) { LubeConfig.BleedingPulses = atoi(value); } else if (strcmp(identifier, "speedsource") == 0) { int index = findIndexByString(value, SpeedSourceString, (int)SPEEDSOURCE_COUNT); if (validIndex(index, (int)SPEEDSOURCE_COUNT)) speedsourcePreselect = (SpeedSource_t)index; else Debug_pushMessage("Invalid speedsource '%s'\n", value); } else if (strcmp(identifier, "cansource") == 0) { int index = findIndexByString(value, CANSourceString, (int)CANSOURCE_COUNT); if (validIndex(index, (int)CANSOURCE_COUNT)) LubeConfig.CANSource = (CANSource_t)index; else Debug_pushMessage("Invalid cansource '%s'\n", value); } else if (strcmp(identifier, "gpsbaud") == 0) { int index = findIndexByString(value, GPSBaudRateString, (int)GPSBAUDRATE_COUNT); if (validIndex(index, (int)GPSBAUDRATE_COUNT)) LubeConfig.GPSBaudRate = (GPSBaudRate_t)index; else Debug_pushMessage("Invalid gpsbaud '%s'\n", value); } else if (strcmp(identifier, "ledmaxbrightness") == 0) { LubeConfig.LED_Max_Brightness = atoi(value); } else if (strcmp(identifier, "ledminbrightness") == 0) { LubeConfig.LED_Min_Brightness = atoi(value); } else if (strcmp(identifier, "pumppulse") == 0) { LubeConfig.BleedingPulses = atoi(value); } else if (strcmp(identifier, "tankwarn") == 0) { LubeConfig.TankRemindAtPercentage = atoi(value); } else if (strcmp(identifier, "tankcap") == 0) { LubeConfig.tankCapacity_ml = atoi(value); } else if (strcmp(identifier, "lubedistancerain") == 0) { LubeConfig.DistancePerLube_Rain = atoi(value); } else if (strcmp(identifier, "lubedistancenormal") == 0) { LubeConfig.DistancePerLube_Default = atoi(value); } else if (strcmp(identifier, "ledmodeflash") == 0) { LubeConfig.LED_Mode_Flash = value[0] == '1'; } else if (strcmp(identifier, "wifi-ssid") == 0) { strncpy(LubeConfig.wifi_client_ssid, value, sizeof(LubeConfig.wifi_client_ssid)); } else if (strcmp(identifier, "wifi-password") == 0) { strncpy(LubeConfig.wifi_client_password, value, sizeof(LubeConfig.wifi_client_password)); } else if (strcmp(identifier, "washinterval") == 0) { LubeConfig.WashMode_Interval = atoi(value); } else if (strcmp(identifier, "washdistance") == 0) { LubeConfig.WashMode_Distance = atoi(value); } else { Debug_pushMessage("Got unknown Settings-id and value '%s' from ws-client\n", identifier); } } /** * @brief Pushes live debug messages to all WebSocket clients. * * This function sends a live debug message to all connected WebSocket clients. * * @param Message The debug message to be sent. */ void Websocket_PushLiveDebug(String Message) { webSocket.textAll("DEBUG:" + Message); } /** * @brief Refreshes client data related to Diagnostic Trouble Codes (DTCs) on WebSocket clients. * * This function constructs a DTC-related string and sends it to a specific WebSocket client or * broadcasts it to all connected WebSocket clients. * * @param client_id The ID of the WebSocket client to which the data should be sent. If 0, the data * will be broadcasted to all connected clients. */ void Websocket_RefreshClientData_DTCs(uint32_t client_id) { String temp = "DTC:"; // Build DTC-String if (globals.hasDTC != true) { temp.concat(String(DTC_NO_DTC) + ";"); } else { for (uint32_t i = 0; i < MAX_DTC_STORAGE; i++) { if (DTCStorage[i].Number < DTC_LAST_DTC) { temp.concat(String(DTCStorage[i].timestamp) + ","); temp.concat(String(DTCStorage[i].Number) + ","); temp.concat(String(getSeverityForDTC(DTCStorage[i].Number)) + ","); temp.concat(String(DTCStorage[i].active) + ","); temp.concat(String(DTCStorage[i].debugVal) + ";"); } } } if (client_id > 0) { webSocket.text(client_id, temp); } else { webSocket.textAll(temp); } } /** * @brief Refreshes client data related to system status and relevant parameters on WebSocket clients. * * This function constructs a status-related string and sends it to a specific WebSocket client or * broadcasts it to all connected WebSocket clients. It also sends a mapping of the status parameters. * * @param client_id The ID of the WebSocket client to which the data should be sent. If 0, the data * will be broadcasted to all connected clients. * @param send_mapping Flag indicating whether to send the parameter mapping to the client(s). */ void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping) { if (send_mapping) { if (client_id > 0) webSocket.text(client_id, kMappingStatus); else webSocket.textAll(kMappingStatus); } String temp = "STATUS:"; temp.concat(String(ToString(globals.systemStatus)) + ";"); // Guard against division by zero (capacity==0) uint32_t cap = LubeConfig.tankCapacity_ml; uint32_t remain10 = (PersistenceData.tankRemain_microL / 10); // keep your original math uint32_t ratio = (cap > 0) ? (remain10 / cap) : 0; temp.concat(String(ratio) + ";"); temp.concat(String(PersistenceData.odometer + (PersistenceData.odometer_mm / 1000)) + ";"); if (client_id > 0) { webSocket.text(client_id, temp); } else { webSocket.textAll(temp); } } /** * @brief Refreshes client data related to static configuration parameters on WebSocket clients. * * This function constructs a static configuration-related string and sends it to a specific WebSocket client or * broadcasts it to all connected WebSocket clients. It also sends a mapping of the static configuration parameters. * * @param client_id The ID of the WebSocket client to which the data should be sent. If 0, the data * will be broadcasted to all connected clients. * @param send_mapping Flag indicating whether to send the parameter mapping to the client(s). */ void Websocket_RefreshClientData_Static(uint32_t client_id, bool send_mapping) { if (send_mapping) { if (client_id > 0) webSocket.text(client_id, kMappingStatic); else webSocket.textAll(kMappingStatic); } String temp = "STATIC:"; temp += String(LubeConfig.DistancePerLube_Default) + ";"; temp += String(LubeConfig.DistancePerLube_Rain) + ";"; temp += String(LubeConfig.WashMode_Distance) + ";"; temp += String(LubeConfig.WashMode_Interval) + ";"; temp += String(LubeConfig.tankCapacity_ml) + ";"; temp += String(LubeConfig.amountPerDose_microL) + ";"; temp += String(LubeConfig.TankRemindAtPercentage) + ";"; temp += String(LubeConfig.PulsePerRevolution) + ";"; temp += String(LubeConfig.TireWidth_mm) + ";"; temp += String(LubeConfig.TireWidthHeight_Ratio) + ";"; temp += String(LubeConfig.RimDiameter_Inch) + ";"; // speedsource + Optionen temp += String(ToString(LubeConfig.SpeedSource)) + ";"; { String csv; appendCsv(csv, SpeedSourceString, SPEEDSOURCE_COUNT); temp += csv + ";"; } // gpsbaud + Optionen temp += String(ToString(LubeConfig.GPSBaudRate)) + ";"; { String csv; appendCsv(csv, GPSBaudRateString, GPSBAUDRATE_COUNT); temp += csv + ";"; } // cansource + Optionen temp += String(ToString(LubeConfig.CANSource)) + ";"; { String csv; appendCsv(csv, CANSourceString, CANSOURCE_COUNT); temp += csv + ";"; } temp += (LubeConfig.LED_Mode_Flash ? "1" : "0"); temp += ";"; temp += String(LubeConfig.LED_Max_Brightness) + ";"; temp += String(LubeConfig.LED_Min_Brightness) + ";"; temp += String(LubeConfig.SpeedSource == SOURCE_IMPULSE ? 1 : 0) + ";"; temp += String(LubeConfig.SpeedSource == SOURCE_GPS ? 1 : 0) + ";"; temp += String(LubeConfig.SpeedSource == SOURCE_CAN ? 1 : 0) + ";"; temp += String(LubeConfig.BleedingPulses) + ";"; temp += String(nz(LubeConfig.wifi_client_ssid)) + ";"; temp += String(nz(LubeConfig.wifi_client_password)) + ";"; // Versionen (x.XX) char ver_fw[8], ver_reqfs[8]; snprintf(ver_fw, sizeof(ver_fw), "%u.%02u", constants.FW_Version_major, constants.FW_Version_minor); snprintf(ver_reqfs, sizeof(ver_reqfs), "%u.%02u", constants.Required_Flash_Version_major, constants.Required_Flash_Version_minor); temp += String(ver_fw) + ";"; temp += String(ver_reqfs) + ";"; temp += String(nz(constants.GitHash)) + ";"; temp += String(nz(globals.FlashVersion)) + ";"; if (client_id > 0) webSocket.text(client_id, temp); else webSocket.textAll(temp); } /** * @brief Parse a WebSocket string into identifier and value components. * * This function takes a WebSocket string, separates it into identifier and value * components using the ":" delimiter, and stores them in the specified buffers. * If no ":" is found, the entire string is considered as the value, and the * identifier buffer is set to an empty string. * * @param data The WebSocket string to parse. * @param identifierBuffer The buffer to store the identifier component. * @param identifierBufferSize The size of the identifier buffer. * @param valueBuffer The buffer to store the value component. * @param valueBufferSize The size of the value buffer. */ void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierBufferSize, char *valueBuffer, size_t valueBufferSize) { // Zerlegen des Strings anhand des Trennzeichens ":" char *token = strtok(data, ":"); // Falls der erste Teil des Strings vorhanden ist if (token != NULL) { // Kopieren des ersten Teils in den Buffer für Identifier strncpy(identifierBuffer, token, identifierBufferSize - 1); identifierBuffer[identifierBufferSize - 1] = '\0'; // Weitere Aufrufe von strtok, um den nächsten Teil zu erhalten token = strtok(NULL, ":"); // Falls der zweite Teil des Strings vorhanden ist if (token != NULL) { // Kopieren des zweiten Teils in den Buffer für Value strncpy(valueBuffer, token, valueBufferSize - 1); valueBuffer[valueBufferSize - 1] = '\0'; } else { // Kein zweiter Teil vorhanden, setzen Sie den Buffer für Value auf leer valueBuffer[0] = '\0'; } } else { // Der erste Teil des Strings fehlt, setzen Sie den Buffer für Identifier auf leer identifierBuffer[0] = '\0'; // Der gesamte String wird als Value betrachtet strncpy(valueBuffer, data, valueBufferSize - 1); valueBuffer[valueBufferSize - 1] = '\0'; } } /** * @brief Find the index of a string in an array. * * This function searches for the given string in the provided array and returns * the index of the first occurrence. If the string is not found, it returns -1. * * @param searchString The string to search for in the array. * @param array The array of strings to search within. * @param arraySize The size of the array. * * @return The index of the first occurrence of the string in the array, * or -1 if the string is not found. */ int findIndexByString(const char *searchString, const char *const *array, int arraySize) { // Durchlaufe das Array und vergleiche jeden String for (int i = 0; i < arraySize; ++i) { if (array[i] && strcmp(array[i], searchString) == 0) { // String gefunden, gib den Index zurück return i; } } // String nicht gefunden, gib -1 zurück return -1; } /** * @brief Pushes a notification to all WebSocket clients. * * This function sends a live debug message to all connected WebSocket clients. * * @param Message The debug message to be sent. * @param type The type of notification (info, success, warning, error). * - Use NotificationType_t::info for informational messages. * - Use NotificationType_t::success for successful operation messages. * - Use NotificationType_t::warning for warning messages. * - Use NotificationType_t::error for error messages. */ void Websocket_PushNotification(String Message, NotificationType_t type) { String typeString; switch (type) { case info: typeString = "info"; break; case success: typeString = "success"; break; case warning: typeString = "warning"; break; case error: typeString = "danger"; break; default: typeString = "info"; break; } webSocket.textAll("NOTIFY:" + typeString + ";" + Message); Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString.c_str()); }