added Function to create CAN-Traces from WebUI
Some checks failed
CI-Build/Kettenoeler/pipeline/head There was a failure building this commit
Some checks failed
CI-Build/Kettenoeler/pipeline/head There was a failure building this commit
This commit is contained in:
@@ -191,6 +191,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 />
|
||||||
|
@@ -23,4 +23,4 @@ document
|
|||||||
var fileName = document.getElementById("fw-update-file").files[0].name;
|
var fileName = document.getElementById("fw-update-file").files[0].name;
|
||||||
var nextSibling = e.target.nextElementSibling;
|
var nextSibling = e.target.nextElementSibling;
|
||||||
nextSibling.innerText = fileName;
|
nextSibling.innerText = fileName;
|
||||||
});
|
});
|
@@ -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) {
|
||||||
|
@@ -1 +1 @@
|
|||||||
1.04
|
1.05
|
@@ -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();
|
||||||
|
@@ -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)
|
||||||
|
@@ -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_
|
||||||
|
@@ -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) Loopback‑Selftest (ohne Bus)
|
// Masken 0 -> alles durchlassen
|
||||||
if (!_selftest_loopback(20)) {
|
|
||||||
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Optional Listen‑Only‑Probe (nur Info)
|
|
||||||
_probe_listen_only(cfg.listenOnlyProbeMs);
|
|
||||||
|
|
||||||
// 4) Default: Filter/Masks neutral, Mode NORMAL
|
|
||||||
// -> Für Masken/Filter müssen wir in CONFIG sein (hier: MODE_CONFIG laut deiner Lib)
|
|
||||||
if (!_trySetMode(MODE_CONFIG, s_modeSettleMs)) {
|
|
||||||
MaintainDTC(DTC_CAN_TRANSCEIVER_FAILED, true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// weit offen (STD)
|
|
||||||
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,85 +277,170 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
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();
|
||||||
|
@@ -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,11 +37,22 @@
|
|||||||
#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_RESP_MIN = 0x7E8; // ECUs antworten 0x7E8..0x7EF
|
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;
|
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,9 +183,23 @@ uint32_t Process_CAN_OBD2_Speed()
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Senden fehlgeschlagen -> harter Timeout-DTC
|
#if OBD2_ALLOW_PHYSICAL_FALLBACK
|
||||||
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true);
|
// einmalig physisch versuchen (0x7E0 → Antwort 0x7E8)
|
||||||
maybeDebug(now, "OBD2-CAN send failed (%u)\n", st);
|
TRACE_OnObdFrame(OBD_REQ_ID_PHYSICAL, /*rx=*/false, req, 8, "req 01 0D (physical)");
|
||||||
|
st = CAN_HAL_Send(OBD_REQ_ID_PHYSICAL, /*ext=*/false, 8, req);
|
||||||
|
s_lastQueryTime = now;
|
||||||
|
if (st == CAN_OK)
|
||||||
|
{
|
||||||
|
s_state = ObdState::Waiting;
|
||||||
|
s_requestDeadline = now + OBD2_RESP_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
// Senden fehlgeschlagen -> harter Timeout-DTC
|
||||||
|
MaintainDTC(DTC_OBD2_CAN_TIMEOUT, true);
|
||||||
|
maybeDebug(now, "OBD2-CAN send failed (%u)\n", st);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,9 +218,9 @@ uint32_t Process_CAN_OBD2_Speed()
|
|||||||
|
|
||||||
// Erwartete Formate:
|
// 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
|
||||||
|
@@ -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,14 +796,11 @@ 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);
|
||||||
|
if (sanity > 0)
|
||||||
{
|
{
|
||||||
uint32_t sanity = ConfigSanityCheck(true);
|
MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity);
|
||||||
if (sanity > 0)
|
Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity);
|
||||||
{
|
|
||||||
MaintainDTC(DTC_EEPROM_CFG_SANITY, true, sanity);
|
|
||||||
Debug_pushMessage("Restore: ConfigSanity corrected (mask=0x%08lX)\n", sanity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ee_done = true;
|
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) + ";");
|
||||||
|
|
||||||
@@ -1087,4 +1456,4 @@ void Websocket_PushNotification(String Message, NotificationType_t type)
|
|||||||
}
|
}
|
||||||
webSocket.textAll("NOTIFY:" + typeString + ";" + Message);
|
webSocket.textAll("NOTIFY:" + typeString + ";" + Message);
|
||||||
Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString.c_str());
|
Debug_pushMessage("Sending Notification to WebUI: %s\n", typeString.c_str());
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user