Visualizer #3
|
@ -1,3 +1,6 @@
|
||||||
|
// For F_SETPIPE_SZ
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
@ -29,6 +32,12 @@ static bool start_message(void)
|
||||||
}
|
}
|
||||||
return false;
|
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);
|
ssize_t ret = write(m_fifo_fd, "{", 1);
|
||||||
|
|
|
@ -94,6 +94,9 @@ add_executable(
|
||||||
../src/var_array.c
|
../src/var_array.c
|
||||||
../src/var_array.h
|
../src/var_array.h
|
||||||
../src/config.h
|
../src/config.h
|
||||||
|
../src/jsonlogger.c
|
||||||
|
../src/jsonlogger.h
|
||||||
|
../src/debug_structs.h
|
||||||
layer1/test_rx_file.c
|
layer1/test_rx_file.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <liquid/liquid.h>
|
#include <liquid/liquid.h>
|
||||||
|
|
||||||
|
#include "jsonlogger.h"
|
||||||
#include "layer1/rx.h"
|
#include "layer1/rx.h"
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
@ -20,12 +21,9 @@
|
||||||
#define CHUNKSIZE_RF (CHUNKSIZE_INPUT/2)
|
#define CHUNKSIZE_RF (CHUNKSIZE_INPUT/2)
|
||||||
#define CHUNKSIZE_BB (CHUNKSIZE_RF/SDR_OVERSAMPLING)
|
#define CHUNKSIZE_BB (CHUNKSIZE_RF/SDR_OVERSAMPLING)
|
||||||
|
|
||||||
static struct {
|
#define JSONLOGGER 0
|
||||||
size_t preambles_found;
|
|
||||||
size_t successful_decodes;
|
static rx_stats_t m_rx_stats;
|
||||||
size_t failed_decodes;
|
|
||||||
size_t header_errors;
|
|
||||||
} m_stats;
|
|
||||||
|
|
||||||
|
|
||||||
static result_t sdr_rf_to_baseband(nco_crcf nco, firdecim_crcf decim,
|
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");
|
//fprintf(stderr, "=== FAILED PAYLOAD ===\n");
|
||||||
//hexdump(packet_data, packet_len);
|
//hexdump(packet_data, packet_len);
|
||||||
//fprintf(stderr, "=======================\n");
|
//fprintf(stderr, "=======================\n");
|
||||||
m_stats.failed_decodes++;
|
m_rx_stats.failed_decodes++;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RX_EVT_HEADER_ERROR:
|
case RX_EVT_HEADER_ERROR:
|
||||||
m_stats.header_errors++;
|
m_rx_stats.header_errors++;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RX_EVT_PACKET_RECEIVED:
|
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);
|
//hexdump(packet_data, packet_len < 64 ? packet_len : 64);
|
||||||
//fprintf(stderr, "====================================\n");
|
//fprintf(stderr, "====================================\n");
|
||||||
|
|
||||||
m_stats.successful_decodes++;
|
m_rx_stats.successful_decodes++;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RX_EVT_PREAMBLE_FOUND:
|
case RX_EVT_PREAMBLE_FOUND:
|
||||||
//fprintf(stderr, "Found preamble!\n");
|
//fprintf(stderr, "Found preamble!\n");
|
||||||
m_stats.preambles_found++;
|
m_rx_stats.preambles_found++;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RX_EVT_PACKET_DEBUG_INFO_COMPLETE:
|
case RX_EVT_PACKET_DEBUG_INFO_COMPLETE:
|
||||||
// FIXME: print debug info
|
#if JSONLOGGER
|
||||||
|
jsonlogger_log_rx_packet_info(&rx->packet_debug_info);
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if JSONLOGGER
|
||||||
|
jsonlogger_log_rx_stats(&m_rx_stats);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,6 +116,13 @@ int main(int argc, char **argv)
|
||||||
|
|
||||||
// ** Initialize **
|
// ** 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);
|
firdecim_crcf decim = firdecim_crcf_create_kaiser(SDR_OVERSAMPLING, 9, 60.0f);
|
||||||
nco_crcf rx_nco = nco_crcf_create(LIQUID_NCO);
|
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);
|
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));
|
RESULT_CHECK(layer1_rx_process(&rx, bb_samples, n_bb_samples));
|
||||||
|
|
||||||
fprintf(stderr, "Receiver statistics:\n");
|
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",
|
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",
|
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",
|
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);
|
layer1_rx_shutdown(&rx);
|
||||||
|
|
||||||
|
#if JSONLOGGER
|
||||||
|
jsonlogger_shutdown();
|
||||||
|
#endif
|
||||||
|
|
||||||
fprintf(stderr, "Done.\n");
|
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