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.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/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..7818605
--- /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" || level == "v") {
+ return RemoteDebug::VERBOSE;
+ }
+
+ if (level == "debug" || level == "d") {
+ return RemoteDebug::DEBUG;
+ }
+
+ if (level == "info" || level == "i") {
+ return RemoteDebug::INFO;
+ }
+
+ if (level == "warning" || level == "warn" || level == "w") {
+ return RemoteDebug::WARNING;
+ }
+
+ if (level == "error" || level == "e") {
+ return RemoteDebug::ERROR;
+ }
+
+ if (level == "any" || level == "a") {
+ 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..a1a1fea 100644
--- a/lib/WebUI/WebUI.cpp
+++ b/lib/WebUI/WebUI.cpp
@@ -11,22 +11,19 @@ const char * WebUI::getError() {
}
void WebUI::begin() {
+ configureDebugWebSocket();
+
server.on("/reboot", HTTP_GET, [&](AsyncWebServerRequest *request) {
debugD("uri: %s", request->url().c_str());
- if (_setSpaCallback != nullptr) {
- _setSpaCallback("reboot", "200");
- request->send(200, "text/html", "Called setSpaCallback for reboot...");
- } else {
- AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "Rebooting ESP...");
- response->addHeader("Connection", "close");
- request->send(response);
- debugD("Rebooting...");
- delay(200);
- request->client()->setNoDelay(true);
- request->client()->stop();
- ESP.restart();
- }
+ AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "Rebooting ESP...");
+ response->addHeader("Connection", "close");
+ request->client()->setNoDelay(true);
+ request->send(response);
+ request->client()->stop();
+ debugD("Rebooting...");
+ delay(200);
+ ESP.restart();
});
server.on("/fota", HTTP_GET, [&](AsyncWebServerRequest *request) {
@@ -170,10 +167,210 @@ 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, reboot"
+ );
+
+ 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" || normalisedCommand == "s") {
+ /*
+ * Send the acknowledgement before silencing.
+ */
+ if (Debug.isWebSilenced()) {
+ client->text("WebSocket logging resumed");
+ } else {
+ client->text("WebSocket logging silenced");
+ }
+ Debug.setWebSilenced(!Debug.isWebSilenced());
+ return true;
+ }
+
+ if (normalisedCommand == "reboot") {
+ client->text("Rebooting ESP32...");
+ delay(200);
+ ESP.restart();
+ return true;
+ }
+
+ if (normalisedCommand.startsWith("level ") || normalisedCommand.length() == 1) {
+ String requestedLevel;
+ if (normalisedCommand.length() > 1) {
+ requestedLevel = normalisedCommand.substring(6);
+
+ requestedLevel.trim();
+ } else {
+ requestedLevel = normalisedCommand;
+ }
+
+ 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 fa49589..797245d 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;
@@ -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();
@@ -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 }}";
@@ -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);
@@ -955,16 +960,9 @@ void loop() {
Debug.handle();
if (setSpaCallbackReady) {
- if (spaCallbackProperty == "reboot") {
- debugI("Rebooting ESP after %d ms", spaCallbackValue.toInt());
- delay(spaCallbackValue.toInt()); // Wait for the specified time before rebooting
- WiFi.disconnect(true); // Force teardown of all socket connections
- ESP.restart();
- } else {
- debugD("Setting Spa Properties...");
- setSpaCallbackReady = false;
- setSpaProperty(spaCallbackProperty, spaCallbackValue);
- }
+ debugD("Setting Spa Properties...");
+ setSpaCallbackReady = false;
+ setSpaProperty(spaCallbackProperty, spaCallbackValue);
}
if (WiFi.status() != WL_CONNECTED) {
@@ -1018,12 +1016,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