hamnet70/impl/utils/visualizer/static/index.js

411 lines
11 KiB
JavaScript
Raw Permalink Normal View History

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2024 Simon Ruderich
const tracePlaceholder = Symbol('trace-placeholder');
const traceEvent = Symbol('trace-event');
const tracePacket = Symbol('trace-packet');
const traceMaxEvents = 200;
const historyMaxPackets = 300;
const state = {
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';
// Hide line markers
chartHistoryHz.options.plugins.lineMarker.index = undefined;
chartHistoryDb.options.plugins.lineMarker.index = undefined;
chartHistoryMisc.options.plugins.lineMarker.index = undefined;
}
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) {
let title = getTime();
if (data.packet !== undefined) {
title = `${data.packet._id} (${title})`;
}
span.title = title;
}
if (type === tracePacket) {
span.onclick = () => {
updatePause(true);
updateChart(data.packet);
updateData(data.packet);
// Mark current packet in history plot
const index = data.packet._id - chartHistoryHz.data.labels[0];
chartHistoryHz.options.plugins.lineMarker.index = index;
chartHistoryDb.options.plugins.lineMarker.index = index;
chartHistoryMisc.options.plugins.lineMarker.index = index;
// Redraw
chartConstellation.update();
chartHistoryHz.update();
chartHistoryDb.update();
chartHistoryMisc.update();
};
span.style.cursor = 'pointer';
}
const trace = document.querySelector('#trace');
trace.prepend(span);
if (trace.childNodes.length > traceMaxEvents) {
trace.removeChild(trace.lastChild);
}
}
const lineMarkerPlugin = {
id: 'lineMarker',
beforeDatasetsDraw: (chart, args, plugins) => {
// Nothing drawn yet
if (chart.scales.x === undefined) {
return;
}
// Not yet configured to show a line
if (plugins.index === undefined) {
return;
}
const x = chart.scales.x.getPixelForValue(plugins.index);
const ctx = chart.ctx;
ctx.save();
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
ctx.moveTo(x, chart.chartArea.top);
ctx.lineTo(x, chart.chartArea.bottom);
ctx.stroke();
},
};
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 chartHistoryHz = new Chart(
document.querySelector('#history-hz-canvas'), {
type: 'line',
data: {
labels: [],
datasets: [],
},
options: {
animation: false,
maintainAspectRatio: false, // use full width
plugins: {
lineMarker: {},
},
},
plugins: [
lineMarkerPlugin,
],
});
const chartHistoryDb = new Chart(
document.querySelector('#history-db-canvas'), {
type: 'line',
data: {
labels: [],
datasets: [],
},
options: {
animation: false,
maintainAspectRatio: false, // use full width
plugins: {
lineMarker: {},
},
},
plugins: [
lineMarkerPlugin,
],
});
const chartHistoryMisc = new Chart(
document.querySelector('#history-misc-canvas'), {
type: 'line',
data: {
labels: [],
datasets: [],
},
options: {
animation: false,
maintainAspectRatio: false, // use full width
plugins: {
lineMarker: {},
},
},
plugins: [
lineMarkerPlugin,
],
});
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 === '_id'
|| x === 'preamble_symbols'
|| x === 'header_symbols'
|| x === 'data_symbols') {
continue
}
keys.push(x);
}
keys.sort();
const divs = [];
{
const div = document.createElement('div');
div.textContent = `Packet: ${packet._id}`;
divs.push(div);
}
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 keysHz = [], keysDb = [], keysMisc = [];
for (const x in packet) {
if (x === '_id'
|| x === 'preamble_symbols'
|| x === 'header_symbols'
|| x === 'data_symbols') {
continue;
}
if (x.endsWith('Hz')) {
keysHz.push(x);
} else if (x.endsWith('dB')) {
keysDb.push(x);
} else {
keysMisc.push(x);
}
}
keysHz.sort();
keysDb.sort();
keysMisc.sort();
if (chartHistoryHz.data.datasets.length === 0) {
const run = (chart, keys) => {
for (const x of keys) {
chart.data.datasets.push({
label: x,
data: [],
})
}
};
run(chartHistoryHz, keysHz);
run(chartHistoryDb, keysDb);
run(chartHistoryMisc, keysMisc);
}
// Limit to historyMaxPackets items
const shift = chartHistoryHz.data.labels.length === historyMaxPackets;
if (shift) {
chartHistoryHz.data.labels.shift();
chartHistoryDb.data.labels.shift();
chartHistoryMisc.data.labels.shift();
}
const run = (chart, keys) => {
let i = 0;
for (const x of keys) {
let data = packet[x];
if (data === -1e38 /* "NaN" */) {
data = undefined;
}
if (shift) {
chart.data.datasets[i].data.shift();
}
chart.data.datasets[i].data.push(data);
i++;
}
chart.data.labels.push(packet._id);
// .update() is deferred, see redrawPlots()
};
run(chartHistoryHz, keysHz);
run(chartHistoryDb, keysDb);
run(chartHistoryMisc, keysMisc);
}
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(() => {
chartHistoryHz.update();
chartHistoryDb.update();
chartHistoryMisc.update();
chartConstellation.update();
redrawPending = false;
}, 10 /* ms */);
}
let packet_id = 0;
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) {
msg._id = packet_id++;
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();
}