diff --git a/Software/data_src/index.htm b/Software/data_src/index.htm index b0c2483..f295dec 100644 --- a/Software/data_src/index.htm +++ b/Software/data_src/index.htm @@ -191,6 +191,29 @@

+
+

+

CAN / OBD2 Trace

+
+
+
+ + + + +
+ + + Trace inaktiv +
+
+


diff --git a/Software/data_src/static/js/script.js b/Software/data_src/static/js/script.js index 51dadd2..3f9613b 100644 --- a/Software/data_src/static/js/script.js +++ b/Software/data_src/static/js/script.js @@ -23,4 +23,4 @@ document var fileName = document.getElementById("fw-update-file").files[0].name; var nextSibling = e.target.nextElementSibling; nextSibling.innerText = fileName; - }); + }); \ No newline at end of file diff --git a/Software/data_src/static/js/websocket.js b/Software/data_src/static/js/websocket.js index 842d8e1..d910716 100644 --- a/Software/data_src/static/js/websocket.js +++ b/Software/data_src/static/js/websocket.js @@ -5,6 +5,106 @@ var statusMapping; var staticMapping; var overlay; +let traceActive = false; +let traceMode = null; +let traceFileName = ""; +let traceUseFsAccess = false; + +// File-System-Access-Stream (Chromium) +let traceWriter = null; +let traceEncoder = null; +let traceWriteQueue = Promise.resolve(); // für geordnete Writes + +// Fallback: In-Memory-Sammeln (für Blob-Download bei STOP) +let traceMemParts = []; + +// Textarea & Status +const TRACE_MAX_CHARS = 200000; // ~200 KB für die Anzeige + +function $(id) { + return document.getElementById(id); +} + +function $(id) { + return document.getElementById(id); +} +function nowIsoCompact() { + return new Date().toISOString().replace(/[:.]/g, "-"); +} +function genTraceFileName(mode) { + return `cantrace-${mode}-${nowIsoCompact()}.log`; +} + +function setTraceUI(active, mode, infoText) { + traceActive = !!active; + traceMode = active ? mode : null; + + const btnRaw = $("trace-start"); + const btnObd = $("trace-start-obd"); + const btnStop = $("trace-stop"); + const status = $("trace-status"); + + if (btnRaw) btnRaw.disabled = active; + if (btnObd) btnObd.disabled = active; + if (btnStop) btnStop.disabled = !active; + + if (status) + status.textContent = + infoText || (active ? `Trace aktiv (${mode})` : "Trace inaktiv"); +} + +function traceClear() { + const out = $("trace-out"); + if (out) out.value = ""; +} + +function traceAppend(text) { + const out = $("trace-out"); + if (!out || !text) return; + out.value += text; + if (out.value.length > TRACE_MAX_CHARS) { + out.value = out.value.slice(-TRACE_MAX_CHARS); + } + out.scrollTop = out.scrollHeight; +} + +function triggerBlobDownload(filename, blob) { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 0); +} + +function parseKv(s) { + const out = Object.create(null); + s.split(";").forEach((part) => { + const eq = part.indexOf("="); + if (eq > 0) { + const k = part.slice(0, eq).trim(); + const v = part.slice(eq + 1).trim(); + if (k) out[k] = v; + } + }); + return out; +} + +// geordnete Writes auf File System Access Writer +function writeToFs(chunk) { + if (!traceUseFsAccess || !traceWriter) return; + const data = traceEncoder + ? traceEncoder.encode(chunk) + : new TextEncoder().encode(chunk); + traceWriteQueue = traceWriteQueue + .then(() => traceWriter.write(data)) + .catch(console.error); +} + document.addEventListener("DOMContentLoaded", function () { // Ihr JavaScript-Code hier, einschließlich der onLoad-Funktion overlay = document.getElementById("overlay"); @@ -45,16 +145,32 @@ function initSettingInputs() { function onOpen(event) { console.log("Connection opened"); + setTraceUI(false, null, "Verbunden – Trace inaktiv"); } function onClose(event) { console.log("Connection closed"); setTimeout(initWebSocket, 1000); overlay.style.display = "flex"; + + // Falls Trace noch aktiv war: lokal finalisieren + if (traceActive) { + const note = "Trace beendet (Verbindung getrennt)"; + if (traceUseFsAccess && traceWriter) { + traceWriteQueue.then(() => traceWriter.close()).catch(console.error); + traceWriter = null; + } else if (traceMemParts.length) { + const blob = new Blob(traceMemParts, { type: "text/plain" }); + triggerBlobDownload(traceFileName || "cantrace.log", blob); + traceMemParts = []; + } + setTraceUI(false, null, note); + showNotification(note, "warning"); + } } -function sendButton(event) { - var targetElement = event.target; +async function sendButton(event) { + const targetElement = event.target; if ( targetElement.classList.contains("confirm") && @@ -62,7 +178,46 @@ function sendButton(event) { ) return; - websocket_sendevent("btn-" + targetElement.id, targetElement.value); + const wsid = targetElement.dataset.wsid || targetElement.id; // z.B. "trace-start" + const val = targetElement.value || ""; + + // File-Ziel *vor* dem WS-Start öffnen (nur bei trace-start; wegen User-Gesture!) + if (wsid === "trace-start") { + const mode = val || "raw"; + traceFileName = genTraceFileName(mode); + + // Anzeige schon mal leeren + traceClear(); + setTraceUI(false, null, "Trace wird gestartet…"); + + traceUseFsAccess = false; + traceWriter = null; + traceEncoder = null; + traceWriteQueue = Promise.resolve(); + traceMemParts = []; // Fallback-Puffer leeren + + if (window.showSaveFilePicker) { + try { + const fh = await showSaveFilePicker({ + suggestedName: traceFileName, + types: [ + { + description: "Text Log", + accept: { "text/plain": [".log", ".txt"] }, + }, + ], + }); + traceWriter = await fh.createWritable(); + traceEncoder = new TextEncoder(); + traceUseFsAccess = true; + } catch (e) { + // Nutzer hat evtl. abgebrochen → Fallback in RAM + traceUseFsAccess = false; + } + } + } + + websocket_sendevent("btn-" + wsid, val); } function onMessage(event) { @@ -101,6 +256,58 @@ function onMessage(event) { fillValuesToHTML(result); overlay.style.display = "none"; } + // --- Trace: Start --- + else if (data.startsWith("STARTTRACE;")) { + const kv = parseKv(data.slice(11)); // mode=..., ts=... + const mode = kv.mode || "?"; + setTraceUI(true, mode, `Trace gestartet (${mode})`); + + // Fallback: wenn kein FS-Access → in RAM sammeln + // (sonst haben wir traceWriter bereits im Klick vorbereitet) + } + // --- Trace: Lines (ggf. mehrere in einer WS-Nachricht) --- + else if (data.startsWith("TRACELINE;")) { + const payload = data.replace(/TRACELINE;/g, ""); // reiner Text inkl. '\n' + traceAppend(payload); + if (traceUseFsAccess && traceWriter) { + writeToFs(payload); + } else { + traceMemParts.push(payload); + } + } + // --- Trace: Stop/Summary --- + else if (data.startsWith("STOPTRACE;")) { + const kv = parseKv(data.slice(10)); + const msg = `Trace beendet (${kv.mode || "?"}), Zeilen=${ + kv.lines || "0" + }, Drops=${kv.drops || "0"}${kv.reason ? ", Grund=" + kv.reason : ""}`; + + // Datei finalisieren + if (traceUseFsAccess && traceWriter) { + traceWriteQueue.then(() => traceWriter.close()).catch(console.error); + traceWriter = null; + } else if (traceMemParts.length) { + const blob = new Blob(traceMemParts, { type: "text/plain" }); + triggerBlobDownload(traceFileName || "cantrace.log", blob); + traceMemParts = []; + } + + setTraceUI(false, null, msg); + showNotification(msg, "info"); + } + // --- Busy/Fehler/Ack --- + else if (data.startsWith("TRACEBUSY;")) { + const kv = parseKv(data.slice(10)); + const owner = kv.owner ? " (Owner #" + kv.owner + ")" : ""; + showNotification("Trace bereits aktiv" + owner, "warning"); + } else if (data.startsWith("TRACEERROR;")) { + const kv = parseKv(data.slice(11)); + showNotification("Trace-Fehler: " + (kv.msg || "unbekannt"), "danger"); + } else if (data.startsWith("TRACEACK;")) { + // optional + const kv = parseKv(data.slice(9)); + console.log("TRACEACK", kv); + } } function createMapping(mappingString) { diff --git a/Software/data_src/version b/Software/data_src/version index 2ee5615..ef2d578 100644 --- a/Software/data_src/version +++ b/Software/data_src/version @@ -1 +1 @@ -1.04 \ No newline at end of file +1.05 \ No newline at end of file diff --git a/Software/include/can_hal.h b/Software/include/can_hal.h index f74b3f3..3eb6878 100644 --- a/Software/include/can_hal.h +++ b/Software/include/can_hal.h @@ -23,9 +23,23 @@ struct CanHalConfig { // ==== Universeller Filter-Descriptor ==== struct CanFilter { uint32_t id; // 11-bit oder 29-bit Roh-ID - bool ext; // false=STD(11-bit), true=EXT(29-bit) + bool ext; // false = STD(11-bit), true = EXT(29-bit) }; +// ===================== +// Trace / Logging Types +// ===================== +struct CanLogFrame { + uint32_t ts_ms; + uint32_t id; + bool ext; + bool rx; // true = RX, false = TX + uint8_t dlc; + uint8_t data[8]; +}; + +using CanTraceSink = void (*)(const CanLogFrame& f); + // ==== API ==== // 1) Einmalige Hardware-Initialisierung + integrierter Loopback-Selftest. @@ -51,9 +65,14 @@ bool CAN_HAL_AddFilter(const CanFilter& f); bool CAN_HAL_SetFilters(const CanFilter* list, size_t count); // Non-blocking IO -bool CAN_HAL_Read(unsigned long& id, uint8_t& len, uint8_t data[8]); // true=Frame gelesen +bool CAN_HAL_Read(unsigned long& id, uint8_t& len, uint8_t data[8]); // true = Frame gelesen uint8_t CAN_HAL_Send(unsigned long id, bool ext, uint8_t len, const uint8_t* data); // CAN_OK bei Erfolg -// Diagnose/Utilities (nutzen MCP-APIs) -uint8_t CAN_HAL_GetErrorFlags(); // Intern: getError() +// Diagnose/Utilities +uint8_t CAN_HAL_GetErrorFlags(); // Intern: getError() void CAN_HAL_GetErrorCounters(uint8_t& tec, uint8_t& rec); // TX/RX Error Counter + +// Trace / Sniffer +void CAN_HAL_SetTraceSink(CanTraceSink sink); +void CAN_HAL_EnableRawSniffer(bool enable); +bool CAN_HAL_IsRawSnifferEnabled(); diff --git a/Software/include/can_obd2.h b/Software/include/can_obd2.h index de8fd82..411c65a 100644 --- a/Software/include/can_obd2.h +++ b/Software/include/can_obd2.h @@ -1,6 +1,5 @@ #pragma once #include -#include "can_hal.h" // Initialisiert das OBD2-CAN-Profil: // - setzt Masken/Filter für 0x7E8..0x7EF (ECU-Antworten) diff --git a/Software/include/webui.h b/Software/include/webui.h index d7afe7a..69ef81a 100644 --- a/Software/include/webui.h +++ b/Software/include/webui.h @@ -46,4 +46,6 @@ void Webserver_Shutdown(); void Websocket_PushLiveDebug(String Message); void Websocket_PushNotification(String Message, NotificationType_t type); +void TRACE_OnObdFrame(uint32_t id, bool rx, const uint8_t *d, uint8_t dlc, const char *note); + #endif // _WEBUI_H_ diff --git a/Software/src/can_hal.cpp b/Software/src/can_hal.cpp index f9349a7..c2f920d 100644 --- a/Software/src/can_hal.cpp +++ b/Software/src/can_hal.cpp @@ -1,24 +1,42 @@ #include "can_hal.h" #include "dtc.h" -// #include "debugger.h" // optional für Logs +#include // memcpy, memcmp -// ==== Interner Zustand/Helper ==== +// ===================== +// Interner Zustand/Helper +// ===================== MCP_CAN CAN0(GPIO_CS_CAN); -static bool s_ready = false; +static bool s_ready = false; static uint8_t s_nextFiltSlot = 0; // 0..5 (MCP2515 hat 6 Filter-Slots) static uint16_t s_modeSettleMs = 10; // Default aus Config +// Trace-Hook +static CanTraceSink s_traceSink = nullptr; + +// RAW-Sniffer-Steuerung (Filter offen + Restore der vorherigen Konfig) +static bool s_rawSnifferEnabled = false; + +// Spiegel der "Normal"-Konfiguration (damit wir nach RAW wiederherstellen können) +static uint16_t s_savedStdMask[2] = {0x000, 0x000}; +static struct +{ + uint32_t id; + bool ext; +} s_savedFilt[6]; +static uint8_t s_savedFiltCount = 0; + // 11-bit: Lib erwartet (value << 16) static inline uint32_t _std_to_hw(uint16_t v11) { return ((uint32_t)v11) << 16; } // „Bestätigter“ Mode-Wechsel mithilfe der Lib-Funktion setMode(newMode) -// Viele Forks nutzen intern mcp2515_requestNewMode(); wir retryen kurz. static bool _trySetMode(uint8_t mode, uint16_t settleMs) { const uint32_t t0 = millis(); - do { - if (CAN0.setMode(mode) == CAN_OK) return true; + do + { + if (CAN0.setMode(mode) == CAN_OK) + return true; delay(1); } while ((millis() - t0) < settleMs); return false; @@ -27,22 +45,32 @@ static bool _trySetMode(uint8_t mode, uint16_t settleMs) // LOOPBACK-Selftest (ohne Bus) static bool _selftest_loopback(uint16_t windowMs) { - if (!_trySetMode(MCP_LOOPBACK, s_modeSettleMs)) return false; + if (!_trySetMode(MCP_LOOPBACK, s_modeSettleMs)) + return false; const unsigned long tid = 0x123; uint8_t tx[8] = {0xA5, 0x5A, 0x11, 0x22, 0x33, 0x44, 0x77, 0x88}; - if (CAN0.sendMsgBuf(tid, 0, 8, tx) != CAN_OK) { + if (CAN0.sendMsgBuf(tid, 0, 8, tx) != CAN_OK) + { (void)_trySetMode(MCP_NORMAL, s_modeSettleMs); return false; } bool got = false; const uint32_t t0 = millis(); - while ((millis() - t0) < windowMs) { - if (CAN0.checkReceive() == CAN_MSGAVAIL) { - unsigned long rid; uint8_t len, rx[8]; - if (CAN0.readMsgBuf(&rid, &len, rx) == CAN_OK) { - if (rid == tid && len == 8 && memcmp(tx, rx, 8) == 0) { got = true; break; } + while ((millis() - t0) < windowMs) + { + if (CAN0.checkReceive() == CAN_MSGAVAIL) + { + unsigned long rid; + uint8_t len, rx[8]; + if (CAN0.readMsgBuf(&rid, &len, rx) == CAN_OK) + { + if (rid == tid && len == 8 && memcmp(tx, rx, 8) == 0) + { + got = true; + break; + } } } delay(1); @@ -55,60 +83,153 @@ static bool _selftest_loopback(uint16_t windowMs) // Optional: kurzer ListenOnly-Hörtest (nur Heuristik, keine DTC-Änderung) static void _probe_listen_only(uint16_t ms) { - if (ms == 0) return; - if (!_trySetMode(MCP_LISTENONLY, s_modeSettleMs)) return; + if (ms == 0) + return; + if (!_trySetMode(MCP_LISTENONLY, s_modeSettleMs)) + return; const uint32_t t0 = millis(); - while ((millis() - t0) < ms) { - if (CAN0.checkReceive() == CAN_MSGAVAIL) break; + while ((millis() - t0) < ms) + { + if (CAN0.checkReceive() == CAN_MSGAVAIL) + break; delay(1); } (void)_trySetMode(MCP_NORMAL, s_modeSettleMs); } -// ==== Öffentliche API ==== - -bool CAN_HAL_Init(const CanHalConfig& cfg) +// Offen konfigurieren (RAW-Sniffer) +static bool _apply_open_filters() { - s_ready = false; - s_modeSettleMs = cfg.modeSettleMs ? cfg.modeSettleMs : 10; - - // 1) SPI/MCP starten - // HIER: MCP_STDEXT statt MCP_STD, damit die Lib nicht ins default/Failure läuft - if (CAN0.begin(MCP_STDEXT, cfg.baud, cfg.clock) != CAN_OK) { - MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); + if (!_trySetMode(MODE_CONFIG, s_modeSettleMs)) return false; - } - // 2) Loopback‑Selftest (ohne Bus) - if (!_selftest_loopback(20)) { - MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); - return false; - } - - // 3) Optional Listen‑Only‑Probe (nur Info) - _probe_listen_only(cfg.listenOnlyProbeMs); - - // 4) Default: Filter/Masks neutral, Mode NORMAL - // -> Für Masken/Filter müssen wir in CONFIG sein (hier: MODE_CONFIG laut deiner Lib) - if (!_trySetMode(MODE_CONFIG, s_modeSettleMs)) { - MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); - return false; - } - - // weit offen (STD) + // Masken 0 -> alles durchlassen CAN0.init_Mask(0, 0, _std_to_hw(0x000)); CAN0.init_Mask(1, 0, _std_to_hw(0x000)); - for (uint8_t i = 0; i < 6; ++i) { + // Filter egal + for (uint8_t i = 0; i < 6; ++i) + { CAN0.init_Filt(i, 0, _std_to_hw(0x000)); } s_nextFiltSlot = 0; - if (!_trySetMode(MCP_NORMAL, s_modeSettleMs)) { + return _trySetMode(MCP_NORMAL, s_modeSettleMs); +} + +// Gespeicherte Normal-Konfiguration anwenden +static bool _apply_saved_filters() +{ + if (!_trySetMode(MODE_CONFIG, s_modeSettleMs)) + return false; + + CAN0.init_Mask(0, 0, _std_to_hw(s_savedStdMask[0])); + CAN0.init_Mask(1, 0, _std_to_hw(s_savedStdMask[1])); + + // Erst alle Filter neutralisieren + for (uint8_t i = 0; i < 6; ++i) + { + CAN0.init_Filt(i, 0, _std_to_hw(0x000)); + } + + // Dann gespeicherte Filter wieder setzen + s_nextFiltSlot = 0; + for (uint8_t i = 0; i < s_savedFiltCount && s_nextFiltSlot < 6; ++i) + { + const auto &F = s_savedFilt[i]; + const uint32_t hwId = F.ext ? F.id : _std_to_hw((uint16_t)F.id); + CAN0.init_Filt(s_nextFiltSlot++, F.ext ? 1 : 0, hwId); + } + + return _trySetMode(MCP_NORMAL, s_modeSettleMs); +} + +// ===================== +// Öffentliche API +// ===================== + +void CAN_HAL_SetTraceSink(CanTraceSink sink) +{ + s_traceSink = sink; +} + +void CAN_HAL_EnableRawSniffer(bool enable) +{ + if (enable == s_rawSnifferEnabled) + return; + + if (enable) + { + // Auf RAW öffnen + if (_apply_open_filters()) + { + s_rawSnifferEnabled = true; + } + else + { + // Falls es nicht klappt, lieber Defekt melden als im Zwischending zu bleiben + MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); + } + } + else + { + // Gespeicherte "Normal"-Konfiguration wieder aktivieren + if (_apply_saved_filters()) + { + s_rawSnifferEnabled = false; + } + else + { + MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); + } + } +} + +bool CAN_HAL_IsRawSnifferEnabled() +{ + return s_rawSnifferEnabled; +} + +bool CAN_HAL_Init(const CanHalConfig &cfg) +{ + s_ready = false; + s_modeSettleMs = cfg.modeSettleMs ? cfg.modeSettleMs : 10; + s_traceSink = nullptr; + s_rawSnifferEnabled = false; + + // 1) SPI/MCP starten (STDEXT ist robust gegen Fehlpfade in Lib-Forks) + if (CAN0.begin(MCP_STDEXT, cfg.baud, cfg.clock) != CAN_OK) + { MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return false; } - // Erfolgreich + // 2) Loopback-Selftest (ohne Bus) + if (!_selftest_loopback(20)) + { + MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); + return false; + } + + // 3) Optional Listen-Only-Probe (nur Info) + _probe_listen_only(cfg.listenOnlyProbeMs); + + // 4) Default: Filter/Masks offen, Mode NORMAL + if (!_apply_open_filters()) + { + MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); + return false; + } + + // Initiale "Normal"-Spiegelung: alles offen (bis die App später echte Filter setzt) + s_savedStdMask[0] = 0x000; + s_savedStdMask[1] = 0x000; + s_savedFiltCount = 0; + for (uint8_t i = 0; i < 6; ++i) + { + s_savedFilt[i].id = 0; + s_savedFilt[i].ext = false; + } + MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, false); s_ready = true; return true; @@ -119,18 +240,30 @@ bool CAN_HAL_IsReady() { return s_ready; } bool CAN_HAL_SetMode(uint8_t mode) { const bool ok = _trySetMode(mode, s_modeSettleMs); - if (!ok) MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); + if (!ok) + MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return ok; } bool CAN_HAL_SetMask(uint8_t bank, bool ext, uint32_t rawMask) { - if (bank > 1) return false; - if (!CAN_HAL_SetMode(MODE_CONFIG)) return false; + if (bank > 1) + return false; + + if (!CAN_HAL_SetMode(MODE_CONFIG)) + return false; const bool ok = (CAN0.init_Mask(bank, ext ? 1 : 0, rawMask) == CAN_OK); - if (!CAN_HAL_SetMode(MCP_NORMAL)) { + // Spiegeln (nur STD-11 Spiegel führen wir – ext-Masken selten; bei ext ignorieren) + if (!ext) + { + const uint16_t m11 = (uint16_t)(rawMask >> 16); + s_savedStdMask[bank] = m11; + } + + if (!CAN_HAL_SetMode(MCP_NORMAL)) + { MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return false; } @@ -144,85 +277,170 @@ bool CAN_HAL_SetStdMask11(uint8_t bank, uint16_t mask11) void CAN_HAL_ClearFilters() { - if (!CAN_HAL_SetMode(MODE_CONFIG)) { + if (!CAN_HAL_SetMode(MODE_CONFIG)) + { MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return; } CAN0.init_Mask(0, 0, _std_to_hw(0x000)); CAN0.init_Mask(1, 0, _std_to_hw(0x000)); - for (uint8_t i = 0; i < 6; ++i) { + for (uint8_t i = 0; i < 6; ++i) + { CAN0.init_Filt(i, 0, _std_to_hw(0x000)); } s_nextFiltSlot = 0; - if (!CAN_HAL_SetMode(MCP_NORMAL)) { + // Spiegel auch zurücksetzen + s_savedStdMask[0] = 0x000; + s_savedStdMask[1] = 0x000; + s_savedFiltCount = 0; + for (uint8_t i = 0; i < 6; ++i) + { + s_savedFilt[i].id = 0; + s_savedFilt[i].ext = false; + } + + if (!CAN_HAL_SetMode(MCP_NORMAL)) + { MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); } } -bool CAN_HAL_AddFilter(const CanFilter& f) +bool CAN_HAL_AddFilter(const CanFilter &f) { - if (s_nextFiltSlot >= 6) return false; - if (!CAN_HAL_SetMode(MODE_CONFIG)) { + if (s_nextFiltSlot >= 6) + return false; + if (!CAN_HAL_SetMode(MODE_CONFIG)) + { MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return false; } const uint32_t hwId = f.ext ? f.id : _std_to_hw((uint16_t)f.id); - const uint8_t slot = s_nextFiltSlot++; - const bool ok = (CAN0.init_Filt(slot, f.ext ? 1 : 0, hwId) == CAN_OK); + const uint8_t slot = s_nextFiltSlot++; + const bool ok = (CAN0.init_Filt(slot, f.ext ? 1 : 0, hwId) == CAN_OK); - if (!CAN_HAL_SetMode(MCP_NORMAL)) { + // Spiegeln + if (ok) + { + if (s_savedFiltCount < 6) + { + s_savedFilt[s_savedFiltCount].id = f.ext ? f.id : (uint32_t)((uint16_t)f.id); + s_savedFilt[s_savedFiltCount].ext = f.ext; + ++s_savedFiltCount; + } + } + + if (!CAN_HAL_SetMode(MCP_NORMAL)) + { MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return false; } return ok; } -bool CAN_HAL_SetFilters(const CanFilter* list, size_t count) +bool CAN_HAL_SetFilters(const CanFilter *list, size_t count) { - if (!CAN_HAL_SetMode(MODE_CONFIG)) { + if (!CAN_HAL_SetMode(MODE_CONFIG)) + { MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return false; } // Slots zurücksetzen s_nextFiltSlot = 0; - for (uint8_t i = 0; i < 6; ++i) { + for (uint8_t i = 0; i < 6; ++i) + { CAN0.init_Filt(i, 0, _std_to_hw(0x000)); } // Setzen - for (size_t i = 0; i < count && s_nextFiltSlot < 6; ++i) { - const auto& f = list[i]; + for (size_t i = 0; i < count && s_nextFiltSlot < 6; ++i) + { + const auto &f = list[i]; const uint32_t hwId = f.ext ? f.id : _std_to_hw((uint16_t)f.id); CAN0.init_Filt(s_nextFiltSlot++, f.ext ? 1 : 0, hwId); } - if (!CAN_HAL_SetMode(MCP_NORMAL)) { + // Spiegel aktualisieren + s_savedFiltCount = 0; + for (size_t i = 0; i < count && i < 6; ++i) + { + s_savedFilt[i].id = list[i].ext ? list[i].id : (uint32_t)((uint16_t)list[i].id); + s_savedFilt[i].ext = list[i].ext; + ++s_savedFiltCount; + } + + if (!CAN_HAL_SetMode(MCP_NORMAL)) + { MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return false; } return true; } -bool CAN_HAL_Read(unsigned long& id, uint8_t& len, uint8_t data[8]) +bool CAN_HAL_Read(unsigned long &id, uint8_t &len, uint8_t data[8]) { - if (CAN0.checkReceive() != CAN_MSGAVAIL) return false; - if (CAN0.readMsgBuf(&id, &len, data) != CAN_OK) { + if (CAN0.checkReceive() != CAN_MSGAVAIL) + return false; + + if (CAN0.readMsgBuf(&id, &len, data) != CAN_OK) + { // Echte Lese-Fehler -> vermutlich SPI/Controller-Problem MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); return false; } + + // MCP_CAN schreibt Flags in das ID-Wort: + // bit31 = EXT, bit30 = RTR, Rest = rohe ID (11 oder 29 Bit) + const bool ext = (id & 0x80000000UL) != 0; + const bool rtr = (id & 0x40000000UL) != 0; // aktuell nur informativ + + // "Saubere" ID für Aufrufer herstellen + const uint32_t clean_id = ext ? (id & 0x1FFFFFFFUL) : (id & 0x7FFUL); + id = clean_id; + + // Trace-Hook (RX) + if (s_traceSink) + { + CanLogFrame f{}; + f.ts_ms = millis(); + f.id = clean_id; + f.ext = ext; + f.rx = true; + f.dlc = len; + if (len) + memcpy(f.data, data, len); + s_traceSink(f); + } + + // Optional: Wenn du RTR-Frames speziell behandeln willst, könntest du hier + // (rtr==true) markieren/loggen oder len=0 erzwingen. Für jetzt: einfach durchreichen. + (void)rtr; + return true; } -uint8_t CAN_HAL_Send(unsigned long id, bool ext, uint8_t len, const uint8_t* data) +uint8_t CAN_HAL_Send(unsigned long id, bool ext, uint8_t len, const uint8_t *data) { - // Sende-Fehler (CAN_FAILTX) müssen nicht zwingend Transceiver-Defekte sein (z. B. Bus-Off). - // Höhere Ebene kann bei Bedarf DTCs setzen. Hier nur durchreichen. - return CAN0.sendMsgBuf(id, ext ? 1 : 0, len, const_cast(data)); + // Senden + uint8_t st = CAN0.sendMsgBuf(id, ext ? 1 : 0, len, const_cast(data)); + + // Trace-Hook (TX) nur bei Erfolg loggen – optional: immer loggen + if (st == CAN_OK && s_traceSink) + { + CanLogFrame f{}; + f.ts_ms = millis(); + f.id = id; + f.ext = ext; + f.rx = false; + f.dlc = len; + if (len) + memcpy(f.data, data, len); + s_traceSink(f); + } + return st; } // ==== Diagnose/Utilities ==== @@ -233,7 +451,7 @@ uint8_t CAN_HAL_GetErrorFlags() return CAN0.getError(); } -void CAN_HAL_GetErrorCounters(uint8_t& tec, uint8_t& rec) +void CAN_HAL_GetErrorCounters(uint8_t &tec, uint8_t &rec) { tec = CAN0.errorCountTX(); rec = CAN0.errorCountRX(); diff --git a/Software/src/can_obd2.cpp b/Software/src/can_obd2.cpp index fbe0386..b40f3ae 100644 --- a/Software/src/can_obd2.cpp +++ b/Software/src/can_obd2.cpp @@ -1,7 +1,12 @@ #include "can_obd2.h" +#include "can_hal.h" #include "dtc.h" #include "debugger.h" -#include "globals.h" // falls du später Einstellungen brauchst +#include "globals.h" +#include + +// Trace-Sink aus webui.cpp (o.ä.) +extern void TRACE_OnObdFrame(uint32_t id, bool rx, const uint8_t *d, uint8_t dlc, const char *note); // ======================= // Konfiguration (anpassbar) @@ -14,7 +19,7 @@ // Antwort-Timeout auf eine einzelne Anfrage #ifndef OBD2_RESP_TIMEOUT_MS -#define OBD2_RESP_TIMEOUT_MS 60 // ~60 ms +#define OBD2_RESP_TIMEOUT_MS 120 // etwas großzügiger für reale ECUs #endif // Wenn so lange keine valide Antwort kam, gilt die Geschwindigkeit als stale -> v=0 @@ -32,11 +37,22 @@ #define OBD2_DEBUG_INTERVAL_MS 1000 #endif +// Max. Delta-Zeit fürs Weg-Integrationsglied (Ausreißer-Klemme) +#ifndef OBD2_MAX_DT_MS +#define OBD2_MAX_DT_MS 200 +#endif + +// Erlaube einmaligen Fallback von funktionaler (0x7DF) auf physische Adresse (0x7E0) +#ifndef OBD2_ALLOW_PHYSICAL_FALLBACK +#define OBD2_ALLOW_PHYSICAL_FALLBACK 1 +#endif + // ======================= // OBD-II IDs (11-bit) // ======================= -static constexpr uint16_t OBD_REQ_ID = 0x7DF; // Broadcast-Request -static constexpr uint16_t OBD_RESP_MIN = 0x7E8; // ECUs antworten 0x7E8..0x7EF +static constexpr uint16_t OBD_REQ_ID_FUNCTIONAL = 0x7DF; // Broadcast-Request +static constexpr uint16_t OBD_REQ_ID_PHYSICAL = 0x7E0; // Engine ECU (Antwort 0x7E8) +static constexpr uint16_t OBD_RESP_MIN = 0x7E8; // ECUs antworten 0x7E8..0x7EF static constexpr uint16_t OBD_RESP_MAX = 0x7EF; // ======================= @@ -79,7 +95,7 @@ static inline void maybeDebug(uint32_t now, const char *fmt, ...) s_lastDbgMs = now; va_list ap; va_start(ap, fmt); - Debug_pushMessage(fmt, ap); + Debug_pushMessage(fmt, ap); // nimmt va_list va_end(ap); #else (void)now; @@ -113,15 +129,17 @@ bool Init_CAN_OBD2() CAN_HAL_SetStdMask11(0, 0x7F0); CAN_HAL_SetStdMask11(1, 0x7F0); - CanFilter flist[6] = { + CanFilter flist[8] = { {0x7E8, false}, {0x7E9, false}, {0x7EA, false}, {0x7EB, false}, {0x7EC, false}, {0x7ED, false}, + {0x7EE, false}, + {0x7EF, false}, }; - CAN_HAL_SetFilters(flist, 6); + CAN_HAL_SetFilters(flist, 8); CAN_HAL_SetMode(MCP_NORMAL); @@ -151,7 +169,11 @@ uint32_t Process_CAN_OBD2_Speed() if (s_state == ObdState::Idle && (now - s_lastQueryTime) >= OBD2_QUERY_INTERVAL_MS) { uint8_t req[8] = {0x02, 0x01, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00}; // Mode 01, PID 0x0D (Speed) - const uint8_t st = CAN_HAL_Send(OBD_REQ_ID, /*ext=*/false, 8, req); + + // Trace: geplanter Request (functional) + TRACE_OnObdFrame(OBD_REQ_ID_FUNCTIONAL, /*rx=*/false, req, 8, "req 01 0D (functional)"); + + uint8_t st = CAN_HAL_Send(OBD_REQ_ID_FUNCTIONAL, /*ext=*/false, 8, req); s_lastQueryTime = now; if (st == CAN_OK) @@ -161,9 +183,23 @@ uint32_t Process_CAN_OBD2_Speed() } else { - // Senden fehlgeschlagen -> harter Timeout-DTC - MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true); - maybeDebug(now, "OBD2-CAN send failed (%u)\n", st); +#if OBD2_ALLOW_PHYSICAL_FALLBACK + // einmalig physisch versuchen (0x7E0 → Antwort 0x7E8) + TRACE_OnObdFrame(OBD_REQ_ID_PHYSICAL, /*rx=*/false, req, 8, "req 01 0D (physical)"); + st = CAN_HAL_Send(OBD_REQ_ID_PHYSICAL, /*ext=*/false, 8, req); + s_lastQueryTime = now; + if (st == CAN_OK) + { + s_state = ObdState::Waiting; + s_requestDeadline = now + OBD2_RESP_TIMEOUT_MS; + } + else +#endif + { + // Senden fehlgeschlagen -> harter Timeout-DTC + MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true); + maybeDebug(now, "OBD2-CAN send failed (%u)\n", st); + } } } @@ -182,9 +218,9 @@ uint32_t Process_CAN_OBD2_Speed() // Erwartete Formate: // - Einfache Antwort: 0x41 0x0D ... - // - Mit Längen-Byte: 0x03 0x41 0x0D ... + // - Mit Längen-Byte: 0x03/0x04 0x41 0x0D ... uint8_t modeResp = 0, pid = 0, speedKmh = 0; - if (rx[0] == 0x03 && len >= 4 && rx[1] == 0x41 && rx[2] == 0x0D) + if ((rx[0] == 0x03 || rx[0] == 0x04) && len >= 4 && rx[1] == 0x41 && rx[2] == 0x0D) { modeResp = rx[1]; pid = rx[2]; @@ -198,7 +234,9 @@ uint32_t Process_CAN_OBD2_Speed() } else { - continue; // anderes PID/Format ignorieren + // Nicht das gesuchte PID → optional trotzdem loggen + TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, "other OBD resp"); + continue; } if (modeResp == 0x41 && pid == 0x0D) @@ -211,10 +249,19 @@ uint32_t Process_CAN_OBD2_Speed() MaintainDTC(DTC_OBD2_CAN_TIMEOUT, false); MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, false); + char note[40]; + snprintf(note, sizeof(note), "speed=%ukmh", (unsigned)speedKmh); + TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, note); + maybeDebug(now, "OBD2 speed: %u km/h (%lu mm/s)\n", (unsigned)speedKmh, (unsigned long)s_lastSpeedMMps); break; // eine valide Antwort pro Zyklus reicht } + else + { + // ist zwar OBD-II Antwort, aber nicht unser PID – optional loggen + TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, "other OBD resp"); + } } // 3) Offene Anfrage: Timeout prüfen @@ -223,13 +270,17 @@ uint32_t Process_CAN_OBD2_Speed() // Keine passende Antwort erhalten MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, true); s_state = ObdState::Idle; + TRACE_OnObdFrame(0x000, /*rx=*/true, nullptr, 0, "timeout 01 0D"); } // 4) Integration (mm) über dt uint32_t add_mm = 0; if (s_lastIntegrateMs == 0) s_lastIntegrateMs = now; - const uint32_t dt_ms = now - s_lastIntegrateMs; + + uint32_t raw_dt = now - s_lastIntegrateMs; + if (raw_dt > OBD2_MAX_DT_MS) raw_dt = OBD2_MAX_DT_MS; // Ausreißer klemmen + const uint32_t dt_ms = raw_dt; s_lastIntegrateMs = now; // Stale-Schutz: wenn lange keine Antwort -> v=0 diff --git a/Software/src/webui.cpp b/Software/src/webui.cpp index b4926cf..a924dc9 100644 --- a/Software/src/webui.cpp +++ b/Software/src/webui.cpp @@ -13,11 +13,13 @@ #include "webui.h" #include "common.h" +#include "can_hal.h" // <-- für CanLogFrame, Trace-Sink #include // std::unique_ptr #include // strlen, strncpy, memcpy #include // std::clamp AsyncWebServer webServer(80); +AsyncWebSocket webSocket("/ws"); const char *PARAM_MESSAGE = "message"; @@ -52,8 +54,6 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f void WebServerEEJSON_Callback(AsyncWebServerRequest *request); void GetFlashVersion(char *buff, size_t buff_size); -AsyncWebSocket webSocket("/ws"); - void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len); void Websocket_RefreshClientData_DTCs(uint32_t client_id); @@ -65,7 +65,10 @@ void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierB int findIndexByString(const char *searchString, const char *const *array, int arraySize); // ---------- small helpers (safety) ---------- -static inline const char *nz(const char *p) { return p ? p : ""; } +static inline const char *nz(const char *p) +{ + return p ? p : ""; +} static inline String tableStr(const char *const *tbl, int idx, int size) { @@ -92,6 +95,290 @@ static inline bool validIndex(int idx, int size) return idx >= 0 && idx < size; } +// ===================================================================== +// WebSocket-basierter Trace +// ===================================================================== + +enum class TraceMode +{ + None, + Raw, + Obd +}; + +static TraceMode g_traceMode = TraceMode::None; +static uint32_t g_traceOwnerId = 0; // WS-Client-ID des Starters +static uint32_t g_traceStartMs = 0; +static uint32_t g_traceLines = 0; +static uint32_t g_traceDrops = 0; + +// Aktueller WS-Client während WS_EVT_DATA (für HandleMessage) +static AsyncWebSocketClient *g_wsCurrentClient = nullptr; + +// Ringpuffer (verlusttolerant) +// ---- Dynamischer Ringpuffer, um BSS klein zu halten ---- +#ifndef TRACE_FMT_BUFSZ +#define TRACE_FMT_BUFSZ 128 // Puffer für ASCII-Zeile (unabhängig vom Ring) +#endif +#ifndef TRACE_DEFAULT_LINES +#define TRACE_DEFAULT_LINES 64 // 64 x 128 = 8KB +#endif +#ifndef TRACE_DEFAULT_LINE_MAX +#define TRACE_DEFAULT_LINE_MAX 128 +#endif + +static char *g_ring = nullptr; // contiguous: lines * lineSize +static uint16_t g_ringLines = 0; +static uint16_t g_lineSize = 0; +static uint16_t g_head = 0, g_tail = 0; + +static inline bool ring_alloc(uint16_t lines, uint16_t lineSize) +{ + size_t bytes = (size_t)lines * (size_t)lineSize; + g_ring = (char *)malloc(bytes); + if (!g_ring) + return false; + g_ringLines = lines; + g_lineSize = lineSize; + g_head = g_tail = 0; + return true; +} +static inline void ring_free() +{ + if (g_ring) + free(g_ring); + g_ring = nullptr; + g_ringLines = 0; + g_lineSize = 0; + g_head = g_tail = 0; +} +static inline bool ring_empty() { return g_head == g_tail; } +static inline bool ring_full() { return (uint16_t)(g_head + 1) == g_tail; } + +static inline char *ring_slot(uint16_t idx) { return g_ring + ((idx % g_ringLines) * g_lineSize); } + +static inline void ring_push_line(const char *s) +{ + if (!g_ring) + return; + if (ring_full()) + { + g_tail++; + g_traceDrops++; + } + char *dst = ring_slot(g_head); + strncpy(dst, s, g_lineSize - 1); + dst[g_lineSize - 1] = '\0'; + g_head++; +} +static inline const char *ring_front() { return ring_empty() ? "" : ring_slot(g_tail); } +static inline void ring_pop() +{ + if (!ring_empty()) + g_tail++; +} + +// Fallback: direkt senden, falls kein Ring (zu wenig RAM) +static inline void trace_emit_line_direct_or_ring(const char *s) +{ + if (g_ring) + { // wenn Ring existiert -> dort ablegen + ring_push_line(s); + return; + } + // sonst: direkt an den Owner senden (Best-Effort) + if (g_traceOwnerId && webSocket.availableForWrite(g_traceOwnerId)) + { + String payload; + payload.reserve(strlen(s) + 16); + payload += "TRACELINE;"; + payload += s; + payload += "\n"; + webSocket.text(g_traceOwnerId, payload); + } + else + { + g_traceDrops++; + } +} + +// ASCII-Formatter (ID 3-stellig 11-bit, 8-stellig 29-bit) +static void TRACE_FormatLine(char *dst, size_t n, const CanLogFrame &f, const char *note) +{ + int off = snprintf(dst, n, "%lu %s 0x%0*lX %u ", + (unsigned long)f.ts_ms, + f.rx ? "RX" : "TX", + f.ext ? 8 : 3, (unsigned long)f.id, + f.dlc); + for (uint8_t i = 0; i < f.dlc && off < (int)n - 3; i++) + off += snprintf(dst + off, n - off, "%02X ", f.data[i]); + if (note && *note) + snprintf(dst + off, n - off, "; %s", note); +} + +// Sinks --------------------------------------------------------------- + +// RAW (wird vom HAL bei RX/TX gerufen) +static void TRACE_SinkRaw(const CanLogFrame &f) +{ + if (g_traceMode != TraceMode::Raw || g_traceOwnerId == 0) + return; + char buf[TRACE_FMT_BUFSZ]; + TRACE_FormatLine(buf, sizeof(buf), f, nullptr); + trace_emit_line_direct_or_ring(buf); + g_traceLines++; +} +/** + * OBD-Trace-Hook: + * Wird von can_obd2.cpp aufgerufen, um einzelne OBD-Frames (ASCII) an den + * WebSocket-Trace zu übergeben. Implementierung liegt in webui.cpp. + * + * @param id CAN-ID (11-bit bei OBD) + * @param rx true=Empfangen, false=Gesendet + * @param d Datenpointer (kann nullptr sein, wenn dlc==0) + * @param dlc Datenlänge (0..8) + * @param note optionale Zusatznotiz (z.B. "Mode01 PID 0x0D") + */ +void TRACE_OnObdFrame(uint32_t id, bool rx, const uint8_t *d, uint8_t dlc, const char *note) +{ + if (g_traceMode != TraceMode::Obd || g_traceOwnerId == 0) + return; + CanLogFrame f{}; + f.ts_ms = millis(); + f.id = id; + f.ext = false; + f.rx = rx; + f.dlc = dlc; + if (d && dlc) + memcpy(f.data, d, dlc); + char buf[TRACE_FMT_BUFSZ]; + TRACE_FormatLine(buf, sizeof(buf), f, note); + trace_emit_line_direct_or_ring(buf); + g_traceLines++; +} + +// Owner noch da? +static inline bool trace_owner_online() +{ + return (g_traceOwnerId != 0) && webSocket.hasClient(g_traceOwnerId); +} + +// Pump: gebündelt senden, Backpressure beachten +static void TRACE_PumpWs() +{ + if (!g_ring) + return; // bei direktem Senden gibt's keinen Ring zu pumpen + if (g_traceMode == TraceMode::None || g_traceOwnerId == 0) + return; + if (!trace_owner_online()) + return; + if (!webSocket.availableForWrite(g_traceOwnerId)) + return; + + String payload; + payload.reserve(2048); + int sent = 0; + + // mehrere Zeilen in eine WS-Nachricht + while (!ring_empty() && sent < 32) + { + payload += "TRACELINE;"; + payload += ring_front(); + payload += "\n"; + ring_pop(); + sent++; + } + + if (payload.length()) + webSocket.text(g_traceOwnerId, payload); +} + +static void TRACE_StopWs(const char *reason) +{ + if (g_traceMode == TraceMode::None) + return; + + // Hooks lösen + CAN_HAL_SetTraceSink(nullptr); + CAN_HAL_EnableRawSniffer(false); + + // Abschlussinfo an Owner (falls noch online) + if (trace_owner_online()) + { + String end = "STOPTRACE;mode="; + end += (g_traceMode == TraceMode::Raw ? "raw" : "obd"); + end += ";lines="; + end += String(g_traceLines); + end += ";drops="; + end += String(g_traceDrops); + if (reason) + { + end += ";reason="; + end += reason; + } + webSocket.text(g_traceOwnerId, end); + } + + // Ring freigeben + ring_free(); + // Reset + g_traceMode = TraceMode::None; + g_traceOwnerId = 0; + g_traceStartMs = 0; + g_traceLines = 0; + g_traceDrops = 0; + g_head = g_tail = 0; +} + +static void TRACE_StartWs(TraceMode m, uint32_t ownerId) +{ + // Falls schon aktiv → erst stoppen (sauber) + if (g_traceMode != TraceMode::None) + { + TRACE_StopWs("restart"); + } + + g_traceMode = m; + g_traceOwnerId = ownerId; + g_traceStartMs = millis(); + g_traceLines = 0; + g_traceDrops = 0; + + // vorhandenen Ring sicherheitshalber freigeben + ring_free(); + + // versuchen: 64x128 (8KB) + if (!ring_alloc(TRACE_DEFAULT_LINES, TRACE_DEFAULT_LINE_MAX)) + { + // Fallback: 48x112 ~ 5.3KB + if (!ring_alloc(48, 112)) + { + // Minimal: 32x96 ~ 3KB + (void)ring_alloc(32, 96); // wenn das auch scheitert -> g_ring bleibt nullptr + } + } + if (m == TraceMode::Raw) + { + CAN_HAL_SetTraceSink(TRACE_SinkRaw); + CAN_HAL_EnableRawSniffer(true); + } + else + { // Obd + CAN_HAL_SetTraceSink(nullptr); + CAN_HAL_EnableRawSniffer(false); + } + + String hdr = "STARTTRACE;mode="; + hdr += (m == TraceMode::Raw ? "raw" : "obd"); + hdr += ";ts="; + hdr += String(g_traceStartMs); + webSocket.text(ownerId, hdr); +} + +// ===================================================================== +// WebUI +// ===================================================================== + /** * @brief Initializes the web-based user interface (WebUI) for the ChainLube application. * @@ -139,10 +426,8 @@ void initWebUI() { request->redirect("/index.htm"); }); webServer.onNotFound(WebserverNotFound_Callback); webServer.on("/eejson", HTTP_GET, WebServerEEJSON_Callback); - webServer.on( - "/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback); - webServer.on( - "/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback); + webServer.on("/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback); + webServer.on("/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback); // Start the web server webServer.begin(); @@ -218,6 +503,29 @@ void Webserver_Process() } } } + + // Trace pumpen (sicher, backpressure-aware) + TRACE_PumpWs(); + + // Watchdog: Owner weg → Stop nach 10s (zusätzlich zu Disconnect-Stop) + static uint32_t ownerMissingSince = 0; + if (g_traceMode != TraceMode::None) + { + if (!trace_owner_online()) + { + if (ownerMissingSince == 0) + ownerMissingSince = millis(); + if (millis() - ownerMissingSince > 10000) + { + TRACE_StopWs("owner-timeout"); + ownerMissingSince = 0; + } + } + else + { + ownerMissingSince = 0; + } + } } /** @@ -235,6 +543,11 @@ void Webserver_Process() */ void Webserver_Shutdown() { + // Bei Shutdown Trace beenden + if (g_traceMode != TraceMode::None) + { + TRACE_StopWs("shutdown"); + } if (webSocket.count() > 0) webSocket.closeAll(); webServer.end(); @@ -298,7 +611,6 @@ void GetFlashVersion(char *buff, size_t buff_size) */ void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { - if (!index) { Debug_pushMessage("Update\n"); @@ -337,6 +649,7 @@ void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const Stri } } } + void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, @@ -440,8 +753,8 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, LubeConfig.RimDiameter_Inch = clamp_u32(json["config"]["RimDiameter_Inch"].as(), 0, 30); LubeConfig.DistancePerRevolution_mm = clamp_u32(json["config"]["DistancePerRevolution_mm"].as(), 0, 10000); LubeConfig.BleedingPulses = clamp_u16(json["config"]["BleedingPulses"].as(), 0, 1000); - LubeConfig.WashMode_Distance = json["config"]["WashMode_Distance"].as(); // ggf. Grenzen anpassen - LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as(); // ggf. Grenzen anpassen + LubeConfig.WashMode_Distance = json["config"]["WashMode_Distance"].as(); + LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as(); LubeConfig.LED_Mode_Flash = json["config"]["LED_Mode_Flash"].as(); LubeConfig.LED_Max_Brightness = json["config"]["LED_Max_Brightness"].as(); LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as(); @@ -483,14 +796,11 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, PersistenceData.odometer = json["persis"]["odometer"].as(); PersistenceData.checksum = json["persis"]["checksum"].as(); - // Optional: Sanity-Autokorrektur im RAM (keine EEPROM-Writes hier!) + uint32_t sanity = ConfigSanityCheck(true); + if (sanity > 0) { - uint32_t sanity = ConfigSanityCheck(true); - if (sanity > 0) - { - MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity); - Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity); - } + MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity); + Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity); } ee_done = true; @@ -554,6 +864,10 @@ void WebServerEEJSON_Callback(AsyncWebServerRequest *request) request->send(response); } +// ===================================================================== +// WebSocket Handling +// ===================================================================== + /** * @brief Callback function for handling WebSocket events. * @@ -584,10 +898,17 @@ void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *clien } case WS_EVT_DISCONNECT: Debug_pushMessage("WebSocket client #%u disconnected\n", client->id()); + // Falls Owner: Trace sofort stoppen + if (g_traceOwnerId == client->id()) + TRACE_StopWs("owner-disconnect"); break; + case WS_EVT_DATA: + g_wsCurrentClient = client; // für HandleMessage → Owner-ID Websocket_HandleMessage(arg, data, len); + g_wsCurrentClient = nullptr; break; + case WS_EVT_PONG: case WS_EVT_ERROR: break; @@ -614,19 +935,69 @@ void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len) memcpy(buf.get(), data, len); buf[len] = '\0'; - Debug_pushMessage("Websocket-Message (len: %d): %s\n", (int)len, buf.get()); + const uint32_t senderId = g_wsCurrentClient ? g_wsCurrentClient->id() : 0; + Debug_pushMessage("Websocket-Message from #%u (len: %d): %s\n", (unsigned)senderId, (int)len, buf.get()); + // Steuerkommandos für Trace hier direkt behandeln (brauchen senderId) if (strncmp(buf.get(), "btn-", 4) == 0) { + // Format: "btn-[:]" + char identifier[32]; + char value[64]; + parseWebsocketString((char *)buf.get() + 4, identifier, sizeof(identifier), value, sizeof(value)); + + if (strcmp(identifier, "trace-start") == 0) + { + // Lock: Nur starten, wenn nicht aktiv + if (g_traceMode != TraceMode::None) + { + String busy = "TRACEBUSY;owner="; + busy += String(g_traceOwnerId); + if (senderId) + webSocket.text(senderId, busy); + } + else + { + TraceMode m = TraceMode::None; + if (!strcmp(value, "raw")) + m = TraceMode::Raw; + else if (!strcmp(value, "obd")) + m = TraceMode::Obd; + + if (m == TraceMode::None) + { + if (senderId) + webSocket.text(senderId, "TRACEERROR;msg=mode-missing"); + } + else + { + TRACE_StartWs(m, senderId); + } + } + return; + } + else if (strcmp(identifier, "trace-stop") == 0) + { + // Stop darf jeder + TRACE_StopWs("user-stop"); + // optional: ACK an Sender (Owner bekommt STOPTRACE ohnehin) + if (senderId) + webSocket.text(senderId, "TRACEACK;cmd=stop"); + return; + } + + // sonst: in "normalen" Button-Handler Websocket_HandleButtons((uint8_t *)buf.get() + 4); + return; } else if (strncmp(buf.get(), "set-", 4) == 0) { Websocket_HandleSettings((uint8_t *)buf.get() + 4); + return; } else { - Debug_pushMessage("Got unknown Websocket-Message '%s' from client\n", buf.get()); + Debug_pushMessage("Got unknown Websocket-Message '%s'\n", buf.get()); } } } @@ -855,7 +1226,6 @@ void Websocket_RefreshClientData_DTCs(uint32_t client_id) */ void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping) { - if (send_mapping) { if (client_id > 0) @@ -865,12 +1235,11 @@ void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping) } String temp = "STATUS:"; - temp.concat(String(ToString(globals.systemStatus)) + ";"); // Guard against division by zero (capacity==0) uint32_t cap = LubeConfig.tankCapacity_ml; - uint32_t remain10 = (PersistenceData.tankRemain_microL / 10); // keep your original math + uint32_t remain10 = (PersistenceData.tankRemain_microL / 10); uint32_t ratio = (cap > 0) ? (remain10 / cap) : 0; temp.concat(String(ratio) + ";"); @@ -1087,4 +1456,4 @@ void Websocket_PushNotification(String Message, NotificationType_t type) } webSocket.textAll("NOTIFY:" + typeString + ";" + Message); Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString.c_str()); -} \ No newline at end of file +}