added Function to create CAN-Traces from WebUI
Some checks failed
CI-Build/Kettenoeler/pipeline/head There was a failure building this commit

This commit is contained in:
2025-08-26 23:31:35 +02:00
parent c8c67551fd
commit 98629b744d
10 changed files with 1011 additions and 123 deletions

View File

@@ -191,6 +191,29 @@
</div> </div>
</div> </div>
</p> </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 LiveDebug -->
<!-- Div Group Device Reboot --> <!-- Div Group Device Reboot -->
<hr /> <hr />

View File

@@ -5,6 +5,106 @@ var statusMapping;
var staticMapping; var staticMapping;
var overlay; 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 () { document.addEventListener("DOMContentLoaded", function () {
// Ihr JavaScript-Code hier, einschließlich der onLoad-Funktion // Ihr JavaScript-Code hier, einschließlich der onLoad-Funktion
overlay = document.getElementById("overlay"); overlay = document.getElementById("overlay");
@@ -45,16 +145,32 @@ function initSettingInputs() {
function onOpen(event) { function onOpen(event) {
console.log("Connection opened"); console.log("Connection opened");
setTraceUI(false, null, "Verbunden Trace inaktiv");
} }
function onClose(event) { function onClose(event) {
console.log("Connection closed"); console.log("Connection closed");
setTimeout(initWebSocket, 1000); setTimeout(initWebSocket, 1000);
overlay.style.display = "flex"; 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) { async function sendButton(event) {
var targetElement = event.target; const targetElement = event.target;
if ( if (
targetElement.classList.contains("confirm") && targetElement.classList.contains("confirm") &&
@@ -62,7 +178,46 @@ function sendButton(event) {
) )
return; 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) { function onMessage(event) {
@@ -101,6 +256,58 @@ function onMessage(event) {
fillValuesToHTML(result); fillValuesToHTML(result);
overlay.style.display = "none"; 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) { function createMapping(mappingString) {

View File

@@ -1 +1 @@
1.04 1.05

View File

@@ -23,9 +23,23 @@ struct CanHalConfig {
// ==== Universeller Filter-Descriptor ==== // ==== Universeller Filter-Descriptor ====
struct CanFilter { struct CanFilter {
uint32_t id; // 11-bit oder 29-bit Roh-ID 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 ==== // ==== API ====
// 1) Einmalige Hardware-Initialisierung + integrierter Loopback-Selftest. // 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); bool CAN_HAL_SetFilters(const CanFilter* list, size_t count);
// Non-blocking IO // 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 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) // Diagnose/Utilities
uint8_t CAN_HAL_GetErrorFlags(); // Intern: getError() uint8_t CAN_HAL_GetErrorFlags(); // Intern: getError()
void CAN_HAL_GetErrorCounters(uint8_t& tec, uint8_t& rec); // TX/RX Error Counter 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();

View File

@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include "can_hal.h"
// Initialisiert das OBD2-CAN-Profil: // Initialisiert das OBD2-CAN-Profil:
// - setzt Masken/Filter für 0x7E8..0x7EF (ECU-Antworten) // - setzt Masken/Filter für 0x7E8..0x7EF (ECU-Antworten)

View File

@@ -46,4 +46,6 @@ void Webserver_Shutdown();
void Websocket_PushLiveDebug(String Message); void Websocket_PushLiveDebug(String Message);
void Websocket_PushNotification(String Message, NotificationType_t type); 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_ #endif // _WEBUI_H_

View File

@@ -1,24 +1,42 @@
#include "can_hal.h" #include "can_hal.h"
#include "dtc.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); 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 uint8_t s_nextFiltSlot = 0; // 0..5 (MCP2515 hat 6 Filter-Slots)
static uint16_t s_modeSettleMs = 10; // Default aus Config 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) // 11-bit: Lib erwartet (value << 16)
static inline uint32_t _std_to_hw(uint16_t v11) { return ((uint32_t)v11) << 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) // „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) static bool _trySetMode(uint8_t mode, uint16_t settleMs)
{ {
const uint32_t t0 = millis(); const uint32_t t0 = millis();
do { do
if (CAN0.setMode(mode) == CAN_OK) return true; {
if (CAN0.setMode(mode) == CAN_OK)
return true;
delay(1); delay(1);
} while ((millis() - t0) < settleMs); } while ((millis() - t0) < settleMs);
return false; return false;
@@ -27,22 +45,32 @@ static bool _trySetMode(uint8_t mode, uint16_t settleMs)
// LOOPBACK-Selftest (ohne Bus) // LOOPBACK-Selftest (ohne Bus)
static bool _selftest_loopback(uint16_t windowMs) 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; const unsigned long tid = 0x123;
uint8_t tx[8] = {0xA5, 0x5A, 0x11, 0x22, 0x33, 0x44, 0x77, 0x88}; 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); (void)_trySetMode(MCP_NORMAL, s_modeSettleMs);
return false; return false;
} }
bool got = false; bool got = false;
const uint32_t t0 = millis(); const uint32_t t0 = millis();
while ((millis() - t0) < windowMs) { while ((millis() - t0) < windowMs)
if (CAN0.checkReceive() == CAN_MSGAVAIL) { {
unsigned long rid; uint8_t len, rx[8]; if (CAN0.checkReceive() == CAN_MSGAVAIL)
if (CAN0.readMsgBuf(&rid, &len, rx) == CAN_OK) { {
if (rid == tid && len == 8 && memcmp(tx, rx, 8) == 0) { got = true; break; } 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); delay(1);
@@ -55,60 +83,153 @@ static bool _selftest_loopback(uint16_t windowMs)
// Optional: kurzer ListenOnly-Hörtest (nur Heuristik, keine DTC-Änderung) // Optional: kurzer ListenOnly-Hörtest (nur Heuristik, keine DTC-Änderung)
static void _probe_listen_only(uint16_t ms) static void _probe_listen_only(uint16_t ms)
{ {
if (ms == 0) return; if (ms == 0)
if (!_trySetMode(MCP_LISTENONLY, s_modeSettleMs)) return; return;
if (!_trySetMode(MCP_LISTENONLY, s_modeSettleMs))
return;
const uint32_t t0 = millis(); const uint32_t t0 = millis();
while ((millis() - t0) < ms) { while ((millis() - t0) < ms)
if (CAN0.checkReceive() == CAN_MSGAVAIL) break; {
if (CAN0.checkReceive() == CAN_MSGAVAIL)
break;
delay(1); delay(1);
} }
(void)_trySetMode(MCP_NORMAL, s_modeSettleMs); (void)_trySetMode(MCP_NORMAL, s_modeSettleMs);
} }
// ==== Öffentliche API ==== // Offen konfigurieren (RAW-Sniffer)
static bool _apply_open_filters()
bool CAN_HAL_Init(const CanHalConfig& cfg)
{ {
s_ready = false; if (!_trySetMode(MODE_CONFIG, s_modeSettleMs))
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);
return false; return false;
}
// 2) LoopbackSelftest (ohne Bus) // Masken 0 -> alles durchlassen
if (!_selftest_loopback(20)) {
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false;
}
// 3) Optional ListenOnlyProbe (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)
CAN0.init_Mask(0, 0, _std_to_hw(0x000)); CAN0.init_Mask(0, 0, _std_to_hw(0x000));
CAN0.init_Mask(1, 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)); CAN0.init_Filt(i, 0, _std_to_hw(0x000));
} }
s_nextFiltSlot = 0; 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); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false; 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); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, false);
s_ready = true; s_ready = true;
return true; return true;
@@ -119,18 +240,30 @@ bool CAN_HAL_IsReady() { return s_ready; }
bool CAN_HAL_SetMode(uint8_t mode) bool CAN_HAL_SetMode(uint8_t mode)
{ {
const bool ok = _trySetMode(mode, s_modeSettleMs); 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; return ok;
} }
bool CAN_HAL_SetMask(uint8_t bank, bool ext, uint32_t rawMask) bool CAN_HAL_SetMask(uint8_t bank, bool ext, uint32_t rawMask)
{ {
if (bank > 1) return false; if (bank > 1)
if (!CAN_HAL_SetMode(MODE_CONFIG)) return false; return false;
if (!CAN_HAL_SetMode(MODE_CONFIG))
return false;
const bool ok = (CAN0.init_Mask(bank, ext ? 1 : 0, rawMask) == CAN_OK); 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); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false; return false;
} }
@@ -144,27 +277,42 @@ bool CAN_HAL_SetStdMask11(uint8_t bank, uint16_t mask11)
void CAN_HAL_ClearFilters() void CAN_HAL_ClearFilters()
{ {
if (!CAN_HAL_SetMode(MODE_CONFIG)) { if (!CAN_HAL_SetMode(MODE_CONFIG))
{
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return; return;
} }
CAN0.init_Mask(0, 0, _std_to_hw(0x000)); CAN0.init_Mask(0, 0, _std_to_hw(0x000));
CAN0.init_Mask(1, 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)); CAN0.init_Filt(i, 0, _std_to_hw(0x000));
} }
s_nextFiltSlot = 0; 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); 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 (s_nextFiltSlot >= 6)
if (!CAN_HAL_SetMode(MODE_CONFIG)) { return false;
if (!CAN_HAL_SetMode(MODE_CONFIG))
{
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false; return false;
} }
@@ -173,56 +321,126 @@ bool CAN_HAL_AddFilter(const CanFilter& f)
const uint8_t slot = s_nextFiltSlot++; const uint8_t slot = s_nextFiltSlot++;
const bool ok = (CAN0.init_Filt(slot, f.ext ? 1 : 0, hwId) == CAN_OK); 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); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false; return false;
} }
return ok; 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); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false; return false;
} }
// Slots zurücksetzen // Slots zurücksetzen
s_nextFiltSlot = 0; 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)); CAN0.init_Filt(i, 0, _std_to_hw(0x000));
} }
// Setzen // Setzen
for (size_t i = 0; i < count && s_nextFiltSlot < 6; ++i) { for (size_t i = 0; i < count && s_nextFiltSlot < 6; ++i)
const auto& f = list[i]; {
const auto &f = list[i];
const uint32_t hwId = f.ext ? f.id : _std_to_hw((uint16_t)f.id); 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); 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); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false; return false;
} }
return true; 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.checkReceive() != CAN_MSGAVAIL)
if (CAN0.readMsgBuf(&id, &len, data) != CAN_OK) { return false;
if (CAN0.readMsgBuf(&id, &len, data) != CAN_OK)
{
// Echte Lese-Fehler -> vermutlich SPI/Controller-Problem // Echte Lese-Fehler -> vermutlich SPI/Controller-Problem
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true); MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false; 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; 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). // Senden
// Höhere Ebene kann bei Bedarf DTCs setzen. Hier nur durchreichen. uint8_t st = CAN0.sendMsgBuf(id, ext ? 1 : 0, len, const_cast<uint8_t *>(data));
return 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 ==== // ==== Diagnose/Utilities ====
@@ -233,7 +451,7 @@ uint8_t CAN_HAL_GetErrorFlags()
return CAN0.getError(); 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(); tec = CAN0.errorCountTX();
rec = CAN0.errorCountRX(); rec = CAN0.errorCountRX();

