Some checks failed
CI-Build/Kettenoeler/pipeline/head There was a failure building this commit
529 lines
15 KiB
JavaScript
529 lines
15 KiB
JavaScript
var gateway = `ws://${window.location.hostname}/ws`;
|
||
var websocket;
|
||
|
||
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");
|
||
onLoad();
|
||
});
|
||
|
||
function initWebSocket() {
|
||
console.log("Trying to open a WebSocket connection...");
|
||
websocket = new WebSocket(gateway);
|
||
websocket.onopen = onOpen;
|
||
websocket.onclose = onClose;
|
||
websocket.onmessage = onMessage; // <-- add this line
|
||
}
|
||
|
||
function initButtons() {
|
||
var elements = document.getElementsByClassName("btn-wsevent");
|
||
|
||
if (elements.length > 0) {
|
||
for (var i = 0; i < elements.length; i++) {
|
||
let element = elements[i];
|
||
element.addEventListener("click", sendButton);
|
||
}
|
||
}
|
||
}
|
||
|
||
function initSettingInputs() {
|
||
var elements = document.getElementsByClassName("set-wsevent");
|
||
|
||
if (elements.length > 0) {
|
||
for (var i = 0; i < elements.length; i++) {
|
||
let element = elements[i];
|
||
element.addEventListener("change", function () {
|
||
websocket_sendevent("set-" + element.id, element.value);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
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");
|
||
}
|
||
}
|
||
|
||
async function sendButton(event) {
|
||
const targetElement = event.target;
|
||
|
||
if (
|
||
targetElement.classList.contains("confirm") &&
|
||
window.confirm("Sicher?") == false
|
||
)
|
||
return;
|
||
|
||
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) {
|
||
var data = event.data;
|
||
|
||
if (data.startsWith("NOTIFY:")) {
|
||
var notify_data = data.slice(7).split(";")[1];
|
||
var notify_type = data.slice(7).split(";")[0];
|
||
showNotification(notify_data, notify_type);
|
||
} else if (data.startsWith("DEBUG:")) {
|
||
var addtext = data.slice(6);
|
||
var livedebug_out = document.getElementById("livedebug-out");
|
||
livedebug_out.value += addtext;
|
||
livedebug_out.scrollTop = livedebug_out.scrollHeight;
|
||
do_resize(livedebug_out);
|
||
} else if (data.startsWith("DTC:")) {
|
||
const dtcs = data.slice(4);
|
||
const dtcArray = dtcs.trim() !== "" ? dtcs.split(";").filter(Boolean) : [];
|
||
|
||
processDTCNotifications(dtcArray);
|
||
fillDTCTable(dtcArray);
|
||
} else if (data.startsWith("MAPPING_STATUS:")) {
|
||
const data_sliced = data.slice(15);
|
||
statusMapping = createMapping(data_sliced);
|
||
} else if (data.startsWith("MAPPING_STATIC:")) {
|
||
const data_sliced = data.slice(15);
|
||
staticMapping = createMapping(data_sliced);
|
||
console.log(staticMapping);
|
||
} else if (data.startsWith("STATUS:")) {
|
||
const data_sliced = data.slice(7);
|
||
const result = processDataString(data_sliced, statusMapping);
|
||
fillValuesToHTML(result);
|
||
} else if (data.startsWith("STATIC:")) {
|
||
const data_sliced = data.slice(7);
|
||
const result = processDataString(data_sliced, staticMapping);
|
||
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) {
|
||
return mappingString
|
||
.split(";")
|
||
.map((s) => s.trim())
|
||
.filter((s) => s !== "");
|
||
}
|
||
|
||
function processDataString(dataString, mapping) {
|
||
const valuesArray = dataString.split(";");
|
||
const dataObject = {};
|
||
|
||
valuesArray.forEach((value, index) => {
|
||
const variable = mapping[index];
|
||
if (variable) {
|
||
dataObject[variable] = value.trim();
|
||
}
|
||
});
|
||
|
||
return dataObject;
|
||
}
|
||
|
||
function onLoad(event) {
|
||
initWebSocket();
|
||
initButtons();
|
||
initSettingInputs();
|
||
overlay.style.display = "flex";
|
||
}
|
||
|
||
function websocket_sendevent(element_id, element_value) {
|
||
websocket.send(element_id + ":" + element_value);
|
||
}
|
||
|
||
function do_resize(textbox) {
|
||
var maxrows = 15;
|
||
var minrows = 3;
|
||
var txt = textbox.value;
|
||
var cols = textbox.cols;
|
||
|
||
var arraytxt = txt.split("\n");
|
||
var rows = arraytxt.length;
|
||
|
||
for (i = 0; i < arraytxt.length; i++)
|
||
rows += parseInt(arraytxt[i].length / cols);
|
||
|
||
if (rows > maxrows) textbox.rows = maxrows;
|
||
else if (rows < minrows) textbox.rows = minrows;
|
||
else textbox.rows = rows;
|
||
}
|
||
|
||
// --- Globale Puffer für Select-Handling ---
|
||
const selectDesiredValue = Object.create(null); // keyBase -> desired value ("speedsource" -> "GPS")
|
||
const selectOptionsReady = Object.create(null); // keyBase -> true/false
|
||
|
||
function splitCsv(s) {
|
||
return (s || "")
|
||
.split(",")
|
||
.map((x) => x.trim())
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function hasOption(selectEl, value) {
|
||
for (let i = 0; i < selectEl.options.length; i++) {
|
||
if (selectEl.options[i].value === value) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function addOption(selectEl, value, selectIt = false) {
|
||
const o = document.createElement("option");
|
||
o.value = value;
|
||
o.textContent = value;
|
||
selectEl.appendChild(o);
|
||
if (selectIt) {
|
||
selectEl.value = value;
|
||
}
|
||
}
|
||
|
||
function populateSelect(selectId, options, currentValue) {
|
||
const sel = document.getElementById(selectId);
|
||
if (!sel) return;
|
||
|
||
// 1) komplett ersetzen
|
||
sel.innerHTML = "";
|
||
options.forEach((opt) => addOption(sel, opt, false));
|
||
|
||
// 2) Zielwert bestimmen (currentValue > bereits gepuffert > vorhandener Wert > erste Option)
|
||
const wanted =
|
||
(currentValue && currentValue.length ? currentValue : null) ??
|
||
selectDesiredValue[selectId] ??
|
||
sel.value ??
|
||
(options[0] || "");
|
||
|
||
// 3) Falls gewünschte Option fehlt, hinzufügen
|
||
if (wanted && !hasOption(sel, wanted)) addOption(sel, wanted, false);
|
||
|
||
// 4) Setzen
|
||
sel.value = wanted;
|
||
|
||
// 5) Markieren: Optionen sind ready
|
||
selectOptionsReady[selectId] = true;
|
||
// Verbrauchte Wunschwerte löschen
|
||
delete selectDesiredValue[selectId];
|
||
}
|
||
|
||
// Robust: setzt den Wert; wenn Option fehlt, füge sie hinzu
|
||
function setDropdownValue(selectElement, value) {
|
||
if (!value) return;
|
||
if (!hasOption(selectElement, value)) {
|
||
addOption(selectElement, value, false);
|
||
}
|
||
selectElement.value = value;
|
||
}
|
||
|
||
// Core: schreibt Werte in DOM, erkennt *-options und füllt Selects
|
||
function fillValuesToHTML(dataset) {
|
||
for (const key in dataset) {
|
||
const val = dataset[key];
|
||
|
||
// A) Optionen-Feld? (z.B. "speedsource-options")
|
||
if (key.endsWith("-options")) {
|
||
const base = key.slice(0, -"-options".length); // "speedsource"
|
||
const selectId = base; // ID = keyBase
|
||
populateSelect(
|
||
selectId,
|
||
splitCsv(val),
|
||
selectDesiredValue[selectId] || undefined
|
||
);
|
||
continue; // fertig mit diesem key
|
||
}
|
||
|
||
// B) Normales Feld
|
||
const key_prefixed = "data-" + key;
|
||
const elements = document.getElementsByClassName(key_prefixed);
|
||
if (elements.length === 0) continue;
|
||
|
||
for (let i = 0; i < elements.length; i++) {
|
||
const el = elements[i];
|
||
|
||
// Checkbox?
|
||
if (el.type === "checkbox") {
|
||
el.checked = val == 1;
|
||
continue;
|
||
}
|
||
|
||
// Select?
|
||
if (el.tagName === "SELECT") {
|
||
const selectId = el.id || key; // ID hat Vorrang, sonst key
|
||
// Wenn die Optionen für dieses Select noch NICHT bereit sind, Wunschwert puffern
|
||
if (!selectOptionsReady[selectId]) {
|
||
selectDesiredValue[selectId] = val;
|
||
// Sicherheitsnetz: falls doch schon Optionen existieren, sofort setzen
|
||
setDropdownValue(el, val);
|
||
} else {
|
||
// Optionen sind ready -> ganz normal setzen (ggf. Option ergänzen)
|
||
setDropdownValue(el, val);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Progress-Bar?
|
||
if (el.classList.contains("progress-bar")) {
|
||
updateProgressBar(el, val);
|
||
continue;
|
||
}
|
||
|
||
// Hideable-Section?
|
||
if (el.classList.contains("hideable")) {
|
||
el.style.display = val == 0 ? "none" : "";
|
||
continue;
|
||
}
|
||
|
||
// Input/Textarea?
|
||
if ("value" in el) {
|
||
el.value = val;
|
||
continue;
|
||
}
|
||
|
||
// Fallback: content nodes (td, span, div)
|
||
el.textContent = val;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Funktion zum Aktualisieren der Fortschrittsleiste
|
||
function updateProgressBar(progressBar, value) {
|
||
// Wert in das aria-valuenow-Attribut einfügen
|
||
progressBar.setAttribute("aria-valuenow", value);
|
||
|
||
// Breite des Fortschrittsbalkens und inneren Text aktualisieren
|
||
progressBar.style.width = value + "%";
|
||
progressBar.textContent = value + "%";
|
||
}
|
||
|
||
function showNotification(message, type) {
|
||
// Erstellen Sie ein Bootstrap-Alert-Element
|
||
var alertElement = $(
|
||
'<div class="alert alert-' +
|
||
type +
|
||
' alert-dismissible fade show notification" role="alert">' +
|
||
"<strong>" +
|
||
message +
|
||
"</strong>" +
|
||
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
|
||
'<span aria-hidden="true">×</span>' +
|
||
"</button>" +
|
||
"</div>"
|
||
);
|
||
|
||
// Fügen Sie das Alert-Element dem Container hinzu
|
||
$("#notification-container").append(alertElement);
|
||
|
||
// Nach 5 Sekunden das Alert-Element ausblenden
|
||
setTimeout(function () {
|
||
alertElement.alert("close");
|
||
}, 5000);
|
||
}
|