updated Project with elementes From ChainLube-Project

This commit is contained in:
2024-05-30 23:38:05 +02:00
parent 7a2e95c126
commit cf76ea7cc7
16 changed files with 1662 additions and 679 deletions

View File

@@ -1,3 +1,16 @@
/**
* @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);
@@ -5,9 +18,7 @@ AsyncWebServer webServer(80);
const char *PARAM_MESSAGE = "message";
String processor(const String &var);
void WebserverPOST_Callback(AsyncWebServerRequest *request);
void WebserverNotFound_Callback(AsyncWebServerRequest *request);
void Webserver_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);
@@ -17,295 +28,137 @@ 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);
/**
* @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, DTC_CRITICAL, true);
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, DTC_WARN, true);
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("/index.htm", HTTP_GET, Webserver_Callback);
webServer.on("/post.htm", HTTP_POST, WebserverPOST_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();
}
}
String processor(const String &var)
/**
* @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 (var == "HOSTNAME")
return String(globals.DeviceName);
if (var == "SYSTEM_STATUS")
return String(sSystem_Status_txt[globals.systemStatus]);
if (var == "SW_VERSION")
{
char buffer[6];
snprintf(buffer, sizeof(buffer), "%d.%02d", constants.FW_Version_major, constants.FW_Version_minor);
return String(buffer);
}
if (var == "FS_VERSION")
return String(globals.FlashVersion);
if (var == "GIT_REV")
return String(constants.GitHash);
if (var == "SHOW_DTC_TABLE")
return globals.systemStatus == sysStat_Error ? "" : "hidden";
if (var == "BAT_REMAIN_CAPACITY")
return String(globals.battery_level);
if (var == "DEVICENAME")
return String(globals.DeviceName);
if (var == "DEVICENAME_ID")
return String(globals.DeviceName_ID);
if (var == "BATTERY_TYPE")
return String(ConfigData.batteryType);
if (var == "BAT_VOLTAGE")
return String((float)globals.loadvoltage_mV / 1000.0);
if (var == "PERSISTANCE_CHECKSUM")
{
char buffer[7];
sprintf(buffer, "0x%04X", PersistenceData.checksum);
return String(buffer);
}
if (var == "WRITE_CYCLE_COUNT")
return String(PersistenceData.writeCycleCounter);
if (var == "PERSISTENCE_MARKER")
return String(globals.eePersistanceAdress);
if (var == "EEPROM_VERSION")
return String(ConfigData.EEPROM_Version);
if (var == "CONFIG_CHECKSUM")
{
char buffer[7];
sprintf(buffer, "0x%04X", ConfigData.checksum);
return String(buffer);
}
if (var == "DTC_TABLE")
{
String temp = "";
char buff_timestamp[16]; // Format: DD-hh:mm:ss:xxx
for (uint32_t i = 0; i < MAX_DTC_STORAGE; i++)
{
if (DTCStorage[i].Number < DTC_LAST_DTC)
{
sprintf(buff_timestamp, "%02d-%02d:%02d:%02d:%03d",
DTCStorage[i].timestamp / 86400000, // Days
DTCStorage[i].timestamp / 360000 % 24, // Hours
DTCStorage[i].timestamp / 60000 % 60, // Minutes
DTCStorage[i].timestamp / 1000 % 60, // Seconds
DTCStorage[i].timestamp % 1000); // milliseconds
temp = temp + "<tr data-dtc=" + String(DTCStorage[i].Number);
temp = temp + " data-debugval=" + String(DTCStorage[i].debugVal) + "><td>" + String(buff_timestamp);
temp = temp + "</td><td>" + String(DTCStorage[i].Number) + "</td><td>";
temp = temp + "<img src=static/img/";
switch (DTCStorage[i].severity)
{
case DTC_CRITICAL:
temp = temp + "critical";
break;
case DTC_WARN:
temp = temp + "warn";
break;
case DTC_INFO:
temp = temp + "info";
break;
}
temp = temp + ".png></td><td>";
if (DTCStorage[i].active == DTC_ACTIVE)
temp = temp + "active";
else if (DTCStorage[i].active == DTC_PREVIOUS)
temp = temp + "previous";
else
temp = temp + "none";
temp = temp + "</td></tr>";
}
}
return temp;
}
if (var == "PLACEHOLDER")
return "placeholder";
if (var == "POINTS_FAC_1")
{
char buff[12];
snprintf(buff, 12, "%3d:%02d:%02d", PersistenceData.faction_1_timer / 3600, (PersistenceData.faction_1_timer / 60) % 60, PersistenceData.faction_1_timer % 60);
return String(buff);
}
if (var == "POINTS_FAC_2")
{
char buff[12];
snprintf(buff, 12, "%3d:%02d:%02d", PersistenceData.faction_2_timer / 3600, (PersistenceData.faction_2_timer / 60) % 60, PersistenceData.faction_2_timer % 60);
return String(buff);
}
if (var == "POINTS_FAC_3")
{
char buff[12];
snprintf(buff, 12, "%3d:%02d:%02d", PersistenceData.faction_3_timer / 3600, (PersistenceData.faction_3_timer / 60) % 60, PersistenceData.faction_3_timer % 60);
return String(buff);
}
if (var == "ACTIVE_FACTION")
return String(PersistenceData.activeFaction);
if (var == "FACTION_1_ACTIVE")
return String(PersistenceData.activeFaction == FACTION_1 ? "bg-primary" : "bg-secondary");
if (var == "FACTION_2_ACTIVE")
return String(PersistenceData.activeFaction == FACTION_2 ? "bg-primary" : "bg-secondary");
if (var == "FACTION_3_ACTIVE")
return String(PersistenceData.activeFaction == FACTION_3 ? "bg-primary" : "bg-secondary");
if (var == "NAME_FAC_1")
return String(ConfigData.Faction_1_Name);
if (var == "NAME_FAC_2")
return String(ConfigData.Faction_2_Name);
if (var == "NAME_FAC_3")
return String(ConfigData.Faction_3_Name);
if (var == "BATTERY_SELECT_OPTIONS")
{
String temp;
for (uint32_t i = 0; i < BatteryString_Elements; i++)
{
String selected = ConfigData.batteryType == i ? " selected " : "";
temp = temp + "<option value=\"" + i + "\"" + selected + ">" + BatteryString[i] + "</option>";
}
return temp;
}
if (var == "FACTIONREBOOT_CHECKED")
return String(ConfigData.active_faction_on_reboot == true ? "checked" : "");
if (var == "FACTION_RECOVERY")
return String(ConfigData.active_faction_on_reboot);
return String();
}
void Webserver_Callback(AsyncWebServerRequest *request)
{
request->send(LittleFS, "/index.htm", "text/html", false, processor);
}
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 Maintenance
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() == "reboot")
{
globals.systemStatus = sysStat_Shutdown;
}
if (p->name() == "resetpoints")
{
PersistenceData.faction_1_timer = 0;
PersistenceData.faction_2_timer = 0;
PersistenceData.faction_3_timer = 0;
PersistenceData.activeFaction = NONE;
globals.requestEEAction == EE_PDS_SAVE;
}
// end: POST Form Maintenance
// begin: POST Form Settings
if (p->name() == "battery_select")
{
batteryType_t temp = (batteryType_t)p->value().toInt();
ConfigData.batteryType = temp;
}
if (request->hasParam("factionreboot_cont", true))
{
AsyncWebParameter *param = request->getParam("factionreboot_cont", true);
if (param->value() == "on")
ConfigData.active_faction_on_reboot = true;
}
else
{
ConfigData.active_faction_on_reboot = false;
}
if (p->name() == "faction_1_name")
{
strncpy(ConfigData.Faction_1_Name, p->value().c_str(), sizeof(ConfigData.Faction_1_Name));
}
if (p->name() == "faction_2_name")
{
strncpy(ConfigData.Faction_2_Name, p->value().c_str(), sizeof(ConfigData.Faction_2_Name));
}
if (p->name() == "faction_3_name")
{
strncpy(ConfigData.Faction_3_Name, p->value().c_str(), sizeof(ConfigData.Faction_3_Name));
}
if (p->name() == "settingssave")
globals.requestEEAction = EE_CFG_SAVE;
// end: POST Form Settings
}
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");
@@ -323,12 +176,27 @@ void GetFlashVersion(char *buff, size_t buff_size)
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");
Debug_pushMessage("Update\n");
size_t content_len = request->contentLength();
int cmd = (filename.indexOf(".fs") > -1) ? U_FS : U_FLASH;
Update.runAsync(true);
@@ -365,6 +233,21 @@ void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const Stri
}
}
/**
* @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;
@@ -381,7 +264,7 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
buffer = (char *)malloc(1536);
read_ptr = 0;
if (buffer == NULL)
Debug_pushMessage("malloc() failed for EEPROM-Restore");
Debug_pushMessage("malloc() failed for EEPROM-Restore\n");
}
}
@@ -396,8 +279,8 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
if (buffer != NULL)
{
Serial.print(buffer);
StaticJsonDocument<1536> doc;
error = deserializeJson(doc, buffer);
JsonDocument json;
error = deserializeJson(json, buffer);
if (error)
{
Debug_pushMessage("deserializeJson() failed: %s\n", error.f_str());
@@ -405,18 +288,22 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
else
{
ConfigData.batteryType = (batteryType_t)doc["config"]["batteryType"].as<uint32_t>();
ConfigData.EEPROM_Version = doc["config"]["EEPROM_Version"].as<uint32_t>();
strncpy(ConfigData.Faction_1_Name, doc["config"]["Faction_1_Name"].as<String>().c_str(), sizeof(ConfigData.Faction_1_Name));
strncpy(ConfigData.Faction_2_Name, doc["config"]["Faction_2_Name"].as<String>().c_str(), sizeof(ConfigData.Faction_2_Name));
strncpy(ConfigData.Faction_3_Name, doc["config"]["Faction_3_Name"].as<String>().c_str(), sizeof(ConfigData.Faction_3_Name));
ConfigData.batteryType = (batteryType_t)json["config"]["batteryType"].as<int>();
ConfigData.active_faction_on_reboot = json["config"]["active_faction_on_reboot"].as<bool>();
strncpy(ConfigData.Faction_1_Name, json["config"]["Faction_1_Name"].as<const char *>(), sizeof(ConfigData.Faction_1_Name));
strncpy(ConfigData.Faction_2_Name, json["config"]["Faction_2_Name"].as<const char *>(), sizeof(ConfigData.Faction_2_Name));
strncpy(ConfigData.Faction_3_Name, json["config"]["Faction_3_Name"].as<const char *>(), sizeof(ConfigData.Faction_3_Name));
strncpy(ConfigData.wifi_ap_ssid, json["config"]["wifi_ap_ssid"].as<const char *>(), sizeof(ConfigData.wifi_ap_ssid));
strncpy(ConfigData.wifi_ap_password, json["config"]["wifi_ap_password"].as<const char *>(), sizeof(ConfigData.wifi_ap_password));
strncpy(ConfigData.wifi_client_ssid, json["config"]["wifi_client_ssid"].as<const char *>(), sizeof(ConfigData.wifi_client_ssid));
strncpy(ConfigData.wifi_client_password, json["config"]["wifi_client_password"].as<const char *>(), sizeof(ConfigData.wifi_client_password));
PersistenceData.writeCycleCounter = doc["persis"]["writeCycleCounter"].as<uint16_t>();
PersistenceData.activeFaction = (factions_t)doc["persis"]["activeFaction"].as<uint32_t>();
PersistenceData.faction_1_timer = doc["persis"]["faction_1_timer"].as<uint32_t>();
PersistenceData.faction_2_timer = doc["persis"]["faction_2_timer"].as<uint32_t>();
PersistenceData.faction_3_timer = doc["persis"]["faction_3_timer"].as<uint32_t>();
PersistenceData.checksum = doc["persis"]["checksum"].as<uint32_t>();
PersistenceData.writeCycleCounter = json["persis"]["writeCycleCounter"].as<uint16_t>();
PersistenceData.activeFaction = (Factions_t)json["persis"]["activeFaction"].as<int>();
PersistenceData.faction_1_timer = json["persis"]["faction_1_timer"].as<uint32_t>();
PersistenceData.faction_2_timer = json["persis"]["faction_2_timer"].as<uint32_t>();
PersistenceData.faction_3_timer = json["persis"]["faction_3_timer"].as<uint32_t>();
PersistenceData.checksum = json["persis"]["checksum"].as<uint32_t>();
ee_done = true;
}
@@ -431,55 +318,44 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
if (ee_done)
{
Debug_pushMessage("Update complete");
Debug_pushMessage("Update complete\n");
globals.systemStatus = sysStat_Shutdown;
}
else
{
}
}
}
/**
* @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");
JsonDocument json;
JsonObject info = json["info"].to<JsonObject>();
char buffer[16];
fwinfo["DeviceName"] = globals.DeviceName;
info["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;
info["FW-Version"] = buffer;
info["FS-Version"] = globals.FlashVersion;
snprintf_P(buffer, sizeof(buffer), "%s", constants.GitHash);
fwinfo["Git-Hash"] = buffer;
info["Git-Hash"] = buffer;
JsonObject config = json.createNestedObject("config");
config["EEPROM_Version"] = ConfigData.EEPROM_Version;
config["batteryType"] = ConfigData.batteryType;
config["Faction_1_Name"] = ConfigData.Faction_1_Name;
config["Faction_2_Name"] = ConfigData.Faction_2_Name;
config["Faction_3_Name"] = ConfigData.Faction_3_Name;
sprintf(buffer, "0x%08X", ConfigData.checksum);
config["checksum"] = buffer;
JsonObject eepart = json.createNestedObject("eepart");
JsonObject config = json["config"].to<JsonObject>();
generateJsonObject_ConfigData(config);
JsonObject persis = json["persis"].to<JsonObject>();
generateJsonObject_PersistenceData(persis);
JsonObject eepart = json["eepart"].to<JsonObject>();
sprintf(buffer, "0x%04X", globals.eePersistanceAdress);
eepart["PersistanceAddress"] = buffer;
JsonObject persis = json.createNestedObject("persis");
persis["writeCycleCounter"] = PersistenceData.writeCycleCounter;
persis["activeFaction"] = PersistenceData.activeFaction;
persis["faction_1_timer"] = PersistenceData.faction_1_timer;
persis["faction_2_timer"] = PersistenceData.faction_2_timer;
persis["faction_3_timer"] = PersistenceData.faction_3_timer;
sprintf(buffer, "0x%08X", PersistenceData.checksum);
persis["checksum"] = buffer;
serializeJsonPretty(json, *response);
response->addHeader("Content-disposition", "attachment; filename=backup.ee.json");
@@ -487,12 +363,28 @@ void WebServerEEJSON_Callback(AsyncWebServerRequest *request)
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());
@@ -506,31 +398,368 @@ void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *clien
}
}
/**
* @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("Websocket-Message (len: %d): %s\n", len, (char *)data);
Debug_pushMessage("Got WebSocket Message: %s \n", (char *)data);
if (strcmp((char *)data, "start") == 0)
if (strncmp((char *)data, "btn-", strlen("btn-")) == 0)
{
SetDebugportStatus(dbg_Webui, enabled);
Websocket_HandleButtons(data + strlen("btn-"));
}
else if (strcmp((char *)data, "stop") == 0)
else if (strncmp((char *)data, "set-", strlen("set-")) == 0)
{
SetDebugportStatus(dbg_Webui, disabled);
Websocket_HandleSettings(data + strlen("set-"));
}
else if (strcmp((char *)data, "foo") == 0)
else
{
Debug_pushMessage("Got WebSocket Message 'foo' from client\n");
Debug_pushMessage("Got unknown Websocket-Message '%s' from client\n", (char *)data);
}
}
}
/**
* @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, "settingssave") == 0)
{
globals.requestEEAction = EE_CFG_SAVE;
}
else if (strcmp(identifier, "reboot") == 0)
{
globals.systemStatus = sysStat_Shutdown;
}
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, "wifi-ssid") == 0)
{
strncpy(ConfigData.wifi_client_ssid, value, sizeof(ConfigData.wifi_client_ssid));
}
else if (strcmp(identifier, "wifi-password") == 0)
{
strncpy(ConfigData.wifi_client_password, value, sizeof(ConfigData.wifi_client_password));
}
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(Message + "\n");
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;"
"activefaction;"
"time_faction1;"
"time_faction2;"
"time_faction3;";
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.activeFaction) + ";");
temp.concat(String(PersistenceData.faction_1_timer) + ";");
temp.concat(String(PersistenceData.faction_2_timer) + ";");
temp.concat(String(PersistenceData.faction_3_timer) + ";");
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:"
"active_faction_on_reboot;"
"batteryType;"
"name_faction1;"
"name_faction2;"
"name_faction3;"
"wifi-ssid;"
"wifi-pass;";
if (client_id > 0)
webSocket.text(client_id, mapping);
else
webSocket.textAll(mapping);
}
String temp = "STATIC:";
temp.concat(String(ConfigData.active_faction_on_reboot) + ";");
temp.concat(String(ConfigData.batteryType) + ";");
temp.concat(String(ConfigData.Faction_1_Name) + ";");
temp.concat(String(ConfigData.Faction_2_Name) + ";");
temp.concat(String(ConfigData.Faction_3_Name) + ";");
temp.concat(String(ConfigData.wifi_client_ssid) + ";");
temp.concat(String(ConfigData.wifi_client_password) + ";");
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'; // Null-Terminierung sicherstellen
// 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'; // Null-Terminierung sicherstellen
}
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'; // Null-Terminierung sicherstellen
}
}
/**
* @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 (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;
}
webSocket.textAll("NOTIFY:" + typeString + ";" + Message);
Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString);
}