View File

@@ -1,7 +1,12 @@
#include "can_obd2.h" #include "can_obd2.h"
#include "can_hal.h"
#include "dtc.h" #include "dtc.h"
#include "debugger.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) // Konfiguration (anpassbar)
@@ -14,7 +19,7 @@
// Antwort-Timeout auf eine einzelne Anfrage // Antwort-Timeout auf eine einzelne Anfrage
#ifndef OBD2_RESP_TIMEOUT_MS #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 #endif
// Wenn so lange keine valide Antwort kam, gilt die Geschwindigkeit als stale -> v=0 // Wenn so lange keine valide Antwort kam, gilt die Geschwindigkeit als stale -> v=0
@@ -32,10 +37,21 @@
#define OBD2_DEBUG_INTERVAL_MS 1000 #define OBD2_DEBUG_INTERVAL_MS 1000
#endif #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) // OBD-II IDs (11-bit)
// ======================= // =======================
static constexpr uint16_t OBD_REQ_ID = 0x7DF; // Broadcast-Request 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_MIN = 0x7E8; // ECUs antworten 0x7E8..0x7EF
static constexpr uint16_t OBD_RESP_MAX = 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; s_lastDbgMs = now;
va_list ap; va_list ap;
va_start(ap, fmt); va_start(ap, fmt);
Debug_pushMessage(fmt, ap); Debug_pushMessage(fmt, ap); // nimmt va_list
va_end(ap); va_end(ap);
#else #else
(void)now; (void)now;
@@ -113,15 +129,17 @@ bool Init_CAN_OBD2()
CAN_HAL_SetStdMask11(0, 0x7F0); CAN_HAL_SetStdMask11(0, 0x7F0);
CAN_HAL_SetStdMask11(1, 0x7F0); CAN_HAL_SetStdMask11(1, 0x7F0);
CanFilter flist[6] = { CanFilter flist[8] = {
{0x7E8, false}, {0x7E8, false},
{0x7E9, false}, {0x7E9, false},
{0x7EA, false}, {0x7EA, false},
{0x7EB, false}, {0x7EB, false},
{0x7EC, false}, {0x7EC, false},
{0x7ED, false}, {0x7ED, false},
{0x7EE, false},
{0x7EF, false},
}; };
CAN_HAL_SetFilters(flist, 6); CAN_HAL_SetFilters(flist, 8);
CAN_HAL_SetMode(MCP_NORMAL); 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) 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) 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; s_lastQueryTime = now;
if (st == CAN_OK) if (st == CAN_OK)
@@ -161,11 +183,25 @@ uint32_t Process_CAN_OBD2_Speed()
} }
else else
{ {
#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 // Senden fehlgeschlagen -> harter Timeout-DTC
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true); MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true);
maybeDebug(now, "OBD2-CAN send failed (%u)\n", st); maybeDebug(now, "OBD2-CAN send failed (%u)\n", st);
} }
} }
}
// 2) Non-blocking Receive: wenige Frames pro Tick ziehen // 2) Non-blocking Receive: wenige Frames pro Tick ziehen
for (uint8_t i = 0; i < OBD2_MAX_READS_PER_CALL; ++i) for (uint8_t i = 0; i < OBD2_MAX_READS_PER_CALL; ++i)
@@ -182,9 +218,9 @@ uint32_t Process_CAN_OBD2_Speed()
// Erwartete Formate: // Erwartete Formate:
// - Einfache Antwort: 0x41 0x0D <A> ... // - 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; 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]; modeResp = rx[1];
pid = rx[2]; pid = rx[2];
@@ -198,7 +234,9 @@ uint32_t Process_CAN_OBD2_Speed()
} }
else 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) 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_TIMEOUT, false);
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, 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", maybeDebug(now, "OBD2 speed: %u km/h (%lu mm/s)\n",
(unsigned)speedKmh, (unsigned long)s_lastSpeedMMps); (unsigned)speedKmh, (unsigned long)s_lastSpeedMMps);
break; // eine valide Antwort pro Zyklus reicht 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 // 3) Offene Anfrage: Timeout prüfen
@@ -223,13 +270,17 @@ uint32_t Process_CAN_OBD2_Speed()
// Keine passende Antwort erhalten // Keine passende Antwort erhalten
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, true); MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, true);
s_state = ObdState::Idle; s_state = ObdState::Idle;
TRACE_OnObdFrame(0x000, /*rx=*/true, nullptr, 0, "timeout 01 0D");
} }
// 4) Integration (mm) über dt // 4) Integration (mm) über dt
uint32_t add_mm = 0; uint32_t add_mm = 0;
if (s_lastIntegrateMs == 0) if (s_lastIntegrateMs == 0)
s_lastIntegrateMs = now; 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; s_lastIntegrateMs = now;
// Stale-Schutz: wenn lange keine Antwort -> v=0 // Stale-Schutz: wenn lange keine Antwort -> v=0

