added Function to create CAN-Traces from WebUI
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				CI-Build/Kettenoeler/pipeline/head There was a failure building this commit
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	CI-Build/Kettenoeler/pipeline/head There was a failure building this commit
				
			This commit is contained in:
		| @@ -191,6 +191,29 @@ | ||||
|           </div> | ||||
|         </div> | ||||
|         </p> | ||||
|         <hr /> | ||||
|         <p> | ||||
|         <h4>CAN / OBD2 Trace</h4> | ||||
|         <div class="form-group row"> | ||||
|           <div class="col"> | ||||
|             <div class="text-center mb-2"> | ||||
|               <!-- Beide Start-Buttons senden btn-trace-start; Modus kommt als value --> | ||||
|               <button id="trace-start" data-wsid="trace-start" value="raw" class="btn-wsevent btn btn-outline-primary"> | ||||
|                 Start CAN-Trace | ||||
|               </button> | ||||
|               <button id="trace-start-obd" data-wsid="trace-start" value="obd" class="btn-wsevent btn btn-outline-primary ml-2"> | ||||
|                 Start OBD-Trace | ||||
|               </button> | ||||
|               <button id="trace-stop" class="btn-wsevent btn btn-outline-danger ml-2"> | ||||
|                 Stop | ||||
|               </button> | ||||
|             </div> | ||||
|  | ||||
|             <textarea id="trace-out" class="form-control" style="font-family:monospace" rows="8" readonly spellcheck="false"></textarea> | ||||
|             <small id="trace-status" class="form-text text-muted">Trace inaktiv</small> | ||||
|           </div> | ||||
|         </div> | ||||
|         </p> | ||||
|         <!-- Div Group LiveDebug --> | ||||
|         <!-- Div Group Device Reboot --> | ||||
|         <hr /> | ||||
|   | ||||
| @@ -23,4 +23,4 @@ document | ||||
|     var fileName = document.getElementById("fw-update-file").files[0].name; | ||||
|     var nextSibling = e.target.nextElementSibling; | ||||
|     nextSibling.innerText = fileName; | ||||
|   }); | ||||
|   }); | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| 1.04 | ||||
| 1.05 | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| #pragma once | ||||
| #include <Arduino.h> | ||||
| #include "can_hal.h" | ||||
|  | ||||
| // Initialisiert das OBD2-CAN-Profil: | ||||
| // - setzt Masken/Filter für 0x7E8..0x7EF (ECU-Antworten) | ||||
|   | ||||
| @@ -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_ | ||||
|   | ||||
| @@ -1,24 +1,42 @@ | ||||
| #include "can_hal.h" | ||||
| #include "dtc.h" | ||||
| // #include "debugger.h"  // optional für Logs | ||||
| #include <string.h> // 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<uint8_t*>(data)); | ||||
|   // Senden | ||||
|   uint8_t st = CAN0.sendMsgBuf(id, ext ? 1 : 0, len, const_cast<uint8_t *>(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(); | ||||
|   | ||||
| @@ -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 <stdarg.h> | ||||
|  | ||||
| // 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 <A> ... | ||||
|     //  - Mit Längen-Byte:    0x03 0x41 0x0D <A> ... | ||||
|     //  - Mit Längen-Byte:    0x03/0x04 0x41 0x0D <A> ... | ||||
|     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 | ||||
|   | ||||
| @@ -13,11 +13,13 @@ | ||||
|  | ||||
| #include "webui.h" | ||||
| #include "common.h" | ||||
| #include "can_hal.h" // <-- für CanLogFrame, Trace-Sink | ||||
| #include <memory>    // std::unique_ptr | ||||
| #include <cstring>   // strlen, strncpy, memcpy | ||||
| #include <algorithm> // 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<uint32_t>(), 0, 30); | ||||
|         LubeConfig.DistancePerRevolution_mm = clamp_u32(json["config"]["DistancePerRevolution_mm"].as<uint32_t>(), 0, 10000); | ||||
|         LubeConfig.BleedingPulses = clamp_u16(json["config"]["BleedingPulses"].as<uint16_t>(), 0, 1000); | ||||
|         LubeConfig.WashMode_Distance = json["config"]["WashMode_Distance"].as<uint16_t>(); // ggf. Grenzen anpassen | ||||
|         LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as<uint16_t>(); // ggf. Grenzen anpassen | ||||
|         LubeConfig.WashMode_Distance = json["config"]["WashMode_Distance"].as<uint16_t>(); | ||||
|         LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as<uint16_t>(); | ||||
|         LubeConfig.LED_Mode_Flash = json["config"]["LED_Mode_Flash"].as<bool>(); | ||||
|         LubeConfig.LED_Max_Brightness = json["config"]["LED_Max_Brightness"].as<uint8_t>(); | ||||
|         LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as<uint8_t>(); | ||||
| @@ -483,14 +796,11 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, | ||||
|         PersistenceData.odometer = json["persis"]["odometer"].as<uint32_t>(); | ||||
|         PersistenceData.checksum = json["persis"]["checksum"].as<uint32_t>(); | ||||
|  | ||||
|         // 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-<identifier>[:<value>]" | ||||
|       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()); | ||||
| } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user