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>
</p>
<hr />
<p>
<h4>CAN / OBD2 Trace</h4>
<div class="form-group row">
<div class="col">
<div class="text-center mb-2">
<!-- Beide Start-Buttons senden btn-trace-start; Modus kommt als value -->
<button id="trace-start" data-wsid="trace-start" value="raw" class="btn-wsevent btn btn-outline-primary">
Start CAN-Trace
</button>
<button id="trace-start-obd" data-wsid="trace-start" value="obd" class="btn-wsevent btn btn-outline-primary ml-2">
Start OBD-Trace
</button>
<button id="trace-stop" class="btn-wsevent btn btn-outline-danger ml-2">
Stop
</button>
</div>
<textarea id="trace-out" class="form-control" style="font-family:monospace" rows="8" readonly spellcheck="false"></textarea>
<small id="trace-status" class="form-text text-muted">Trace inaktiv</small>
</div>
</div>
</p>
<!-- Div Group LiveDebug -->
<!-- Div Group Device Reboot -->
<hr />

View File

@@ -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) {

View File

@@ -1 +1 @@
1.04
1.05

View File

@@ -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)
// 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();

View File

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

View File

@@ -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_

View File

@@ -1,24 +1,42 @@
#include "can_hal.h"
#include "dtc.h"
// #include "debugger.h" // optional für Logs
#include <string.h> // memcpy, memcmp
// ==== Interner Zustand/Helper ====
// =====================
// Interner Zustand/Helper
// =====================
MCP_CAN CAN0(GPIO_CS_CAN);
static bool s_ready = false;
static 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) LoopbackSelftest (ohne Bus)
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)
// 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,27 +277,42 @@ 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;
}
@@ -173,56 +321,126 @@ bool CAN_HAL_AddFilter(const CanFilter& f)
const uint8_t slot = s_nextFiltSlot++;
const bool ok = (CAN0.init_Filt(slot, f.ext ? 1 : 0, hwId) == CAN_OK);
if (!CAN_HAL_SetMode(MCP_NORMAL)) {
// Spiegeln
if (ok)
{
if (s_savedFiltCount < 6)
{
s_savedFilt[s_savedFiltCount].id = f.ext ? f.id : (uint32_t)((uint16_t)f.id);
s_savedFilt[s_savedFiltCount].ext = f.ext;
++s_savedFiltCount;
}
}
if (!CAN_HAL_SetMode(MCP_NORMAL))
{
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false;
}
return ok;
}
bool CAN_HAL_SetFilters(const CanFilter* list, size_t count)
bool CAN_HAL_SetFilters(const CanFilter *list, size_t count)
{
if (!CAN_HAL_SetMode(MODE_CONFIG)) {
if (!CAN_HAL_SetMode(MODE_CONFIG))
{
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false;
}
// Slots zurücksetzen
s_nextFiltSlot = 0;
for (uint8_t i = 0; i < 6; ++i) {
for (uint8_t i = 0; i < 6; ++i)
{
CAN0.init_Filt(i, 0, _std_to_hw(0x000));
}
// Setzen
for (size_t i = 0; i < count && s_nextFiltSlot < 6; ++i) {
const auto& f = list[i];
for (size_t i = 0; i < count && s_nextFiltSlot < 6; ++i)
{
const auto &f = list[i];
const uint32_t hwId = f.ext ? f.id : _std_to_hw((uint16_t)f.id);
CAN0.init_Filt(s_nextFiltSlot++, f.ext ? 1 : 0, hwId);
}
if (!CAN_HAL_SetMode(MCP_NORMAL)) {
// Spiegel aktualisieren
s_savedFiltCount = 0;
for (size_t i = 0; i < count && i < 6; ++i)
{
s_savedFilt[i].id = list[i].ext ? list[i].id : (uint32_t)((uint16_t)list[i].id);
s_savedFilt[i].ext = list[i].ext;
++s_savedFiltCount;
}
if (!CAN_HAL_SetMode(MCP_NORMAL))
{
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false;
}
return true;
}
bool CAN_HAL_Read(unsigned long& id, uint8_t& len, uint8_t data[8])
bool CAN_HAL_Read(unsigned long &id, uint8_t &len, uint8_t data[8])
{
if (CAN0.checkReceive() != CAN_MSGAVAIL) return false;
if (CAN0.readMsgBuf(&id, &len, data) != CAN_OK) {
if (CAN0.checkReceive() != CAN_MSGAVAIL)
return false;
if (CAN0.readMsgBuf(&id, &len, data) != CAN_OK)
{
// Echte Lese-Fehler -> vermutlich SPI/Controller-Problem
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
return false;
}
// MCP_CAN schreibt Flags in das ID-Wort:
// bit31 = EXT, bit30 = RTR, Rest = rohe ID (11 oder 29 Bit)
const bool ext = (id & 0x80000000UL) != 0;
const bool rtr = (id & 0x40000000UL) != 0; // aktuell nur informativ
// "Saubere" ID für Aufrufer herstellen
const uint32_t clean_id = ext ? (id & 0x1FFFFFFFUL) : (id & 0x7FFUL);
id = clean_id;
// Trace-Hook (RX)
if (s_traceSink)
{
CanLogFrame f{};
f.ts_ms = millis();
f.id = clean_id;
f.ext = ext;
f.rx = true;
f.dlc = len;
if (len)
memcpy(f.data, data, len);
s_traceSink(f);
}
// Optional: Wenn du RTR-Frames speziell behandeln willst, könntest du hier
// (rtr==true) markieren/loggen oder len=0 erzwingen. Für jetzt: einfach durchreichen.
(void)rtr;
return true;
}
uint8_t CAN_HAL_Send(unsigned long id, bool ext, uint8_t len, const uint8_t* data)
uint8_t CAN_HAL_Send(unsigned long id, bool ext, uint8_t len, const uint8_t *data)
{
// Sende-Fehler (CAN_FAILTX) müssen nicht zwingend Transceiver-Defekte sein (z. B. Bus-Off).
// Höhere Ebene kann bei Bedarf DTCs setzen. Hier nur durchreichen.
return CAN0.sendMsgBuf(id, ext ? 1 : 0, len, const_cast<uint8_t*>(data));
// Senden
uint8_t st = CAN0.sendMsgBuf(id, ext ? 1 : 0, len, const_cast<uint8_t *>(data));
// Trace-Hook (TX) nur bei Erfolg loggen optional: immer loggen
if (st == CAN_OK && s_traceSink)
{
CanLogFrame f{};
f.ts_ms = millis();
f.id = id;
f.ext = ext;
f.rx = false;
f.dlc = len;
if (len)
memcpy(f.data, data, len);
s_traceSink(f);
}
return st;
}
// ==== Diagnose/Utilities ====
@@ -233,7 +451,7 @@ uint8_t CAN_HAL_GetErrorFlags()
return CAN0.getError();
}
void CAN_HAL_GetErrorCounters(uint8_t& tec, uint8_t& rec)
void CAN_HAL_GetErrorCounters(uint8_t &tec, uint8_t &rec)
{
tec = CAN0.errorCountTX();
rec = CAN0.errorCountRX();

View File

@@ -1,7 +1,12 @@
#include "can_obd2.h"
#include "can_hal.h"
#include "dtc.h"
#include "debugger.h"
#include "globals.h" // falls du später Einstellungen brauchst
#include "globals.h"
#include <stdarg.h>
// Trace-Sink aus webui.cpp (o.ä.)
extern void TRACE_OnObdFrame(uint32_t id, bool rx, const uint8_t *d, uint8_t dlc, const char *note);
// =======================
// Konfiguration (anpassbar)
@@ -14,7 +19,7 @@
// Antwort-Timeout auf eine einzelne Anfrage
#ifndef OBD2_RESP_TIMEOUT_MS
#define OBD2_RESP_TIMEOUT_MS 60 // ~60 ms
#define OBD2_RESP_TIMEOUT_MS 120 // etwas großzügiger für reale ECUs
#endif
// Wenn so lange keine valide Antwort kam, gilt die Geschwindigkeit als stale -> v=0
@@ -32,10 +37,21 @@
#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_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,11 +183,25 @@ uint32_t Process_CAN_OBD2_Speed()
}
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
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true);
maybeDebug(now, "OBD2-CAN send failed (%u)\n", st);
}
}
}
// 2) Non-blocking Receive: wenige Frames pro Tick ziehen
for (uint8_t i = 0; i < OBD2_MAX_READS_PER_CALL; ++i)
@@ -182,9 +218,9 @@ uint32_t Process_CAN_OBD2_Speed()
// Erwartete Formate:
// - Einfache Antwort: 0x41 0x0D <A> ...
// - Mit Längen-Byte: 0x03 0x41 0x0D <A> ...
// - Mit Längen-Byte: 0x03/0x04 0x41 0x0D <A> ...
uint8_t modeResp = 0, pid = 0, speedKmh = 0;
if (rx[0] == 0x03 && len >= 4 && rx[1] == 0x41 && rx[2] == 0x0D)
if ((rx[0] == 0x03 || rx[0] == 0x04) && len >= 4 && rx[1] == 0x41 && rx[2] == 0x0D)
{
modeResp = rx[1];
pid = rx[2];
@@ -198,7 +234,9 @@ uint32_t Process_CAN_OBD2_Speed()
}
else
{
continue; // anderes PID/Format ignorieren
// Nicht das gesuchte PID → optional trotzdem loggen
TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, "other OBD resp");
continue;
}
if (modeResp == 0x41 && pid == 0x0D)
@@ -211,10 +249,19 @@ uint32_t Process_CAN_OBD2_Speed()
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, false);
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, false);
char note[40];
snprintf(note, sizeof(note), "speed=%ukmh", (unsigned)speedKmh);
TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, note);
maybeDebug(now, "OBD2 speed: %u km/h (%lu mm/s)\n",
(unsigned)speedKmh, (unsigned long)s_lastSpeedMMps);
break; // eine valide Antwort pro Zyklus reicht
}
else
{
// ist zwar OBD-II Antwort, aber nicht unser PID optional loggen
TRACE_OnObdFrame(rxId, /*rx=*/true, rx, len, "other OBD resp");
}
}
// 3) Offene Anfrage: Timeout prüfen
@@ -223,13 +270,17 @@ uint32_t Process_CAN_OBD2_Speed()
// Keine passende Antwort erhalten
MaintainDTC(DTC_OBD2_CAN_NO_RESPONSE, true);
s_state = ObdState::Idle;
TRACE_OnObdFrame(0x000, /*rx=*/true, nullptr, 0, "timeout 01 0D");
}
// 4) Integration (mm) über dt
uint32_t add_mm = 0;
if (s_lastIntegrateMs == 0)
s_lastIntegrateMs = now;
const uint32_t dt_ms = now - s_lastIntegrateMs;
uint32_t raw_dt = now - s_lastIntegrateMs;
if (raw_dt > OBD2_MAX_DT_MS) raw_dt = OBD2_MAX_DT_MS; // Ausreißer klemmen
const uint32_t dt_ms = raw_dt;
s_lastIntegrateMs = now;
// Stale-Schutz: wenn lange keine Antwort -> v=0

