From 6ac98b6d5388c5eeaa2cfc373061a07696865e29 Mon Sep 17 00:00:00 2001 From: lucas_nz <606314+lucasnz@users.noreply.github.com> Date: Mon, 20 Jul 2026 08:22:20 +1200 Subject: [PATCH 1/5] Updated comment --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index fa49589..1b0e194 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -438,7 +438,7 @@ void mqttHaAutoDiscovery() { generateTextAdJSON(output, ADConf, spa, discoveryTopic, "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}"); mqttClient.publish(discoveryTopic.c_str(), output.c_str(), true); - // Simply used to populate the select options for filtration hours 1 to 24 + // Simply used to populate the select options for days of week const std::array DaysOfWeekStrings = {"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"}; ADConf.displayName = "Day of Week"; ADConf.valueTemplate = "{{ value_json.status.dayOfWeek }}"; From febdb7fa54b65f04cd624116a30ada9f8dd5b495 Mon Sep 17 00:00:00 2001 From: lucas_nz <606314+lucasnz@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:40:14 +1200 Subject: [PATCH 2/5] Web debugging --- data/www/debug.htm | 178 +++++++ lib/Config/Config.h | 4 +- lib/MultiBlinker/MultiBlinker.h | 4 +- lib/SpaInterface/SpaInterface.h | 4 +- lib/SpaUtils/SpaUtils.h | 4 +- lib/WebRemoteDebug/WebRemoteDebug.h | 715 ++++++++++++++++++++++++++++ lib/WebRemoteDebug/library.json | 10 + lib/WebUI/WebUI.cpp | 195 +++++++- lib/WebUI/WebUI.h | 29 +- src/main.cpp | 15 +- 10 files changed, 1142 insertions(+), 16 deletions(-) create mode 100644 data/www/debug.htm create mode 100644 lib/WebRemoteDebug/WebRemoteDebug.h create mode 100644 lib/WebRemoteDebug/library.json diff --git a/data/www/debug.htm b/data/www/debug.htm new file mode 100644 index 0000000..4394397 --- /dev/null +++ b/data/www/debug.htm @@ -0,0 +1,178 @@ + + + + + + ESP32 Debug + + + + + +
+ + + + + + + + + + + + Connecting... +
+ +
+ + + + diff --git a/lib/Config/Config.h b/lib/Config/Config.h index 17f917b..962f3df 100644 --- a/lib/Config/Config.h +++ b/lib/Config/Config.h @@ -4,9 +4,9 @@ #include #include #include -#include +#include "WebRemoteDebug.h" -extern RemoteDebug Debug; +extern WebRemoteDebug Debug; template class Setting { diff --git a/lib/MultiBlinker/MultiBlinker.h b/lib/MultiBlinker/MultiBlinker.h index f292492..359371e 100644 --- a/lib/MultiBlinker/MultiBlinker.h +++ b/lib/MultiBlinker/MultiBlinker.h @@ -23,14 +23,14 @@ */ #include -#include +#include "WebRemoteDebug.h" #if defined(ESPA_V2) && defined(RGB_LED_PIN) #include #define USE_RGB_LED #endif -extern RemoteDebug Debug; +extern WebRemoteDebug Debug; // These are the four LEDs on the PCB (for ESPA_V1 and legacy boards) #if defined(ESPA_V1) diff --git a/lib/SpaInterface/SpaInterface.h b/lib/SpaInterface/SpaInterface.h index 7fa8452..d34d2bc 100644 --- a/lib/SpaInterface/SpaInterface.h +++ b/lib/SpaInterface/SpaInterface.h @@ -5,12 +5,12 @@ #include #include #include -#include +#include "WebRemoteDebug.h" #include #include -extern RemoteDebug Debug; +extern WebRemoteDebug Debug; #define FAILEDREADFREQUENCY 1000 //(ms) Frequency to retry on a failed read of the status registers. #define V2FIRMWARE_STRING "SW V2" // String to identify V2 firmware template diff --git a/lib/SpaUtils/SpaUtils.h b/lib/SpaUtils/SpaUtils.h index 24fcac9..0a09e28 100644 --- a/lib/SpaUtils/SpaUtils.h +++ b/lib/SpaUtils/SpaUtils.h @@ -3,7 +3,7 @@ #include #include -#include +#include "WebRemoteDebug.h" #include #include #include "SpaInterface.h" @@ -15,7 +15,7 @@ #define xstr(a) str(a) #define str(a) #a -extern RemoteDebug Debug; +extern WebRemoteDebug Debug; String convertToTime(int data); int convertToInteger(String &timeStr); diff --git a/lib/WebRemoteDebug/WebRemoteDebug.h b/lib/WebRemoteDebug/WebRemoteDebug.h new file mode 100644 index 0000000..964b291 --- /dev/null +++ b/lib/WebRemoteDebug/WebRemoteDebug.h @@ -0,0 +1,715 @@ +#ifndef WEB_REMOTE_DEBUG_H +#define WEB_REMOTE_DEBUG_H + +#include +#include +#include +#include + +class WebRemoteDebug : public Print { +public: + static constexpr uint8_t PROFILER = RemoteDebug::PROFILER; + static constexpr uint8_t VERBOSE = RemoteDebug::VERBOSE; + static constexpr uint8_t DEBUG = RemoteDebug::DEBUG; + static constexpr uint8_t INFO = RemoteDebug::INFO; + static constexpr uint8_t WARNING = RemoteDebug::WARNING; + static constexpr uint8_t ERROR = RemoteDebug::ERROR; + static constexpr uint8_t ANY = RemoteDebug::ANY; + + WebRemoteDebug() = default; + + bool begin( + const String& hostName, + uint8_t startingDebugLevel = RemoteDebug::DEBUG + ) { + _webLevel = startingDebugLevel; + return _remote.begin(hostName, startingDebugLevel); + } + + bool begin( + const String& hostName, + uint16_t telnetPort, + uint8_t startingDebugLevel = RemoteDebug::DEBUG + ) { + _webLevel = startingDebugLevel; + return _remote.begin( + hostName, + telnetPort, + startingDebugLevel + ); + } + + /** + * Attach an AsyncWebSocket owned elsewhere, normally by WebUI. + * + * The socket must remain alive for as long as this wrapper uses it. + */ + void attachWebSocket(AsyncWebSocket* webSocket) { + _webSocket = webSocket; + + /* + * Reserve these buffers once to reduce repeated heap allocations + * and fragmentation during bursts of verbose logging. + */ + _webLine.reserve(MAX_WEB_LINE_LENGTH); + _webBatch.reserve(MAX_WEB_BATCH_LENGTH); + } + + void detachWebSocket() { + flushWebSocketLine(); + sendPendingWebBatch(true); + + _webSocket = nullptr; + clearWebBuffers(); + } + + /** + * Call regularly from loop(). + */ + void handle() { + _remote.handle(); + + if (_webSocket == nullptr) { + return; + } + + sendPendingWebBatch(); + + const uint32_t now = millis(); + + if (now - _lastCleanup >= CLEANUP_INTERVAL_MS) { + _lastCleanup = now; + _webSocket->cleanupClients(); + } + } + + /** + * RemoteDebug's debug macros call isActive(level) before printf(). + * + * The result is remembered so the following write() calls can route + * the formatted output independently to RemoteDebug and WebSocket. + */ + bool isActive(uint8_t level = RemoteDebug::DEBUG) { + _levelCheckPending = true; + _currentLevel = level; + + _currentRemoteActive = _remote.isActive(level); + _currentWebActive = + hasWebClients() && + levelIsEnabled(level, _webLevel) && + !_webSilenced; + + return _currentRemoteActive || _currentWebActive; + } + + size_t write(uint8_t value) override { + return write(&value, 1); + } + + size_t write( + const uint8_t* buffer, + size_t size + ) override { + if (buffer == nullptr || size == 0) { + return 0; + } + + /* + * Direct calls such as Debug.println() do not call isActive() first, + * so they are treated as unconditional output. + */ + const bool sendRemote = _levelCheckPending + ? _currentRemoteActive + : true; + + const bool sendWeb = _levelCheckPending + ? _currentWebActive + : (hasWebClients() && !_webSilenced); + + if (sendRemote) { + _remote.write(buffer, size); + } + + if (sendWeb) { + appendWebSocketData(buffer, size); + } + + /* + * A newline completes the current logging operation and resets the + * routing state for the next message. + */ + for (size_t i = 0; i < size; ++i) { + if (buffer[i] == '\n') { + resetRoutingState(); + } + } + + return size; + } + + void flush() override { + flushWebSocketLine(); + sendPendingWebBatch(true); + _remote.flush(); + } + + void setWebDebugLevel(uint8_t level) { + if (level <= RemoteDebug::ANY) { + _webLevel = level; + } + } + + uint8_t getWebDebugLevel() const { + return _webLevel; + } + + void setWebSilenced(bool silenced) { + _webSilenced = silenced; + + if (silenced) { + clearWebBuffers(); + } + } + + bool isWebSilenced() const { + return _webSilenced; + } + + size_t webClientCount() const { + if (_webSocket == nullptr) { + return 0; + } + + return _webSocket->count(); + } + + bool hasWebClients() const { + return webClientCount() > 0; + } + + size_t webBatchLength() const { + return _webBatch.length(); + } + + size_t webBatchCapacity() const { + return MAX_WEB_BATCH_LENGTH; + } + + uint32_t droppedWebLines() const { + return _droppedWebLines; + } + + uint32_t droppedWebBytes() const { + return _droppedWebBytes; + } + + /** + * Send a message to all connected WebSocket clients. + * + * This bypasses the debug-level filter, but still respects WebSocket + * silencing. + */ + void sendWebMessage(const String& message) { + if ( + _webSocket == nullptr || + !hasWebClients() || + _webSilenced + ) { + return; + } + + _webSocket->textAll(message); + } + + /** + * Send a message to one WebSocket client. + * + * This is intended for command acknowledgements and therefore does not + * respect global WebSocket silencing. + */ + void sendWebMessage( + AsyncWebSocketClient* client, + const String& message + ) { + if (client != nullptr) { + client->text(message); + } + } + + /* + * Common RemoteDebug API forwarding methods. + */ + + void setPassword(const String& password) { + _remote.setPassword(password); + } + + void setSerialEnabled(bool enabled) { + _remote.setSerialEnabled(enabled); + } + + void setResetCmdEnabled(bool enabled) { + _remote.setResetCmdEnabled(enabled); + } + + void setHelpProjectsCmds(const String& help) { + _remote.setHelpProjectsCmds(help); + } + + void setCallBackProjectCmds(void (*callback)()) { + _remote.setCallBackProjectCmds(callback); + } + + void setCallBackNewClient(void (*callback)()) { + _remote.setCallBackNewClient(callback); + } + + String getLastCommand() { + return _remote.getLastCommand(); + } + + void clearLastCommand() { + _remote.clearLastCommand(); + } + + void showTime(bool enabled) { + _remote.showTime(enabled); + } + + void showProfiler( + bool enabled, + uint32_t minimumTime = 0 + ) { + _remote.showProfiler(enabled, minimumTime); + } + + void showDebugLevel(bool enabled) { + _remote.showDebugLevel(enabled); + } + + void showColors(bool enabled) { + _remote.showColors(enabled); + } + + void showRaw(bool enabled) { + _remote.showRaw(enabled); + } + + void setFilter(const String& filter) { + _remote.setFilter(filter); + } + + void setNoFilter() { + _remote.setNoFilter(); + } + + void silence( + bool enabled, + bool showMessage = true, + bool fromBreak = false, + uint32_t timeout = 0 + ) { + _remote.silence( + enabled, + showMessage, + fromBreak, + timeout + ); + + setWebSilenced(enabled); + } + + bool isSilence() { + return _remote.isSilence() && _webSilenced; + } + + bool isConnected() { + return _remote.isConnected() || hasWebClients(); + } + + void disconnect(bool onlyTelnetClient = false) { + _remote.disconnect(onlyTelnetClient); + + if ( + !onlyTelnetClient && + _webSocket != nullptr + ) { + _webSocket->closeAll(); + clearWebBuffers(); + } + } + + void stop() { + flushWebSocketLine(); + sendPendingWebBatch(true); + + if (_webSocket != nullptr) { + _webSocket->closeAll(); + } + + clearWebBuffers(); + _remote.stop(); + } + + RemoteDebug& remote() { + return _remote; + } + + const RemoteDebug& remote() const { + return _remote; + } + + static int parseLevel(String level) { + level.trim(); + level.toLowerCase(); + + if (level == "profiler") { + return RemoteDebug::PROFILER; + } + + if (level == "verbose") { + return RemoteDebug::VERBOSE; + } + + if (level == "debug") { + return RemoteDebug::DEBUG; + } + + if (level == "info") { + return RemoteDebug::INFO; + } + + if (level == "warning" || level == "warn") { + return RemoteDebug::WARNING; + } + + if (level == "error") { + return RemoteDebug::ERROR; + } + + if (level == "any") { + return RemoteDebug::ANY; + } + + return -1; + } + + static const char* levelName(uint8_t level) { + switch (level) { + case RemoteDebug::PROFILER: + return "profiler"; + + case RemoteDebug::VERBOSE: + return "verbose"; + + case RemoteDebug::DEBUG: + return "debug"; + + case RemoteDebug::INFO: + return "info"; + + case RemoteDebug::WARNING: + return "warning"; + + case RemoteDebug::ERROR: + return "error"; + + case RemoteDebug::ANY: + return "any"; + + default: + return "unknown"; + } + } + + void showWebDebugLevel(bool enabled) { + _webShowDebugLevel = enabled; + } + + void showWebProfiler(bool enabled) { + _webShowProfiler = enabled; + + /* + * Restart the elapsed-time measurement when enabling it. + */ + if (enabled) { + _lastWebLogTime = millis(); + } + } + +private: + static constexpr size_t MAX_WEB_LINE_LENGTH = 1024; + static constexpr size_t MAX_WEB_BATCH_LENGTH = 16384; + + static constexpr uint32_t WEB_BATCH_INTERVAL_MS = 25; + static constexpr uint32_t CLEANUP_INTERVAL_MS = 1000; + + RemoteDebug _remote; + AsyncWebSocket* _webSocket = nullptr; + + uint8_t _webLevel = RemoteDebug::DEBUG; + + bool _webSilenced = false; + bool _levelCheckPending = false; + bool _currentRemoteActive = false; + bool _currentWebActive = false; + + String _webLine; + String _webBatch; + + uint32_t _lastWebBatchSend = 0; + uint32_t _lastCleanup = 0; + + uint32_t _droppedWebLines = 0; + uint32_t _droppedWebBytes = 0; + + uint8_t _currentLevel = RemoteDebug::ANY; + + uint32_t _lastWebLogTime = 0; + + bool _webShowDebugLevel = true; + bool _webShowProfiler = true; + + void appendWebSocketData( + const uint8_t* buffer, + size_t size + ) { + for (size_t i = 0; i < size; ++i) { + const char character = + static_cast(buffer[i]); + + if (character == '\r') { + continue; + } + + if (character == '\n') { + flushWebSocketLine(); + continue; + } + + /* + * Bound individual line growth. If one log line exceeds the + * limit, flush it as a partial line and continue. + */ + if (_webLine.length() >= MAX_WEB_LINE_LENGTH) { + flushWebSocketLine(); + } + + _webLine += character; + } + } + + void flushWebSocketLine() { + if (_webLine.isEmpty()) { + return; + } + + String formattedLine; + formattedLine.reserve( + _webLine.length() + 24 + ); + + formattedLine = makeWebPrefix(_currentLevel); + formattedLine += _webLine; + + const size_t lineLength = + formattedLine.length() + 1; + + const size_t requiredLength = + _webBatch.length() + lineLength; + + if (requiredLength <= MAX_WEB_BATCH_LENGTH) { + _webBatch += formattedLine; + _webBatch += '\n'; + } else { + ++_droppedWebLines; + _droppedWebBytes += lineLength; + } + + _webLine = ""; + } + + void sendPendingWebBatch(bool force = false) { + if (_webSocket == nullptr) { + clearWebBuffers(); + return; + } + + if (!hasWebClients() || _webSilenced) { + clearWebBuffers(); + return; + } + + const uint32_t now = millis(); + + if ( + !force && + now - _lastWebBatchSend < WEB_BATCH_INTERVAL_MS + ) { + return; + } + + if (_webBatch.isEmpty() && _droppedWebLines == 0) { + return; + } + + /* + * Send the accumulated log batch first. + */ + if (!_webBatch.isEmpty()) { + _webSocket->textAll(_webBatch); + _webBatch = ""; + } + + /* + * Report dropped output separately so the warning cannot itself be + * prevented by a full log batch. + */ + if (_droppedWebLines > 0) { + String droppedMessage; + droppedMessage.reserve(112); + + droppedMessage = "[Web debug dropped "; + droppedMessage += String(_droppedWebLines); + droppedMessage += " lines / "; + droppedMessage += String(_droppedWebBytes); + droppedMessage += + " bytes because the output buffer was full]"; + + _webSocket->textAll(droppedMessage); + + _droppedWebLines = 0; + _droppedWebBytes = 0; + } + + _lastWebBatchSend = now; + } + + void clearWebBuffers() { + _webLine = ""; + _webBatch = ""; + _droppedWebLines = 0; + _droppedWebBytes = 0; + } + + void resetRoutingState() { + _levelCheckPending = false; + _currentRemoteActive = false; + _currentWebActive = false; + _currentLevel = RemoteDebug::ANY; + } + + static bool levelIsEnabled( + uint8_t messageLevel, + uint8_t configuredLevel + ) { + /* + * RemoteDebug treats ANY and ERROR as always visible. + */ + if ( + messageLevel == RemoteDebug::ANY || + messageLevel == RemoteDebug::ERROR + ) { + return true; + } + + if (messageLevel == RemoteDebug::PROFILER) { + return configuredLevel == RemoteDebug::PROFILER; + } + + /* + * RemoteDebug levels become less verbose as the numeric value rises: + * + * VERBOSE=1, DEBUG=2, INFO=3, WARNING=4, ERROR=5 + */ + return messageLevel >= configuredLevel; + } + + static char levelCharacter(uint8_t level) { + switch (level) { + case RemoteDebug::PROFILER: + return 'P'; + + case RemoteDebug::VERBOSE: + return 'V'; + + case RemoteDebug::DEBUG: + return 'D'; + + case RemoteDebug::INFO: + return 'I'; + + case RemoteDebug::WARNING: + return 'W'; + + case RemoteDebug::ERROR: + return 'E'; + + case RemoteDebug::ANY: + default: + return 'A'; + } + } + + String makeWebPrefix(uint8_t level) { + if (!_webShowDebugLevel && !_webShowProfiler) { + return ""; + } + + String prefix; + prefix.reserve(24); + + prefix += '('; + + if (_webShowDebugLevel) { + prefix += levelCharacter(level); + } + + if (_webShowDebugLevel && _webShowProfiler) { + prefix += ' '; + } + + if (_webShowProfiler) { + const uint32_t now = millis(); + + uint32_t elapsed = 0; + + if (_lastWebLogTime != 0) { + elapsed = now - _lastWebLogTime; + } + + _lastWebLogTime = now; + + prefix += "p:^"; + + /* + * Match RemoteDebug's four-digit minimum formatting: + * + * 1 -> 0001 + * 52 -> 0052 + * 830 -> 0830 + * 1002 -> 1002 + */ + if (elapsed < 1000) { + prefix += '0'; + } + + if (elapsed < 100) { + prefix += '0'; + } + + if (elapsed < 10) { + prefix += '0'; + } + + prefix += String(elapsed); + prefix += "ms"; + } + + prefix += ") "; + + return prefix; + } +}; + +#endif // WEB_REMOTE_DEBUG_H diff --git a/lib/WebRemoteDebug/library.json b/lib/WebRemoteDebug/library.json new file mode 100644 index 0000000..f37c0ae --- /dev/null +++ b/lib/WebRemoteDebug/library.json @@ -0,0 +1,10 @@ +{ + "name": "WebRemoteDebug", + "version": "1.0.0", + "frameworks": "arduino", + "platforms": "espressif32", + "dependencies": { + "RemoteDebug": "*", + "ESPAsyncWebServer": "*" + } +} diff --git a/lib/WebUI/WebUI.cpp b/lib/WebUI/WebUI.cpp index cafe2cf..e901af7 100644 --- a/lib/WebUI/WebUI.cpp +++ b/lib/WebUI/WebUI.cpp @@ -11,6 +11,8 @@ const char * WebUI::getError() { } void WebUI::begin() { + configureDebugWebSocket(); + server.on("/reboot", HTTP_GET, [&](AsyncWebServerRequest *request) { debugD("uri: %s", request->url().c_str()); @@ -170,10 +172,201 @@ void WebUI::begin() { request->send(response); }); + server.on("/debug", HTTP_GET, [&](AsyncWebServerRequest *request) { + debugD("uri: %s", request->url().c_str()); + request->send(SPIFFS, "/www/debug.htm"); + }); + // As a fallback we try to load from /www any requested URL server.serveStatic("/", SPIFFS, "/www/"); server.begin(); initialised = true; -} \ No newline at end of file +} + +void WebUI::configureDebugWebSocket() { + /* + * Give WebRemoteDebug access to the WebSocket, without giving it + * ownership of the web server. + */ + Debug.attachWebSocket(&_debugSocket); + + _debugSocket.onEvent( + [this]( + AsyncWebSocket* server, + AsyncWebSocketClient* client, + AwsEventType type, + void* arg, + uint8_t* data, + size_t len + ) { + handleDebugWebSocketEvent( + server, + client, + type, + arg, + data, + len + ); + } + ); + + server.addHandler(&_debugSocket); + +} + +void WebUI::handleDebugWebSocketEvent( + AsyncWebSocket* server, + AsyncWebSocketClient* client, + AwsEventType type, + void* arg, + uint8_t* data, + size_t len +) { + (void)server; + + switch (type) { + case WS_EVT_CONNECT: { + String message = + "Connected to ESP32 debug WebSocket; level="; + + message += WebRemoteDebug::levelName( + Debug.getWebDebugLevel() + ); + + message += ". Send 'help' for commands."; + + client->text(message); + break; + } + + case WS_EVT_DATA: + handleDebugWebSocketData( + client, + static_cast(arg), + data, + len + ); + break; + + case WS_EVT_DISCONNECT: + case WS_EVT_PONG: + case WS_EVT_ERROR: + default: + break; + } +} + +void WebUI::handleDebugWebSocketData( + AsyncWebSocketClient* client, + AwsFrameInfo* info, + uint8_t* data, + size_t len +) { + if ( + info == nullptr || + !info->final || + info->index != 0 || + info->len != len || + info->opcode != WS_TEXT + ) { + return; + } + + String command; + command.reserve(len); + + for (size_t i = 0; i < len; ++i) { + command += static_cast(data[i]); + } + + command.trim(); + + if (!processDebugCommand(command, client)) { + client->text( + "Unknown command: " + command + ); + } +} + +bool WebUI::processDebugCommand( + const String& command, + AsyncWebSocketClient* client +) { + String normalisedCommand = command; + normalisedCommand.trim(); + normalisedCommand.toLowerCase(); + + if (normalisedCommand == "help") { + client->text( + "Commands: help, status, level verbose, " + "level debug, level info, level warning, " + "level error, level any, silence, resume" + ); + + return true; + } + + if (normalisedCommand == "status") { + String response = "WebSocket clients="; + response += String(Debug.webClientCount()); + response += ", level="; + response += WebRemoteDebug::levelName( + Debug.getWebDebugLevel() + ); + response += ", silenced="; + response += Debug.isWebSilenced() + ? "yes" + : "no"; + + client->text(response); + return true; + } + + if (normalisedCommand == "silence") { + /* + * Send the acknowledgement before silencing. + */ + client->text("WebSocket logging silenced"); + Debug.setWebSilenced(true); + return true; + } + + if (normalisedCommand == "resume") { + Debug.setWebSilenced(false); + client->text("WebSocket logging resumed"); + return true; + } + + if (normalisedCommand.startsWith("level ")) { + String requestedLevel = + normalisedCommand.substring(6); + + requestedLevel.trim(); + + const int parsedLevel = + WebRemoteDebug::parseLevel(requestedLevel); + + if (parsedLevel < 0) { + client->text("Unknown debug level"); + return true; + } + + Debug.setWebDebugLevel( + static_cast(parsedLevel) + ); + + String response = + "WebSocket debug level set to "; + + response += WebRemoteDebug::levelName( + Debug.getWebDebugLevel() + ); + + client->text(response); + return true; + } + + return false; +} diff --git a/lib/WebUI/WebUI.h b/lib/WebUI/WebUI.h index 7c5178e..c2f3f93 100644 --- a/lib/WebUI/WebUI.h +++ b/lib/WebUI/WebUI.h @@ -4,14 +4,14 @@ #include #include #include +#include #include "SpaInterface.h" #include "SpaUtils.h" #include "Config.h" #include "MQTTClientWrapper.h" -#include "ESPAsyncWebServer.h" -extern RemoteDebug Debug; +extern WebRemoteDebug Debug; class WebUI { public: @@ -35,11 +35,36 @@ class WebUI { SpaInterface *_spa; Config *_config; MQTTClientWrapper *_mqttClient; + AsyncWebSocket _debugSocket{"/debug/ws"}; + void (*_wifiManagerCallback)() = nullptr; void (*_setSpaCallback)(const String, const String) = nullptr; const char* getError(); + void configureDebugWebSocket(); + + void handleDebugWebSocketEvent( + AsyncWebSocket* server, + AsyncWebSocketClient* client, + AwsEventType type, + void* arg, + uint8_t* data, + size_t len + ); + + void handleDebugWebSocketData( + AsyncWebSocketClient* client, + AwsFrameInfo* info, + uint8_t* data, + size_t len + ); + + bool processDebugCommand( + const String& command, + AsyncWebSocketClient* client + ); + // hard-coded FOTA page in case file system gets wiped static constexpr const char *fotaPage PROGMEM = R"( diff --git a/src/main.cpp b/src/main.cpp index 1b0e194..be323fc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include "WebRemoteDebug.h" #include #include #include @@ -16,10 +16,10 @@ #include "SpaUtils.h" #include "HAAutoDiscovery.h" #include "MQTTClientWrapper.h" - +#include "ESPAsyncWebServer.h" unsigned long bootStartMillis; // To track when the device started -RemoteDebug Debug; +WebRemoteDebug Debug; SpaInterface si; Config config; @@ -920,9 +920,14 @@ void setup() { } } - Debug.begin(WiFi.getHostname()); // Hostname seems to be for display purposes only, no functional impact. + Debug.begin(WiFi.getHostname(), WebRemoteDebug::DEBUG); // Hostname seems to be for display purposes only, no functional impact. Debug.setResetCmdEnabled(true); // This seems to be not needed to be in Setup. - Debug.showProfiler(true); // This seems to be not needed to be in Setup. + Debug.showTime(true); + Debug.showProfiler(true); + Debug.showDebugLevel(true); + + Debug.showWebProfiler(true); + Debug.showWebDebugLevel(true); mqttClient.setServer(config.MqttServer.getValue(), config.MqttPort.getValue()); mqttClient.setCallback(mqttCallback); From 79b3b80143da794736c75332e3eab13a8775990e Mon Sep 17 00:00:00 2001 From: lucas_nz <606314+lucasnz@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:18:07 +1200 Subject: [PATCH 3/5] Fix mqttLastConnect check --- lib/SpaInterface/SpaInterface.cpp | 2 +- src/main.cpp | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/SpaInterface/SpaInterface.cpp b/lib/SpaInterface/SpaInterface.cpp index f8a13e9..24097ec 100644 --- a/lib/SpaInterface/SpaInterface.cpp +++ b/lib/SpaInterface/SpaInterface.cpp @@ -981,7 +981,7 @@ void SpaInterface::loop(){ } if ( _lastWaitMessage + 1000 < millis()) { - debugV("Waiting..."); + debugV("Waiting... %u ms", millis()); _lastWaitMessage = millis(); } diff --git a/src/main.cpp b/src/main.cpp index be323fc..65615c9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,7 +42,7 @@ WebUI ui(&si, &config, &mqttClient); bool WMsaveConfig = false; -ulong mqttLastConnect = 0; +ulong mqttLastConnect = millis(); ulong wifiLastConnect = millis(); ulong bootTime = millis(); ulong statusLastPublish = millis(); @@ -1023,12 +1023,11 @@ void loop() { */ if (!mqttClient.connected()) { // MQTT broker reconnect if not connected - long now=millis(); - if (now - mqttLastConnect > 1000) { + if (millis() - mqttLastConnect > 1000) { blinker.setState(STATE_MQTT_NOT_CONNECTED); debugW("MQTT not connected, attempting connection to %s:%i", config.MqttServer.getValue(), config.MqttPort.getValue()); - mqttLastConnect = now; + mqttLastConnect = millis(); /* String macAddress = WiFi.macAddress(); macAddress.replace(':', 'X'); // Replace colons with 'X' to avoid issues with MQTT topic names From 6cd72bee8d201c39c5ff11845fb41ee8ab097dba Mon Sep 17 00:00:00 2001 From: lucas_nz <606314+lucasnz@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:17:04 +1200 Subject: [PATCH 4/5] WenUI debug logs updates --- data/www/debug.htm | 6 +++--- data/www/index.htm | 7 +++++++ lib/WebRemoteDebug/WebRemoteDebug.h | 12 ++++++------ lib/WebUI/WebUI.cpp | 27 ++++++++++++++++++--------- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/data/www/debug.htm b/data/www/debug.htm index 4394397..217f393 100644 --- a/data/www/debug.htm +++ b/data/www/debug.htm @@ -55,7 +55,7 @@ - + { - sendCommand("resume"); + document.getElementById("reboot").onclick = () => { + sendCommand("reboot"); }; document.getElementById("send").onclick = () => { diff --git a/data/www/index.htm b/data/www/index.htm index 5c7c8ea..f53bd9c 100644 --- a/data/www/index.htm +++ b/data/www/index.htm @@ -49,6 +49,7 @@ @@ -257,6 +258,12 @@ Loading... +
+ + +