View File

@@ -13,11 +13,13 @@
#include "webui.h" #include "webui.h"
#include "common.h" #include "common.h"
#include "can_hal.h" // <-- für CanLogFrame, Trace-Sink
#include <memory> // std::unique_ptr #include <memory> // std::unique_ptr
#include <cstring> // strlen, strncpy, memcpy #include <cstring> // strlen, strncpy, memcpy
#include <algorithm> // std::clamp #include <algorithm> // std::clamp
AsyncWebServer webServer(80); AsyncWebServer webServer(80);
AsyncWebSocket webSocket("/ws");
const char *PARAM_MESSAGE = "message"; const char *PARAM_MESSAGE = "message";
@@ -52,8 +54,6 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
void WebServerEEJSON_Callback(AsyncWebServerRequest *request); void WebServerEEJSON_Callback(AsyncWebServerRequest *request);
void GetFlashVersion(char *buff, size_t buff_size); 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 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_HandleMessage(void *arg, uint8_t *data, size_t len);
void Websocket_RefreshClientData_DTCs(uint32_t client_id); 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); int findIndexByString(const char *searchString, const char *const *array, int arraySize);
// ---------- small helpers (safety) ---------- // ---------- 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) 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; 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. * @brief Initializes the web-based user interface (WebUI) for the ChainLube application.
* *
@@ -139,10 +426,8 @@ void initWebUI()
{ request->redirect("/index.htm"); }); { request->redirect("/index.htm"); });
webServer.onNotFound(WebserverNotFound_Callback); webServer.onNotFound(WebserverNotFound_Callback);
webServer.on("/eejson", HTTP_GET, WebServerEEJSON_Callback); webServer.on("/eejson", HTTP_GET, WebServerEEJSON_Callback);
webServer.on( webServer.on("/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback);
"/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback); webServer.on("/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback);
webServer.on(
"/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback);
// Start the web server // Start the web server
webServer.begin(); 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() void Webserver_Shutdown()
{ {
// Bei Shutdown Trace beenden
if (g_traceMode != TraceMode::None)
{
TRACE_StopWs("shutdown");
}
if (webSocket.count() > 0) if (webSocket.count() > 0)
webSocket.closeAll(); webSocket.closeAll();
webServer.end(); 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) void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final)
{ {
if (!index) if (!index)
{ {
Debug_pushMessage("Update\n"); Debug_pushMessage("Update\n");
@@ -337,6 +649,7 @@ void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const Stri
} }
} }
} }
void WebserverEERestore_Callback(AsyncWebServerRequest *request, void WebserverEERestore_Callback(AsyncWebServerRequest *request,
const String &filename, const String &filename,
size_t index, 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.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.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.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_Distance = json["config"]["WashMode_Distance"].as<uint16_t>();
LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as<uint16_t>(); // ggf. Grenzen anpassen LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as<uint16_t>();
LubeConfig.LED_Mode_Flash = json["config"]["LED_Mode_Flash"].as<bool>(); 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_Max_Brightness = json["config"]["LED_Max_Brightness"].as<uint8_t>();
LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as<uint8_t>(); LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as<uint8_t>();
@@ -483,15 +796,12 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request,
PersistenceData.odometer = json["persis"]["odometer"].as<uint32_t>(); PersistenceData.odometer = json["persis"]["odometer"].as<uint32_t>();
PersistenceData.checksum = json["persis"]["checksum"].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); uint32_t sanity = ConfigSanityCheck(true);
if (sanity > 0) if (sanity > 0)
{ {
MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity); MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity);
Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity); Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity);
} }
}
ee_done = true; ee_done = true;
} }
@@ -554,6 +864,10 @@ void WebServerEEJSON_Callback(AsyncWebServerRequest *request)
request->send(response); request->send(response);
} }
// =====================================================================
// WebSocket Handling
// =====================================================================
/** /**
* @brief Callback function for handling WebSocket events. * @brief Callback function for handling WebSocket events.
* *
@@ -584,10 +898,17 @@ void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *clien
} }
case WS_EVT_DISCONNECT: case WS_EVT_DISCONNECT:
Debug_pushMessage("WebSocket client #%u disconnected\n", client->id()); Debug_pushMessage("WebSocket client #%u disconnected\n", client->id());
// Falls Owner: Trace sofort stoppen
if (g_traceOwnerId == client->id())
TRACE_StopWs("owner-disconnect");
break; break;
case WS_EVT_DATA: case WS_EVT_DATA:
g_wsCurrentClient = client; // für HandleMessage → Owner-ID
Websocket_HandleMessage(arg, data, len); Websocket_HandleMessage(arg, data, len);
g_wsCurrentClient = nullptr;
break; break;
case WS_EVT_PONG: case WS_EVT_PONG:
case WS_EVT_ERROR: case WS_EVT_ERROR:
break; break;
@@ -614,19 +935,69 @@ void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len)
memcpy(buf.get(), data, len); memcpy(buf.get(), data, len);
buf[len] = '\0'; 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) 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); Websocket_HandleButtons((uint8_t *)buf.get() + 4);
return;
} }
else if (strncmp(buf.get(), "set-", 4) == 0) else if (strncmp(buf.get(), "set-", 4) == 0)
{ {
Websocket_HandleSettings((uint8_t *)buf.get() + 4); Websocket_HandleSettings((uint8_t *)buf.get() + 4);
return;
} }
else 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) void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
{ {
if (send_mapping) if (send_mapping)
{ {
if (client_id > 0) if (client_id > 0)
@@ -865,12 +1235,11 @@ void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
} }
String temp = "STATUS:"; String temp = "STATUS:";
temp.concat(String(ToString(globals.systemStatus)) + ";"); temp.concat(String(ToString(globals.systemStatus)) + ";");
// Guard against division by zero (capacity==0) // Guard against division by zero (capacity==0)
uint32_t cap = LubeConfig.tankCapacity_ml; 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; uint32_t ratio = (cap > 0) ? (remain10 / cap) : 0;
temp.concat(String(ratio) + ";"); temp.concat(String(ratio) + ";");