Files
Kettenoeler/Software/data_src/static/js/websocket.js
Marcel Peterkau 98629b744d
Some checks failed
CI-Build/Kettenoeler/pipeline/head There was a failure building this commit
added Function to create CAN-Traces from WebUI
2025-08-26 23:31:35 +02:00

529 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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">&times;</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);
}