diff --git a/css/main.css b/css/main.css index 84c4708a..c9341e84 100644 --- a/css/main.css +++ b/css/main.css @@ -534,6 +534,27 @@ html.has-analyser-fullscreen.has-analyser .analyser input { position: absolute; font-size: 18px; } + +.analyser:hover #spectrumType { + opacity: 1; + transition: opacity 500ms ease-in; +} + +.analyser #spectrumType { + color: #bbb; + opacity: 0; + left: 5px; + float: left; + z-index: 9; + position: absolute; + font-size: 9px; +} + +analyser #spectrumType select { + border-radius: 3px; + padding: 0px 5px; +} + .analyser #analyserResize:hover { color: white; cursor: pointer; @@ -555,7 +576,7 @@ html.has-analyser-fullscreen.has-analyser .analyser input { top: 30px; } -.analyser input { +.analyser input.onlyFullScreen { display: none; padding: 3px; margin-right: 3px; diff --git a/index.html b/index.html index f84bd7eb..531b8807 100644 --- a/index.html +++ b/index.html @@ -444,15 +444,22 @@

Log sync

- + +
+ +
+
- - diff --git a/js/graph_spectrum.js b/js/graph_spectrum.js index 28e7a223..da5f2a2d 100644 --- a/js/graph_spectrum.js +++ b/js/graph_spectrum.js @@ -2,14 +2,16 @@ function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { -var +const ANALYSER_LARGE_LEFT_MARGIN = 10, ANALYSER_LARGE_TOP_MARGIN = 10, ANALYSER_LARGE_HEIGHT_MARGIN = 20, - ANALYSER_LARGE_WIDTH_MARGIN = 20; + ANALYSER_LARGE_WIDTH_MARGIN = 20, + FIELD_THROTTLE_NAME = 'rcCommands[3]', + FREQ_VS_THR_CHUNK_TIME_MS = 300, + FREQ_VS_THR_WINDOW_DIVISOR = 6; var that = this; - var mouseFrequency= null; var analyserZoomX = 1.0; /* 100% */ var analyserZoomY = 1.0; /* 100% */ @@ -48,6 +50,8 @@ try { var analyserZoomXElem = $("#analyserZoomX"); var analyserZoomYElem = $("#analyserZoomY"); + var spectrumTypeElem = $("#spectrumTypeSelect"); + // Correct the PID rate if we know the pid_process_denom (from log header) if (sysConfig.pid_process_denom != null) { pidRate = gyroRate / sysConfig.pid_process_denom; @@ -123,7 +127,7 @@ try { }; - var getFlightSamples = function(samples) { + var getFlightChunks = function() { //load all samples var logStart = flightLog.getMinTime(); var logEnd = ((flightLog.getMaxTime() - logStart) <= MAX_ANALYSER_LENGTH)? flightLog.getMaxTime() : (logStart+MAX_ANALYSER_LENGTH); @@ -135,16 +139,56 @@ try { } var allChunks = flightLog.getChunksInTimeRange(logStart, logEnd); //Max 300 seconds + return allChunks; + } + + var getFlightSamplesFreq = function() { + + var allChunks = getFlightChunks(); + + var samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * blackBoxRate); + + // Loop through all the samples in the chunks and assign them to a sample array ready to pass to the FFT. + var samplesCount = 0; + for (var chunkIndex = 0; chunkIndex < allChunks.length; chunkIndex++) { + var chunk = allChunks[chunkIndex]; + for (var frameIndex = 0; frameIndex < chunk.frames.length; frameIndex++) { + samples[samplesCount] = (dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][dataBuffer.fieldIndex])); + samplesCount++; + } + } + + return { + samples : samples, + count : samplesCount + }; + }; + + var getFlightSamplesFreqVsThrottle = function() { + + var allChunks = getFlightChunks(); + + var samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * blackBoxRate); + var throttle = new Uint16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * blackBoxRate); + + var FIELD_THROTTLE_INDEX = flightLog.getMainFieldIndexByName(FIELD_THROTTLE_NAME); + // Loop through all the samples in the chunks and assign them to a sample array ready to pass to the FFT. var samplesCount = 0; for (var chunkIndex = 0; chunkIndex < allChunks.length; chunkIndex++) { var chunk = allChunks[chunkIndex]; for (var frameIndex = 0; frameIndex < chunk.frames.length; frameIndex++) { - samples[samplesCount++] = (dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][dataBuffer.fieldIndex])); + samples[samplesCount] = (dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][dataBuffer.fieldIndex])); + throttle[samplesCount] = chunk.frames[frameIndex][FIELD_THROTTLE_INDEX]*10; + samplesCount++; } } - return samplesCount; + return { + samples : samples, + throttle : throttle, + count : samplesCount + }; }; var hanningWindow = function(samples, size) { @@ -206,21 +250,112 @@ try { return fftData; }; - var dataLoad = function() { - - var samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * blackBoxRate); + var dataLoadFrequency = function() { - var samplesCount = getFlightSamples(samples); + var flightSamples = getFlightSamplesFreq(); if(userSettings.analyserHanning) { - hanningWindow(samples, samplesCount); + hanningWindow(flightSamples.samples, flightSamples.count); } //calculate fft - var fftOutput = fft(samples); + var fftOutput = fft(flightSamples.samples); // Normalize the result - fftData = normalizeFft(fftOutput, samples.length); + fftData = normalizeFft(fftOutput, flightSamples.samples.length); + } + + var dataLoadFrequencyVsThrottle = function() { + + var flightSamples = getFlightSamplesFreqVsThrottle(); + + // We divide it into FREQ_VS_THR_CHUNK_TIME_MS FFT chunks, we calculate the average throttle + // for each chunk. We use a moving window to get more chunks available. + var fftChunkLength = blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; + var fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); + + var matrixFftOutput = new Array(100); // One for each throttle value, without decimal part + var maxNoiseThrottle = 0; // Stores the max noise produced + var numberSamplesThrottle = new Uint32Array(100); // Number of samples in each throttle value, used to average them later. + + var fft = new FFT.complex(fftChunkLength, false); + for (var fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { + + var fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); + var fftOutput = new Float64Array(fftChunkLength * 2); + + fft.simple(fftOutput, fftInput, 'real'); + + if(userSettings.analyserHanning) { + hanningWindow(fftOutput, fftChunkLength * 2); + } + + fftOutput = fftOutput.slice(0, fftChunkLength); + + // Use only abs values + for (var i = 0; i < fftChunkLength; i++) { + fftOutput[i] = Math.abs(fftOutput[i]); + if (fftOutput[i] > maxNoiseThrottle) { + maxNoiseThrottle = fftOutput[i]; + } + } + + // Calculate average throttle, removing the decimal part + var avgThrottle = 0; + for (var indexThrottle = fftChunkIndex; indexThrottle < fftChunkIndex + fftChunkLength; indexThrottle++) { + avgThrottle += flightSamples.throttle[indexThrottle]; + } + avgThrottle = Math.round(avgThrottle / 10 / fftChunkLength); + + numberSamplesThrottle[avgThrottle]++; + if (!matrixFftOutput[avgThrottle]) { + matrixFftOutput[avgThrottle] = fftOutput; + } else { + matrixFftOutput[avgThrottle] = matrixFftOutput[avgThrottle].map(function (num, idx) { + return num + fftOutput[idx]; + }); + } + } + + // Divide by the number of samples + for (var i = 0; i < 100; i++) { + if (numberSamplesThrottle[i] > 1) { + for (var j = 0; j < matrixFftOutput[i].length; j++) { + matrixFftOutput[i][j] /= numberSamplesThrottle[i]; + } + } else if (numberSamplesThrottle[i] == 0) { + matrixFftOutput[i] = new Float64Array(fftChunkLength * 2); + } + } + + // The output data needs to be smoothed, the sampling is not perfect + // but after some tests we let the data as is, an we prefer to apply a + // blur algorithm to the heat map image + + fftData = { + fieldIndex : dataBuffer.fieldIndex, + fieldName : dataBuffer.fieldName, + fftLength : fftChunkLength, + fftOutput : matrixFftOutput, + maxNoise : maxNoiseThrottle, + blackBoxRate : blackBoxRate, + }; + + } + + var dataLoad = function() { + + switch(spectrumType) { + + case SPECTRUM_TYPE.FREQUENCY: + dataLoadFrequency(); + break; + + case SPECTRUM_TYPE.FREQ_VS_THROTTLE: + dataLoadFrequencyVsThrottle(); + break; + + } }; @@ -240,7 +375,7 @@ try { if ((fieldIndex != fftData.fieldIndex) || dataReload) { dataReload = false; dataLoad(); - GraphSpectrumPlot.setData(fftData); + GraphSpectrumPlot.setData(fftData, spectrumType); } that.draw(); // draw the analyser on the canvas.... @@ -286,6 +421,22 @@ try { } catch (e) { console.log('Failed to create analyser... error:' + e); } + + // Spectrum type to show + var spectrumType = parseInt(spectrumTypeElem.val(), 10); + + spectrumTypeElem.change(function() { + var optionSelected = parseInt(spectrumTypeElem.val(), 10); + + if (optionSelected != spectrumType) { + spectrumType = optionSelected; + + // Recalculate the data, for the same curve than now, and draw it + dataReload = true; + that.plotSpectrum(dataBuffer.fieldIndex, dataBuffer.curve, dataBuffer.fieldName); + } + }); + // track frequency under mouse var lastFrequency; function trackFrequency(e, analyser) { diff --git a/js/graph_spectrum_plot.js b/js/graph_spectrum_plot.js index be48f893..f4dc2754 100644 --- a/js/graph_spectrum_plot.js +++ b/js/graph_spectrum_plot.js @@ -1,16 +1,27 @@ "use strict"; -const DEFAULT_FONT_FACE = "Verdana, Arial, sans-serif", +const BLUR_FILTER_PIXEL = 1, + DEFAULT_FONT_FACE = "Verdana, Arial, sans-serif", DEFAULT_MARK_LINE_WIDTH = 2, - MARGIN = 10; // pixels; + MARGIN = 10, + MARGIN_BOTTOM = 10, + MARGIN_LEFT = 25, + MARGIN_LEFT_FULLSCREEN = 35; + +const SPECTRUM_TYPE = { + FREQUENCY : 0, + FREQ_VS_THROTTLE : 1, + }; var GraphSpectrumPlot = GraphSpectrumPlot || { - _isFullScreen : false, - _cachedCanvas : null, - _canvasCtx : null, - _fftData : null, - _mouseFrequency : null, - _sysConfig : null, + _isFullScreen : false, + _cachedCanvas : null, + _cachedDataCanvas : null, + _canvasCtx : null, + _fftData : null, + _mouseFrequency : null, + _spectrumType : null, + _sysConfig : null, _zoomX : 1.0, _zoomY : 1.0, _drawingParams : { @@ -22,29 +33,39 @@ var GraphSpectrumPlot = GraphSpectrumPlot || { GraphSpectrumPlot.initialize = function(canvas, sysConfig) { this._canvasCtx = canvas.getContext("2d"); this._sysConfig = sysConfig; - this._invalicateCache(); + this._invalidateCache(); + this._invalidateDataCache(); }; GraphSpectrumPlot.setZoom = function(zoomX, zoomY) { + + var modifiedZoomY = (this._zoomY != zoomY); + this._zoomX = zoomX; this._zoomY = zoomY; - this._invalicateCache(); + this._invalidateCache(); + + if (modifiedZoomY) { + this._invalidateDataCache(); + } }; GraphSpectrumPlot.setSize = function(width, height) { this._canvasCtx.canvas.width = width; this._canvasCtx.canvas.height = height; - this._invalicateCache(); + this._invalidateCache(); }; GraphSpectrumPlot.setFullScreen = function(isFullScreen) { this._isFullScreen = isFullScreen; - this._invalicateCache(); + this._invalidateCache(); }; -GraphSpectrumPlot.setData = function(fftData) { +GraphSpectrumPlot.setData = function(fftData, spectrumType) { this._fftData = fftData; - this._invalicateCache(); + this._spectrumType = spectrumType; + this._invalidateCache(); + this._invalidateDataCache(); }; GraphSpectrumPlot.setMouseFrequency = function(mouseFrequency) { @@ -78,9 +99,23 @@ GraphSpectrumPlot._drawCachedElements = function() { GraphSpectrumPlot._drawGraph = function(canvasCtx) { + switch(this._spectrumType) { + + case SPECTRUM_TYPE.FREQUENCY: + this._drawFrequencyGraph(canvasCtx); + break; + + case SPECTRUM_TYPE.FREQ_VS_THROTTLE: + this._drawFrequencyVsThrottleGraph(canvasCtx); + break; + } + +} + +GraphSpectrumPlot._drawFrequencyGraph = function(canvasCtx) { + canvasCtx.lineWidth = 1; - - + var HEIGHT = canvasCtx.canvas.height - MARGIN; var WIDTH = canvasCtx.canvas.width; var LEFT = canvasCtx.canvas.left; @@ -126,8 +161,78 @@ GraphSpectrumPlot._drawGraph = function(canvasCtx) { }; +GraphSpectrumPlot._drawFrequencyVsThrottleGraph = function(canvasCtx) { + + var PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; + + var ACTUAL_MARGIN_LEFT = (this._isFullScreen)? MARGIN_LEFT_FULLSCREEN : MARGIN_LEFT; + var WIDTH = canvasCtx.canvas.width - ACTUAL_MARGIN_LEFT; + var HEIGHT = canvasCtx.canvas.height - MARGIN_BOTTOM; + var LEFT = canvasCtx.canvas.offsetLeft + ACTUAL_MARGIN_LEFT; + var TOP = canvasCtx.canvas.offsetTop; + + canvasCtx.translate(LEFT, TOP); + + if (this._cachedDataCanvas == null) { + this._cachedDataCanvas = this._drawHeatMap(); + } + + canvasCtx.drawImage(this._cachedDataCanvas, 0, 0, WIDTH, HEIGHT); + + canvasCtx.drawImage(this._cachedDataCanvas, + 0, 0, this._cachedDataCanvas.width / this._zoomX, this._cachedDataCanvas.height, + 0, 0, WIDTH, HEIGHT); + + + this._drawAxisLabel(canvasCtx, this._fftData.fieldName, WIDTH - 4, HEIGHT - 6, 'right'); + this._drawGridLines(canvasCtx, PLOTTED_BLACKBOX_RATE, LEFT, TOP, WIDTH, HEIGHT, MARGIN_BOTTOM); + this._drawThrottleLines(canvasCtx, LEFT, TOP, WIDTH, HEIGHT, ACTUAL_MARGIN_LEFT); + +} + +GraphSpectrumPlot._drawHeatMap = function() { + + const THROTTLE_VALUES_SIZE = 100; + const SCALE_HEATMAP = 1.3; // Value decided after some tests to be similar to the scale of frequency graph + // This value will be maximum color + + var heatMapCanvas = document.createElement('canvas'); + var canvasCtx = heatMapCanvas.getContext("2d", { alpha: false }); + + // We use always a canvas of the size of the FFT data (is not too big) + canvasCtx.canvas.width = this._fftData.fftLength; + canvasCtx.canvas.height = THROTTLE_VALUES_SIZE; + + var fftColorScale = 100 / (this._zoomY * SCALE_HEATMAP); + + // Loop for throttle + for(var j = 0; j < 100; j++) { + // Loop for frequency + for(var i = 0; i < this._fftData.fftLength; i ++) { + let valuePlot = Math.round(Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100)); + + // The fillStyle is slow, but I haven't found a way to do this faster... + canvasCtx.fillStyle = `hsl(360, 100%, ${valuePlot}%)`; + canvasCtx.fillRect(i, 99 - j, 1, 1); + + } + } + + // The resulting image has imperfections, usually we not have all the data in the input, so we apply a little of blur + canvasCtx.filter = `blur(${BLUR_FILTER_PIXEL}px)`; + canvasCtx.drawImage(heatMapCanvas, 0, 0); + canvasCtx.filter = 'none'; + + return heatMapCanvas; +} + GraphSpectrumPlot._drawFiltersAndMarkers = function(canvasCtx) { + // At this moment we not draw filters or markers for the spectrum vs throttle graph + if (this._spectrumType == SPECTRUM_TYPE.FREQ_VS_THROTTLE) { + return; + } + var HEIGHT = this._canvasCtx.canvas.height - MARGIN; var WIDTH = this._canvasCtx.canvas.width; var PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; @@ -204,6 +309,11 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function(canvasCtx) { GraphSpectrumPlot._drawNotCachedElements = function() { + // At this moment we not draw filters or markers for the spectrum vs throttle graph + if (this._spectrumType == SPECTRUM_TYPE.FREQ_VS_THROTTLE) { + return; + } + var canvasCtx = this._canvasCtx; // Not cached canvas var HEIGHT = this._canvasCtx.canvas.height - MARGIN; @@ -216,7 +326,7 @@ GraphSpectrumPlot._drawNotCachedElements = function() { } } -GraphSpectrumPlot._drawAxisLabel = function(canvasCtx, axisLabel, X, Y, align) { +GraphSpectrumPlot._drawAxisLabel = function(canvasCtx, axisLabel, X, Y, align, baseline) { canvasCtx.font = ((this._isFullScreen)? this._drawingParams.fontSizeFrameLabelFullscreen : this._drawingParams.fontSizeFrameLabel) + "pt " + DEFAULT_FONT_FACE; canvasCtx.fillStyle = "rgba(255,255,255,0.9)"; if(align) { @@ -224,13 +334,17 @@ GraphSpectrumPlot._drawAxisLabel = function(canvasCtx, axisLabel, X, Y, align) { } else { canvasCtx.textAlign = 'center'; } - + if (baseline) { + canvasCtx.textBaseline = baseline; + } else { + canvasCtx.textBaseline = 'alphabetic'; + } canvasCtx.fillText(axisLabel, X, Y); }; GraphSpectrumPlot._drawGridLines = function(canvasCtx, sampleRate, LEFT, TOP, WIDTH, HEIGHT, MARGIN) { - var ticks = 5; + const ticks = 5; var frequencyInterval = (sampleRate / ticks) / 2; var frequency = 0; @@ -249,6 +363,25 @@ GraphSpectrumPlot._drawGridLines = function(canvasCtx, sampleRate, LEFT, TOP, WI } }; +GraphSpectrumPlot._drawThrottleLines = function(canvasCtx, LEFT, TOP, WIDTH, HEIGHT, MARGIN) { + + const ticks = 5; + for(var i=0; i<=ticks; i++) { + canvasCtx.beginPath(); + canvasCtx.lineWidth = 1; + canvasCtx.strokeStyle = "rgba(255,255,255,0.25)"; + + var verticalPosition = i * (HEIGHT / ticks); + canvasCtx.moveTo(0, verticalPosition); + canvasCtx.lineTo(WIDTH, verticalPosition); + + canvasCtx.stroke(); + var throttleValue = 100 - i*20; + var textBaseline = (i==0)?'top':((i==ticks)?'bottom':'middle'); + this._drawAxisLabel(canvasCtx, throttleValue + "%", 0, verticalPosition, "right", textBaseline); + } +}; + GraphSpectrumPlot._drawMarkerLine = function(canvasCtx, frequency, sampleRate, label, WIDTH, HEIGHT, OFFSET, stroke, lineWidth){ var x = WIDTH * frequency / (sampleRate / 2); // percentage of range where frequncy lies @@ -329,6 +462,10 @@ GraphSpectrumPlot._drawNotchFilter = function(canvasCtx, center, cutoff, sampleR }; -GraphSpectrumPlot._invalicateCache = function() { +GraphSpectrumPlot._invalidateCache = function() { this._cachedCanvas = null; +}; + +GraphSpectrumPlot._invalidateDataCache = function() { + this._cachedDataCanvas = null; }; \ No newline at end of file