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
|
||||
thomas marked this conversation as resolved
Outdated
|
||||
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
Eine Idee dazu noch: wenn man wirklich ein Replay von einem früheren JSON-Dump startet, indem man diesen statt dem FIFO als Args[1] angibt, wäre es cool, wenn man Args[2] weglassen könnte oder zumindest /dev/null angeben könnte. Letzteres geht leider nicht wegen dem Anhängen der Uhrzeit (was an sich aber schon sinnvoll ist).
Ist implementiert. Habs auch gleich rebased.
Schaut gut aus. Dann merge ich das mal :)