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 = $( '