/* 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; } } function onMarteConnected() { marteConnected = true; document.getElementById('conn-status').classList.add('ok'); document.getElementById('btn-connect').textContent = 'Disconnect'; appendLog('sys','INFO','Connected to MARTe2'); // Auto-discover sendCmd('DISCOVER'); sendCmd('TREE'); // Start polling step status startStepPoll(); } function onMarteDisconnected() { marteConnected = false; document.getElementById('conn-status').classList.remove('ok'); document.getElementById('btn-connect').textContent = 'Connect'; appendLog('sys','WARNING','Disconnected from MARTe2'); stopStepPoll(); } function onSignalCache(msg) { // Server sends cached signal list when a new browser connects mid-session if (msg.signals) processDiscovery(msg.signals); } function onResponse(msg) { try { const data = msg.data ? JSON.parse(msg.data) : null; switch (msg.tag) { case 'DISCOVER': if (data) processDiscovery(data.Signals || []); break; case 'TREE': if (data) processTree(data); break; case 'INFO': showInfo(msg.tag, msg.data); break; case 'CONFIG': showInfo('CONFIG', msg.data); break; case 'STEP_STATUS': processStepStatus(data); break; case 'VALUE': processValue(data); break; } } catch(e) { // non-JSON responses (TRACE, FORCE, etc.) — silently ignore } } function onTextLine(line) { if (line.startsWith('OK ')) return; // handled by response handler appendLog('sys','DEBUG', line); } function onLog(msg) { // Normalise level names from MARTe2 let lv = msg.level; if (lv === 'Information') lv = 'INFO'; else if (lv === 'Warning') lv = 'WARNING'; else if (lv === 'Debug') lv = 'DEBUG'; else if (lv === 'FatalError' || lv === 'ParametersError' || lv === 'OSError') lv = 'ERROR'; appendLog(msg.time, lv, msg.message); } function onServiceConfig(msg) { appendLog('sys','INFO',`ServiceConfig: UDP=${msg.udp_port} LOG=${msg.log_port}`); } function onUdpStats(msg) { udpPackets = msg.packets; udpDropped = msg.dropped; document.getElementById('udp-stats').textContent = `${udpPackets} pkts${udpDropped>0?' ⚠'+udpDropped+' drop':''}`; } function onTelemetry(msg) { if (!msg.signals) return; for (const s of msg.signals) { const names = s.names || []; const vals = s.values || []; for (let e = 0; e < vals.length; e++) { const n = vals.length > 1 ? `${names[0]}[${e}]` : (names[0] || ''); if (!n || !tracedSet.has(n)) continue; let td = traceData[n]; if (!td) { td = {ts:[], vals:[], lastVal:0, color: nextColor()}; traceData[n] = td; } td.ts.push(s.ts); td.vals.push(vals[e]); td.lastVal = vals[e]; // Trim to max points if (td.ts.length > MAX_TRACE_PTS) { const trim = td.ts.length - MAX_TRACE_PTS; td.ts.splice(0, trim); td.vals.splice(0, trim); } } // Mark plots dirty for (const p of plots) { for (const sr of p.series) { if (names.includes(sr.name) || (vals.length>1 && names.length>0 && p.series.some(x => x.name.startsWith(names[0])))) { p.dirty = true; break; } } } } // Update only the value spans — never rebuild the DOM here updateTracedValues(); } // ════════════════════════════════════════════════════════════════════ // Discovery // ════════════════════════════════════════════════════════════════════ function processDiscovery(sigs) { signalsById = {}; signalsByName = {}; for (const s of sigs) { const el = s.elements || 1; const meta = {id:s.id, name:s.name, type:s.type||'', elements:el, dimensions:s.dimensions||0, names:[s.name]}; signalsById[s.id] = meta; signalsByName[s.name] = meta; // Array element names if (el > 1) { for (let i=0; i!n.includes('[')); ['force-sig-list','break-sig-list'].forEach(id=>{ const dl = document.getElementById(id); dl.innerHTML = names.map(n=>`' + threads.map(t=>``).join(''); } else if (el.tagName === 'DATALIST') { el.innerHTML = threads.map(t=>`