Files
Kettenoeler/Software/src/debugger.cpp
2025-08-24 16:38:36 +02:00

969 lines
33 KiB
C++

/**
* @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 <map>
#include <vector>
#include <functional>
#include <type_traits>
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<String> splitArgs(const String &input)
{
std::vector<String> 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<size_t>(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<void(const void *, const String &)> printer;
};
#define MAX_DEBUG_WATCHES 8
static DebugWatchEntry debugWatches[MAX_DEBUG_WATCHES];
template <typename T>
static void debugPrinterImpl(const void *ptr, const String &name)
{
const T *typed = static_cast<const T *>(ptr);
if constexpr (std::is_same<T, bool>::value)
Debug_pushMessage("%s = %s\n", name.c_str(), *typed ? "true" : "false");
else if constexpr (std::is_floating_point<T>::value)
Debug_pushMessage("%s = %.3f\n", name.c_str(), *typed);
else if constexpr (std::is_signed<T>::value)
Debug_pushMessage("%s = %ld\n", name.c_str(), static_cast<long>(*typed));
else if constexpr (std::is_unsigned<T>::value)
Debug_pushMessage("%s = %lu\n", name.c_str(), static_cast<unsigned long>(*typed));
}
template <typename T>
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<T>};
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 <start> <len>") \
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 <n pulses> (default 10)") \
CMD(wifi_toggle, "Toggle WiFi on/off") \
CMD(dtc_add, "Add DTC <code>") \
CMD(tank_refill, "Set tank to 100%") \
CMD(watch_isr, "Watch globals.isr_debug for 20s") \
/* setters */ \
CMD(set_speed_source, "Set speed source <index|name>") \
CMD(set_can_source, "Set CAN source <index|name>") \
CMD(set_gps_baud, "Set GPS baud <index|name>") \
CMD(set_system_status, "Set system status <index|name>") \
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<CmdEntry> &getCmdRegistry()
{
static const std::vector<CmdEntry> reg = {
#define CMD(name, desc) {#name, &processCmd_##name, desc},
DEBUG_COMMANDS
#undef CMD
};
return reg;
}
static const std::map<String, DebugCmdHandler> &getCmdMap()
{
static std::map<String, DebugCmdHandler> 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 <size_t N>
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<int>(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 <code>\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 <index|name>\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 <index|name>\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 <index|name>\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 <index|name>\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;
}