Visualizer #3
|
@ -1,3 +1,6 @@
|
|||
// For F_SETPIPE_SZ
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
@ -29,6 +32,12 @@ static bool start_message(void)
|
|||
}
|
||||
return false;
|
||||
}
|
||||
// Increase pipe buffer to prevent/reduce EAGAIN during a JSON
|
||||
// message to prevent corrupted JSON messages. 1048576 is the
|
||||
// current maximum as permitted by the Linux kernel.
|
||||
if (fcntl(m_fifo_fd, F_SETPIPE_SZ, 1048576) < 0) {
|
||||
perror("fcntl");
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t ret = write(m_fifo_fd, "{", 1);
|
||||
|
|
|
@ -94,6 +94,9 @@ add_executable(
|
|||
../src/var_array.c
|
||||
../src/var_array.h
|
||||
../src/config.h
|
||||
../src/jsonlogger.c
|
||||
../src/jsonlogger.h
|
||||
../src/debug_structs.h
|
||||
layer1/test_rx_file.c
|
||||
)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include <liquid/liquid.h>
|
||||
|
||||
#include "jsonlogger.h"
|
||||
#include "layer1/rx.h"
|
||||
|
||||
#include "config.h"
|
||||
|
@ -20,12 +21,9 @@
|
|||
#define CHUNKSIZE_RF (CHUNKSIZE_INPUT/2)
|
||||
#define CHUNKSIZE_BB (CHUNKSIZE_RF/SDR_OVERSAMPLING)
|
||||
|
||||
static struct {
|
||||
size_t preambles_found;
|
||||
size_t successful_decodes;
|
||||
size_t failed_decodes;
|
||||
size_t header_errors;
|
||||
} m_stats;
|
||||
#define JSONLOGGER 0
|
||||
|
||||
static rx_stats_t m_rx_stats;
|
||||
|
||||
|
||||
static result_t sdr_rf_to_baseband(nco_crcf nco, firdecim_crcf decim,
|
||||
|
@ -69,11 +67,11 @@ void cb_rx(rx_evt_t evt, const layer1_rx_t *rx, uint8_t *packet_data, size_t pac
|
|||
//fprintf(stderr, "=== FAILED PAYLOAD ===\n");
|
||||
//hexdump(packet_data, packet_len);
|
||||
//fprintf(stderr, "=======================\n");
|
||||
m_stats.failed_decodes++;
|
||||
m_rx_stats.failed_decodes++;
|
||||
break;
|
||||
|
||||
case RX_EVT_HEADER_ERROR:
|
||||
m_stats.header_errors++;
|
||||
m_rx_stats.header_errors++;
|
||||
break;
|
||||
|
||||
case RX_EVT_PACKET_RECEIVED:
|
||||
|
@ -82,18 +80,24 @@ void cb_rx(rx_evt_t evt, const layer1_rx_t *rx, uint8_t *packet_data, size_t pac
|
|||
//hexdump(packet_data, packet_len < 64 ? packet_len : 64);
|
||||
//fprintf(stderr, "====================================\n");
|
||||
|
||||
m_stats.successful_decodes++;
|
||||
m_rx_stats.successful_decodes++;
|
||||
break;
|
||||
|
||||
case RX_EVT_PREAMBLE_FOUND:
|
||||
//fprintf(stderr, "Found preamble!\n");
|
||||
m_stats.preambles_found++;
|
||||
m_rx_stats.preambles_found++;
|
||||
break;
|
||||
|
||||
case RX_EVT_PACKET_DEBUG_INFO_COMPLETE:
|
||||
// FIXME: print debug info
|
||||
#if JSONLOGGER
|
||||
jsonlogger_log_rx_packet_info(&rx->packet_debug_info);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
#if JSONLOGGER
|
||||
jsonlogger_log_rx_stats(&m_rx_stats);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
@ -112,6 +116,13 @@ int main(int argc, char **argv)
|
|||
|
||||
// ** Initialize **
|
||||
|
||||
#if JSONLOGGER
|
||||
if(!jsonlogger_init("jsonlog_test.fifo")) {
|
||||
fprintf(stderr, "Could not initialize JSON logger.\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
#endif
|
||||
|
||||
firdecim_crcf decim = firdecim_crcf_create_kaiser(SDR_OVERSAMPLING, 9, 60.0f);
|
||||
nco_crcf rx_nco = nco_crcf_create(LIQUID_NCO);
|
||||
nco_crcf_set_frequency(rx_nco, 2 * 3.14159 * SDR_RX_IF_SHIFT / SDR_RX_SAMPLING_RATE);
|
||||
|
@ -147,13 +158,13 @@ int main(int argc, char **argv)
|
|||
RESULT_CHECK(layer1_rx_process(&rx, bb_samples, n_bb_samples));
|
||||
|
||||
fprintf(stderr, "Receiver statistics:\n");
|
||||
fprintf(stderr, " Preambles found: %8zd\n", m_stats.preambles_found);
|
||||
fprintf(stderr, " Preambles found: %8zd\n", m_rx_stats.preambles_found);
|
||||
fprintf(stderr, " Successful decodes: %8zd (%6.2f %%)\n",
|
||||
m_stats.successful_decodes, m_stats.successful_decodes * 100.0f / m_stats.preambles_found);
|
||||
m_rx_stats.successful_decodes, m_rx_stats.successful_decodes * 100.0f / m_rx_stats.preambles_found);
|
||||
fprintf(stderr, " Header errors: %8zd (%6.2f %%)\n",
|
||||
m_stats.header_errors, m_stats.header_errors * 100.0f / m_stats.preambles_found);
|
||||
m_rx_stats.header_errors, m_rx_stats.header_errors * 100.0f / m_rx_stats.preambles_found);
|
||||
fprintf(stderr, " Failed decodes: %8zd (%6.2f %%)\n",
|
||||
m_stats.failed_decodes, m_stats.failed_decodes * 100.0f / m_stats.preambles_found);
|
||||
m_rx_stats.failed_decodes, m_rx_stats.failed_decodes * 100.0f / m_rx_stats.preambles_found);
|
||||
|
||||
}
|
||||
|
||||
|
@ -161,5 +172,9 @@ int main(int argc, char **argv)
|
|||
|
||||
layer1_rx_shutdown(&rx);
|
||||
|
||||
#if JSONLOGGER
|
||||
jsonlogger_shutdown();
|
||||
#endif
|
||||
|
||||
fprintf(stderr, "Done.\n");
|
||||
}
|
||||
|
|
1
impl/utils/visualizer/.gitignore
vendored
Normal file
1
impl/utils/visualizer/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/visualizer
|
7
impl/utils/visualizer/Makefile
Normal file
7
impl/utils/visualizer/Makefile
Normal file
|
@ -0,0 +1,7 @@
|
|||
all:
|
||||
go build
|
||||
|
||||
clean:
|
||||
rm -f visualizer
|
||||
|
||||
.PHONY: all clean
|
5
impl/utils/visualizer/go.mod
Normal file
5
impl/utils/visualizer/go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module visualizer
|
||||
|
||||
go 1.21
|
||||
|
||||
require golang.org/x/net v0.25.0
|
2
impl/utils/visualizer/go.sum
Normal file
2
impl/utils/visualizer/go.sum
Normal file
|
@ -0,0 +1,2 @@
|
|||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
135
impl/utils/visualizer/main.go
Normal file
135
impl/utils/visualizer/main.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
// Visualize debug information from hamnet70.
|
||||
//
|
||||
// This program is a HTTP server which reads JSON messages (one per line) from
|
||||
// a FIFO filled by hamnet70 and passes them to the browser via WebSocket.
|
||||
// Concurrent browser sessions and reconnects are supported. This binary is
|
||||
// standalone and can be used without the static/ directory.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var static embed.FS
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 && len(os.Args) != 3 {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("usage: %s <fifo> [<json-dup-prefix>]", os.Args[0])
|
||||
}
|
||||
|
||||
var mutex sync.Mutex
|
||||
listeners := make(map[*chan string]struct{})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
path := os.Args[1]
|
||||
var dupPath string
|
||||
if len(os.Args) == 3 {
|
||||
dupPath = os.Args[2] + "." + time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var dupFh *os.File
|
||||
if dupPath != "" {
|
||||
dupFh, err = os.Create(dupPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
r := bufio.NewReader(fh)
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Printf("read %q: %v", path, err)
|
||||
break
|
||||
}
|
||||
|
||||
// Duplicate JSON input to a file so it can be replayed later
|
||||
// if necessary
|
||||
if dupFh != nil {
|
||||
_, err = dupFh.WriteString(line)
|
||||
if err != nil {
|
||||
log.Fatalf("write %q: %v", dupPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Send to all listeners
|
||||
mutex.Lock()
|
||||
for x := range listeners {
|
||||
*x <- line
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
err = fh.Close()
|
||||
if err != nil {
|
||||
log.Fatalf("close %q: %v", path, err)
|
||||
}
|
||||
if dupFh != nil {
|
||||
err = dupFh.Close()
|
||||
if err != nil {
|
||||
log.Fatalf("close %q: %v", dupPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Use existing "./static/" directory for quick changes
|
||||
var staticFS http.FileSystem
|
||||
_, err := os.Stat("static")
|
||||
if err == nil {
|
||||
staticFS = http.Dir("static")
|
||||
} else {
|
||||
x, err := fs.Sub(static, "static")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
staticFS = http.FS(x)
|
||||
}
|
||||
http.Handle("/", http.FileServer(staticFS))
|
||||
|
||||
http.Handle("/events", websocket.Handler(func(conn *websocket.Conn) {
|
||||
// Register new listener for this connection
|
||||
ch := make(chan string, 512) // buffer up to x number of messages
|
||||
mutex.Lock()
|
||||
listeners[&ch] = struct{}{}
|
||||
mutex.Unlock()
|
||||
defer func() {
|
||||
mutex.Lock()
|
||||
delete(listeners, &ch)
|
||||
mutex.Unlock()
|
||||
}()
|
||||
|
||||
for {
|
||||
x := <-ch
|
||||
err := websocket.Message.Send(conn, x)
|
||||
if err != nil {
|
||||
log.Printf("Websocket error: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
err = http.ListenAndServe("localhost:8080", nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// vi: set noet ts=4 sw=4 sts=4:
|
20
impl/utils/visualizer/static/chart-v4.4.2.js
Normal file
20
impl/utils/visualizer/static/chart-v4.4.2.js
Normal file
File diff suppressed because one or more lines are too long
28
impl/utils/visualizer/static/index.css
Normal file
28
impl/utils/visualizer/static/index.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
#error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
#trace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#main {
|
||||
display: flex;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
#constellation {
|
||||
width: 30vw;
|
||||
}
|
||||
#data {
|
||||
width: 20vw;
|
||||
}
|
||||
#history {
|
||||
width: 50vw;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#history-hz, #history-db, #history-misc {
|
||||
height: 30vh;
|
||||
}
|
50
impl/utils/visualizer/static/index.html
Normal file
50
impl/utils/visualizer/static/index.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>hamnet70 Visualizer</title>
|
||||
|
||||
<link rel="stylesheet" href="index.css"/>
|
||||
|
||||
<script src="chart-v4.4.2.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="error" hidden></div>
|
||||
<div>
|
||||
<input id="pause" type="button" value="">
|
||||
</div>
|
||||
<div>
|
||||
Trace: <span id="trace"></span>
|
||||
</div>
|
||||
<div>
|
||||
Preambles found: <span id="preambles_found"></span>,
|
||||
Successful decodes: <span id="successful_decodes"></span>,
|
||||
Header errors: <span id="header_errors"></span>,
|
||||
Failed decodes: <span id="failed_decodes"></span>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="constellation">
|
||||
<canvas id="constellation-canvas"></canvas>
|
||||
</div>
|
||||
<div id="data"></div>
|
||||
<div id="history">
|
||||
<div id="history-hz">
|
||||
<canvas id="history-hz-canvas"></canvas>
|
||||
</div>
|
||||
<div id="history-db">
|
||||
<canvas id="history-db-canvas"></canvas>
|
||||
</div>
|
||||
<div id="history-misc">
|
||||
<canvas id="history-misc-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="index.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
407
impl/utils/visualizer/static/index.js
Normal file
407
impl/utils/visualizer/static/index.js
Normal file
|
@ -0,0 +1,407 @@
|
|||
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();
|
||||
}
|
Loading…
Reference in a new issue