Simon Ruderich
bc2f5ca76c
The id is displayed in the graph, the packet information and in the trace (on mouse over).
297 lines
7.2 KiB
JavaScript
297 lines
7.2 KiB
JavaScript
const tracePlaceholder = Symbol('trace-placeholder');
|
|
const traceEvent = Symbol('trace-event');
|
|
const tracePacket = Symbol('trace-packet');
|
|
|
|
const traceMaxEvents = 200;
|
|
|
|
|
|
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';
|
|
}
|
|
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);
|
|
// 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 === '_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 keys = [];
|
|
for (const x in packet) {
|
|
if (x === '_id'
|
|
|| 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(packet._id);
|
|
// .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 */);
|
|
}
|
|
|
|
|
|
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();
|
|
}
|