/** * @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 09.04.2024 */ #include "debugger.h" #include #include #include DebugStatus_t DebuggerStatus[dbg_cntElements]; void processCmdDebug(String command); void Debug_formatCFG(); void Debug_formatPersistence(); void Debug_printSystemInfo(); void Debug_printWifiInfo(); void Debug_CheckEEPOM(bool autocorrect); void Debug_dumpConfig(); void Debug_dumpPersistance(); void Debug_ShowDTCs(); void Debug_dumpGlobals(); void Debug_printHelp(); void Debug_Purge(); const char *uint32_to_binary_string(uint32_t num); /** * @brief Initializes the debugger by setting the initial status for different debug ports. * Serial debug output is turned off. */ void initDebugger() { // 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 { 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) { 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: 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) { 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; } if (InputProcessed != IDLE) Serial.print(">"); InputProcessed = IDLE; } /** * @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)) { 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)); } } } /** * @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, ...) { // 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)); } } } /** * @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) { std::vector tokens; int start = 0, end = 0; while ((end = input.indexOf(' ', start)) != -1) { tokens.push_back(input.substring(start, end)); start = end + 1; } if (start < input.length()) tokens.push_back(input.substring(start)); return tokens; } // === getArg helper === String getArg(const std::vector &args, size_t index, const String &defaultVal = "") { if (index < args.size()) return args[index]; return defaultVal; } // === Command Handler Map === typedef std::function DebugCmdHandler; 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); }}, {"resetPageEE", [](const String &) { MovePersistencePage_EEPROM(true); }}, {"dumpCFG", [](const String &) { Debug_dumpConfig(); }}, {"dumpPDS", [](const String &) { Debug_dumpPersistance(); }}, {"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()); } }} }; return cmdMap; } 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()); } /** * @brief Formats the Config-EEPROM and resets it to default values. * Prints a debug message after formatting. */ void Debug_formatCFG() { Debug_pushMessage("Formatting Config-EEPROM and resetting to default\n"); FormatConfig_EEPROM(); } /** * @brief Formats the Persistence-EEPROM and resets it to default values. * Prints a debug message after formatting. */ void Debug_formatPersistence() { Debug_pushMessage("Formatting Persistence-EEPROM and resetting to default\n"); FormatPersistence_EEPROM(); } /** * @brief Prints system information and status to the debug output. */ void Debug_printSystemInfo() { 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); } /** * @brief Dumps the current configuration parameters to the debug output. */ void Debug_dumpConfig() { 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); } /** * @brief Dumps the global variables and their values to the debug output. */ void Debug_dumpGlobals() { Debug_pushMessage("systemStatus: %d\n", globals.systemStatus); Debug_pushMessage("resumeStatus: %d\n", globals.resumeStatus); Debug_pushMessage("systemStatustxt: %s\n", globals.systemStatustxt); 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("eePersistanceAdress: %d\n", globals.eePersistanceAdress); Debug_pushMessage("TankPercentage: %d\n", globals.TankPercentage); Debug_pushMessage("hasDTC: %d\n", globals.hasDTC); } /** * @brief Dumps the persistence data variables and their values to the debug output. */ void Debug_dumpPersistance() { 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.eePersistanceAdress); } /** * @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_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 // Determine DTC status 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"); // Display DTC information Debug_pushMessage("%s %7d %8s %8d\n", buff_timestamp, DTCStorage[i].Number, buff_active); } } } /** * @brief Start purging for 10 pulses. */ void Debug_Purge() { globals.purgePulses = 10; globals.resumeStatus = globals.systemStatus; globals.systemStatus = sysStat_Purge; Debug_pushMessage("Purging 10 pulses\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. */ const char *uint32_to_binary_string(uint32_t num) { static char binary_str[65]; // 32 bits + 31 spaces + null terminator 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] = '\0'; // Null terminator return binary_str; }