mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 10:04:33 +02:00
added waterfall to assets list
This commit is contained in:
parent
8493ff18ea
commit
56d2cd7ca4
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
Before Width: | Height: | Size: 496 B |
21
gui_vue/src/assets/waterfall/LICENSE
Normal file
21
gui_vue/src/assets/waterfall/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Jeppe Ledet-Pedersen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
13
gui_vue/src/assets/waterfall/README.rst
Normal file
13
gui_vue/src/assets/waterfall/README.rst
Normal file
|
@ -0,0 +1,13 @@
|
|||
********************************
|
||||
HTML Canvas/WebSockets Waterfall
|
||||
********************************
|
||||
|
||||
This is a small experiment to create a waterfall plot with HTML Canvas and WebSockets to stream live FFT data from an SDR:
|
||||
|
||||
.. image:: img/waterfall.png
|
||||
|
||||
``spectrum.js`` contains the main JavaScript source code for the plot, while ``colormap.js`` contains colormaps generated using ``make_colormap.py``.
|
||||
|
||||
``index.html``, ``style.css``, ``script.js`` contain an example page that receives FFT data on a WebSocket and plots it on the waterfall plot.
|
||||
|
||||
``server.py`` contains a example `Bottle <https://bottlepy.org/docs/dev/>`_ and `gevent-websocket <https://pypi.org/project/gevent-websocket/>`_ server that broadcasts FFT data to connected clients. The FFT data is generated using `GNU radio <https://www.gnuradio.org/>`_ using a USRP but it should be fairly easy to change it to a different SDR.
|
1807
gui_vue/src/assets/waterfall/colormap.js
Normal file
1807
gui_vue/src/assets/waterfall/colormap.js
Normal file
File diff suppressed because it is too large
Load diff
16
gui_vue/src/assets/waterfall/index.html
Normal file
16
gui_vue/src/assets/waterfall/index.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="author" content="Jeppe Ledet-Pedersen" />
|
||||
<title>Spectrum Plot</title>
|
||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<canvas id="waterfall"></canvas>
|
||||
<script src="colormap.js"></script>
|
||||
<script src="spectrum.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
13
gui_vue/src/assets/waterfall/make_colormap.py
Normal file
13
gui_vue/src/assets/waterfall/make_colormap.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
colormaps = ('viridis', 'inferno', 'magma', 'jet', 'binary')
|
||||
for c in colormaps:
|
||||
cmap_name = c
|
||||
cmap = plt.get_cmap(cmap_name)
|
||||
|
||||
colors = [[int(round(255 * x)) for x in cmap(i)[:3]] for i in range(256)]
|
||||
print(f'var {c} = {colors}')
|
||||
|
||||
print(f'var colormaps = [{", ".join(colormaps)}];')
|
62
gui_vue/src/assets/waterfall/script.js
Normal file
62
gui_vue/src/assets/waterfall/script.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
"use strict";
|
||||
/*
|
||||
function connectWebSocket(spectrum) {
|
||||
// var ws = new WebSocket("ws://" + window.location.host + "/websocket");
|
||||
var ws = new WebSocket("ws://192.168.178.163:3000");
|
||||
|
||||
|
||||
ws.onopen = function(evt) {
|
||||
console.log("connected!");
|
||||
}
|
||||
ws.onclose = function(evt) {
|
||||
console.log("closed");
|
||||
setTimeout(function() {
|
||||
connectWebSocket(spectrum);
|
||||
}, 1000);
|
||||
}
|
||||
ws.onerror = function(evt) {
|
||||
console.log("error: " + evt.message);
|
||||
}
|
||||
ws.onmessage = function (evt) {
|
||||
var data = JSON.parse(evt.data);
|
||||
if (data.s) {
|
||||
spectrum.addData(data.s);
|
||||
} else {
|
||||
if (data.center) {
|
||||
spectrum.setCenterHz(data.center);
|
||||
}
|
||||
if (data.span) {
|
||||
spectrum.setSpanHz(data.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function main() {
|
||||
// Create spectrum object on canvas with ID "waterfall"
|
||||
var spectrum = new Spectrum("waterfall", {
|
||||
spectrumPercent: 20,
|
||||
});
|
||||
|
||||
// Connect to websocket
|
||||
//connectWebSocket(spectrum);
|
||||
|
||||
//spectrum.setCenterHz("2000");
|
||||
//spectrum.setSpanHz("1");
|
||||
|
||||
/*
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
var randomstring = Math.floor(Math.random())
|
||||
spectrum.addData(randomstring.toString());
|
||||
// more statements
|
||||
}
|
||||
*/
|
||||
|
||||
// Bind keypress handler
|
||||
window.addEventListener("keydown", function (e) {
|
||||
spectrum.onKeypress(e);
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = main;
|
149
gui_vue/src/assets/waterfall/server.py
Normal file
149
gui_vue/src/assets/waterfall/server.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2019 Jeppe Ledet-Pedersen
|
||||
# This software is released under the MIT license.
|
||||
# See the LICENSE file for further details.
|
||||
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
|
||||
from gnuradio import gr
|
||||
from gnuradio import uhd
|
||||
from gnuradio.fft import logpwrfft
|
||||
|
||||
import numpy as np
|
||||
|
||||
from gevent.pywsgi import WSGIServer
|
||||
from geventwebsocket import WebSocketError
|
||||
from geventwebsocket.handler import WebSocketHandler
|
||||
|
||||
from bottle import request, Bottle, abort, static_file
|
||||
|
||||
|
||||
app = Bottle()
|
||||
connections = set()
|
||||
opts = {}
|
||||
|
||||
|
||||
@app.route('/websocket')
|
||||
def handle_websocket():
|
||||
wsock = request.environ.get('wsgi.websocket')
|
||||
if not wsock:
|
||||
abort(400, 'Expected WebSocket request.')
|
||||
|
||||
connections.add(wsock)
|
||||
|
||||
# Send center frequency and span
|
||||
wsock.send(json.dumps(opts))
|
||||
|
||||
while True:
|
||||
try:
|
||||
wsock.receive()
|
||||
except WebSocketError:
|
||||
break
|
||||
|
||||
connections.remove(wsock)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return static_file('index.html', root='.')
|
||||
|
||||
|
||||
@app.route('/<filename>')
|
||||
def static(filename):
|
||||
return static_file(filename, root='.')
|
||||
|
||||
|
||||
class fft_broadcast_sink(gr.sync_block):
|
||||
def __init__(self, fft_size):
|
||||
gr.sync_block.__init__(self,
|
||||
name="plotter",
|
||||
in_sig=[(np.float32, fft_size)],
|
||||
out_sig=[])
|
||||
|
||||
def work(self, input_items, output_items):
|
||||
ninput_items = len(input_items[0])
|
||||
|
||||
for bins in input_items[0]:
|
||||
p = np.around(bins).astype(int)
|
||||
p = np.fft.fftshift(p)
|
||||
for c in connections.copy():
|
||||
try:
|
||||
c.send(json.dumps({'s': p.tolist()}, separators=(',', ':')))
|
||||
except Exception:
|
||||
connections.remove(c)
|
||||
|
||||
self.consume(0, ninput_items)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class fft_receiver(gr.top_block):
|
||||
def __init__(self, samp_rate, freq, gain, fft_size, framerate):
|
||||
gr.top_block.__init__(self, "Top Block")
|
||||
|
||||
self.usrp = uhd.usrp_source(
|
||||
",".join(("", "")),
|
||||
uhd.stream_args(
|
||||
cpu_format="fc32",
|
||||
channels=range(1),
|
||||
),
|
||||
)
|
||||
self.usrp.set_samp_rate(samp_rate)
|
||||
self.usrp.set_center_freq(freq, 0)
|
||||
self.usrp.set_gain(gain, 0)
|
||||
|
||||
self.fft = logpwrfft.logpwrfft_c(
|
||||
sample_rate=samp_rate,
|
||||
fft_size=fft_size,
|
||||
ref_scale=1,
|
||||
frame_rate=framerate,
|
||||
avg_alpha=1,
|
||||
average=False,
|
||||
)
|
||||
self.fft_broadcast = fft_broadcast_sink(fft_size)
|
||||
|
||||
self.connect((self.fft, 0), (self.fft_broadcast, 0))
|
||||
self.connect((self.usrp, 0), (self.fft, 0))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-s', '--sample-rate', type=float, default=40e6)
|
||||
parser.add_argument('-f', '--frequency', type=float, default=940e6)
|
||||
parser.add_argument('-g', '--gain', type=float, default=40)
|
||||
parser.add_argument('-n', '--fft-size', type=int, default=4096)
|
||||
parser.add_argument('-r', '--frame-rate', type=int, default=25)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if gr.enable_realtime_scheduling() != gr.RT_OK or 0:
|
||||
print("Error: failed to enable real-time scheduling.")
|
||||
|
||||
tb = fft_receiver(
|
||||
samp_rate=args.sample_rate,
|
||||
freq=args.frequency,
|
||||
gain=args.gain,
|
||||
fft_size=args.fft_size,
|
||||
framerate=args.frame_rate
|
||||
)
|
||||
tb.start()
|
||||
|
||||
opts['center'] = args.frequency
|
||||
opts['span'] = args.sample_rate
|
||||
|
||||
server = WSGIServer(("0.0.0.0", 8000), app,
|
||||
handler_class=WebSocketHandler)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
tb.stop()
|
||||
tb.wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
607
gui_vue/src/assets/waterfall/spectrogram.js
Normal file
607
gui_vue/src/assets/waterfall/spectrogram.js
Normal file
|
@ -0,0 +1,607 @@
|
|||
/*=============================================================
|
||||
Filename: Spectrogram-1v00.js
|
||||
|
||||
JavaScript graphics functions to draw Spectrograms.
|
||||
|
||||
Date Description By
|
||||
-------|-------------------------------------------------|---
|
||||
12Nov18 First beta ARC
|
||||
17Nov18 Added offset into data buffer ARC
|
||||
08May19 this.imageURL URL added
|
||||
bugfix: fixed isNaN test
|
||||
Changed sgStart, sgStop to start, stop
|
||||
Added options object to constructors ARC
|
||||
10May19 Enabled Left to Right as well as Top to Bottom ARC
|
||||
11May19 Added RasterscanSVG ARC
|
||||
12May19 Added blnkline for horizontal ratser scans ARC
|
||||
13May19 Eliminated unneccessary putImageData ARC
|
||||
14May19 Removed toDataURL, not used drawImage is better
|
||||
bugfix: SVG RHC names swapped ARC
|
||||
02Jun19 bugfix: startOfs not honored in horizontalNewLine ARC
|
||||
03Jun19 Flipped the SVG and RHC names for waterfalls ARC
|
||||
04Jun19 Unflip SVG and RHC for horizontal mode ARC
|
||||
Swap "SVG" & "RHC" strings to match fn names ARC
|
||||
05Jun19 bugfix: WaterfallSVG scrolling wrong way ARC
|
||||
10Jun19 bugfix: support lineRate=0 for static display
|
||||
bugfix: ipBufPtr must be a ptr to a ptr ARC
|
||||
11Jun19 Make ipBuffers an Array of Arrays, if lineRate=0
|
||||
use all buffers else use only ipBuffer[0] ARC
|
||||
13Jun19 Use Waterfall and Rasterscan plus direction
|
||||
Use Boolean rater than string compare ARC
|
||||
16Jun19 Use const and let ARC
|
||||
20Jun19 Change order of parameters ARC
|
||||
21Jun19 Add setLineRate method ARC
|
||||
06Jul19 Released as Rev 1v00 ARC
|
||||
==============================================================*/
|
||||
|
||||
var Waterfall, Rasterscan;
|
||||
|
||||
(function () {
|
||||
Waterfall = function (ipBufAry, w, h, dir, options) {
|
||||
var direction = typeof dir === "string" ? dir.toLowerCase() : "down";
|
||||
|
||||
switch (direction) {
|
||||
case "up":
|
||||
return new Spectrogram(ipBufAry, w, h, "WF", false, true, options);
|
||||
case "down":
|
||||
default:
|
||||
return new Spectrogram(ipBufAry, w, h, "WF", true, true, options);
|
||||
case "left":
|
||||
return new Spectrogram(ipBufAry, w, h, "WF", false, false, options);
|
||||
case "right":
|
||||
return new Spectrogram(ipBufAry, w, h, "WF", true, false, options);
|
||||
}
|
||||
};
|
||||
|
||||
Rasterscan = function (ipBufAry, w, h, dir, options) {
|
||||
const direction = typeof dir === "string" ? dir.toLowerCase() : "down";
|
||||
|
||||
switch (direction) {
|
||||
case "up":
|
||||
return new Spectrogram(ipBufAry, w, h, "RS", true, true, options);
|
||||
case "down":
|
||||
default:
|
||||
return new Spectrogram(ipBufAry, w, h, "RS", false, true, options);
|
||||
case "left":
|
||||
return new Spectrogram(ipBufAry, w, h, "RS", false, false, options);
|
||||
case "right":
|
||||
return new Spectrogram(ipBufAry, w, h, "RS", true, false, options);
|
||||
}
|
||||
};
|
||||
|
||||
function Spectrogram(ipBufAry, w, h, sgMode, rhc, vert, options) {
|
||||
const opt = typeof options === "object" ? options : {}; // avoid undeclared object errors
|
||||
let offScreenCtx; // offscreen canvas drawing context
|
||||
const pxPerLine = w || 200;
|
||||
const lines = h || 200;
|
||||
let lineRate = 30; // requested line rate for dynamic waterfalls
|
||||
let interval = 0; // msec
|
||||
let startOfs = 0;
|
||||
const lineBuf = new ArrayBuffer(pxPerLine * 4); // 1 line
|
||||
const lineBuf8 = new Uint8ClampedArray(lineBuf);
|
||||
const lineImgData = new ImageData(lineBuf8, pxPerLine, 1); // 1 line of canvas pixels
|
||||
let pageImgData; // lines * pxPerLine of canvas pixels
|
||||
let ipBuf8; // map input data to 0..255 unsigned bytes
|
||||
const blankBuf = new ArrayBuffer(pxPerLine * 4); // 1 line
|
||||
const blankBuf8 = new Uint8ClampedArray(blankBuf);
|
||||
const blankImgData = new ImageData(blankBuf8, pxPerLine, 1); // 1 line of canvas pixels
|
||||
const clearBuf = new ArrayBuffer(pxPerLine * lines * 4); // fills with 0s ie. rgba 0,0,0,0 = transparent
|
||||
const clearBuf8 = new Uint8ClampedArray(clearBuf);
|
||||
let clearImgData;
|
||||
let nextLine = 0;
|
||||
let timerID = null;
|
||||
let running = false;
|
||||
let sgTime = 0;
|
||||
let sgStartTime = 0;
|
||||
|
||||
// Matlab Jet ref: stackoverflow.com grayscale-to-red-green-blue-matlab-jet-color-scale
|
||||
let colMap = [
|
||||
[0, 0, 128, 255],
|
||||
[0, 0, 131, 255],
|
||||
[0, 0, 135, 255],
|
||||
[0, 0, 139, 255],
|
||||
[0, 0, 143, 255],
|
||||
[0, 0, 147, 255],
|
||||
[0, 0, 151, 255],
|
||||
[0, 0, 155, 255],
|
||||
[0, 0, 159, 255],
|
||||
[0, 0, 163, 255],
|
||||
[0, 0, 167, 255],
|
||||
[0, 0, 171, 255],
|
||||
[0, 0, 175, 255],
|
||||
[0, 0, 179, 255],
|
||||
[0, 0, 183, 255],
|
||||
[0, 0, 187, 255],
|
||||
[0, 0, 191, 255],
|
||||
[0, 0, 195, 255],
|
||||
[0, 0, 199, 255],
|
||||
[0, 0, 203, 255],
|
||||
[0, 0, 207, 255],
|
||||
[0, 0, 211, 255],
|
||||
[0, 0, 215, 255],
|
||||
[0, 0, 219, 255],
|
||||
[0, 0, 223, 255],
|
||||
[0, 0, 227, 255],
|
||||
[0, 0, 231, 255],
|
||||
[0, 0, 235, 255],
|
||||
[0, 0, 239, 255],
|
||||
[0, 0, 243, 255],
|
||||
[0, 0, 247, 255],
|
||||
[0, 0, 251, 255],
|
||||
[0, 0, 255, 255],
|
||||
[0, 4, 255, 255],
|
||||
[0, 8, 255, 255],
|
||||
[0, 12, 255, 255],
|
||||
[0, 16, 255, 255],
|
||||
[0, 20, 255, 255],
|
||||
[0, 24, 255, 255],
|
||||
[0, 28, 255, 255],
|
||||
[0, 32, 255, 255],
|
||||
[0, 36, 255, 255],
|
||||
[0, 40, 255, 255],
|
||||
[0, 44, 255, 255],
|
||||
[0, 48, 255, 255],
|
||||
[0, 52, 255, 255],
|
||||
[0, 56, 255, 255],
|
||||
[0, 60, 255, 255],
|
||||
[0, 64, 255, 255],
|
||||
[0, 68, 255, 255],
|
||||
[0, 72, 255, 255],
|
||||
[0, 76, 255, 255],
|
||||
[0, 80, 255, 255],
|
||||
[0, 84, 255, 255],
|
||||
[0, 88, 255, 255],
|
||||
[0, 92, 255, 255],
|
||||
[0, 96, 255, 255],
|
||||
[0, 100, 255, 255],
|
||||
[0, 104, 255, 255],
|
||||
[0, 108, 255, 255],
|
||||
[0, 112, 255, 255],
|
||||
[0, 116, 255, 255],
|
||||
[0, 120, 255, 255],
|
||||
[0, 124, 255, 255],
|
||||
[0, 128, 255, 255],
|
||||
[0, 131, 255, 255],
|
||||
[0, 135, 255, 255],
|
||||
[0, 139, 255, 255],
|
||||
[0, 143, 255, 255],
|
||||
[0, 147, 255, 255],
|
||||
[0, 151, 255, 255],
|
||||
[0, 155, 255, 255],
|
||||
[0, 159, 255, 255],
|
||||
[0, 163, 255, 255],
|
||||
[0, 167, 255, 255],
|
||||
[0, 171, 255, 255],
|
||||
[0, 175, 255, 255],
|
||||
[0, 179, 255, 255],
|
||||
[0, 183, 255, 255],
|
||||
[0, 187, 255, 255],
|
||||
[0, 191, 255, 255],
|
||||
[0, 195, 255, 255],
|
||||
[0, 199, 255, 255],
|
||||
[0, 203, 255, 255],
|
||||
[0, 207, 255, 255],
|
||||
[0, 211, 255, 255],
|
||||
[0, 215, 255, 255],
|
||||
[0, 219, 255, 255],
|
||||
[0, 223, 255, 255],
|
||||
[0, 227, 255, 255],
|
||||
[0, 231, 255, 255],
|
||||
[0, 235, 255, 255],
|
||||
[0, 239, 255, 255],
|
||||
[0, 243, 255, 255],
|
||||
[0, 247, 255, 255],
|
||||
[0, 251, 255, 255],
|
||||
[0, 255, 255, 255],
|
||||
[4, 255, 251, 255],
|
||||
[8, 255, 247, 255],
|
||||
[12, 255, 243, 255],
|
||||
[16, 255, 239, 255],
|
||||
[20, 255, 235, 255],
|
||||
[24, 255, 231, 255],
|
||||
[28, 255, 227, 255],
|
||||
[32, 255, 223, 255],
|
||||
[36, 255, 219, 255],
|
||||
[40, 255, 215, 255],
|
||||
[44, 255, 211, 255],
|
||||
[48, 255, 207, 255],
|
||||
[52, 255, 203, 255],
|
||||
[56, 255, 199, 255],
|
||||
[60, 255, 195, 255],
|
||||
[64, 255, 191, 255],
|
||||
[68, 255, 187, 255],
|
||||
[72, 255, 183, 255],
|
||||
[76, 255, 179, 255],
|
||||
[80, 255, 175, 255],
|
||||
[84, 255, 171, 255],
|
||||
[88, 255, 167, 255],
|
||||
[92, 255, 163, 255],
|
||||
[96, 255, 159, 255],
|
||||
[100, 255, 155, 255],
|
||||
[104, 255, 151, 255],
|
||||
[108, 255, 147, 255],
|
||||
[112, 255, 143, 255],
|
||||
[116, 255, 139, 255],
|
||||
[120, 255, 135, 255],
|
||||
[124, 255, 131, 255],
|
||||
[128, 255, 128, 255],
|
||||
[131, 255, 124, 255],
|
||||
[135, 255, 120, 255],
|
||||
[139, 255, 116, 255],
|
||||
[143, 255, 112, 255],
|
||||
[147, 255, 108, 255],
|
||||
[151, 255, 104, 255],
|
||||
[155, 255, 100, 255],
|
||||
[159, 255, 96, 255],
|
||||
[163, 255, 92, 255],
|
||||
[167, 255, 88, 255],
|
||||
[171, 255, 84, 255],
|
||||
[175, 255, 80, 255],
|
||||
[179, 255, 76, 255],
|
||||
[183, 255, 72, 255],
|
||||
[187, 255, 68, 255],
|
||||
[191, 255, 64, 255],
|
||||
[195, 255, 60, 255],
|
||||
[199, 255, 56, 255],
|
||||
[203, 255, 52, 255],
|
||||
[207, 255, 48, 255],
|
||||
[211, 255, 44, 255],
|
||||
[215, 255, 40, 255],
|
||||
[219, 255, 36, 255],
|
||||
[223, 255, 32, 255],
|
||||
[227, 255, 28, 255],
|
||||
[231, 255, 24, 255],
|
||||
[235, 255, 20, 255],
|
||||
[239, 255, 16, 255],
|
||||
[243, 255, 12, 255],
|
||||
[247, 255, 8, 255],
|
||||
[251, 255, 4, 255],
|
||||
[255, 255, 0, 255],
|
||||
[255, 251, 0, 255],
|
||||
[255, 247, 0, 255],
|
||||
[255, 243, 0, 255],
|
||||
[255, 239, 0, 255],
|
||||
[255, 235, 0, 255],
|
||||
[255, 231, 0, 255],
|
||||
[255, 227, 0, 255],
|
||||
[255, 223, 0, 255],
|
||||
[255, 219, 0, 255],
|
||||
[255, 215, 0, 255],
|
||||
[255, 211, 0, 255],
|
||||
[255, 207, 0, 255],
|
||||
[255, 203, 0, 255],
|
||||
[255, 199, 0, 255],
|
||||
[255, 195, 0, 255],
|
||||
[255, 191, 0, 255],
|
||||
[255, 187, 0, 255],
|
||||
[255, 183, 0, 255],
|
||||
[255, 179, 0, 255],
|
||||
[255, 175, 0, 255],
|
||||
[255, 171, 0, 255],
|
||||
[255, 167, 0, 255],
|
||||
[255, 163, 0, 255],
|
||||
[255, 159, 0, 255],
|
||||
[255, 155, 0, 255],
|
||||
[255, 151, 0, 255],
|
||||
[255, 147, 0, 255],
|
||||
[255, 143, 0, 255],
|
||||
[255, 139, 0, 255],
|
||||
[255, 135, 0, 255],
|
||||
[255, 131, 0, 255],
|
||||
[255, 128, 0, 255],
|
||||
[255, 124, 0, 255],
|
||||
[255, 120, 0, 255],
|
||||
[255, 116, 0, 255],
|
||||
[255, 112, 0, 255],
|
||||
[255, 108, 0, 255],
|
||||
[255, 104, 0, 255],
|
||||
[255, 100, 0, 255],
|
||||
[255, 96, 0, 255],
|
||||
[255, 92, 0, 255],
|
||||
[255, 88, 0, 255],
|
||||
[255, 84, 0, 255],
|
||||
[255, 80, 0, 255],
|
||||
[255, 76, 0, 255],
|
||||
[255, 72, 0, 255],
|
||||
[255, 68, 0, 255],
|
||||
[255, 64, 0, 255],
|
||||
[255, 60, 0, 255],
|
||||
[255, 56, 0, 255],
|
||||
[255, 52, 0, 255],
|
||||
[255, 48, 0, 255],
|
||||
[255, 44, 0, 255],
|
||||
[255, 40, 0, 255],
|
||||
[255, 36, 0, 255],
|
||||
[255, 32, 0, 255],
|
||||
[255, 28, 0, 255],
|
||||
[255, 24, 0, 255],
|
||||
[255, 20, 0, 255],
|
||||
[255, 16, 0, 255],
|
||||
[255, 12, 0, 255],
|
||||
[255, 8, 0, 255],
|
||||
[255, 4, 0, 255],
|
||||
[255, 0, 0, 255],
|
||||
[251, 0, 0, 255],
|
||||
[247, 0, 0, 255],
|
||||
[243, 0, 0, 255],
|
||||
[239, 0, 0, 255],
|
||||
[235, 0, 0, 255],
|
||||
[231, 0, 0, 255],
|
||||
[227, 0, 0, 255],
|
||||
[223, 0, 0, 255],
|
||||
[219, 0, 0, 255],
|
||||
[215, 0, 0, 255],
|
||||
[211, 0, 0, 255],
|
||||
[207, 0, 0, 255],
|
||||
[203, 0, 0, 255],
|
||||
[199, 0, 0, 255],
|
||||
[195, 0, 0, 255],
|
||||
[191, 0, 0, 255],
|
||||
[187, 0, 0, 255],
|
||||
[183, 0, 0, 255],
|
||||
[179, 0, 0, 255],
|
||||
[175, 0, 0, 255],
|
||||
[171, 0, 0, 255],
|
||||
[167, 0, 0, 255],
|
||||
[163, 0, 0, 255],
|
||||
[159, 0, 0, 255],
|
||||
[155, 0, 0, 255],
|
||||
[151, 0, 0, 255],
|
||||
[147, 0, 0, 255],
|
||||
[143, 0, 0, 255],
|
||||
[139, 0, 0, 255],
|
||||
[135, 0, 0, 255],
|
||||
[131, 0, 0, 255],
|
||||
[0, 0, 0, 0],
|
||||
];
|
||||
|
||||
function incrLine() {
|
||||
if ((vert && !rhc) || (!vert && rhc)) {
|
||||
nextLine++;
|
||||
if (nextLine >= lines) {
|
||||
nextLine = 0;
|
||||
}
|
||||
} else {
|
||||
nextLine--;
|
||||
if (nextLine < 0) {
|
||||
nextLine = lines - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateWaterfall() {
|
||||
// update dynamic waterfalls at a fixed rate
|
||||
let sgDiff;
|
||||
|
||||
// grab latest line of data, write it to off screen buffer, inc 'nextLine'
|
||||
sgNewLine();
|
||||
// loop to write data data at the desired rate, data is being updated asynchronously
|
||||
// ref for accurate timeout: http://www.sitepoint.com/creating-accurate-timers-in-javascript
|
||||
sgTime += interval;
|
||||
sgDiff = Date.now() - sgStartTime - sgTime;
|
||||
if (running) {
|
||||
timerID = setTimeout(updateWaterfall, interval - sgDiff);
|
||||
}
|
||||
}
|
||||
|
||||
function sgSetLineRate(newRate) {
|
||||
if (isNaN(newRate) || newRate > 50 || newRate < 0) {
|
||||
console.error("invalid line rate [0 <= lineRate < 50 lines/sec]");
|
||||
// don't change the lineRate;
|
||||
} else if (newRate === 0) {
|
||||
// static (one pass) raster
|
||||
lineRate = 0;
|
||||
} else {
|
||||
lineRate = newRate;
|
||||
interval = 1000 / lineRate; // msec
|
||||
}
|
||||
}
|
||||
|
||||
this.setLineRate = sgSetLineRate;
|
||||
|
||||
function setProperty(propertyName, value) {
|
||||
if (typeof propertyName !== "string" || value === undefined) {
|
||||
// null is OK, forces default
|
||||
return;
|
||||
}
|
||||
switch (propertyName.toLowerCase()) {
|
||||
case "linerate":
|
||||
sgSetLineRate(value); // setLine does checks for number etc
|
||||
break;
|
||||
case "startbin":
|
||||
if (!isNaN(value) && value > 0) {
|
||||
startOfs = value;
|
||||
}
|
||||
break;
|
||||
case "onscreenparentid":
|
||||
if (typeof value === "string" && document.getElementById(value)) {
|
||||
demoCvsId = value;
|
||||
}
|
||||
break;
|
||||
case "colormap":
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
Array.isArray(value[0]) &&
|
||||
value[0].length == 4
|
||||
) {
|
||||
colMap = value; // value must be an array of 4 element arrays to get here
|
||||
if (colMap.length < 256) {
|
||||
// fill out the remaining colors with last color
|
||||
for (let i = colMap.length; i < 256; i++) {
|
||||
colMap[i] = colMap[colMap.length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function verticalNewLine() {
|
||||
let tmpImgData, ipBuf8;
|
||||
|
||||
if (sgMode == "WF") {
|
||||
if (rhc) {
|
||||
// shift the current display down 1 line, oldest line drops off
|
||||
tmpImgData = offScreenCtx.getImageData(0, 0, pxPerLine, lines - 1);
|
||||
offScreenCtx.putImageData(tmpImgData, 0, 1);
|
||||
} else {
|
||||
// shift the current display up 1 line, oldest line drops off
|
||||
tmpImgData = offScreenCtx.getImageData(0, 1, pxPerLine, lines - 1);
|
||||
offScreenCtx.putImageData(tmpImgData, 0, 0);
|
||||
}
|
||||
}
|
||||
ipBuf8 = Uint8ClampedArray.from(ipBufAry[0]);
|
||||
for (
|
||||
let sigVal, rgba, opIdx = 0, ipIdx = startOfs;
|
||||
ipIdx < pxPerLine + startOfs;
|
||||
opIdx += 4, ipIdx++
|
||||
) {
|
||||
sigVal = ipBuf8[ipIdx] || 0; // if input line too short add zeros
|
||||
rgba = colMap[sigVal]; // array of rgba values
|
||||
// byte reverse so number aa bb gg rr
|
||||
lineBuf8[opIdx] = rgba[0]; // red
|
||||
lineBuf8[opIdx + 1] = rgba[1]; // green
|
||||
lineBuf8[opIdx + 2] = rgba[2]; // blue
|
||||
lineBuf8[opIdx + 3] = rgba[3]; // alpha
|
||||
}
|
||||
offScreenCtx.putImageData(lineImgData, 0, nextLine);
|
||||
if (sgMode === "RS") {
|
||||
incrLine();
|
||||
// if not static draw a white line in front of the current line to indicate new data point
|
||||
if (lineRate) {
|
||||
offScreenCtx.putImageData(blankImgData, 0, nextLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function horizontalNewLine() {
|
||||
let tmpImgData, ipBuf8;
|
||||
|
||||
if (sgMode == "WF") {
|
||||
if (rhc) {
|
||||
// shift the current display right 1 line, oldest line drops off
|
||||
tmpImgData = offScreenCtx.getImageData(0, 0, lines - 1, pxPerLine);
|
||||
offScreenCtx.putImageData(tmpImgData, 1, 0);
|
||||
} else {
|
||||
// shift the current display left 1 line, oldest line drops off
|
||||
tmpImgData = offScreenCtx.getImageData(1, 0, lines - 1, pxPerLine);
|
||||
offScreenCtx.putImageData(tmpImgData, 0, 0);
|
||||
}
|
||||
}
|
||||
// refresh the page image (it was just shifted)
|
||||
pageImgData = offScreenCtx.getImageData(0, 0, lines, pxPerLine);
|
||||
if (ipBufAry[0].constructor !== Uint8Array) {
|
||||
ipBuf8 = Uint8ClampedArray.from(ipBufAry[0]); // clamp input values to 0..255 range
|
||||
} else {
|
||||
ipBuf8 = ipBufAry[0]; // conversion already done
|
||||
}
|
||||
|
||||
for (let sigVal, rgba, opIdx, ipIdx = 0; ipIdx < pxPerLine; ipIdx++) {
|
||||
sigVal = ipBuf8[ipIdx + startOfs] || 0; // if input line too short add zeros
|
||||
rgba = colMap[sigVal]; // array of rgba values
|
||||
opIdx = 4 * ((pxPerLine - ipIdx - 1) * lines + nextLine);
|
||||
// byte reverse so number aa bb gg rr
|
||||
pageImgData.data[opIdx] = rgba[0]; // red
|
||||
pageImgData.data[opIdx + 1] = rgba[1]; // green
|
||||
pageImgData.data[opIdx + 2] = rgba[2]; // blue
|
||||
pageImgData.data[opIdx + 3] = rgba[3]; // alpha
|
||||
}
|
||||
if (sgMode === "RS") {
|
||||
incrLine();
|
||||
// if not draw a white line in front of the current line to indicate new data point
|
||||
if (lineRate) {
|
||||
for (let j = 0; j < pxPerLine; j++) {
|
||||
if (rhc) {
|
||||
opIdx = 4 * (j * lines + nextLine);
|
||||
} else {
|
||||
opIdx = 4 * ((pxPerLine - j - 1) * lines + nextLine);
|
||||
}
|
||||
// byte reverse so number aa bb gg rr
|
||||
pageImgData.data[opIdx] = 255; // red
|
||||
pageImgData.data[opIdx + 1] = 255; // green
|
||||
pageImgData.data[opIdx + 2] = 255; // blue
|
||||
pageImgData.data[opIdx + 3] = 255; // alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
offScreenCtx.putImageData(pageImgData, 0, 0);
|
||||
}
|
||||
|
||||
const sgNewLine = vert ? verticalNewLine : horizontalNewLine; // function pointers
|
||||
|
||||
//===== set all the options ================
|
||||
for (let prop in opt) {
|
||||
// check that this is opt's own property, not inherited from prototype
|
||||
if (opt.hasOwnProperty(prop)) {
|
||||
setProperty(prop, opt[prop]);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== now make the exposed properties and methods ===============
|
||||
this.newLine = sgNewLine;
|
||||
|
||||
this.offScreenCvs = document.createElement("canvas");
|
||||
if (vert) {
|
||||
this.offScreenCvs.setAttribute("width", pxPerLine); // reset canvas pixels width
|
||||
this.offScreenCvs.setAttribute("height", lines); // don't use style for this
|
||||
clearImgData = new ImageData(clearBuf8, pxPerLine, lines);
|
||||
} // data written in columns
|
||||
else {
|
||||
this.offScreenCvs.setAttribute("width", lines); // reset canvas pixels width
|
||||
this.offScreenCvs.setAttribute("height", pxPerLine); // don't use style for this
|
||||
clearImgData = new ImageData(clearBuf8, lines, pxPerLine);
|
||||
}
|
||||
offScreenCtx = this.offScreenCvs.getContext("2d");
|
||||
|
||||
this.clear = function () {
|
||||
offScreenCtx.putImageData(clearImgData, 0, 0);
|
||||
};
|
||||
|
||||
this.start = function () {
|
||||
sgStartTime = Date.now();
|
||||
sgTime = 0;
|
||||
running = true;
|
||||
updateWaterfall(); // start the update loop
|
||||
};
|
||||
|
||||
this.stop = function () {
|
||||
running = false;
|
||||
if (timerID) {
|
||||
clearTimeout(timerID);
|
||||
}
|
||||
// reset where the next line is to be written
|
||||
if (sgMode === "RS") {
|
||||
if (vert) {
|
||||
nextLine = rhc ? lines - 1 : 0;
|
||||
} else {
|
||||
nextLine = rhc ? 0 : lines - 1;
|
||||
}
|
||||
} // WF
|
||||
else {
|
||||
nextLine = rhc ? 0 : lines - 1;
|
||||
}
|
||||
};
|
||||
|
||||
// make a white line, it will show the input line for RS displays
|
||||
blankBuf8.fill(255);
|
||||
// make a full canvas of the color map 0 values
|
||||
for (let i = 0; i < pxPerLine * lines * 4; i += 4) {
|
||||
// byte reverse so number aa bb gg rr
|
||||
clearBuf8[i] = colMap[0][0]; // red
|
||||
clearBuf8[i + 1] = colMap[0][1]; // green
|
||||
clearBuf8[i + 2] = colMap[0][2]; // blue
|
||||
clearBuf8[i + 3] = colMap[0][3]; // alpha
|
||||
}
|
||||
// for diagnostics only
|
||||
if (typeof demoCvsId == "string") {
|
||||
document.getElementById(demoCvsId).appendChild(this.offScreenCvs);
|
||||
}
|
||||
// initialize the direction and first line position
|
||||
this.stop();
|
||||
|
||||
// everything is set
|
||||
// if dynamic, wait for the start or newLine methods to be called
|
||||
}
|
||||
})();
|
488
gui_vue/src/assets/waterfall/spectrum.js
Normal file
488
gui_vue/src/assets/waterfall/spectrum.js
Normal file
|
@ -0,0 +1,488 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Jeppe Ledet-Pedersen
|
||||
* This software is released under the MIT license.
|
||||
* See the LICENSE file for further details.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
Spectrum.prototype.squeeze = function (value, out_min, out_max) {
|
||||
if (value <= this.min_db) return out_min;
|
||||
else if (value >= this.max_db) return out_max;
|
||||
else
|
||||
return Math.round(
|
||||
((value - this.min_db) / (this.max_db - this.min_db)) * out_max
|
||||
);
|
||||
};
|
||||
|
||||
Spectrum.prototype.rowToImageData = function (bins) {
|
||||
for (var i = 0; i < this.imagedata.data.length; i += 4) {
|
||||
var cindex = this.squeeze(bins[i / 4], 0, 255);
|
||||
var color = this.colormap[cindex];
|
||||
this.imagedata.data[i + 0] = color[0];
|
||||
this.imagedata.data[i + 1] = color[1];
|
||||
this.imagedata.data[i + 2] = color[2];
|
||||
this.imagedata.data[i + 3] = 255;
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.addWaterfallRow = function (bins) {
|
||||
// Shift waterfall 1 row down
|
||||
this.ctx_wf.drawImage(
|
||||
this.ctx_wf.canvas,
|
||||
0,
|
||||
0,
|
||||
this.wf_size,
|
||||
this.wf_rows - 1,
|
||||
0,
|
||||
1,
|
||||
this.wf_size,
|
||||
this.wf_rows - 1
|
||||
);
|
||||
|
||||
// Draw new line on waterfall canvas
|
||||
this.rowToImageData(bins);
|
||||
this.ctx_wf.putImageData(this.imagedata, 0, 0);
|
||||
|
||||
var width = this.ctx.canvas.width;
|
||||
var height = this.ctx.canvas.height;
|
||||
|
||||
// Copy scaled FFT canvas to screen. Only copy the number of rows that will
|
||||
// fit in waterfall area to avoid vertical scaling.
|
||||
this.ctx.imageSmoothingEnabled = false;
|
||||
var rows = Math.min(this.wf_rows, height - this.spectrumHeight);
|
||||
this.ctx.drawImage(
|
||||
this.ctx_wf.canvas,
|
||||
0,
|
||||
0,
|
||||
this.wf_size,
|
||||
rows,
|
||||
0,
|
||||
this.spectrumHeight,
|
||||
width,
|
||||
height - this.spectrumHeight
|
||||
);
|
||||
};
|
||||
|
||||
Spectrum.prototype.drawFFT = function (bins) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(-1, this.spectrumHeight + 1);
|
||||
for (var i = 0; i < bins.length; i++) {
|
||||
var y = this.spectrumHeight - this.squeeze(bins[i], 0, this.spectrumHeight);
|
||||
if (y > this.spectrumHeight - 1) y = this.spectrumHeight + 1; // Hide underflow
|
||||
if (y < 0) y = 0;
|
||||
if (i == 0) this.ctx.lineTo(-1, y);
|
||||
this.ctx.lineTo(i, y);
|
||||
if (i == bins.length - 1) this.ctx.lineTo(this.wf_size + 1, y);
|
||||
}
|
||||
this.ctx.lineTo(this.wf_size + 1, this.spectrumHeight + 1);
|
||||
this.ctx.strokeStyle = "#fefefe";
|
||||
this.ctx.stroke();
|
||||
};
|
||||
|
||||
//Spectrum.prototype.drawSpectrum = function(bins) {
|
||||
Spectrum.prototype.drawSpectrum = function () {
|
||||
var width = this.ctx.canvas.width;
|
||||
var height = this.ctx.canvas.height;
|
||||
|
||||
// Modification by DJ2LS
|
||||
// Draw bandwidth lines
|
||||
// TODO: Math not correct. But a first attempt
|
||||
// it seems position is more or less equal to frequenzy by factor 10
|
||||
// eg. position 150 == 1500Hz
|
||||
/*
|
||||
// CENTER LINE
|
||||
this.ctx_wf.beginPath();
|
||||
this.ctx_wf.moveTo(150,0);
|
||||
this.ctx_wf.lineTo(150, height);
|
||||
this.ctx_wf.lineWidth = 1;
|
||||
this.ctx_wf.strokeStyle = '#8C8C8C';
|
||||
this.ctx_wf.stroke()
|
||||
*/
|
||||
|
||||
// 586Hz and 1700Hz LINES
|
||||
var linePositionLow = 121.6; //150 - bandwith/20
|
||||
var linePositionHigh = 178.4; //150 + bandwidth/20
|
||||
var linePositionLow2 = 65; //150 - bandwith/20
|
||||
var linePositionHigh2 = 235; //150 + bandwith/20
|
||||
this.ctx_wf.beginPath();
|
||||
this.ctx_wf.moveTo(linePositionLow, 0);
|
||||
this.ctx_wf.lineTo(linePositionLow, height);
|
||||
this.ctx_wf.moveTo(linePositionHigh, 0);
|
||||
this.ctx_wf.lineTo(linePositionHigh, height);
|
||||
this.ctx_wf.moveTo(linePositionLow2, 0);
|
||||
this.ctx_wf.lineTo(linePositionLow2, height);
|
||||
this.ctx_wf.moveTo(linePositionHigh2, 0);
|
||||
this.ctx_wf.lineTo(linePositionHigh2, height);
|
||||
this.ctx_wf.lineWidth = 1;
|
||||
this.ctx_wf.strokeStyle = "#C3C3C3";
|
||||
this.ctx_wf.stroke();
|
||||
|
||||
// ---- END OF MODIFICATION ------
|
||||
|
||||
// Fill with black
|
||||
this.ctx.fillStyle = "white";
|
||||
this.ctx.fillRect(0, 0, width, height);
|
||||
|
||||
//Commenting out the remainder of this code, it's not needed and unused as of 6.9.11 and saves three if statements
|
||||
return;
|
||||
/*
|
||||
// FFT averaging
|
||||
if (this.averaging > 0) {
|
||||
if (!this.binsAverage || this.binsAverage.length != bins.length) {
|
||||
this.binsAverage = Array.from(bins);
|
||||
} else {
|
||||
for (var i = 0; i < bins.length; i++) {
|
||||
this.binsAverage[i] += this.alpha * (bins[i] - this.binsAverage[i]);
|
||||
}
|
||||
}
|
||||
bins = this.binsAverage;
|
||||
}
|
||||
|
||||
// Max hold
|
||||
if (this.maxHold) {
|
||||
if (!this.binsMax || this.binsMax.length != bins.length) {
|
||||
this.binsMax = Array.from(bins);
|
||||
} else {
|
||||
for (var i = 0; i < bins.length; i++) {
|
||||
if (bins[i] > this.binsMax[i]) {
|
||||
this.binsMax[i] = bins[i];
|
||||
} else {
|
||||
// Decay
|
||||
this.binsMax[i] = 1.0025 * this.binsMax[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not draw anything if spectrum is not visible
|
||||
if (this.ctx_axes.canvas.height < 1)
|
||||
return;
|
||||
|
||||
// Scale for FFT
|
||||
this.ctx.save();
|
||||
this.ctx.scale(width / this.wf_size, 1);
|
||||
|
||||
// Draw maxhold
|
||||
if (this.maxHold)
|
||||
this.drawFFT(this.binsMax);
|
||||
|
||||
// Draw FFT bins
|
||||
this.drawFFT(bins);
|
||||
|
||||
// Restore scale
|
||||
this.ctx.restore();
|
||||
|
||||
// Fill scaled path
|
||||
this.ctx.fillStyle = this.gradient;
|
||||
this.ctx.fill();
|
||||
|
||||
// Copy axes from offscreen canvas
|
||||
this.ctx.drawImage(this.ctx_axes.canvas, 0, 0);
|
||||
*/
|
||||
};
|
||||
|
||||
//Allow setting colormap
|
||||
Spectrum.prototype.setColorMap = function (index) {
|
||||
this.colormap = colormaps[index];
|
||||
};
|
||||
|
||||
Spectrum.prototype.updateAxes = function () {
|
||||
var width = this.ctx_axes.canvas.width;
|
||||
var height = this.ctx_axes.canvas.height;
|
||||
|
||||
// Clear axes canvas
|
||||
this.ctx_axes.clearRect(0, 0, width, height);
|
||||
|
||||
// Draw axes
|
||||
this.ctx_axes.font = "12px sans-serif";
|
||||
this.ctx_axes.fillStyle = "white";
|
||||
this.ctx_axes.textBaseline = "middle";
|
||||
|
||||
this.ctx_axes.textAlign = "left";
|
||||
var step = 10;
|
||||
for (var i = this.min_db + 10; i <= this.max_db - 10; i += step) {
|
||||
var y = height - this.squeeze(i, 0, height);
|
||||
this.ctx_axes.fillText(i, 5, y);
|
||||
|
||||
this.ctx_axes.beginPath();
|
||||
this.ctx_axes.moveTo(20, y);
|
||||
this.ctx_axes.lineTo(width, y);
|
||||
this.ctx_axes.strokeStyle = "rgba(200, 200, 200, 0.10)";
|
||||
this.ctx_axes.stroke();
|
||||
}
|
||||
|
||||
this.ctx_axes.textBaseline = "bottom";
|
||||
for (var i = 0; i < 11; i++) {
|
||||
var x = Math.round(width / 10) * i;
|
||||
|
||||
if (this.spanHz > 0) {
|
||||
var adjust = 0;
|
||||
if (i == 0) {
|
||||
this.ctx_axes.textAlign = "left";
|
||||
adjust = 3;
|
||||
} else if (i == 10) {
|
||||
this.ctx_axes.textAlign = "right";
|
||||
adjust = -3;
|
||||
} else {
|
||||
this.ctx_axes.textAlign = "center";
|
||||
}
|
||||
|
||||
var freq = this.centerHz + (this.spanHz / 10) * (i - 5);
|
||||
if (this.centerHz + this.spanHz > 1e6) freq = freq / 1e6 + "M";
|
||||
else if (this.centerHz + this.spanHz > 1e3) freq = freq / 1e3 + "k";
|
||||
this.ctx_axes.fillText(freq, x + adjust, height - 3);
|
||||
}
|
||||
|
||||
this.ctx_axes.beginPath();
|
||||
this.ctx_axes.moveTo(x, 0);
|
||||
this.ctx_axes.lineTo(x, height);
|
||||
this.ctx_axes.strokeStyle = "rgba(200, 200, 200, 0.10)";
|
||||
this.ctx_axes.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.addData = function (data) {
|
||||
if (!this.paused) {
|
||||
if (data.length != this.wf_size) {
|
||||
this.wf_size = data.length;
|
||||
this.ctx_wf.canvas.width = data.length;
|
||||
this.ctx_wf.fillStyle = "white";
|
||||
this.ctx_wf.fillRect(0, 0, this.wf.width, this.wf.height);
|
||||
this.imagedata = this.ctx_wf.createImageData(data.length, 1);
|
||||
}
|
||||
//this.drawSpectrum(data);
|
||||
this.drawSpectrum();
|
||||
this.addWaterfallRow(data);
|
||||
this.resize();
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.updateSpectrumRatio = function () {
|
||||
this.spectrumHeight = Math.round(
|
||||
(this.canvas.height * this.spectrumPercent) / 100.0
|
||||
);
|
||||
|
||||
this.gradient = this.ctx.createLinearGradient(0, 0, 0, this.spectrumHeight);
|
||||
for (var i = 0; i < this.colormap.length; i++) {
|
||||
var c = this.colormap[this.colormap.length - 1 - i];
|
||||
this.gradient.addColorStop(
|
||||
i / this.colormap.length,
|
||||
"rgba(" + c[0] + "," + c[1] + "," + c[2] + ", 1.0)"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.resize = function () {
|
||||
var width = this.canvas.clientWidth;
|
||||
var height = this.canvas.clientHeight;
|
||||
|
||||
if (this.canvas.width != width || this.canvas.height != height) {
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
this.updateSpectrumRatio();
|
||||
}
|
||||
|
||||
if (this.axes.width != width || this.axes.height != this.spectrumHeight) {
|
||||
this.axes.width = width;
|
||||
this.axes.height = this.spectrumHeight;
|
||||
this.updateAxes();
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.setSpectrumPercent = function (percent) {
|
||||
if (percent >= 0 && percent <= 100) {
|
||||
this.spectrumPercent = percent;
|
||||
this.updateSpectrumRatio();
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.incrementSpectrumPercent = function () {
|
||||
if (this.spectrumPercent + this.spectrumPercentStep <= 100) {
|
||||
this.setSpectrumPercent(this.spectrumPercent + this.spectrumPercentStep);
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.decrementSpectrumPercent = function () {
|
||||
if (this.spectrumPercent - this.spectrumPercentStep >= 0) {
|
||||
this.setSpectrumPercent(this.spectrumPercent - this.spectrumPercentStep);
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.toggleColor = function () {
|
||||
this.colorindex++;
|
||||
if (this.colorindex >= colormaps.length) this.colorindex = 0;
|
||||
this.colormap = colormaps[this.colorindex];
|
||||
this.updateSpectrumRatio();
|
||||
};
|
||||
|
||||
Spectrum.prototype.setRange = function (min_db, max_db) {
|
||||
this.min_db = min_db;
|
||||
this.max_db = max_db;
|
||||
this.updateAxes();
|
||||
};
|
||||
|
||||
Spectrum.prototype.rangeUp = function () {
|
||||
this.setRange(this.min_db - 5, this.max_db - 5);
|
||||
};
|
||||
|
||||
Spectrum.prototype.rangeDown = function () {
|
||||
this.setRange(this.min_db + 5, this.max_db + 5);
|
||||
};
|
||||
|
||||
Spectrum.prototype.rangeIncrease = function () {
|
||||
this.setRange(this.min_db - 5, this.max_db + 5);
|
||||
};
|
||||
|
||||
Spectrum.prototype.rangeDecrease = function () {
|
||||
if (this.max_db - this.min_db > 10)
|
||||
this.setRange(this.min_db + 5, this.max_db - 5);
|
||||
};
|
||||
|
||||
Spectrum.prototype.setCenterHz = function (hz) {
|
||||
this.centerHz = hz;
|
||||
this.updateAxes();
|
||||
};
|
||||
|
||||
Spectrum.prototype.setSpanHz = function (hz) {
|
||||
this.spanHz = hz;
|
||||
this.updateAxes();
|
||||
};
|
||||
|
||||
Spectrum.prototype.setAveraging = function (num) {
|
||||
if (num >= 0) {
|
||||
this.averaging = num;
|
||||
this.alpha = 2 / (this.averaging + 1);
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.incrementAveraging = function () {
|
||||
this.setAveraging(this.averaging + 1);
|
||||
};
|
||||
|
||||
Spectrum.prototype.decrementAveraging = function () {
|
||||
if (this.averaging > 0) {
|
||||
this.setAveraging(this.averaging - 1);
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.setPaused = function (paused) {
|
||||
this.paused = paused;
|
||||
};
|
||||
|
||||
Spectrum.prototype.togglePaused = function () {
|
||||
this.setPaused(!this.paused);
|
||||
};
|
||||
|
||||
Spectrum.prototype.setMaxHold = function (maxhold) {
|
||||
this.maxHold = maxhold;
|
||||
this.binsMax = undefined;
|
||||
};
|
||||
|
||||
Spectrum.prototype.toggleMaxHold = function () {
|
||||
this.setMaxHold(!this.maxHold);
|
||||
};
|
||||
|
||||
Spectrum.prototype.toggleFullscreen = function () {
|
||||
if (!this.fullscreen) {
|
||||
if (this.canvas.requestFullscreen) {
|
||||
this.canvas.requestFullscreen();
|
||||
} else if (this.canvas.mozRequestFullScreen) {
|
||||
this.canvas.mozRequestFullScreen();
|
||||
} else if (this.canvas.webkitRequestFullscreen) {
|
||||
this.canvas.webkitRequestFullscreen();
|
||||
} else if (this.canvas.msRequestFullscreen) {
|
||||
this.canvas.msRequestFullscreen();
|
||||
}
|
||||
this.fullscreen = true;
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
this.fullscreen = false;
|
||||
}
|
||||
};
|
||||
|
||||
Spectrum.prototype.onKeypress = function (e) {
|
||||
if (e.key == " ") {
|
||||
this.togglePaused();
|
||||
} else if (e.key == "f") {
|
||||
this.toggleFullscreen();
|
||||
} else if (e.key == "c") {
|
||||
this.toggleColor();
|
||||
} else if (e.key == "ArrowUp") {
|
||||
this.rangeUp();
|
||||
} else if (e.key == "ArrowDown") {
|
||||
this.rangeDown();
|
||||
} else if (e.key == "ArrowLeft") {
|
||||
this.rangeDecrease();
|
||||
} else if (e.key == "ArrowRight") {
|
||||
this.rangeIncrease();
|
||||
} else if (e.key == "s") {
|
||||
this.incrementSpectrumPercent();
|
||||
} else if (e.key == "w") {
|
||||
this.decrementSpectrumPercent();
|
||||
} else if (e.key == "+") {
|
||||
this.incrementAveraging();
|
||||
} else if (e.key == "-") {
|
||||
this.decrementAveraging();
|
||||
} else if (e.key == "m") {
|
||||
this.toggleMaxHold();
|
||||
}
|
||||
};
|
||||
|
||||
function Spectrum(id, options) {
|
||||
// Handle options
|
||||
this.centerHz = options && options.centerHz ? options.centerHz : 1500;
|
||||
this.spanHz = options && options.spanHz ? options.spanHz : 0;
|
||||
this.wf_size = options && options.wf_size ? options.wf_size : 0;
|
||||
this.wf_rows = options && options.wf_rows ? options.wf_rows : 1024;
|
||||
this.spectrumPercent =
|
||||
options && options.spectrumPercent ? options.spectrumPercent : 0;
|
||||
this.spectrumPercentStep =
|
||||
options && options.spectrumPercentStep ? options.spectrumPercentStep : 0;
|
||||
this.averaging = options && options.averaging ? options.averaging : 0;
|
||||
this.maxHold = options && options.maxHold ? options.maxHold : false;
|
||||
|
||||
// Setup state
|
||||
this.paused = false;
|
||||
this.fullscreen = false;
|
||||
this.min_db = 0;
|
||||
this.max_db = 70;
|
||||
this.spectrumHeight = 0;
|
||||
|
||||
// Colors
|
||||
this.colorindex = 0;
|
||||
this.colormap = colormaps[2];
|
||||
|
||||
// Create main canvas and adjust dimensions to match actual
|
||||
this.canvas = document.getElementById(id);
|
||||
this.canvas.height = this.canvas.clientHeight;
|
||||
this.canvas.width = this.canvas.clientWidth;
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
this.ctx.fillStyle = "white";
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Create offscreen canvas for axes
|
||||
this.axes = document.createElement("canvas");
|
||||
this.axes.height = 1; // Updated later
|
||||
this.axes.width = this.canvas.width;
|
||||
this.ctx_axes = this.axes.getContext("2d");
|
||||
|
||||
// Create offscreen canvas for waterfall
|
||||
this.wf = document.createElement("canvas");
|
||||
this.wf.height = this.wf_rows;
|
||||
this.wf.width = this.wf_size;
|
||||
this.ctx_wf = this.wf.getContext("2d");
|
||||
|
||||
// Trigger first render
|
||||
this.setAveraging(this.averaging);
|
||||
this.updateSpectrumRatio();
|
||||
this.resize();
|
||||
}
|
12
gui_vue/src/assets/waterfall/waterfall.css
Normal file
12
gui_vue/src/assets/waterfall/waterfall.css
Normal file
|
@ -0,0 +1,12 @@
|
|||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#waterfall {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -46,7 +46,7 @@ const settings = useSettingsStore(pinia);
|
|||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="true"
|
||||
v-bind:class="{ 'bg-danger' : state.busy_state === 'IDLE', 'bg-success' : state.busy_state === 'BUSY'}"
|
||||
v-bind:class="{ 'bg-danger' : state.busy_state === 'BUSY', 'bg-success' : state.busy_state === 'IDLE'}"
|
||||
title="TNC busy state: <strong class='text-success'>IDLE</strong> / <strong class='text-danger'>BUSY</strong>"
|
||||
>
|
||||
<i class="bi bi-cpu" style="font-size: 0.8rem"></i>
|
||||
|
|
|
@ -17,6 +17,10 @@ const settings = useSettingsStore(pinia);
|
|||
var client = new net.Socket();
|
||||
var socketchunk = ""; // Current message, per connection.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// split character
|
||||
//const split_char = "\0;\1;";
|
||||
const split_char = "0;1;";
|
||||
|
@ -193,6 +197,17 @@ client.on("data", function (socketdata) {
|
|||
stateStore.dxcallsign = data["dxcallsign"]
|
||||
stateStore.arq_session_state = data["arq_session"]
|
||||
stateStore.arq_state = data["arq_state"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// TODO: Remove ported objects
|
||||
let Data = {
|
||||
mycallsign: data["mycallsign"],
|
||||
|
|
Loading…
Reference in a new issue