From 3e69485696fe360b18410d5017a142f7e7aef046 Mon Sep 17 00:00:00 2001 From: Marcel Peterkau Date: Sun, 24 Aug 2025 16:55:23 +0200 Subject: [PATCH] reworked the debugger --- Software/src/debugger.cpp | 1324 ++++++++++++++++++++++--------------- 1 file changed, 802 insertions(+), 522 deletions(-) diff --git a/Software/src/debugger.cpp b/Software/src/debugger.cpp index 61dbb29..71e1fa8 100644 --- a/Software/src/debugger.cpp +++ b/Software/src/debugger.cpp @@ -8,373 +8,284 @@ * Diagnostic Trouble Codes (DTCs), and more. * * @author Marcel Peterkau - * @date 09.04.2024 + * @date 24.08.2025 */ #include "debugger.h" +#include "common.h" +#include "globals.h" + #include -#include #include +#include #include -void processCmdDebug(String command); -void Debug_formatCFG(); -void Debug_formatPersistence(); -void Debug_printSystemInfo(); -void Debug_printWifiInfo(); -void Debug_CheckEEPOM(bool autocorrect); +void Debug_log(LogLevel, const char *format, ...); +const char *uint32_to_binary_string(uint32_t num); + +// ------------------------------------------------------------------- +// Forward declarations for functions used by command handlers +// (Implementations are further below) +void Debug_CheckEEPROM(bool autocorrect); void Debug_dumpConfig(); void Debug_dumpPersistence(); void Debug_ShowDTCs(); void Debug_dumpGlobals(); -void Debug_printHelp(); -void Debug_Purge(); +void Debug_PurgeN(uint16_t pulses); void Debug_refillTank(); -const char *uint32_to_binary_string(uint32_t num); -template -void RegisterDebugPrintAuto(const T* ptr, const String& name, uint32_t interval_ms, uint32_t duration_ms); void Debug_UpdateWatches(); +void processCmdDebug(String command); -/** - * @brief Initializes the debugger by setting the initial status for different debug ports. - * Serial debug output is turned off. - */ -void initDebugger() +// ------------------------------------------------------------------- +// Helpers +static inline uint8_t clamp_u8(int v, int lo, int hi) { return (uint8_t)(v < lo ? lo : (v > hi ? hi : v)); } +static inline uint16_t clamp_u16(int v, int lo, int hi) { return (uint16_t)(v < lo ? lo : (v > hi ? hi : v)); } +static inline uint32_t clamp_u32(int v, int lo, int hi) { return (uint32_t)(v < lo ? lo : (v > hi ? hi : v)); } + +static int parseIndexOrName(const String &token, const char *const *table, int count) { - // Set the initial status of debug ports - DebuggerStatus[dbg_Serial] = disabled; - DebuggerStatus[dbg_Webui] = disabled; - - // Disable serial debug output - Serial.setDebugOutput(false); -} -/** - * @brief Processes incoming debug commands from the Serial interface. - * It reads characters from Serial and interprets them as commands. - * The recognized commands are processed accordingly. - */ -void Debug_Process() -{ - // Enumeration for tracking the state of input processing - typedef enum InputProcessed_e + if (token.length() == 0) + return -1; + bool numeric = true; + for (size_t i = 0; i < token.length(); ++i) { - IDLE, ///< No command processing is in progress - CMD_COMPLETE, ///< Received a complete command - CMD_ABORT, ///< Received an abort command (Esc) - CMD_OVERFLOW ///< Input buffer overflow occurred - } InputProcessed_t; - - static unsigned int inputCnt = 0; ///< Counter for characters in the input buffer - static char inputBuffer[32]; ///< Buffer to store the received characters - InputProcessed_t InputProcessed = IDLE; ///< State variable for input processing - - // Check if there are characters available in the Serial input buffer - if (Serial.available()) - { - char inputChar = Serial.read(); - - // Process the received character based on its value - switch (inputChar) + char c = token.charAt(i); + if (c < '0' || c > '9') { - case '\n': - inputBuffer[inputCnt] = 0; // terminate the String - inputCnt = 0; - InputProcessed = CMD_COMPLETE; - Serial.write(inputChar); - break; - - case 0x1B: // Esc - inputBuffer[0] = 0; - inputCnt = 0; - InputProcessed = CMD_ABORT; - break; - - case 0x21 ... 0x7E: // it's a real letter or sign and not some control-chars - inputBuffer[inputCnt] = inputChar; - inputCnt++; - Serial.write(inputChar); - break; - - default: + numeric = false; break; } - - // Check for input buffer overflow - if (inputCnt >= sizeof(inputBuffer) - 1) - { - inputBuffer[sizeof(inputBuffer) - 1] = '\0'; - inputCnt = 0; - InputProcessed = CMD_OVERFLOW; - } } - - // Process the command based on the detected state of input processing - switch (InputProcessed) + if (numeric) { - case CMD_ABORT: - Debug_pushMessage("Abort\n"); - break; - - case CMD_COMPLETE: - processCmdDebug(String(inputBuffer)); - break; - - case CMD_OVERFLOW: - Debug_pushMessage("Input buffer overflow\n"); - break; - - default: - break; + int idx = token.toInt(); + return (idx >= 0 && idx < count) ? idx : -1; } - - if (InputProcessed != IDLE) - Serial.print(">"); - - InputProcessed = IDLE; - - Debug_UpdateWatches(); - -} - -/** - * @brief Sets the status of a specific debug port (Serial or WebUI). - * Updates the status in the DebuggerStatus array and provides debug messages. - * - * @param port The debug port to set the status for (dbg_Serial or dbg_Webui). - * @param status The status to set (enabled or disabled). - */ -void SetDebugportStatus(DebugPorts_t port, DebugStatus_t status) -{ - // Display a debug message based on the provided status - if (status == disabled) - Debug_pushMessage("Disable DebugPort %s\n", sDebugPorts[port]); - - // Update the status in the DebuggerStatus array - DebuggerStatus[port] = status; - - // Display a debug message based on the updated status - if (status == enabled) - Debug_pushMessage("Enabled DebugPort %s\n", sDebugPorts[port]); -} - -void Debug_log(LogLevel level, const char *format, ...) -{ - if ((DebuggerStatus[dbg_Serial] == enabled) || (DebuggerStatus[dbg_Webui] == enabled)) + for (int i = 0; i < count; ++i) { - char buff[128]; - va_list arg; - va_start(arg, format); - vsnprintf(buff, sizeof(buff), format, arg); - va_end(arg); - - if (DebuggerStatus[dbg_Serial] == enabled) - { - Serial.print(buff); - } - if (DebuggerStatus[dbg_Webui] == enabled) - { - Websocket_PushLiveDebug(String(buff)); - } + if (table[i] && token.equalsIgnoreCase(table[i])) + return i; } + return -1; } -/** - * @brief Pushes a formatted debug message to the enabled debug ports (Serial or WebUI). - * - * @param format The format string for the debug message. - * @param ... Additional arguments for formatting the message. - */ -void Debug_pushMessage(const char *format, ...) +static inline const char *safeName(const char *const *table, int count, int idx) { - // Check if either the Serial or WebUI debug port is enabled - if ((DebuggerStatus[dbg_Serial] == enabled) || (DebuggerStatus[dbg_Webui] == enabled)) - { - char buff[128]; // Buffer to hold the formatted message - va_list arg; // Variable argument list for vsnprintf - va_start(arg, format); - - // Format the message and store it in the buffer - vsnprintf(buff, sizeof(buff), format, arg); - va_end(arg); - - // Send the message to the Serial debug port if enabled - if (DebuggerStatus[dbg_Serial] == enabled) - { - Serial.print(buff); - } - - // Push the message to the WebUI debug port if enabled - if (DebuggerStatus[dbg_Webui] == enabled) - { - Websocket_PushLiveDebug(String(buff)); - } - } + return (idx >= 0 && idx < count && table[idx]) ? table[idx] : "?"; } -/** - * @brief Pushes a formatted CAN debug message to the enabled debug ports (Serial or WebUI). - * - * @param id CAN message ID. - * @param dlc Data Length Code of the CAN message. - * @param data Pointer to the data array of the CAN message. - */ -void pushCANDebug(uint32_t id, uint8_t dlc, uint8_t *data) -{ - // Check if either the Serial or WebUI debug port is enabled - if ((DebuggerStatus[dbg_Serial] == enabled) || (DebuggerStatus[dbg_Webui] == enabled)) - { - char buff[100]; // Buffer to hold the formatted message - char *p = buff; // Pointer to navigate the buffer - - // Format the CAN message information into the buffer - p += snprintf(p, sizeof(buff), "CAN: 0x%08X | %d | ", id, dlc); - for (int i = 0; i < dlc; i++) - { - p += snprintf(p, sizeof(buff) - (p - buff), "%02X ", data[i]); - } - *(p++) = '\n'; - *p = '\0'; - - // Send the formatted CAN message to the Serial debug port if enabled - if (DebuggerStatus[dbg_Serial] == enabled) - { - Serial.print(buff); - } - - // Push the formatted CAN message to the WebUI debug port if enabled - if (DebuggerStatus[dbg_Webui] == enabled) - { - Websocket_PushLiveDebug(String(buff)); - } - } -} - -// === splitArgs Helper === -std::vector splitArgs(const String &input) +static std::vector splitArgs(const String &input) { std::vector tokens; size_t start = 0; - while (true) { int end = input.indexOf(' ', start); - if (end == -1) break; + if (end == -1) + break; tokens.push_back(input.substring(start, end)); start = static_cast(end) + 1; } - if (start < input.length()) tokens.push_back(input.substring(start)); return tokens; } +// ------------------------------------------------------------------- +// Debug watches (place before handlers so templates are visible) +DebugStatus_t DebuggerStatus[dbg_cntElements]; -// === getArg helper === -String getArg(const std::vector &args, size_t index, const String &defaultVal = "") +struct DebugWatchEntry { - if (index < args.size()) - return args[index]; - return defaultVal; + const void *ptr; + String name; + uint32_t interval_ms; + uint32_t duration_ms; + uint32_t lastPrint_ms; + uint32_t start_ms; + std::function printer; +}; + +#define MAX_DEBUG_WATCHES 8 +static DebugWatchEntry debugWatches[MAX_DEBUG_WATCHES]; + +template +static void debugPrinterImpl(const void *ptr, const String &name) +{ + const T *typed = static_cast(ptr); + if constexpr (std::is_same::value) + Debug_pushMessage("%s = %s\n", name.c_str(), *typed ? "true" : "false"); + else if constexpr (std::is_floating_point::value) + Debug_pushMessage("%s = %.3f\n", name.c_str(), *typed); + else if constexpr (std::is_signed::value) + Debug_pushMessage("%s = %ld\n", name.c_str(), static_cast(*typed)); + else if constexpr (std::is_unsigned::value) + Debug_pushMessage("%s = %lu\n", name.c_str(), static_cast(*typed)); } -// === Command Handler Map === -typedef std::function DebugCmdHandler; +template +void RegisterDebugPrintAuto(const T *ptr, const String &name, uint32_t interval_ms, uint32_t duration_ms) +{ + for (int i = 0; i < MAX_DEBUG_WATCHES; ++i) + { + if (debugWatches[i].ptr == nullptr) + { + debugWatches[i] = {ptr, name, interval_ms, duration_ms, 0, millis(), debugPrinterImpl}; + Debug_pushMessage("Registered Watch: %s\n", name.c_str()); + return; + } + } + Debug_pushMessage("Debug Watch list full!\n"); +} + +void Debug_UpdateWatches() +{ + uint32_t now = millis(); + for (int i = 0; i < MAX_DEBUG_WATCHES; ++i) + { + auto &w = debugWatches[i]; + if (!w.ptr) + continue; + + if (now - w.start_ms >= w.duration_ms) + { + Debug_pushMessage("Watch expired: %s\n", w.name.c_str()); + w.ptr = nullptr; + continue; + } + + if (now - w.lastPrint_ms >= w.interval_ms) + { + w.lastPrint_ms = now; + if (w.printer) + w.printer(w.ptr, w.name); + } + } +} + +// ------------------------------------------------------------------- +// X-Macro command list (name + short description) +// Consistent, guessable CLI command names + clear help strings +#define DEBUG_COMMANDS \ + /* help */ \ + CMD(help, "Show this help") \ + /* info */ \ + CMD(info_sys, "Print system information") \ + CMD(info_net, "Print WiFi/network information") \ + /* EEPROM / config */ \ + CMD(ee_format_cfg, "Format Config-EEPROM") \ + CMD(ee_format_pds, "Format Persistence-EEPROM") \ + CMD(ee_check, "Check EEPROM (no fix)") \ + CMD(ee_check_fix, "Check EEPROM and autocorrect") \ + CMD(ee_dump_1k, "Dump EEPROM [0..1023]") \ + CMD(ee_dump, "Dump EEPROM ") \ + CMD(ee_reinit, "Reinitialize EEPROM") \ + CMD(ee_page_reset, "Reset persistence page to start") \ + CMD(cfg_dump, "Dump configuration struct") \ + CMD(pds_dump, "Dump persistence struct") \ + CMD(ee_save_all, "Save both CFG and PDS") \ + /* dumps / globals */ \ + CMD(globals_dump, "Dump globals struct") \ + /* debug port control */ \ + CMD(debug_serial_on, "Enable Serial debug") \ + CMD(debug_webui_on, "Enable WebUI debug") \ + CMD(debug_serial_off, "Disable Serial debug") \ + CMD(debug_webui_off, "Disable WebUI debug") \ + /* DTC + notifications */ \ + CMD(dtc_show, "Show DTCs") \ + CMD(dtc_clear, "Clear all DTCs") \ + CMD(dtc_fake_crit, "Raise a fake CRIT DTC") \ + CMD(dtc_fake_warn, "Raise a fake WARN DTC") \ + CMD(dtc_fake_info, "Raise a fake INFO DTC") \ + CMD(notify_error, "WebUI toast: error") \ + CMD(notify_warning, "WebUI toast: warning") \ + CMD(notify_success, "WebUI toast: success") \ + CMD(notify_info, "WebUI toast: info") \ + /* actions */ \ + CMD(purge, "Purge (default 10)") \ + CMD(wifi_toggle, "Toggle WiFi on/off") \ + CMD(dtc_add, "Add DTC ") \ + CMD(tank_refill, "Set tank to 100%") \ + CMD(watch_isr, "Watch globals.isr_debug for 20s") \ + /* setters */ \ + CMD(set_speed_source, "Set speed source ") \ + CMD(set_can_source, "Set CAN source ") \ + CMD(set_gps_baud, "Set GPS baud ") \ + CMD(set_system_status, "Set system status ") \ + CMD(tank_set_pct, "Set tank fill <0..100> %") + +// Prototypes for all handlers (linker will error if you add a CMD but forget the function) +#define CMD(name, desc) static void processCmd_##name(const String &args); +DEBUG_COMMANDS +#undef CMD + +using DebugCmdHandler = void (*)(const String &args); +struct CmdEntry +{ + const char *name; + DebugCmdHandler fn; + const char *desc; +}; + +static const std::vector &getCmdRegistry() +{ + static const std::vector reg = { +#define CMD(name, desc) {#name, &processCmd_##name, desc}, + DEBUG_COMMANDS +#undef CMD + }; + return reg; +} static const std::map &getCmdMap() { - static const std::map cmdMap = { - {"help", [](const String &) - { - Debug_log(LOG_INFO, "Available commands:\n"); - for (const auto &entry : getCmdMap()) - Debug_log(LOG_INFO, " - %s\n", entry.first.c_str()); - }}, - {"sysinfo", [](const String &) - { Debug_printSystemInfo(); }}, - {"netinfo", [](const String &) - { Debug_printWifiInfo(); }}, - {"formatCFG", [](const String &) - { Debug_formatCFG(); }}, - {"formatPDS", [](const String &) - { Debug_formatPersistence(); }}, - {"checkEE", [](const String &) - { Debug_CheckEEPOM(false); }}, - {"checkEEfix", [](const String &) - { Debug_CheckEEPOM(true); }}, - {"dumpEE1k", [](const String &) - { dumpEEPROM(0, 1024); }}, - {"dumpEE", [](const String &args) - { - int start = 0, len = EEPROM_SIZE_BYTES; - auto tokens = splitArgs(args); - if (tokens.size() >= 2) - { - start = tokens[0].toInt(); - len = tokens[1].toInt(); - } - dumpEEPROM(start, len); - }}, - {"reinitEE", [](const String &args) - { globals.requestEEAction = EE_REINITIALIZE; }}, - {"resetPageEE", [](const String &) - { MovePersistencePage_EEPROM(true); }}, - {"dumpCFG", [](const String &) - { Debug_dumpConfig(); }}, - {"dumpPDS", [](const String &) - { Debug_dumpPersistence(); }}, - {"saveEE", [](const String &) - { globals.requestEEAction = EE_ALL_SAVE; }}, - {"dumpGlobals", [](const String &) - { Debug_dumpGlobals(); }}, - {"sdbg", [](const String &) - { SetDebugportStatus(dbg_Serial, enabled); }}, - {"dtc_show", [](const String &) - { Debug_ShowDTCs(); }}, - {"dtc_clear", [](const String &) - { ClearAllDTC(); }}, - {"dtc_crit", [](const String &) - { MaintainDTC(DTC_FAKE_DTC_CRIT, true, millis()); }}, - {"dtc_warn", [](const String &) - { MaintainDTC(DTC_FAKE_DTC_WARN, true, millis()); }}, - {"dtc_info", [](const String &) - { MaintainDTC(DTC_FAKE_DTC_INFO, true, millis()); }}, - {"notify_error", [](const String &) - { Websocket_PushNotification("Debug Error Notification", error); }}, - {"notify_warning", [](const String &) - { Websocket_PushNotification("Debug Warning Notification", warning); }}, - {"notify_success", [](const String &) - { Websocket_PushNotification("Debug Success Notification", success); }}, - {"notify_info", [](const String &) - { Websocket_PushNotification("Debug Info Notification", info); }}, - {"purge", [](const String &) - { Debug_Purge(); }}, - {"toggle_wifi", [](const String &) - { globals.toggle_wifi = true; }}, - {"dtc_add", [](const String &args) - { - auto tokens = splitArgs(args); - if (!tokens.empty()) - { - int code = tokens[0].toInt(); - MaintainDTC((DTCNum_t)code, true, millis()); - } - }}, - {"tank_refill", [](const String &) - { Debug_refillTank(); }}, - {"isr_debug", [](const String &) - { - RegisterDebugPrintAuto(&globals.isr_debug, "isr_debug", 100, 20000); - }}, - }; - return cmdMap; + static std::map m; + if (m.empty()) + for (const auto &e : getCmdRegistry()) + m.emplace(String(e.name), e.fn); + return m; } +// --- compile-time width for help column --------------------------------- +#ifndef HELP_NAME_PADDING +#define HELP_NAME_PADDING 2 // extra spaces after longest command +#endif +#ifndef HELP_MIN_COLW +#define HELP_MIN_COLW 14 // optional floor (can be 0) +#endif + +namespace dbgmeta +{ + // 1) Build a constexpr array of command-name lengths via X-macro expansion + constexpr size_t name_lengths[] = { +#define CMD(name, desc) (sizeof(#name) - 1), + DEBUG_COMMANDS +#undef CMD + }; + + // 2) constexpr recursion to compute max of the array (C++11-friendly) + template + constexpr size_t array_max(const size_t (&a)[N], size_t i = 0, size_t m = 0) + { + return (i == N) ? m + : array_max(a, i + 1, (a[i] > m ? a[i] : m)); + } + + constexpr size_t longest = array_max(name_lengths); +} + +// Final compile-time column width +constexpr int HELP_COLW_RAW = static_cast(dbgmeta::longest) + HELP_NAME_PADDING; +constexpr int HELP_COLW = (HELP_COLW_RAW < HELP_MIN_COLW) ? HELP_MIN_COLW : HELP_COLW_RAW; + +// ------------------------------------------------------------------- +// Dispatcher void processCmdDebug(String input) { input.trim(); int splitIndex = input.indexOf(' '); - String command = splitIndex == -1 ? input : input.substring(0, splitIndex); - String args = splitIndex == -1 ? "" : input.substring(splitIndex + 1); + String command = (splitIndex == -1) ? input : input.substring(0, splitIndex); + String args = (splitIndex == -1) ? "" : input.substring(splitIndex + 1); auto &cmdMap = getCmdMap(); auto it = cmdMap.find(command); @@ -384,31 +295,302 @@ void processCmdDebug(String input) Debug_log(LOG_WARN, "Unknown command: '%s'\n", command.c_str()); } -/** - * @brief Formats the Config-EEPROM and resets it to default values. - * Prints a debug message after formatting. - */ -void Debug_formatCFG() +// ------------------------------------------------------------------- +// Lifecycle / I/O +void initDebugger() { - Debug_pushMessage("Formatting Config-EEPROM and resetting to default\n"); - FormatConfig_EEPROM(); + DebuggerStatus[dbg_Serial] = disabled; + DebuggerStatus[dbg_Webui] = disabled; + Serial.setDebugOutput(false); } -/** - * @brief Formats the Persistence-EEPROM and resets it to default values. - * Prints a debug message after formatting. - */ -void Debug_formatPersistence() +void Debug_Process() { - Debug_pushMessage("Formatting Persistence-EEPROM and resetting to default\n"); - FormatPersistence_EEPROM(); + // === States === + typedef enum + { + IDLE, + CMD_COMPLETE, + CMD_ABORT, + CMD_OVERFLOW + } InputProcessed_t; + enum EscState + { + ESC_NONE, + ESC_GOT_ESC, + ESC_GOT_BRACKET + }; + +// === Config === +#ifndef DBG_HISTORY_CAP +#define DBG_HISTORY_CAP 4 // wie viele letzte Commands gemerkt werden +#endif +#ifndef DBG_ESC_TIMEOUT_MS +#define DBG_ESC_TIMEOUT_MS 100 // Timeout, um lone-ESC von Arrow-ESC zu unterscheiden +#endif + + // === Line buffer / history === + static char inputBuffer[32]; + static unsigned int inputCnt = 0; + + static char history[DBG_HISTORY_CAP][sizeof(inputBuffer)]; + static uint8_t histSize = 0; // wie viele Slots belegt (<= CAP) + static uint8_t histHead = 0; // nächste Schreibposition (Ringpuffer) + static uint8_t histOffset = 0; // 0: live editing, >0: n zurück (1 = letzter cmd) + + static char editBackup[sizeof(inputBuffer)]; // was vor dem ersten ↑ im Eingabefeld stand + + static EscState escState = ESC_NONE; + static uint32_t escTs = 0; + + auto replaceInputLine = [&](const char *newData, unsigned int newLen) + { + // Cursor ans Zeilenende => lösche aktuelle Eingabe „sichtbar“ und schreibe neuen Inhalt + for (unsigned int i = 0; i < inputCnt; ++i) + Serial.write('\b'); + for (unsigned int i = 0; i < inputCnt; ++i) + Serial.write(' '); + for (unsigned int i = 0; i < inputCnt; ++i) + Serial.write('\b'); + + // kopieren + echo + newLen = (newLen >= sizeof(inputBuffer)) ? (sizeof(inputBuffer) - 1) : newLen; + memcpy(inputBuffer, newData, newLen); + inputBuffer[newLen] = '\0'; + inputCnt = newLen; + Serial.write((const uint8_t *)inputBuffer, inputCnt); + }; + + auto recallHistory = [&](int direction) // +1 = UP (älter), -1 = DOWN (neuer) + { + if (histSize == 0) + return; + + if (direction > 0) + { // UP + if (histOffset == 0) + { + // ersten Sprung in die History -> aktuellen Edit-Stand sichern + memcpy(editBackup, inputBuffer, sizeof(editBackup)); + } + if (histOffset < histSize) + { + histOffset++; + uint8_t idx = (uint8_t)((histHead + DBG_HISTORY_CAP - histOffset) % DBG_HISTORY_CAP); + replaceInputLine(history[idx], strlen(history[idx])); + } + } + else if (direction < 0) + { // DOWN + if (histOffset > 1) + { + histOffset--; + uint8_t idx = (uint8_t)((histHead + DBG_HISTORY_CAP - histOffset) % DBG_HISTORY_CAP); + replaceInputLine(history[idx], strlen(history[idx])); + } + else if (histOffset == 1) + { + // zurück in den Live-Edit-Puffer + histOffset = 0; + replaceInputLine(editBackup, strlen(editBackup)); + } + } + }; + + auto commitToHistory = [&](const char *line) + { + if (!line || !line[0]) + return; // leer -> nicht speichern + // optional: Duplikatfilter (gleiche wie letzte nicht erneut speichern) + if (histSize > 0) + { + uint8_t lastIdx = (uint8_t)((histHead + DBG_HISTORY_CAP - 1) % DBG_HISTORY_CAP); + if (strncmp(history[lastIdx], line, sizeof(history[0])) == 0) + return; + } + strncpy(history[histHead], line, sizeof(history[0]) - 1); + history[histHead][sizeof(history[0]) - 1] = '\0'; + histHead = (uint8_t)((histHead + 1) % DBG_HISTORY_CAP); + if (histSize < DBG_HISTORY_CAP) + histSize++; + }; + + InputProcessed_t InputProcessed = IDLE; + + // Lone-ESC Timeout behandeln (ESC ohne Folge -> Abort) + if (escState != ESC_NONE && (millis() - escTs) > DBG_ESC_TIMEOUT_MS) + { + InputProcessed = CMD_ABORT; + escState = ESC_NONE; + inputCnt = 0; + inputBuffer[0] = '\0'; + } + + // Alle verfügbaren Bytes verarbeiten (reaktiver auf Escape-Sequenzen) + while (Serial.available()) + { + char c = Serial.read(); + + // --- NEU: CRLF „schlucken“ --- + if (c == '\r') + { + // behandle als Zeilenende und konsumiere direkt folgendes '\n', falls vorhanden + if (Serial.peek() == '\n') + (void)Serial.read(); + c = '\n'; + } + + // Escape-Sequenzen ... + if (escState == ESC_NONE) + { + if ((uint8_t)c == 0x1B) + { // ESC + escState = ESC_GOT_ESC; + escTs = millis(); + continue; + } + } + else if (escState == ESC_GOT_ESC) + { + if (c == '[') + { + escState = ESC_GOT_BRACKET; + continue; + } + else + { + // ESC allein (kein '[') -> Abort + InputProcessed = CMD_ABORT; + escState = ESC_NONE; + inputCnt = 0; + inputBuffer[0] = '\0'; + continue; + } + } + else if (escState == ESC_GOT_BRACKET) + { + // UP/DOWN + if (c == 'A') + { // Up + recallHistory(+1); + } + else if (c == 'B') + { // Down + recallHistory(-1); + } + escState = ESC_NONE; + continue; + } + + switch ((uint8_t)c) + { + case '\n': + // Abschluss der Eingabe + inputBuffer[inputCnt] = '\0'; + Serial.write('\n'); + InputProcessed = CMD_COMPLETE; + // History-Context verlassen + histOffset = 0; + break; + + case 0x08: // Backspace + case 0x7F: // DEL + if (inputCnt > 0) + { + // Puffer kürzen + visuell löschen + inputCnt--; + inputBuffer[inputCnt] = '\0'; + Serial.write('\b'); + Serial.write(' '); + Serial.write('\b'); + } + // Falls wir gerade in der History sind, zurück in Live-Edit wechseln? + // (optional: hier NICHT automatisch, damit man editierten History-Text senden kann) + break; + + default: + // printable ASCII (inkl. Leerzeichen): 0x20..0x7E + if (c >= 0x20 && c <= 0x7E) + { + if (inputCnt < sizeof(inputBuffer) - 1) + { + inputBuffer[inputCnt++] = c; + Serial.write(c); + // Sobald der Nutzer tippt, verlassen wir ggf. den History-View + if (histOffset != 0) + { + histOffset = 0; + // editBackup war unser Startzustand vor History — jetzt tippt der Nutzer aktiv; + // nichts weiter zu tun, da inputBuffer bereits der sichtbare Stand ist. + } + } + else + { + // overflow -> sofort melden und zurücksetzen + inputBuffer[sizeof(inputBuffer) - 1] = '\0'; + inputCnt = 0; + InputProcessed = CMD_OVERFLOW; + } + } + break; + } + + // Wenn schon eine komplette Aktion erkannt wurde, beenden wir die Schleife + if (InputProcessed == CMD_COMPLETE || InputProcessed == CMD_OVERFLOW || InputProcessed == CMD_ABORT) + break; + } + + // Post-Processing + switch (InputProcessed) + { + case CMD_ABORT: + Debug_pushMessage("Abort\n"); + break; + + case CMD_COMPLETE: + // --- NEU: Leere Zeilen ignorieren --- + if (inputCnt > 0) + { + commitToHistory(inputBuffer); + processCmdDebug(String(inputBuffer)); + } + inputCnt = 0; + inputBuffer[0] = '\0'; + break; + + case CMD_OVERFLOW: + Debug_pushMessage("Input buffer overflow\n"); + inputCnt = 0; + inputBuffer[0] = '\0'; + break; + + default: + break; + } + + if (InputProcessed != IDLE) + Serial.print(">"); + + Debug_UpdateWatches(); } -/** - * @brief Prints system information and status to the debug output. - */ -void Debug_printSystemInfo() +// ------------------------------------------------------------------- +// Command handlers + +// Info +static void processCmd_help(const String &) { + Debug_log(LOG_INFO, "Available commands:\n"); + for (const auto &e : getCmdRegistry()) + { + // %-*s nimmt die Breite zur Laufzeit entgegen — hier ist sie aber ein constexpr + Debug_log(LOG_INFO, " - %-*s : %s\n", HELP_COLW, e.name, e.desc); + } +} + +static void processCmd_info_sys(const String &) +{ + Debug_pushMessage("Souko's ChainOiler Mk1\n"); Debug_pushMessage("Hostname: %s\n", globals.DeviceName); @@ -430,10 +612,42 @@ void Debug_printSystemInfo() Debug_pushMessage("Sw-Version: %d.%02d\n", constants.FW_Version_major, constants.FW_Version_minor); } -/** - * @brief Dumps the current configuration parameters to the debug output. - */ -void Debug_dumpConfig() +static void processCmd_info_net(const String &) +{ + Debug_pushMessage("IP Address: %s\n", WiFi.localIP().toString().c_str()); +} + +// EEPROM +static void processCmd_ee_format_cfg(const String &) +{ + Debug_pushMessage("Formatting Config-EEPROM and resetting to default\n"); + FormatConfig_EEPROM(); +} + +static void processCmd_ee_format_pds(const String &) +{ + Debug_pushMessage("Formatting Persistence-EEPROM and resetting to default\n"); + FormatPersistence_EEPROM(); +} + +static void processCmd_ee_check(const String &) { Debug_CheckEEPROM(false); } +static void processCmd_ee_check_fix(const String &) { Debug_CheckEEPROM(true); } +static void processCmd_ee_dump_1k(const String &) { dumpEEPROM(0, 1024); } +static void processCmd_ee_dump(const String &args) +{ + int start = 0, len = EEPROM_SIZE_BYTES; + auto tokens = splitArgs(args); + if (tokens.size() >= 2) + { + start = tokens[0].toInt(); + len = tokens[1].toInt(); + } + dumpEEPROM(start, len); +} + +static void processCmd_ee_reinit(const String &) { globals.requestEEAction = EE_REINITIALIZE; } +static void processCmd_ee_page_reset(const String &) { MovePersistencePage_EEPROM(true); } +static void processCmd_cfg_dump(const String &) { Debug_pushMessage("DistancePerLube_Default: %d\n", LubeConfig.DistancePerLube_Default); Debug_pushMessage("DistancePerLube_Rain: %d\n", LubeConfig.DistancePerLube_Rain); @@ -452,10 +666,18 @@ void Debug_dumpConfig() Debug_pushMessage("checksum: 0x%08X\n", LubeConfig.checksum); } -/** - * @brief Dumps the global variables and their values to the debug output. - */ -void Debug_dumpGlobals() +static void processCmd_pds_dump(const String &) +{ + Debug_pushMessage("writeCycleCounter: %d\n", PersistenceData.writeCycleCounter); + Debug_pushMessage("tankRemain_microL: %d\n", PersistenceData.tankRemain_microL); + Debug_pushMessage("TravelDistance_highRes_mm: %d\n", PersistenceData.TravelDistance_highRes_mm); + Debug_pushMessage("checksum: %d\n", PersistenceData.checksum); + Debug_pushMessage("PSD Address: 0x%04X\n", globals.eePersistenceAddress); +} + +static void processCmd_ee_save_all(const String &) { globals.requestEEAction = EE_ALL_SAVE; } + +static void processCmd_globals_dump(const String &) { Debug_pushMessage("systemStatus: %d\n", globals.systemStatus); Debug_pushMessage("resumeStatus: %d\n", globals.resumeStatus); @@ -468,99 +690,29 @@ void Debug_dumpGlobals() Debug_pushMessage("hasDTC: %d\n", globals.hasDTC); } -/** - * @brief Dumps the persistence data variables and their values to the debug output. - */ -void Debug_dumpPersistence() +// Debug port toggles +static void processCmd_debug_serial_on(const String &) { SetDebugportStatus(dbg_Serial, enabled); } +static void processCmd_debug_webui_on(const String &) { SetDebugportStatus(dbg_Webui, enabled); } +static void processCmd_debug_serial_off(const String &) { SetDebugportStatus(dbg_Serial, disabled); } +static void processCmd_debug_webui_off(const String &) { SetDebugportStatus(dbg_Webui, disabled); } + +// DTC / notifications +static void processCmd_dtc_show(const String &) { - Debug_pushMessage("writeCycleCounter: %d\n", PersistenceData.writeCycleCounter); - Debug_pushMessage("tankRemain_microL: %d\n", PersistenceData.tankRemain_microL); - Debug_pushMessage("TravelDistance_highRes_mm: %d\n", PersistenceData.TravelDistance_highRes_mm); - Debug_pushMessage("checksum: %d\n", PersistenceData.checksum); - Debug_pushMessage("PSD Adress: 0x%04X\n", globals.eePersistenceAddress); -} - -/** - * @brief Prints information related to WiFi to the debug output. - */ -void Debug_printWifiInfo() -{ - Debug_pushMessage("IP Adress: %s\n", WiFi.localIP().toString().c_str()); -} - -/** - * @brief Checks the EEPROM data integrity by calculating and comparing checksums. - * Prints the result to the debug output. - */ -void Debug_CheckEEPOM(bool autocorrect) -{ - // Check PersistenceData EEPROM checksum - uint32_t checksum = PersistenceData.checksum; - PersistenceData.checksum = 0; - - if (Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData)) == checksum) - { - Debug_pushMessage("PersistenceData EEPROM Checksum OK\n"); - } - else - { - Debug_pushMessage("PersistenceData EEPROM Checksum BAD\n"); - } - - PersistenceData.checksum = checksum; - - // Check LubeConfig EEPROM checksum - checksum = LubeConfig.checksum; - LubeConfig.checksum = 0; - - if (Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig)) == checksum) - { - Debug_pushMessage("LubeConfig EEPROM Checksum OK\n"); - } - else - { - Debug_pushMessage("LubeConfig EEPROM Checksum BAD\n"); - } - LubeConfig.checksum = checksum; - - uint32_t sanitycheck = ConfigSanityCheck(autocorrect); - - if (sanitycheck == 0) - { - Debug_pushMessage("LubeConfig Sanity Check OK\n"); - } - else - { - Debug_pushMessage("LubeConfig Sanity Check BAD: %s\n", uint32_to_binary_string(sanitycheck)); - } -} - -/** - * @brief Displays Diagnostic Trouble Codes (DTCs) along with their timestamps, - * status, and severity in a formatted manner. - */ -void Debug_ShowDTCs() -{ - char buff_timestamp[16]; // Format: DD-hh:mm:ss:xxx + char buff_timestamp[16]; // DD-hh:mm:ss:xxx char buff_active[9]; - - // Header for the DTC display Debug_pushMessage("\n timestamp | DTC-Nr. | status | severity\n"); - - // Iterate through DTCStorage and display each entry for (uint32_t i = 0; i < MAX_DTC_STORAGE; i++) { if (DTCStorage[i].Number < DTC_LAST_DTC) { - // Format timestamp 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 + DTCStorage[i].timestamp / 86400000, + DTCStorage[i].timestamp / 360000 % 24, + DTCStorage[i].timestamp / 60000 % 60, + DTCStorage[i].timestamp / 1000 % 60, + DTCStorage[i].timestamp % 1000); - // Determine DTC status if (DTCStorage[i].active == DTC_ACTIVE) strcpy(buff_active, "active"); else if (DTCStorage[i].active == DTC_PREVIOUS) @@ -568,129 +720,257 @@ void Debug_ShowDTCs() else strcpy(buff_active, "none"); - // Display DTC information - Debug_pushMessage("%s %7d %8s %8d\n", buff_timestamp, DTCStorage[i].Number, buff_active); + Debug_pushMessage("%s %7d %8s %8d\n", + buff_timestamp, + DTCStorage[i].Number, + buff_active, + getSeverityForDTC(DTCStorage[i].Number)); } } } -/** - * @brief Start purging for 10 pulses. - */ -void Debug_Purge() +static void processCmd_dtc_clear(const String &) { ClearAllDTC(); } +static void processCmd_dtc_fake_crit(const String &) { MaintainDTC(DTC_FAKE_DTC_CRIT, true, millis()); } +static void processCmd_dtc_fake_warn(const String &) { MaintainDTC(DTC_FAKE_DTC_WARN, true, millis()); } +static void processCmd_dtc_fake_info(const String &) { MaintainDTC(DTC_FAKE_DTC_INFO, true, millis()); } + +static void processCmd_notify_error(const String &) { Websocket_PushNotification("Debug Error Notification", error); } +static void processCmd_notify_warning(const String &) { Websocket_PushNotification("Debug Warning Notification", warning); } +static void processCmd_notify_success(const String &) { Websocket_PushNotification("Debug Success Notification", success); } +static void processCmd_notify_info(const String &) { Websocket_PushNotification("Debug Info Notification", info); } + +// Purge / WiFi / Tank / ISR +static void processCmd_purge(const String &args) { - globals.purgePulses = 10; + auto tokens = splitArgs(args); + uint16_t n = tokens.empty() ? 10 : (uint16_t)tokens[0].toInt(); + n = clamp_u16(n, 1, 1000); + globals.purgePulses = n; globals.resumeStatus = globals.systemStatus; globals.systemStatus = sysStat_Purge; - - Debug_pushMessage("Purging 10 pulses\n"); + Debug_pushMessage("Purging %u pulses\n", n); } -void Debug_refillTank() +static void processCmd_wifi_toggle(const String &) +{ + globals.toggle_wifi = true; +} + +static void processCmd_dtc_add(const String &args) +{ + auto tokens = splitArgs(args); + if (tokens.empty()) + { + Debug_log(LOG_WARN, "Usage: dtc_add \n"); + return; + } + int code = tokens[0].toInt(); + MaintainDTC((DTCNum_t)code, true, millis()); +} + +static void processCmd_tank_refill(const String &) { PersistenceData.tankRemain_microL = LubeConfig.tankCapacity_ml * 1000; globals.requestEEAction = EE_PDS_SAVE; Debug_pushMessage("Setting Tank to 100%\n"); } -/** - * @brief Convert a uint32_t value to a binary string with nibbles separated by a space. - * - * This function takes a uint32_t value and converts it to a binary string - * representation. The binary string is stored in a static buffer and returned - * as a const char pointer. Each nibble (4 bits) in the binary representation - * is separated by a space. The buffer is overwritten on subsequent calls to - * this function. - * - * @param num The uint32_t value to convert. - * @return A pointer to a const char string containing the binary representation - * of the input number with nibbles separated by a space. - */ +static void processCmd_watch_isr(const String &) +{ + RegisterDebugPrintAuto(&globals.isr_debug, "watch_isr", 100, 20000); +} + +// Setters with bounds/string lookup +static void processCmd_set_speed_source(const String &args) +{ + auto tokens = splitArgs(args); + if (tokens.empty()) + { + Debug_log(LOG_WARN, "Usage: set_speed_source \n"); + return; + } + int idx = parseIndexOrName(tokens[0], SpeedSourceString, SPEEDSOURCE_COUNT); + if (idx < 0) + { + Debug_log(LOG_WARN, "Invalid speed source '%s'\n", tokens[0].c_str()); + return; + } + LubeConfig.SpeedSource = (SpeedSource_t)idx; + Debug_log(LOG_INFO, "SpeedSource = %d (%s)\n", idx, safeName(SpeedSourceString, SPEEDSOURCE_COUNT, idx)); +} + +static void processCmd_set_can_source(const String &args) +{ + auto tokens = splitArgs(args); + if (tokens.empty()) + { + Debug_log(LOG_WARN, "Usage: set_can_source \n"); + return; + } + int idx = parseIndexOrName(tokens[0], CANSourceString, CANSOURCE_COUNT); + if (idx < 0) + { + Debug_log(LOG_WARN, "Invalid CAN source '%s'\n", tokens[0].c_str()); + return; + } + LubeConfig.CANSource = (CANSource_t)idx; + Debug_log(LOG_INFO, "CANSource = %d (%s)\n", idx, safeName(CANSourceString, CANSOURCE_COUNT, idx)); +} + +static void processCmd_set_gps_baud(const String &args) +{ + auto tokens = splitArgs(args); + if (tokens.empty()) + { + Debug_log(LOG_WARN, "Usage: set_gps_baud \n"); + return; + } + int idx = parseIndexOrName(tokens[0], GPSBaudRateString, GPSBAUDRATE_COUNT); + if (idx < 0) + { + Debug_log(LOG_WARN, "Invalid GPS baud '%s'\n", tokens[0].c_str()); + return; + } + LubeConfig.GPSBaudRate = (GPSBaudRate_t)idx; + Debug_log(LOG_INFO, "GPSBaudRate = %d (%s)\n", idx, safeName(GPSBaudRateString, GPSBAUDRATE_COUNT, idx)); +} + +static void processCmd_set_system_status(const String &args) +{ + auto tokens = splitArgs(args); + if (tokens.empty()) + { + Debug_log(LOG_WARN, "Usage: set_system_status \n"); + return; + } + int idx = parseIndexOrName(tokens[0], SystemStatusString, SYSSTAT_COUNT); + if (idx < 0) + { + Debug_log(LOG_WARN, "Invalid status '%s'\n", tokens[0].c_str()); + return; + } + globals.systemStatus = (tSystem_Status)idx; + Debug_log(LOG_INFO, "SystemStatus = %d (%s)\n", idx, safeName(SystemStatusString, SYSSTAT_COUNT, idx)); +} + +static void processCmd_tank_set_pct(const String &args) +{ + auto tokens = splitArgs(args); + if (tokens.empty()) + { + Debug_log(LOG_WARN, "Usage: tank_pct <0..100>\n"); + return; + } + int pct = clamp_u16(tokens[0].toInt(), 0, 100); + PersistenceData.tankRemain_microL = (uint32_t)LubeConfig.tankCapacity_ml * (uint32_t)pct * 10u; // ml * 1000 * pct/100 + globals.requestEEAction = EE_PDS_SAVE; + Debug_log(LOG_INFO, "Tank set to %d%% (%lu uL)\n", pct, (unsigned long)PersistenceData.tankRemain_microL); +} + +// ------------------------------------------------------------------- +// Debug plumbing + +void SetDebugportStatus(DebugPorts_t port, DebugStatus_t status) +{ + if (status == disabled) + Debug_pushMessage("Disable DebugPort %s\n", sDebugPorts[port]); + DebuggerStatus[port] = status; + if (status == enabled) + Debug_pushMessage("Enabled DebugPort %s\n", sDebugPorts[port]); +} + +void Debug_log(LogLevel, const char *format, ...) +{ + if ((DebuggerStatus[dbg_Serial] != enabled) && (DebuggerStatus[dbg_Webui] != enabled)) + return; + + char buff[128]; + va_list arg; + va_start(arg, format); + vsnprintf(buff, sizeof(buff), format, arg); + va_end(arg); + + if (DebuggerStatus[dbg_Serial] == enabled) + Serial.print(buff); + if (DebuggerStatus[dbg_Webui] == enabled) + Websocket_PushLiveDebug(String(buff)); +} + +void Debug_pushMessage(const char *format, ...) +{ + if ((DebuggerStatus[dbg_Serial] != enabled) && (DebuggerStatus[dbg_Webui] != enabled)) + return; + + char buff[128]; + va_list arg; + va_start(arg, format); + vsnprintf(buff, sizeof(buff), format, arg); + va_end(arg); + + if (DebuggerStatus[dbg_Serial] == enabled) + Serial.print(buff); + if (DebuggerStatus[dbg_Webui] == enabled) + Websocket_PushLiveDebug(String(buff)); +} + +void pushCANDebug(uint32_t id, uint8_t dlc, uint8_t *data) +{ + if ((DebuggerStatus[dbg_Serial] != enabled) && (DebuggerStatus[dbg_Webui] != enabled)) + return; + + char buff[100]; + char *p = buff; + + p += snprintf(p, sizeof(buff), "CAN: 0x%08X | %d | ", id, dlc); + for (int i = 0; i < dlc; i++) + p += snprintf(p, sizeof(buff) - (p - buff), "%02X ", data[i]); + *(p++) = '\n'; + *p = '\0'; + + if (DebuggerStatus[dbg_Serial] == enabled) + Serial.print(buff); + if (DebuggerStatus[dbg_Webui] == enabled) + Websocket_PushLiveDebug(String(buff)); +} + +// ------------------------------------------------------------------- +// Debug utilities + +void Debug_CheckEEPROM(bool autocorrect) +{ + uint32_t checksum = PersistenceData.checksum; + PersistenceData.checksum = 0; + if (Checksum_EEPROM((uint8_t *)&PersistenceData, sizeof(PersistenceData)) == checksum) + Debug_pushMessage("PersistenceData EEPROM Checksum OK\n"); + else + Debug_pushMessage("PersistenceData EEPROM Checksum BAD\n"); + PersistenceData.checksum = checksum; + + checksum = LubeConfig.checksum; + LubeConfig.checksum = 0; + if (Checksum_EEPROM((uint8_t *)&LubeConfig, sizeof(LubeConfig)) == checksum) + Debug_pushMessage("LubeConfig EEPROM Checksum OK\n"); + else + Debug_pushMessage("LubeConfig EEPROM Checksum BAD\n"); + LubeConfig.checksum = checksum; + + uint32_t sanitycheck = ConfigSanityCheck(autocorrect); + if (sanitycheck == 0) + Debug_pushMessage("LubeConfig Sanity Check OK\n"); + else + Debug_pushMessage("LubeConfig Sanity Check BAD: %s\n", uint32_to_binary_string(sanitycheck)); +} + const char *uint32_to_binary_string(uint32_t num) { - static char binary_str[65]; // 32 bits + 31 spaces + null terminator + static char binary_str[65]; // 32 bits + 31 spaces + '\0' int i, j; for (i = 31, j = 0; i >= 0; i--, j++) { binary_str[j] = ((num >> i) & 1) ? '1' : '0'; if (i % 4 == 0 && i != 0) - { - binary_str[++j] = ' '; // Insert space after every nibble - } + binary_str[++j] = ' '; } - binary_str[j] = '\0'; // Null terminator + binary_str[j] = '\0'; return binary_str; } - -DebugStatus_t DebuggerStatus[dbg_cntElements]; - -struct DebugWatchEntry -{ - const void *ptr; - String name; - uint32_t interval_ms; - uint32_t duration_ms; - uint32_t lastPrint_ms; - uint32_t start_ms; - std::function printer; -}; - -#define MAX_DEBUG_WATCHES 8 -DebugWatchEntry debugWatches[MAX_DEBUG_WATCHES]; - -// === Typabhängige Druckfunktion === -template -void debugPrinterImpl(const void* ptr, const String& name) { - const T* typed = static_cast(ptr); - if constexpr (std::is_same::value) { - Debug_pushMessage("%s = %s\n", name.c_str(), *typed ? "true" : "false"); - } else if constexpr (std::is_floating_point::value) { - Debug_pushMessage("%s = %.3f\n", name.c_str(), *typed); - } else if constexpr (std::is_signed::value) { - Debug_pushMessage("%s = %ld\n", name.c_str(), static_cast(*typed)); - } else if constexpr (std::is_unsigned::value) { - Debug_pushMessage("%s = %lu\n", name.c_str(), static_cast(*typed)); - } -} - -// === Automatisches DebugPrint-Register === -template -void RegisterDebugPrintAuto(const T* ptr, const String& name, uint32_t interval_ms, uint32_t duration_ms) -{ - for (int i = 0; i < MAX_DEBUG_WATCHES; ++i) { - if (debugWatches[i].ptr == nullptr) { - debugWatches[i] = { - ptr, - name, - interval_ms, - duration_ms, - 0, - millis(), - debugPrinterImpl - }; - Debug_pushMessage("Registered Watch: %s\n", name.c_str()); - return; - } - } - Debug_pushMessage("Debug Watch list full!\n"); -} - -// Debug-Ausgabe in Debug_Process(): -void Debug_UpdateWatches() { - uint32_t now = millis(); - for (int i = 0; i < MAX_DEBUG_WATCHES; ++i) { - auto& w = debugWatches[i]; - if (!w.ptr) continue; - - if (now - w.start_ms >= w.duration_ms) { - Debug_pushMessage("Watch expired: %s\n", w.name.c_str()); - w.ptr = nullptr; - continue; - } - - if (now - w.lastPrint_ms >= w.interval_ms) { - w.lastPrint_ms = now; - if (w.printer) w.printer(w.ptr, w.name); - } - } -}