/** * @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" AsyncWebServer webServer(80); const char *PARAM_MESSAGE = "message"; String processor(const String &var); void WebserverPOST_Callback(AsyncWebServerRequest *request); 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); /** * @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(); } } void WebserverPOST_Callback(AsyncWebServerRequest *request) { request->send(LittleFS, "/post.htm", "text/html", false, processor); Debug_pushMessage("POST:\n"); int paramsNr = request->params(); for (int i = 0; i < paramsNr; i++) { AsyncWebParameter *p = request->getParam(i); Debug_pushMessage("%s : %s\n", p->name().c_str(), p->value().c_str()); // begin: POST Form Source Changed if (p->name() == "sourceselect") { SpeedSource_t temp = (SpeedSource_t)p->value().toInt(); Debug_pushMessage("temp: %d", temp); Debug_pushMessage("SpeedSource: %d", LubeConfig.SpeedSource); if (LubeConfig.SpeedSource != temp) { LubeConfig.SpeedSource = temp; globals.systemStatus = sysStat_Shutdown; } } // end: POST Form Source Changed // begin: POST Form Source Pulse Settings if (p->name() == "tirewidth") LubeConfig.TireWidth_mm = p->value().toInt(); if (p->name() == "tireratio") LubeConfig.TireWidthHeight_Ratio = p->value().toInt(); if (p->name() == "tiredia") LubeConfig.RimDiameter_Inch = p->value().toInt(); if (p->name() == "pulserev") LubeConfig.PulsePerRevolution = p->value().toInt(); if (p->name() == "pulsesave") globals.requestEEAction = EE_CFG_SAVE; // end: POST Form Source Pulse Settings // begin: POST Form Source GPS Settings if (p->name() == "gpsbaud") LubeConfig.GPSBaudRate = (GPSBaudRate_t)p->value().toInt(); if (p->name() == "gpssave") globals.requestEEAction = EE_CFG_SAVE; // end: POST Form Source GPS Settings // begin: POST Form Source CAN Settings if (p->name() == "cansource") LubeConfig.CANSource = (CANSource_t)p->value().toInt(); if (p->name() == "cansave") globals.requestEEAction = EE_CFG_SAVE; // end: POST Form Source CAN Settings // begin: POST Form Lubrication if (p->name() == "lubedistancenormal") LubeConfig.DistancePerLube_Default = p->value().toInt(); if (p->name() == "lubedistancerain") LubeConfig.DistancePerLube_Rain = p->value().toInt(); if (p->name() == "lubesave") globals.requestEEAction = EE_CFG_SAVE; // end: POST Form Lubrication // begin: POST Form Oiltank if (p->name() == "tankcap") LubeConfig.tankCapacity_ml = p->value().toInt(); if (p->name() == "tankwarn") LubeConfig.TankRemindAtPercentage = p->value().toInt(); if (p->name() == "pumppulse") LubeConfig.amountPerDose_microL = p->value().toInt(); if (p->name() == "oilsave") globals.requestEEAction = EE_CFG_SAVE; // end: POST Form Oiltank // begin: POST Form Maintenance if (p->name() == "purgepulse") LubeConfig.BleedingPulses = p->value().toInt(); if (p->name() == "maintsave") globals.requestEEAction = EE_CFG_SAVE; if (p->name() == "resettank") { PersistenceData.tankRemain_microL = LubeConfig.tankCapacity_ml * 1000; globals.requestEEAction = EE_PDS_SAVE; } if (p->name() == "reset_ee_btn") { if (request->hasParam("reset_ee_pds", true)) { AsyncWebParameter *param = request->getParam("reset_ee_pds", true); if (param->value() == "on") globals.requestEEAction = globals.requestEEAction == EE_CFG_FORMAT ? EE_FORMAT_ALL : EE_PDS_FORMAT; } if (request->hasParam("reset_ee_cfg", true)) { AsyncWebParameter *param = request->getParam("reset_ee_cfg", true); if (param->value() == "on") globals.requestEEAction = globals.requestEEAction == EE_PDS_FORMAT ? EE_FORMAT_ALL : EE_CFG_FORMAT; } } if (p->name() == "purgenow") { globals.systemStatus = sysStat_Purge; globals.purgePulses = LubeConfig.BleedingPulses; } if (p->name() == "reboot") { globals.systemStatus = sysStat_Shutdown; } // end: POST Form Maintenance // begin: POST Form LED Settings if (p->name() == "ledmaxbrightness") LubeConfig.LED_Max_Brightness = p->value().toInt(); if (p->name() == "ledminbrightness") LubeConfig.LED_Min_Brightness = p->value().toInt(); if (p->name() == "ledsave") { if (request->hasParam("ledmodeflash", true)) { AsyncWebParameter *param = request->getParam("ledmodeflash", true); if (param->value() == "on") LubeConfig.LED_Mode_Flash = true; } else { LubeConfig.LED_Mode_Flash = false; } globals.requestEEAction = EE_CFG_SAVE; } // end: POST Form LED SEttings // begin: POST Form Measure Pulses if (p->name() == "measurereset") globals.measuredPulses = 0; if (p->name() == "measurestartstop") globals.measurementActive = !globals.measurementActive; // end: POST Form Measure Pulses } } /** * @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, retrn empty result buff[0] = '\0'; return; } if (this_file.available()) { int bytes_read; bytes_read = this_file.readBytesUntil('\r', buff, buff_size - 1); 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"); 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; } } } /** * @brief Callback function for handling EEPROM restore via the web server. * * This function is invoked during the EEPROM restore process when a new EEPROM file * is received. It handles the restore process by reading the data from the received file, * deserializing the JSON data, and updating the configuration and persistence data accordingly. * If the restore is successful, it triggers a system shutdown. * * @param request Pointer to the AsyncWebServerRequest object. * @param filename The name of the file being restored. * @param index The index of the file being restored. * @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 WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { bool ee_done = false; static bool validext = false; static char *buffer = NULL; static uint32_t read_ptr = 0; DeserializationError error; if (!index) { validext = (filename.indexOf(".ee.json") > -1); if (validext) { buffer = (char *)malloc(1536); read_ptr = 0; if (buffer == NULL) Debug_pushMessage("malloc() failed for EEPROM-Restore"); } } if (buffer != NULL) { memcpy(buffer + read_ptr, data, len); read_ptr = read_ptr + len; } if (final) { if (buffer != NULL) { Serial.print(buffer); StaticJsonDocument<1536> doc; error = deserializeJson(doc, buffer); if (error) { Debug_pushMessage("deserializeJson() failed: %s\n", error.f_str()); } else { LubeConfig.DistancePerLube_Default = doc["config"]["DistancePerLube_Default"].as(); LubeConfig.DistancePerLube_Rain = doc["config"]["DistancePerLube_Rain"].as(); LubeConfig.tankCapacity_ml = doc["config"]["tankCapacity_ml"].as(); LubeConfig.amountPerDose_microL = doc["config"]["amountPerDose_microL"].as(); LubeConfig.TankRemindAtPercentage = doc["config"]["TankRemindAtPercentage"].as(); LubeConfig.PulsePerRevolution = doc["config"]["PulsePerRevolution"].as(); LubeConfig.TireWidth_mm = doc["config"]["TireWidth_mm"].as(); LubeConfig.TireWidthHeight_Ratio = doc["config"]["TireWidthHeight_Ratio"].as(); LubeConfig.RimDiameter_Inch = doc["config"]["RimDiameter_Inch"].as(); LubeConfig.DistancePerRevolution_mm = doc["config"]["DistancePerRevolution_mm"].as(); LubeConfig.BleedingPulses = doc["config"]["BleedingPulses"].as(); LubeConfig.SpeedSource = (SpeedSource_t)doc["config"]["SpeedSource"].as(); LubeConfig.GPSBaudRate = (GPSBaudRate_t)doc["config"]["GPSBaudRate"].as(); LubeConfig.CANSource = (CANSource_t)doc["config"]["CANSource"].as(); LubeConfig.LED_Mode_Flash = doc["config"]["LED_Mode_Flash"].as(); LubeConfig.LED_Max_Brightness = doc["config"]["LED_Max_Brightness"].as(); LubeConfig.LED_Min_Brightness = doc["config"]["LED_Min_Brightness"].as(); PersistenceData.writeCycleCounter = doc["persis"]["writeCycleCounter"].as(); PersistenceData.tankRemain_microL = doc["persis"]["tankRemain_microL"].as(); PersistenceData.TravelDistance_highRes_mm = doc["persis"]["TravelDistance_highRes_mm"].as(); PersistenceData.odometer_mm = doc["persis"]["odometer_mm"].as(); PersistenceData.odometer = doc["persis"]["odometer"].as(); PersistenceData.checksum = doc["persis"]["checksum"].as(); ee_done = true; } } free(buffer); 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("Update complete"); 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"); DynamicJsonDocument json(1024); JsonObject fwinfo = json.createNestedObject("info"); char buffer[16]; fwinfo["DeviceName"] = globals.DeviceName; sprintf(buffer, "%d.%02d", constants.Required_Flash_Version_major, constants.Required_Flash_Version_minor); fwinfo["FW-Version"] = buffer; fwinfo["FS-Version"] = globals.FlashVersion; snprintf_P(buffer, sizeof(buffer), "%s", constants.GitHash); fwinfo["Git-Hash"] = buffer; JsonObject config = json.createNestedObject("config"); config["EEPROM_Version"] = LubeConfig.EEPROM_Version; config["DistancePerLube_Default"] = LubeConfig.DistancePerLube_Default; config["DistancePerLube_Rain"] = LubeConfig.DistancePerLube_Rain; config["tankCapacity_ml"] = LubeConfig.tankCapacity_ml; config["amountPerDose_microL"] = LubeConfig.amountPerDose_microL; config["TankRemindAtPercentage"] = LubeConfig.TankRemindAtPercentage; config["PulsePerRevolution"] = LubeConfig.PulsePerRevolution; config["TireWidth_mm"] = LubeConfig.TireWidth_mm; config["TireWidthHeight_Ratio"] = LubeConfig.TireWidthHeight_Ratio; config["RimDiameter_Inch"] = LubeConfig.RimDiameter_Inch; config["DistancePerRevolution_mm"] = LubeConfig.DistancePerRevolution_mm; config["BleedingPulses"] = LubeConfig.BleedingPulses; config["SpeedSource"] = LubeConfig.SpeedSource; config["SpeedSource_Str"] = SpeedSourceString[LubeConfig.SpeedSource]; config["GPSBaudRate"] = LubeConfig.GPSBaudRate; config["GPSBaudRate_Str"] = GPSBaudRateString[LubeConfig.GPSBaudRate]; config["CANSource"] = LubeConfig.CANSource; config["CANSource_Str"] = CANSourceString[LubeConfig.CANSource]; config["LED_Mode_Flash"] = LubeConfig.LED_Mode_Flash; config["LED_Max_Brightness"] = LubeConfig.LED_Max_Brightness; config["LED_Min_Brightness"] = LubeConfig.LED_Min_Brightness; sprintf(buffer, "0x%08X", LubeConfig.checksum); config["checksum"] = buffer; JsonObject eepart = json.createNestedObject("eepart"); sprintf(buffer, "0x%04X", globals.eePersistanceAdress); eepart["PersistanceAddress"] = buffer; JsonObject persis = json.createNestedObject("persis"); persis["writeCycleCounter"] = PersistenceData.writeCycleCounter; persis["tankRemain_microL"] = PersistenceData.tankRemain_microL; persis["TravelDistance_highRes_mm"] = PersistenceData.TravelDistance_highRes_mm; persis["odometer_mm"] = PersistenceData.odometer_mm; persis["odometer"] = PersistenceData.odometer; sprintf(buffer, "0x%08X", PersistenceData.checksum); persis["checksum"] = 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()); Websocket_RefreshClientData_Status(client->id(), true); Websocket_RefreshClientData_Static(client->id(), true); Websocket_RefreshClientData_DTCs(client->id()); 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) { data[len] = 0; Debug_pushMessage("Got WebSocket Message: %s \n", (char *)data); if (strcmp((char *)data, "start") == 0) { SetDebugportStatus(dbg_Webui, enabled); } else if (strcmp((char *)data, "stop") == 0) { SetDebugportStatus(dbg_Webui, disabled); } else if (strcmp((char *)data, "foo") == 0) { Debug_pushMessage("Got WebSocket Message 'foo' from client\n"); } } } /** * @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) { const char mapping[] = "MAPPING_STATUS:" "SystemStatus;" "tankremain;" "Odometer;"; if (client_id > 0) webSocket.text(client_id, mapping); else webSocket.textAll(mapping); } String temp = "STATUS:"; temp.concat(String(globals.systemStatustxt) + ";"); temp.concat(String((PersistenceData.tankRemain_microL / 10) / LubeConfig.tankCapacity_ml) + ";"); 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) { const char mapping[] = "MAPPING_STATIC:" "lubedistancenormal;" "lubedistancerain;" "tankcap;" "pumppulse;" "tankwarn;" "pulserev;" "tirewidth;" "tireratio;" "tiredia;" "DistancePerRevolution_mm;" "sourceselect;" "gpsbaud;" "cansource;" "ledmodeflash;" "ledmaxbrightness;" "ledminbrightness;" "showimpulse;" "showgps;" "showcan;" "BleedingPulses;"; if (client_id > 0) webSocket.text(client_id, mapping); else webSocket.textAll(mapping); } String temp = "STATIC:"; temp.concat(String(LubeConfig.DistancePerLube_Default) + ";"); temp.concat(String(LubeConfig.DistancePerLube_Rain) + ";"); temp.concat(String(LubeConfig.tankCapacity_ml) + ";"); temp.concat(String(LubeConfig.amountPerDose_microL) + ";"); temp.concat(String(LubeConfig.TankRemindAtPercentage) + ";"); temp.concat(String(LubeConfig.PulsePerRevolution) + ";"); temp.concat(String(LubeConfig.TireWidth_mm) + ";"); temp.concat(String(LubeConfig.TireWidthHeight_Ratio) + ";"); temp.concat(String(LubeConfig.RimDiameter_Inch) + ";"); temp.concat(String(LubeConfig.DistancePerRevolution_mm) + ";"); temp.concat(String(SpeedSourceString[LubeConfig.SpeedSource]) + ";"); temp.concat(String(GPSBaudRateString[LubeConfig.GPSBaudRate]) + ";"); temp.concat(String(CANSourceString[LubeConfig.CANSource]) + ";"); temp.concat(String(LubeConfig.LED_Mode_Flash == true ? "1" : "0") + ";"); temp.concat(String(LubeConfig.LED_Max_Brightness) + ";"); temp.concat(String(LubeConfig.LED_Min_Brightness) + ";"); temp.concat(String(LubeConfig.SpeedSource == SOURCE_IMPULSE ? "1" : "0") + ";"); temp.concat(String(LubeConfig.SpeedSource == SOURCE_GPS ? "1" : "0") + ";"); temp.concat(String(LubeConfig.SpeedSource == SOURCE_CAN ? "1" : "0") + ";"); temp.concat(String(LubeConfig.BleedingPulses) + ";"); for (uint32_t i = 0; i < SpeedSourceString_Elements; i++) { temp.concat(String(SpeedSourceString[i]) + ","); } temp.concat(";"); if (client_id > 0) { webSocket.text(client_id, temp); } else { webSocket.textAll(temp); } }