View File

@@ -13,11 +13,13 @@
#include "webui.h"
#include "common.h"
#include "can_hal.h" // <-- für CanLogFrame, Trace-Sink
#include <memory> // std::unique_ptr
#include <cstring> // strlen, strncpy, memcpy
#include <algorithm> // std::clamp
AsyncWebServer webServer(80);
AsyncWebSocket webSocket("/ws");
const char *PARAM_MESSAGE = "message";
@@ -52,8 +54,6 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request, const String &f
void WebServerEEJSON_Callback(AsyncWebServerRequest *request);
void GetFlashVersion(char *buff, size_t buff_size);
AsyncWebSocket webSocket("/ws");
void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len);
void Websocket_RefreshClientData_DTCs(uint32_t client_id);
@@ -65,7 +65,10 @@ void parseWebsocketString(char *data, char *identifierBuffer, size_t identifierB
int findIndexByString(const char *searchString, const char *const *array, int arraySize);
// ---------- small helpers (safety) ----------
static inline const char *nz(const char *p) { return p ? p : ""; }
static inline const char *nz(const char *p)
{
return p ? p : "";
}
static inline String tableStr(const char *const *tbl, int idx, int size)
{
@@ -92,6 +95,290 @@ static inline bool validIndex(int idx, int size)
return idx >= 0 && idx < size;
}
// =====================================================================
// WebSocket-basierter Trace
// =====================================================================
enum class TraceMode
{
None,
Raw,
Obd
};
static TraceMode g_traceMode = TraceMode::None;
static uint32_t g_traceOwnerId = 0; // WS-Client-ID des Starters
static uint32_t g_traceStartMs = 0;
static uint32_t g_traceLines = 0;
static uint32_t g_traceDrops = 0;
// Aktueller WS-Client während WS_EVT_DATA (für HandleMessage)
static AsyncWebSocketClient *g_wsCurrentClient = nullptr;
// Ringpuffer (verlusttolerant)
// ---- Dynamischer Ringpuffer, um BSS klein zu halten ----
#ifndef TRACE_FMT_BUFSZ
#define TRACE_FMT_BUFSZ 128 // Puffer für ASCII-Zeile (unabhängig vom Ring)
#endif
#ifndef TRACE_DEFAULT_LINES
#define TRACE_DEFAULT_LINES 64 // 64 x 128 = 8KB
#endif
#ifndef TRACE_DEFAULT_LINE_MAX
#define TRACE_DEFAULT_LINE_MAX 128
#endif
static char *g_ring = nullptr; // contiguous: lines * lineSize
static uint16_t g_ringLines = 0;
static uint16_t g_lineSize = 0;
static uint16_t g_head = 0, g_tail = 0;
static inline bool ring_alloc(uint16_t lines, uint16_t lineSize)
{
size_t bytes = (size_t)lines * (size_t)lineSize;
g_ring = (char *)malloc(bytes);
if (!g_ring)
return false;
g_ringLines = lines;
g_lineSize = lineSize;
g_head = g_tail = 0;
return true;
}
static inline void ring_free()
{
if (g_ring)
free(g_ring);
g_ring = nullptr;
g_ringLines = 0;
g_lineSize = 0;
g_head = g_tail = 0;
}
static inline bool ring_empty() { return g_head == g_tail; }
static inline bool ring_full() { return (uint16_t)(g_head + 1) == g_tail; }
static inline char *ring_slot(uint16_t idx) { return g_ring + ((idx % g_ringLines) * g_lineSize); }
static inline void ring_push_line(const char *s)
{
if (!g_ring)
return;
if (ring_full())
{
g_tail++;
g_traceDrops++;
}
char *dst = ring_slot(g_head);
strncpy(dst, s, g_lineSize - 1);
dst[g_lineSize - 1] = '\0';
g_head++;
}
static inline const char *ring_front() { return ring_empty() ? "" : ring_slot(g_tail); }
static inline void ring_pop()
{
if (!ring_empty())
g_tail++;
}
// Fallback: direkt senden, falls kein Ring (zu wenig RAM)
static inline void trace_emit_line_direct_or_ring(const char *s)
{
if (g_ring)
{ // wenn Ring existiert -> dort ablegen
ring_push_line(s);
return;
}
// sonst: direkt an den Owner senden (Best-Effort)
if (g_traceOwnerId && webSocket.availableForWrite(g_traceOwnerId))
{
String payload;
payload.reserve(strlen(s) + 16);
payload += "TRACELINE;";
payload += s;
payload += "\n";
webSocket.text(g_traceOwnerId, payload);
}
else
{
g_traceDrops++;
}
}
// ASCII-Formatter (ID 3-stellig 11-bit, 8-stellig 29-bit)
static void TRACE_FormatLine(char *dst, size_t n, const CanLogFrame &f, const char *note)
{
int off = snprintf(dst, n, "%lu %s 0x%0*lX %u ",
(unsigned long)f.ts_ms,
f.rx ? "RX" : "TX",
f.ext ? 8 : 3, (unsigned long)f.id,
f.dlc);
for (uint8_t i = 0; i < f.dlc && off < (int)n - 3; i++)
off += snprintf(dst + off, n - off, "%02X ", f.data[i]);
if (note && *note)
snprintf(dst + off, n - off, "; %s", note);
}
// Sinks ---------------------------------------------------------------
// RAW (wird vom HAL bei RX/TX gerufen)
static void TRACE_SinkRaw(const CanLogFrame &f)
{
if (g_traceMode != TraceMode::Raw || g_traceOwnerId == 0)
return;
char buf[TRACE_FMT_BUFSZ];
TRACE_FormatLine(buf, sizeof(buf), f, nullptr);
trace_emit_line_direct_or_ring(buf);
g_traceLines++;
}
/**
* OBD-Trace-Hook:
* Wird von can_obd2.cpp aufgerufen, um einzelne OBD-Frames (ASCII) an den
* WebSocket-Trace zu übergeben. Implementierung liegt in webui.cpp.
*
* @param id CAN-ID (11-bit bei OBD)
* @param rx true=Empfangen, false=Gesendet
* @param d Datenpointer (kann nullptr sein, wenn dlc==0)
* @param dlc Datenlänge (0..8)
* @param note optionale Zusatznotiz (z.B. "Mode01 PID 0x0D")
*/
void TRACE_OnObdFrame(uint32_t id, bool rx, const uint8_t *d, uint8_t dlc, const char *note)
{
if (g_traceMode != TraceMode::Obd || g_traceOwnerId == 0)
return;
CanLogFrame f{};
f.ts_ms = millis();
f.id = id;
f.ext = false;
f.rx = rx;
f.dlc = dlc;
if (d && dlc)
memcpy(f.data, d, dlc);
char buf[TRACE_FMT_BUFSZ];
TRACE_FormatLine(buf, sizeof(buf), f, note);
trace_emit_line_direct_or_ring(buf);
g_traceLines++;
}
// Owner noch da?
static inline bool trace_owner_online()
{
return (g_traceOwnerId != 0) && webSocket.hasClient(g_traceOwnerId);
}
// Pump: gebündelt senden, Backpressure beachten
static void TRACE_PumpWs()
{
if (!g_ring)
return; // bei direktem Senden gibt's keinen Ring zu pumpen
if (g_traceMode == TraceMode::None || g_traceOwnerId == 0)
return;
if (!trace_owner_online())
return;
if (!webSocket.availableForWrite(g_traceOwnerId))
return;
String payload;
payload.reserve(2048);
int sent = 0;
// mehrere Zeilen in eine WS-Nachricht
while (!ring_empty() && sent < 32)
{
payload += "TRACELINE;";
payload += ring_front();
payload += "\n";
ring_pop();
sent++;
}
if (payload.length())
webSocket.text(g_traceOwnerId, payload);
}
static void TRACE_StopWs(const char *reason)
{
if (g_traceMode == TraceMode::None)
return;
// Hooks lösen
CAN_HAL_SetTraceSink(nullptr);
CAN_HAL_EnableRawSniffer(false);
// Abschlussinfo an Owner (falls noch online)
if (trace_owner_online())
{
String end = "STOPTRACE;mode=";
end += (g_traceMode == TraceMode::Raw ? "raw" : "obd");
end += ";lines=";
end += String(g_traceLines);
end += ";drops=";
end += String(g_traceDrops);
if (reason)
{
end += ";reason=";
end += reason;
}
webSocket.text(g_traceOwnerId, end);
}
// Ring freigeben
ring_free();
// Reset
g_traceMode = TraceMode::None;
g_traceOwnerId = 0;
g_traceStartMs = 0;
g_traceLines = 0;
g_traceDrops = 0;
g_head = g_tail = 0;
}
static void TRACE_StartWs(TraceMode m, uint32_t ownerId)
{
// Falls schon aktiv → erst stoppen (sauber)
if (g_traceMode != TraceMode::None)
{
TRACE_StopWs("restart");
}
g_traceMode = m;
g_traceOwnerId = ownerId;
g_traceStartMs = millis();
g_traceLines = 0;
g_traceDrops = 0;
// vorhandenen Ring sicherheitshalber freigeben
ring_free();
// versuchen: 64x128 (8KB)
if (!ring_alloc(TRACE_DEFAULT_LINES, TRACE_DEFAULT_LINE_MAX))
{
// Fallback: 48x112 ~ 5.3KB
if (!ring_alloc(48, 112))
{
// Minimal: 32x96 ~ 3KB
(void)ring_alloc(32, 96); // wenn das auch scheitert -> g_ring bleibt nullptr
}
}
if (m == TraceMode::Raw)
{
CAN_HAL_SetTraceSink(TRACE_SinkRaw);
CAN_HAL_EnableRawSniffer(true);
}
else
{ // Obd
CAN_HAL_SetTraceSink(nullptr);
CAN_HAL_EnableRawSniffer(false);
}
String hdr = "STARTTRACE;mode=";
hdr += (m == TraceMode::Raw ? "raw" : "obd");
hdr += ";ts=";
hdr += String(g_traceStartMs);
webSocket.text(ownerId, hdr);
}
// =====================================================================
// WebUI
// =====================================================================
/**
* @brief Initializes the web-based user interface (WebUI) for the ChainLube application.
*
@@ -139,10 +426,8 @@ void initWebUI()
{ request->redirect("/index.htm"); });
webServer.onNotFound(WebserverNotFound_Callback);
webServer.on("/eejson", HTTP_GET, WebServerEEJSON_Callback);
webServer.on(
"/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback);
webServer.on(
"/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback);
webServer.on("/doUpdate", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverFirmwareUpdate_Callback);
webServer.on("/eeRestore", HTTP_POST, [](AsyncWebServerRequest *request) {}, WebserverEERestore_Callback);
// Start the web server
webServer.begin();
@@ -218,6 +503,29 @@ void Webserver_Process()
}
}
}
// Trace pumpen (sicher, backpressure-aware)
TRACE_PumpWs();
// Watchdog: Owner weg → Stop nach 10s (zusätzlich zu Disconnect-Stop)
static uint32_t ownerMissingSince = 0;
if (g_traceMode != TraceMode::None)
{
if (!trace_owner_online())
{
if (ownerMissingSince == 0)
ownerMissingSince = millis();
if (millis() - ownerMissingSince > 10000)
{
TRACE_StopWs("owner-timeout");
ownerMissingSince = 0;
}
}
else
{
ownerMissingSince = 0;
}
}
}
/**
@@ -235,6 +543,11 @@ void Webserver_Process()
*/
void Webserver_Shutdown()
{
// Bei Shutdown Trace beenden
if (g_traceMode != TraceMode::None)
{
TRACE_StopWs("shutdown");
}
if (webSocket.count() > 0)
webSocket.closeAll();
webServer.end();
@@ -298,7 +611,6 @@ void GetFlashVersion(char *buff, size_t buff_size)
*/
void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final)
{
if (!index)
{
Debug_pushMessage("Update\n");
@@ -337,6 +649,7 @@ void WebserverFirmwareUpdate_Callback(AsyncWebServerRequest *request, const Stri
}
}
}
void WebserverEERestore_Callback(AsyncWebServerRequest *request,
const String &filename,
size_t index,
@@ -440,8 +753,8 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request,
LubeConfig.RimDiameter_Inch = clamp_u32(json["config"]["RimDiameter_Inch"].as<uint32_t>(), 0, 30);
LubeConfig.DistancePerRevolution_mm = clamp_u32(json["config"]["DistancePerRevolution_mm"].as<uint32_t>(), 0, 10000);
LubeConfig.BleedingPulses = clamp_u16(json["config"]["BleedingPulses"].as<uint16_t>(), 0, 1000);
LubeConfig.WashMode_Distance = json["config"]["WashMode_Distance"].as<uint16_t>(); // ggf. Grenzen anpassen
LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as<uint16_t>(); // ggf. Grenzen anpassen
LubeConfig.WashMode_Distance = json["config"]["WashMode_Distance"].as<uint16_t>();
LubeConfig.WashMode_Interval = json["config"]["WashMode_Interval"].as<uint16_t>();
LubeConfig.LED_Mode_Flash = json["config"]["LED_Mode_Flash"].as<bool>();
LubeConfig.LED_Max_Brightness = json["config"]["LED_Max_Brightness"].as<uint8_t>();
LubeConfig.LED_Min_Brightness = json["config"]["LED_Min_Brightness"].as<uint8_t>();
@@ -483,15 +796,12 @@ void WebserverEERestore_Callback(AsyncWebServerRequest *request,
PersistenceData.odometer = json["persis"]["odometer"].as<uint32_t>();
PersistenceData.checksum = json["persis"]["checksum"].as<uint32_t>();
// Optional: Sanity-Autokorrektur im RAM (keine EEPROM-Writes hier!)
{
uint32_t sanity = ConfigSanityCheck(true);
if (sanity > 0)
{
MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity);
Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity);
}
}
ee_done = true;
}
@@ -554,6 +864,10 @@ void WebServerEEJSON_Callback(AsyncWebServerRequest *request)
request->send(response);
}
// =====================================================================
// WebSocket Handling
// =====================================================================
/**
* @brief Callback function for handling WebSocket events.
*
@@ -584,10 +898,17 @@ void WebsocketEvent_Callback(AsyncWebSocket *server, AsyncWebSocketClient *clien
}
case WS_EVT_DISCONNECT:
Debug_pushMessage("WebSocket client #%u disconnected\n", client->id());
// Falls Owner: Trace sofort stoppen
if (g_traceOwnerId == client->id())
TRACE_StopWs("owner-disconnect");
break;
case WS_EVT_DATA:
g_wsCurrentClient = client; // für HandleMessage → Owner-ID
Websocket_HandleMessage(arg, data, len);
g_wsCurrentClient = nullptr;
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
@@ -614,19 +935,69 @@ void Websocket_HandleMessage(void *arg, uint8_t *data, size_t len)
memcpy(buf.get(), data, len);
buf[len] = '\0';
Debug_pushMessage("Websocket-Message (len: %d): %s\n", (int)len, buf.get());
const uint32_t senderId = g_wsCurrentClient ? g_wsCurrentClient->id() : 0;
Debug_pushMessage("Websocket-Message from #%u (len: %d): %s\n", (unsigned)senderId, (int)len, buf.get());
// Steuerkommandos für Trace hier direkt behandeln (brauchen senderId)
if (strncmp(buf.get(), "btn-", 4) == 0)
{
// Format: "btn-<identifier>[:<value>]"
char identifier[32];
char value[64];
parseWebsocketString((char *)buf.get() + 4, identifier, sizeof(identifier), value, sizeof(value));
if (strcmp(identifier, "trace-start") == 0)
{
// Lock: Nur starten, wenn nicht aktiv
if (g_traceMode != TraceMode::None)
{
String busy = "TRACEBUSY;owner=";
busy += String(g_traceOwnerId);
if (senderId)
webSocket.text(senderId, busy);
}
else
{
TraceMode m = TraceMode::None;
if (!strcmp(value, "raw"))
m = TraceMode::Raw;
else if (!strcmp(value, "obd"))
m = TraceMode::Obd;
if (m == TraceMode::None)
{
if (senderId)
webSocket.text(senderId, "TRACEERROR;msg=mode-missing");
}
else
{
TRACE_StartWs(m, senderId);
}
}
return;
}
else if (strcmp(identifier, "trace-stop") == 0)
{
// Stop darf jeder
TRACE_StopWs("user-stop");
// optional: ACK an Sender (Owner bekommt STOPTRACE ohnehin)
if (senderId)
webSocket.text(senderId, "TRACEACK;cmd=stop");
return;
}
// sonst: in "normalen" Button-Handler
Websocket_HandleButtons((uint8_t *)buf.get() + 4);
return;
}
else if (strncmp(buf.get(), "set-", 4) == 0)
{
Websocket_HandleSettings((uint8_t *)buf.get() + 4);
return;
}
else
{
Debug_pushMessage("Got unknown Websocket-Message '%s' from client\n", buf.get());
Debug_pushMessage("Got unknown Websocket-Message '%s'\n", buf.get());
}
}
}
@@ -855,7 +1226,6 @@ void Websocket_RefreshClientData_DTCs(uint32_t client_id)
*/
void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
{
if (send_mapping)
{
if (client_id > 0)
@@ -865,12 +1235,11 @@ void Websocket_RefreshClientData_Status(uint32_t client_id, bool send_mapping)
}
String temp = "STATUS:";
temp.concat(String(ToString(globals.systemStatus)) + ";");
// Guard against division by zero (capacity==0)
uint32_t cap = LubeConfig.tankCapacity_ml;
uint32_t remain10 = (PersistenceData.tankRemain_microL / 10); // keep your original math
uint32_t remain10 = (PersistenceData.tankRemain_microL / 10);
uint32_t ratio = (cap > 0) ? (remain10 / cap) : 0;
temp.concat(String(ratio) + ";");