improved WebUI

This commit is contained in:
2025-08-12 14:21:53 +02:00
parent 546e4a1885
commit 8393f24ae2
3 changed files with 382 additions and 164 deletions

View File

@@ -12,6 +12,10 @@
*/
#include "webui.h"
#include "common.h"
#include <memory> // std::unique_ptr
#include <cstring> // strlen, strncpy, memcpy
#include <algorithm> // std::clamp
AsyncWebServer webServer(80);
@@ -19,6 +23,28 @@ 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);
@@ -38,6 +64,34 @@ 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.
*
@@ -116,6 +170,54 @@ void Webserver_Process()
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;
}
}
}
}
/**
@@ -165,14 +267,15 @@ 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
{ // failed to open the file, return empty result
buff[0] = '\0';
return;
}
if (this_file.available())
{
int bytes_read;
bytes_read = this_file.readBytesUntil('\r', buff, buff_size - 1);
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();
@@ -270,7 +373,7 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
}
}
if (buffer != NULL)
if (buffer != NULL && len > 0)
{
memcpy(buffer + read_ptr, data, len);
read_ptr = read_ptr + len;
@@ -280,6 +383,11 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
{
if (buffer != NULL)
{
// Ensure zero-termination just in case
if (read_ptr >= 1536)
read_ptr = 1535;
buffer[read_ptr] = '\0';
Serial.print(buffer);
JsonDocument json;
error = deserializeJson(json, buffer);
@@ -323,7 +431,11 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
}
}
free(buffer);
if (buffer)
{
free(buffer);
buffer = NULL;
}
AsyncWebServerResponse *response = request->beginResponse(302, "text/plain", "Please wait while the device reboots");
response->addHeader("Refresh", "20");
@@ -354,10 +466,10 @@ void WebServerEEJSON_Callback(AsyncWebServerRequest *request)
char buffer[16];
info["DeviceName"] = globals.DeviceName;
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"] = globals.FlashVersion;
info["FS-Version"] = nz(globals.FlashVersion);
snprintf_P(buffer, sizeof(buffer), "%s", constants.GitHash);
info["Git-Hash"] = buffer;
@@ -395,11 +507,16 @@ void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *clien
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());
{
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;
@@ -427,20 +544,24 @@ 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);
// Create a safe, null-terminated local copy
std::unique_ptr<char[]> buf(new char[len + 1]);
memcpy(buf.get(), data, len);
buf[len] = '\0';
if (strncmp((char *)data, "btn-", strlen("btn-")) == 0)
Debug_pushMessage("Websocket-Message (len: %d): %s\n", (int)len, buf.get());
if (strncmp(buf.get(), "btn-", 4) == 0)
{
Websocket_HandleButtons(data + strlen("btn-"));
Websocket_HandleButtons((uint8_t *)buf.get() + 4);
}
else if (strncmp((char *)data, "set-", strlen("set-")) == 0)
else if (strncmp(buf.get(), "set-", 4) == 0)
{
Websocket_HandleSettings(data + strlen("set-"));
Websocket_HandleSettings((uint8_t *)buf.get() + 4);
}
else
{
Debug_pushMessage("Got unknown Websocket-Message '%s' from client\n", (char *)data);
Debug_pushMessage("Got unknown Websocket-Message '%s' from client\n", buf.get());
}
}
}
@@ -527,18 +648,27 @@ void Websocket_HandleSettings(uint8_t *data)
}
else if (strcmp(identifier, "speedsource") == 0)
{
int index = findIndexByString(value, SpeedSourceString, SpeedSourceString_Elements);
speedsourcePreselect = (SpeedSource_t)index;
int index = findIndexByString(value, SpeedSourceString, (int)SpeedSourceString_Elements);
if (validIndex(index, (int)SpeedSourceString_Elements))
speedsourcePreselect = (SpeedSource_t)index;
else
Debug_pushMessage("Invalid speedsource '%s'\n", value);
}
else if (strcmp(identifier, "cansource") == 0)
{
int index = findIndexByString(value, CANSourceString, CANSourceString_Elements);
LubeConfig.CANSource = (CANSource_t)index;
int index = findIndexByString(value, CANSourceString, (int)CANSourceString_Elements);
if (validIndex(index, (int)CANSourceString_Elements))
LubeConfig.CANSource = (CANSource_t)index;
else
Debug_pushMessage("Invalid cansource '%s'\n", value);
}
else if (strcmp(identifier, "gpsbaud") == 0)
{
int index = findIndexByString(value, GPSBaudRateString, GPSBaudRateString_Elements);
LubeConfig.GPSBaudRate = (GPSBaudRate_t)index;
int index = findIndexByString(value, GPSBaudRateString, (int)GPSBaudRateString_Elements);
if (validIndex(index, (int)GPSBaudRateString_Elements))
LubeConfig.GPSBaudRate = (GPSBaudRate_t)index;
else
Debug_pushMessage("Invalid gpsbaud '%s'\n", value);
}
else if (strcmp(identifier, "ledmaxbrightness") == 0)
{
@@ -570,7 +700,7 @@ void Websocket_HandleSettings(uint8_t *data)
}
else if (strcmp(identifier, "ledmodeflash") == 0)
{
LubeConfig.LED_Mode_Flash = value[0] == '1' ? true : false;
LubeConfig.LED_Mode_Flash = value[0] == '1';
}
else if (strcmp(identifier, "wifi-ssid") == 0)
{
@@ -663,21 +793,22 @@ 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);
webSocket.text(client_id, kMappingStatus);
else
webSocket.textAll(mapping);
webSocket.textAll(kMappingStatus);
}
String temp = "STATUS:";
temp.concat(String(globals.systemStatustxt) + ";");
temp.concat(String((PersistenceData.tankRemain_microL / 10) / LubeConfig.tankCapacity_ml) + ";");
temp.concat(String(nz(globals.systemStatustxt)) + ";");
// 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)
@@ -702,80 +833,76 @@ void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
*/
void Websocket_RefreshClientData_Static(uint32_t client_id, bool send_mapping)
{
if (send_mapping)
{
const char mapping[] = "MAPPING_STATIC:"
"lubedistancenormal;"
"lubedistancerain;"
"washdistance;"
"washinterval;"
"tankcap;"
"pumppulse;"
"tankwarn;"
"pulserev;"
"tirewidth;"
"tireratio;"
"tiredia;"
"speedsource;"
"gpsbaud;"
"cansource;"
"ledmodeflash;"
"ledmaxbrightness;"
"ledminbrightness;"
"showimpulse;"
"showgps;"
"showcan;"
"bleedingpulses;"
"wifi-ssid;"
"wifi-pass;";
if (client_id > 0)
webSocket.text(client_id, mapping);
webSocket.text(client_id, kMappingStatic);
else
webSocket.textAll(mapping);
webSocket.textAll(kMappingStatic);
}
String temp = "STATIC:";
temp.concat(String(LubeConfig.DistancePerLube_Default) + ";");
temp.concat(String(LubeConfig.DistancePerLube_Rain) + ";");
temp.concat(String(LubeConfig.WashMode_Distance) + ";");
temp.concat(String(LubeConfig.WashMode_Interval) + ";");
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(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) + ";");
temp.concat(String(LubeConfig.wifi_client_ssid) + ";");
temp.concat(String(LubeConfig.wifi_client_password) + ";");
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) + ";";
for (uint32_t i = 0; i < SpeedSourceString_Elements; i++)
// speedsource + Optionen
temp += tableStr(SpeedSourceString, (int)LubeConfig.SpeedSource, (int)SpeedSourceString_Elements) + ";";
{
temp.concat(String(SpeedSourceString[i]) + ",");
String csv;
appendCsv(csv, SpeedSourceString, SpeedSourceString_Elements);
temp += csv + ";";
}
temp.concat(";");
// gpsbaud + Optionen
temp += tableStr(GPSBaudRateString, (int)LubeConfig.GPSBaudRate, (int)GPSBaudRateString_Elements) + ";";
{
String csv;
appendCsv(csv, GPSBaudRateString, GPSBaudRateString_Elements);
temp += csv + ";";
}
// cansource + Optionen
temp += tableStr(CANSourceString, (int)LubeConfig.CANSource, (int)CANSourceString_Elements) + ";";
{
String csv;
appendCsv(csv, CANSourceString, CANSourceString_Elements);
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);
}
}
/**
@@ -803,7 +930,7 @@ void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierB
{
// Kopieren des ersten Teils in den Buffer für Identifier
strncpy(identifierBuffer, token, identifierBufferSize - 1);
identifierBuffer[identifierBufferSize - 1] = '\0'; // Null-Terminierung sicherstellen
identifierBuffer[identifierBufferSize - 1] = '\0';
// Weitere Aufrufe von strtok, um den nächsten Teil zu erhalten
token = strtok(NULL, ":");
@@ -813,7 +940,7 @@ void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierB
{
// Kopieren des zweiten Teils in den Buffer für Value
strncpy(valueBuffer, token, valueBufferSize - 1);
valueBuffer[valueBufferSize - 1] = '\0'; // Null-Terminierung sicherstellen
valueBuffer[valueBufferSize - 1] = '\0';
}
else
{
@@ -828,7 +955,7 @@ void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierB
// Der gesamte String wird als Value betrachtet
strncpy(valueBuffer, data, valueBufferSize - 1);
valueBuffer[valueBufferSize - 1] = '\0'; // Null-Terminierung sicherstellen
valueBuffer[valueBufferSize - 1] = '\0';
}
}
@@ -850,7 +977,7 @@ int findIndexByString(const char *searchString, const char *const *array, int ar
// Durchlaufe das Array und vergleiche jeden String
for (int i = 0; i < arraySize; ++i)
{
if (strcmp(array[i], searchString) == 0)
if (array[i] && strcmp(array[i], searchString) == 0)
{
// String gefunden, gib den Index zurück
return i;
@@ -874,7 +1001,7 @@ int findIndexByString(const char *searchString, const char *const *array, int ar
*/
void Websocket_PushNotification(String Message, NotificationType_t type)
{
String typeString = "";
String typeString;
switch (type)
{
case info:
@@ -889,7 +1016,10 @@ void Websocket_PushNotification(String Message, NotificationType_t type)
case error:
typeString = "danger";
break;
default:
typeString = "info";
break;
}
webSocket.textAll("NOTIFY:" + typeString + ";" + Message);
Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString);
Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString.c_str());
}