408 lines
11 KiB
JavaScript
408 lines
11 KiB
JavaScript
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();
|
|
}
|