/** * @file debugger.cpp * @brief Implementation of debugging functions for monitoring and diagnostics. * * This file contains the implementation of various debugging functions to monitor * and diagnose the system. It includes functions to print system information, WiFi * details, EEPROM status, dump configuration settings, dump persistence data, show * Diagnostic Trouble Codes (DTCs), and more. * * @author Marcel Peterkau * @date 24.08.2025 */ #include "debugger.h" #include "common.h" #include "globals.h" #include #include #include #include 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_PurgeN(uint16_t pulses); void Debug_refillTank(); void Debug_UpdateWatches(); void processCmdDebug(String command); // ------------------------------------------------------------------- // 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) { if (token.length() == 0) return -1; bool numeric = true; for (size_t i = 0; i < token.length(); ++i) { char c = token.charAt(i); if (c < '0' || c > '9') { numeric = false; break; } } if (numeric) { int idx = token.toInt(); return (idx >= 0 && idx < count) ? idx : -1; } for (int i = 0; i < count; ++i) { if (table[i] && token.equalsIgnoreCase(table[i])) return i; } return -1; } static inline const char *safeName(const char *const *table, int count, int idx) { return (idx >= 0 && idx < count && table[idx]) ? table[idx] : "?"; } 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; 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]; 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 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)); } 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 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); auto &cmdMap = getCmdMap(); auto it = cmdMap.find(command); if (it != cmdMap.end()) it->second(args); else Debug_log(LOG_WARN, "Unknown command: '%s'\n", command.c_str()); } // ------------------------------------------------------------------- // Lifecycle / I/O void initDebugger() { DebuggerStatus[dbg_Serial] = disabled; DebuggerStatus[dbg_Webui] = disabled; Serial.setDebugOutput(false); } void Debug_Process() { // === 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(); // CR zu LF normalisieren (einige Terminals senden \r oder \r\n) if (c == '\r') c = '\n'; // Escape-Sequenzen (Pfeiltasten) erkennen: ESC [ A / B 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: // in History legen (vor Ausführung) 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(); } // ------------------------------------------------------------------- // 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); FlashMode_t ideMode = ESP.getFlashChipMode(); Debug_pushMessage("Sdk version: %s\n", ESP.getSdkVersion()); Debug_pushMessage("Core Version: %s\n", ESP.getCoreVersion().c_str()); Debug_pushMessage("Boot Version: %u\n", ESP.getBootVersion()); Debug_pushMessage("Boot Mode: %u\n", ESP.getBootMode()); Debug_pushMessage("CPU Frequency: %u MHz\n", ESP.getCpuFreqMHz()); Debug_pushMessage("Reset reason: %s\n", ESP.getResetReason().c_str()); Debug_pushMessage("Flash Size: %d\n", ESP.getFlashChipRealSize()); Debug_pushMessage("Flash Size IDE: %d\n", ESP.getFlashChipSize()); Debug_pushMessage("Flash ide mode: %s\n", (ideMode == FM_QIO ? "QIO" : ideMode == FM_QOUT ? "QOUT" : ideMode == FM_DIO ? "DIO" : ideMode == FM_DOUT ? "DOUT" : "UNKNOWN")); Debug_pushMessage("OTA-Pass: %s\n", QUOTE(ADMIN_PASSWORD)); Debug_pushMessage("Git-Revision: %s\n", constants.GitHash); Debug_pushMessage("Sw-Version: %d.%02d\n", constants.FW_Version_major, constants.FW_Version_minor); } 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); Debug_pushMessage("tankCapacity_ml: %d\n", LubeConfig.tankCapacity_ml); Debug_pushMessage("amountPerDose_microL: %d\n", LubeConfig.amountPerDose_microL); Debug_pushMessage("TankRemindAtPercentage: %d\n", LubeConfig.TankRemindAtPercentage); Debug_pushMessage("PulsePerRevolution: %d\n", LubeConfig.PulsePerRevolution); Debug_pushMessage("TireWidth_mm: %d\n", LubeConfig.TireWidth_mm); Debug_pushMessage("TireWidthHeight_Ratio: %d\n", LubeConfig.TireWidthHeight_Ratio); Debug_pushMessage("RimDiameter_Inch: %d\n", LubeConfig.RimDiameter_Inch); Debug_pushMessage("DistancePerRevolution_mm: %d\n", LubeConfig.DistancePerRevolution_mm); Debug_pushMessage("BleedingPulses: %d\n", LubeConfig.BleedingPulses); Debug_pushMessage("SpeedSource: %d\n", LubeConfig.SpeedSource); Debug_pushMessage("GPSBaudRate: %d\n", LubeConfig.GPSBaudRate); Debug_pushMessage("CANSource: %d\n", LubeConfig.CANSource); Debug_pushMessage("checksum: 0x%08X\n", LubeConfig.checksum); } 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); Debug_pushMessage("purgePulses: %d\n", globals.purgePulses); Debug_pushMessage("requestEEAction: %d\n", globals.requestEEAction); Debug_pushMessage("DeviceName: %s\n", globals.DeviceName); Debug_pushMessage("FlashVersion: %s\n", globals.FlashVersion); Debug_pushMessage("eePersistenceAddress: %d\n", globals.eePersistenceAddress); Debug_pushMessage("TankPercentage: %d\n", globals.TankPercentage); Debug_pushMessage("hasDTC: %d\n", globals.hasDTC); } // 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 &) { char buff_timestamp[16]; // DD-hh:mm:ss:xxx char buff_active[9]; Debug_pushMessage("\n timestamp | DTC-Nr. | status | severity\n"); for (uint32_t i = 0; i < MAX_DTC_STORAGE; i++) { if (DTCStorage[i].Number < DTC_LAST_DTC) { sprintf(buff_timestamp, "%02d-%02d:%02d:%02d:%03d", DTCStorage[i].timestamp / 86400000, DTCStorage[i].timestamp / 360000 % 24, DTCStorage[i].timestamp / 60000 % 60, DTCStorage[i].timestamp / 1000 % 60, DTCStorage[i].timestamp % 1000); if (DTCStorage[i].active == DTC_ACTIVE) strcpy(buff_active, "active"); else if (DTCStorage[i].active == DTC_PREVIOUS) strcpy(buff_active, "previous"); else strcpy(buff_active, "none"); Debug_pushMessage("%s %7d %8s %8d\n", buff_timestamp, DTCStorage[i].Number, buff_active, getSeverityForDTC(DTCStorage[i].Number)); } } } 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) { 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 %u pulses\n", n); } 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"); } 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 + '\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] = ' '; } binary_str[j] = '\0'; return binary_str; }