From 98629b744dfce2bd6fb8d1a16775be73aadf3ef5 Mon Sep 17 00:00:00 2001
From: Marcel Peterkau
Date: Tue, 26 Aug 2025 23:31:35 +0200
Subject: [PATCH] added Function to create CAN-Traces from WebUI
---
Software/data_src/index.htm | 23 ++
Software/data_src/static/js/script.js | 2 +-
Software/data_src/static/js/websocket.js | 213 +++++++++++-
Software/data_src/version | 2 +-
Software/include/can_hal.h | 27 +-
Software/include/can_obd2.h | 1 -
Software/include/webui.h | 2 +
Software/src/can_hal.cpp | 368 ++++++++++++++++----
Software/src/can_obd2.cpp | 81 ++++-
Software/src/webui.cpp | 415 +++++++++++++++++++++--
10 files changed, 1011 insertions(+), 123 deletions(-)
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
+
+
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
+}