/* global uPlot */ 'use strict'; // ════════════════════════════════════════════════════════════════════ // State // ════════════════════════════════════════════════════════════════════ const MAX_TRACE_PTS = 100000; const COLORS = [ '#89b4fa','#f38ba8','#a6e3a1','#fab387','#f9e2af', '#cba6f7','#89dceb','#b4befe','#eba0ac','#94e2d5' ]; let ws = null; let marteConnected = false; // Signal registry: id(number) → {id, name, type, elements, dimensions, names:[]} let signalsById = {}; // name → meta (same objects, multiple name entries per signal for array elems) let signalsByName = {}; // Trace data: signalName → {ts:[], vals:[], lastVal:0, color:''} let traceData = {}; // Set of names currently being traced let tracedSet = new Set(); // Forced: name → value string let forcedSignals = {}; // Breakpoints: name → {op, threshold} let breakpoints = {}; // Thread list from TREE let threads = []; // All object paths (for autocomplete) let allObjects = []; // Plots: [{id, container, chart, series:[{name, color, gain, offset}], maxPts, follow, dirty}] let plots = []; let plotSeq = 0; // Logs let logs = []; const MAX_LOGS = 2000; let logDirty = false; // Step status let stepStatus = null; // {paused, gam, remaining, thread} let stepPollTimer = null; // Pending unforce target let unforceTarget = ''; // UDP stats let udpPackets = 0, udpDropped = 0; // ════════════════════════════════════════════════════════════════════ // WebSocket // ════════════════════════════════════════════════════════════════════ function connectWS() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; ws = new WebSocket(proto + '//' + location.host + '/ws'); ws.onopen = () => { appendLog('sys','INFO','WebSocket connected to server'); }; ws.onclose = () => { appendLog('sys','WARNING','WebSocket disconnected — reconnecting…'); setTimeout(connectWS, 2000); }; ws.onerror = () => {}; ws.onmessage = (e) => handleMsg(JSON.parse(e.data)); } function sendWS(obj) { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj)); } function sendCmd(cmd) { sendWS({type:'cmd', data:{cmd}}); } // ════════════════════════════════════════════════════════════════════ // Message handlers // ════════════════════════════════════════════════════════════════════ function handleMsg(msg) { switch (msg.type) { case 'connected': onMarteConnected(); break; case 'disconnected': onMarteDisconnected(); break; case 'signal_cache': onSignalCache(msg); break; case 'response': onResponse(msg); break; case 'text_line': onTextLine(msg.data); break; case 'telemetry': onTelemetry(msg); break; case 'log': onLog(msg); break; case 'service_config': onServiceConfig(msg); break; case 'udp_stats': onUdpStats(msg); break; case 'tree_clear': onTreeClear(msg.total); break; case 'tree_batch': onTreeBatch(msg); break; } } // ════════════════════════════════════════════════════════════════════ // Incremental tree rendering (server-side pre-processed batches) // // Batches are stored purely in memory — no DOM work during receipt. // DOM nodes are created lazily: only the top-level containers are // built immediately; everything inside a closed
is deferred // until the user opens it. This keeps the initial render O(1) in the // number of signals regardless of tree size. // ════════════════════════════════════════════════════════════════════ let _treeBuild = null; // { threads:[], objects:[] } — null when idle let _flatNodes = null; // Map let _childrenOf = null; // Map function onTreeClear(total) { _treeBuild = { threads: [], objects: [] }; _flatNodes = new Map(); _childrenOf = new Map(); document.getElementById('tree-body').innerHTML = ''; appendLog('sys', 'INFO', `Receiving tree (${total} nodes)…`); } function onTreeBatch(msg) { if (!_treeBuild) return; // Pure memory work — no DOM touched here. for (const node of (msg.nodes || [])) { _flatNodes.set(node.p, node); const pk = node.par || ''; let ch = _childrenOf.get(pk); if (!ch) { ch = []; _childrenOf.set(pk, ch); } ch.push(node); if (node.c === 'RealTimeThread' && node.n) _treeBuild.threads.push(node.n); if (node.c && node.c !== 'Signal' && node.n) _treeBuild.objects.push(node.p); } if (!msg.done) return; // All batches received — build only top-level nodes now. const body = document.getElementById('tree-body'); const frag = document.createDocumentFragment(); for (const node of (_childrenOf.get('') || [])) { frag.appendChild(_buildFlatNode(node)); } body.appendChild(frag); // Finalise autocompletes. threads = _treeBuild.threads; allObjects = _treeBuild.objects; ['step-thread','step-thread-list','step-thread-inp'].forEach(id => { const el = document.getElementById(id); if (!el) return; if (el.tagName === 'SELECT') { el.innerHTML = '' + threads.map(t => ``).join(''); } else if (el.tagName === 'DATALIST') { el.innerHTML = threads.map(t => `