const tracePlaceholder = Symbol('trace-placeholder'); const traceEvent = Symbol('trace-event'); const tracePacket = Symbol('trace-packet'); const traceMaxEvents = 200; const state = { start: new Date(), paused: false, global: { preambles_found: 0, successful_decodes: 0, failed_decodes: 0, header_errors: 0, }, }; function updatePause(pause) { const button = document.querySelector('#pause'); if (pause) { button.value = 'Continue'; } else { button.value = 'Pause'; } state.paused = pause; } function updateGlobal() { for (const key in state.global) { document.querySelector(`#${key}`).textContent = state.global[key]; } } function getTime() { const x = new Date(); // HH:MM:SS.ms, thanks JavaScript const hh = ('0' + x.getHours()).slice(-2); const mm = ('0' + x.getMinutes()).slice(-2); const ss = ('0' + x.getSeconds()).slice(-2); const ms = ('00' + x.getMilliseconds()).slice(-3); return `${hh}:${mm}:${ss}.${ms}`; } function addTrace(type, data) { const span = document.createElement('span'); span.textContent = data.string; span.style.color = data.color; span.style.cursor = 'default'; if (type !== tracePlaceholder) { span.title = getTime(); } if (type === tracePacket) { span.onclick = () => { updatePause(true); updateChart(data.packet); updateData(data.packet); // Redraw chartConstellation.update(); }; span.style.cursor = 'pointer'; } const trace = document.querySelector('#trace'); trace.prepend(span); if (trace.childNodes.length > traceMaxEvents) { trace.removeChild(trace.lastChild); } } const chartConstellation = new Chart( document.querySelector('#constellation-canvas'), { type: 'scatter', data: { datasets: [ { label: 'Preamble', data: [], }, { label: 'Header', data: [], }, { label: 'Data', data: [], }, ], }, options: { animation: false, aspectRatio: 1, elements: { point: { radius: 1, }, }, }, }); const chartHistory = new Chart( document.querySelector('#history-canvas'), { type: 'line', data: { labels: [], datasets: [], }, options: { animation: false, }, }); function updateChart(packet) { chartConstellation.data.datasets[0].data = packet['preamble_symbols']; chartConstellation.data.datasets[1].data = packet['header_symbols']; chartConstellation.data.datasets[2].data = packet['data_symbols']; // .update() is deferred, see redrawPlots() } function updateData(packet) { const keys = []; for (const x in packet) { if (x === 'preamble_symbols' || x === 'header_symbols' || x === 'data_symbols') { continue } keys.push(x); } keys.sort(); const divs = []; for (const x of keys) { const div = document.createElement('div'); div.textContent = `${x}: ${packet[x]}`; divs.push(div); } document.querySelector('#data').replaceChildren(...divs); } function addHistory(packet) { const keys = []; for (const x in packet) { if (x === 'preamble_symbols' || x === 'header_symbols' || x === 'data_symbols') { continue; } keys.push(x); } keys.sort(); if (chartHistory.data.datasets.length === 0) { for (const x of keys) { chartHistory.data.datasets.push({ label: x, data: [], }) } } let i = 0; for (const x of keys) { let data = packet[x]; if (data == -1e38 /* "NaN" */) { data = undefined; } chartHistory.data.datasets[i].data.push(data); i++; } chartHistory.data.labels.push((new Date() - state.start)/1000); // .update() is deferred, see redrawPlots() } let redrawPending = false; function redrawPlots() { // Updating the plots is too slow for live updates for all received // messages. Instead of falling behind, skip redraws when the messages // arrive too quickly. if (redrawPending) { return; } redrawPending = true; window.setTimeout(() => { chartHistory.update(); chartConstellation.update(); redrawPending = false; }, 10 /* ms */); } const ws = new WebSocket(location.origin.replace(/^http/, 'ws') + '/events'); ws.onmessage = (evt) => { if (state.paused) { return; } const msg = JSON.parse(evt.data); if ('preambles_found' in msg) { const old = {}; for (const x in state.global) { old[x] = state.global[x]; } for (const x in msg) { state.global[x] = msg[x]; old[x] = msg[x] - old[x]; // delta } updateGlobal(); // Update trace with new events for (const x in old) { if (old[x] === 0) { continue; } let string, color; if (x === 'preambles_found') { string = 'p'; color = 'gray'; } else if (x === 'successful_decodes') { // Nothing to do as packet will be added to trace continue; } else if (x === 'header_errors') { string = 'H'; color = 'red'; } else if (x === 'failed_decodes') { string = 'D'; color = 'red'; } else { console.log(`Unknown event "${x}"!`); continue; } addTrace(traceEvent, { string: string, color: color, }); } } else if ('data_symbols' in msg) { updateChart(msg); updateData(msg); addHistory(msg); redrawPlots(); addTrace(tracePacket, { string: 'P', color: 'black', packet: msg, }); } }; ws.onclose = (evt) => { const div = document.querySelector('#error'); div.textContent = 'Websocket connection closed!'; div.hidden = false; // Log details console.log(evt); }; ws.onerror = ws.onclose; { const pause = document.querySelector('#pause'); pause.onclick = () => { updatePause(!state.paused); } updatePause(false); // Initialize trace for (let i = 0; i < traceMaxEvents; i++) { addTrace(tracePlaceholder, { string: '.', color: 'lightgray', }); } updateGlobal(); }