diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbdb28f --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# 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: + +![alt](img/waterfall.png) + +## Installation + +```bash +$ npm install git+https://github.com/jledet/waterfall.git +``` + +## Usage + +```ecmascript 6 +import Spectrum from 'fft-waterfall'; +``` + +## Files definition +`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. diff --git a/README.rst b/README.rst deleted file mode 100644 index 5a47473..0000000 --- a/README.rst +++ /dev/null @@ -1,13 +0,0 @@ -******************************** -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 `_ and `gevent-websocket `_ server that broadcasts FFT data to connected clients. The FFT data is generated using `GNU radio `_ using a USRP but it should be fairly easy to change it to a different SDR. diff --git a/colormap.js b/colormap.js index 89a0912..6ab8db6 100644 --- a/colormap.js +++ b/colormap.js @@ -4,3 +4,5 @@ var magma = [[0, 0, 4], [1, 0, 5], [1, 1, 6], [1, 1, 8], [2, 1, 9], [2, 2, 11], var jet = [[0, 0, 128], [0, 0, 132], [0, 0, 137], [0, 0, 141], [0, 0, 146], [0, 0, 150], [0, 0, 155], [0, 0, 159], [0, 0, 164], [0, 0, 168], [0, 0, 173], [0, 0, 178], [0, 0, 182], [0, 0, 187], [0, 0, 191], [0, 0, 196], [0, 0, 200], [0, 0, 205], [0, 0, 209], [0, 0, 214], [0, 0, 218], [0, 0, 223], [0, 0, 227], [0, 0, 232], [0, 0, 237], [0, 0, 241], [0, 0, 246], [0, 0, 250], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 0, 255], [0, 4, 255], [0, 8, 255], [0, 12, 255], [0, 16, 255], [0, 20, 255], [0, 24, 255], [0, 28, 255], [0, 32, 255], [0, 36, 255], [0, 40, 255], [0, 44, 255], [0, 48, 255], [0, 52, 255], [0, 56, 255], [0, 60, 255], [0, 64, 255], [0, 68, 255], [0, 72, 255], [0, 76, 255], [0, 80, 255], [0, 84, 255], [0, 88, 255], [0, 92, 255], [0, 96, 255], [0, 100, 255], [0, 104, 255], [0, 108, 255], [0, 112, 255], [0, 116, 255], [0, 120, 255], [0, 124, 255], [0, 128, 255], [0, 132, 255], [0, 136, 255], [0, 140, 255], [0, 144, 255], [0, 148, 255], [0, 152, 255], [0, 156, 255], [0, 160, 255], [0, 164, 255], [0, 168, 255], [0, 172, 255], [0, 176, 255], [0, 180, 255], [0, 184, 255], [0, 188, 255], [0, 192, 255], [0, 196, 255], [0, 200, 255], [0, 204, 255], [0, 208, 255], [0, 212, 255], [0, 216, 255], [0, 220, 254], [0, 224, 251], [0, 228, 248], [2, 232, 244], [6, 236, 241], [9, 240, 238], [12, 244, 235], [15, 248, 231], [19, 252, 228], [22, 255, 225], [25, 255, 222], [28, 255, 219], [31, 255, 215], [35, 255, 212], [38, 255, 209], [41, 255, 206], [44, 255, 202], [48, 255, 199], [51, 255, 196], [54, 255, 193], [57, 255, 190], [60, 255, 186], [64, 255, 183], [67, 255, 180], [70, 255, 177], [73, 255, 173], [77, 255, 170], [80, 255, 167], [83, 255, 164], [86, 255, 160], [90, 255, 157], [93, 255, 154], [96, 255, 151], [99, 255, 148], [102, 255, 144], [106, 255, 141], [109, 255, 138], [112, 255, 135], [115, 255, 131], [119, 255, 128], [122, 255, 125], [125, 255, 122], [128, 255, 119], [131, 255, 115], [135, 255, 112], [138, 255, 109], [141, 255, 106], [144, 255, 102], [148, 255, 99], [151, 255, 96], [154, 255, 93], [157, 255, 90], [160, 255, 86], [164, 255, 83], [167, 255, 80], [170, 255, 77], [173, 255, 73], [177, 255, 70], [180, 255, 67], [183, 255, 64], [186, 255, 60], [190, 255, 57], [193, 255, 54], [196, 255, 51], [199, 255, 48], [202, 255, 44], [206, 255, 41], [209, 255, 38], [212, 255, 35], [215, 255, 31], [219, 255, 28], [222, 255, 25], [225, 255, 22], [228, 255, 19], [231, 255, 15], [235, 255, 12], [238, 255, 9], [241, 252, 6], [244, 248, 2], [248, 245, 0], [251, 241, 0], [254, 237, 0], [255, 234, 0], [255, 230, 0], [255, 226, 0], [255, 222, 0], [255, 219, 0], [255, 215, 0], [255, 211, 0], [255, 208, 0], [255, 204, 0], [255, 200, 0], [255, 196, 0], [255, 193, 0], [255, 189, 0], [255, 185, 0], [255, 182, 0], [255, 178, 0], [255, 174, 0], [255, 171, 0], [255, 167, 0], [255, 163, 0], [255, 159, 0], [255, 156, 0], [255, 152, 0], [255, 148, 0], [255, 145, 0], [255, 141, 0], [255, 137, 0], [255, 134, 0], [255, 130, 0], [255, 126, 0], [255, 122, 0], [255, 119, 0], [255, 115, 0], [255, 111, 0], [255, 108, 0], [255, 104, 0], [255, 100, 0], [255, 96, 0], [255, 93, 0], [255, 89, 0], [255, 85, 0], [255, 82, 0], [255, 78, 0], [255, 74, 0], [255, 71, 0], [255, 67, 0], [255, 63, 0], [255, 59, 0], [255, 56, 0], [255, 52, 0], [255, 48, 0], [255, 45, 0], [255, 41, 0], [255, 37, 0], [255, 34, 0], [255, 30, 0], [255, 26, 0], [255, 22, 0], [255, 19, 0], [250, 15, 0], [246, 11, 0], [241, 8, 0], [237, 4, 0], [232, 0, 0], [228, 0, 0], [223, 0, 0], [218, 0, 0], [214, 0, 0], [209, 0, 0], [205, 0, 0], [200, 0, 0], [196, 0, 0], [191, 0, 0], [187, 0, 0], [182, 0, 0], [178, 0, 0], [173, 0, 0], [168, 0, 0], [164, 0, 0], [159, 0, 0], [155, 0, 0], [150, 0, 0], [146, 0, 0], [141, 0, 0], [137, 0, 0], [132, 0, 0], [128, 0, 0]] var binary = [[255, 255, 255], [254, 254, 254], [253, 253, 253], [252, 252, 252], [251, 251, 251], [250, 250, 250], [249, 249, 249], [248, 248, 248], [247, 247, 247], [246, 246, 246], [245, 245, 245], [244, 244, 244], [243, 243, 243], [242, 242, 242], [241, 241, 241], [240, 240, 240], [239, 239, 239], [238, 238, 238], [237, 237, 237], [236, 236, 236], [235, 235, 235], [234, 234, 234], [233, 233, 233], [232, 232, 232], [231, 231, 231], [230, 230, 230], [229, 229, 229], [228, 228, 228], [227, 227, 227], [226, 226, 226], [225, 225, 225], [224, 224, 224], [223, 223, 223], [222, 222, 222], [221, 221, 221], [220, 220, 220], [219, 219, 219], [218, 218, 218], [217, 217, 217], [216, 216, 216], [215, 215, 215], [214, 214, 214], [213, 213, 213], [212, 212, 212], [211, 211, 211], [210, 210, 210], [209, 209, 209], [208, 208, 208], [207, 207, 207], [206, 206, 206], [205, 205, 205], [204, 204, 204], [203, 203, 203], [202, 202, 202], [201, 201, 201], [200, 200, 200], [199, 199, 199], [198, 198, 198], [197, 197, 197], [196, 196, 196], [195, 195, 195], [194, 194, 194], [193, 193, 193], [192, 192, 192], [191, 191, 191], [190, 190, 190], [189, 189, 189], [188, 188, 188], [187, 187, 187], [186, 186, 186], [185, 185, 185], [184, 184, 184], [183, 183, 183], [182, 182, 182], [181, 181, 181], [180, 180, 180], [179, 179, 179], [178, 178, 178], [177, 177, 177], [176, 176, 176], [175, 175, 175], [174, 174, 174], [173, 173, 173], [172, 172, 172], [171, 171, 171], [170, 170, 170], [169, 169, 169], [168, 168, 168], [167, 167, 167], [166, 166, 166], [165, 165, 165], [164, 164, 164], [163, 163, 163], [162, 162, 162], [161, 161, 161], [160, 160, 160], [159, 159, 159], [158, 158, 158], [157, 157, 157], [156, 156, 156], [155, 155, 155], [154, 154, 154], [153, 153, 153], [152, 152, 152], [151, 151, 151], [150, 150, 150], [149, 149, 149], [148, 148, 148], [147, 147, 147], [146, 146, 146], [145, 145, 145], [144, 144, 144], [143, 143, 143], [142, 142, 142], [141, 141, 141], [140, 140, 140], [139, 139, 139], [138, 138, 138], [137, 137, 137], [136, 136, 136], [135, 135, 135], [134, 134, 134], [133, 133, 133], [132, 132, 132], [131, 131, 131], [130, 130, 130], [129, 129, 129], [128, 128, 128], [127, 127, 127], [126, 126, 126], [125, 125, 125], [124, 124, 124], [123, 123, 123], [122, 122, 122], [121, 121, 121], [120, 120, 120], [119, 119, 119], [118, 118, 118], [117, 117, 117], [116, 116, 116], [115, 115, 115], [114, 114, 114], [113, 113, 113], [112, 112, 112], [111, 111, 111], [110, 110, 110], [109, 109, 109], [108, 108, 108], [107, 107, 107], [106, 106, 106], [105, 105, 105], [104, 104, 104], [103, 103, 103], [102, 102, 102], [101, 101, 101], [100, 100, 100], [99, 99, 99], [98, 98, 98], [97, 97, 97], [96, 96, 96], [95, 95, 95], [94, 94, 94], [93, 93, 93], [92, 92, 92], [91, 91, 91], [90, 90, 90], [89, 89, 89], [88, 88, 88], [87, 87, 87], [86, 86, 86], [85, 85, 85], [84, 84, 84], [83, 83, 83], [82, 82, 82], [81, 81, 81], [80, 80, 80], [79, 79, 79], [78, 78, 78], [77, 77, 77], [76, 76, 76], [75, 75, 75], [74, 74, 74], [73, 73, 73], [72, 72, 72], [71, 71, 71], [70, 70, 70], [69, 69, 69], [68, 68, 68], [67, 67, 67], [66, 66, 66], [65, 65, 65], [64, 64, 64], [63, 63, 63], [62, 62, 62], [61, 61, 61], [60, 60, 60], [59, 59, 59], [58, 58, 58], [57, 57, 57], [56, 56, 56], [55, 55, 55], [54, 54, 54], [53, 53, 53], [52, 52, 52], [51, 51, 51], [50, 50, 50], [49, 49, 49], [48, 48, 48], [47, 47, 47], [46, 46, 46], [45, 45, 45], [44, 44, 44], [43, 43, 43], [42, 42, 42], [41, 41, 41], [40, 40, 40], [39, 39, 39], [38, 38, 38], [37, 37, 37], [36, 36, 36], [35, 35, 35], [34, 34, 34], [33, 33, 33], [32, 32, 32], [31, 31, 31], [30, 30, 30], [29, 29, 29], [28, 28, 28], [27, 27, 27], [26, 26, 26], [25, 25, 25], [24, 24, 24], [23, 23, 23], [22, 22, 22], [21, 21, 21], [20, 20, 20], [19, 19, 19], [18, 18, 18], [17, 17, 17], [16, 16, 16], [15, 15, 15], [14, 14, 14], [13, 13, 13], [12, 12, 12], [11, 11, 11], [10, 10, 10], [9, 9, 9], [8, 8, 8], [7, 7, 7], [6, 6, 6], [5, 5, 5], [4, 4, 4], [3, 3, 3], [2, 2, 2], [1, 1, 1], [0, 0, 0]] var colormaps = [viridis, inferno, magma, jet, binary]; + +module.exports = colormaps; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..20b0fab --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "fft-waterfall", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e29c1f9 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "fft-waterfall", + "version": "1.0.0", + "description": "This is a small experiment to create a waterfall plot with HTML Canvas and WebSockets to stream live FFT data from an SDR", + "main": "spectrum.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/jledet/waterfall" + }, + "keywords": [ + "waterfall", + "sdr", + "fft", + "canvas", + "plot", + "websocket" + ], + "author": "Jeppe Ledet-Pedersen", + "license": "MIT" +} diff --git a/spectrum.js b/spectrum.js index f6e6b09..c5e2567 100644 --- a/spectrum.js +++ b/spectrum.js @@ -5,6 +5,7 @@ */ 'use strict'; +var colormaps = require('./colormap.js'); Spectrum.prototype.squeeze = function(value, out_min, out_max) { if (value <= this.min_db) @@ -128,27 +129,36 @@ Spectrum.prototype.updateAxes = function() { } this.ctx_axes.textBaseline = "bottom"; - for (var i = 0; i < 11; i++) { - var x = Math.round(width / 10) * i; + for (var i = 0; i < this.ticksHz; i++) { + var x = Math.round(width / (this.ticksHz - 1)) * i; if (this.spanHz > 0) { var adjust = 0; - if (i == 0) { + if (i === 0) { this.ctx_axes.textAlign = "left"; adjust = 3; - } else if (i == 10) { + } else if (i === (this.ticksHz - 1)) { this.ctx_axes.textAlign = "right"; adjust = -3; } else { this.ctx_axes.textAlign = "center"; } - var freq = this.centerHz + this.spanHz / 10 * (i - 5); + var freqFactor = (2 / (this.ticksHz - 1)) * i - 1; // range <-1; +1> + var freq = this.centerHz + this.spanHz * freqFactor; if (this.centerHz + this.spanHz > 1e6) - freq = freq / 1e6 + "M"; + freq = (freq / 1e6).toFixed(2) + "M"; else if (this.centerHz + this.spanHz > 1e3) - freq = freq / 1e3 + "k"; - this.ctx_axes.fillText(freq, x + adjust, height - 3); + freq = (freq / 1e3).toFixed(2) + "k"; + + if(this.horizontalAxisPosition === 'both') { + this.ctx_axes.fillText(freq, x + adjust, height); + this.ctx_axes.fillText(freq, x + adjust, 12); + } else if(this.horizontalAxisPosition === 'bottom') { + this.ctx_axes.fillText(freq, x + adjust, height); + } else if(this.horizontalAxisPosition === 'top') { + this.ctx_axes.fillText(freq, x + adjust, 12); + } } this.ctx_axes.beginPath(); @@ -328,6 +338,8 @@ function Spectrum(id, options) { this.spectrumPercent = (options && options.spectrumPercent) ? options.spectrumPercent : 25; this.spectrumPercentStep = (options && options.spectrumPercentStep) ? options.spectrumPercentStep : 5; this.averaging = (options && options.averaging) ? options.averaging : 0.5; + this.ticksHz = (options && options.ticksHz) ? options.ticksHz : 10; + this.horizontalAxisPosition = (options && options.horizontalAxisPosition) ? options.horizontalAxisPosition : 'bottom'; // either 'top', 'bottom' or 'both' // Setup state this.paused = false; @@ -342,6 +354,9 @@ function Spectrum(id, options) { // Create main canvas and adjust dimensions to match actual this.canvas = document.getElementById(id); + if(this.canvas === null) { + throw "There is no declared with id #" + id + } this.canvas.height = this.canvas.clientHeight; this.canvas.width = this.canvas.clientWidth; this.ctx = this.canvas.getContext("2d"); @@ -364,3 +379,5 @@ function Spectrum(id, options) { this.updateSpectrumRatio(); this.resize(); } + +module.exports = Spectrum;