From 1f5e4a47578c90d58a13c9267abecad7c353f731 Mon Sep 17 00:00:00 2001 From: demvlad Date: Tue, 13 May 2025 12:25:44 +0300 Subject: [PATCH 01/63] The FFT is used power 2 input length to get maximal fft performance --- src/graph_spectrum_calc.js | 96 +++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 15e085ef..0c2b8fe2 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -96,18 +96,50 @@ GraphSpectrumCalc.dataLoadFrequency = function() { const flightSamples = this._getFlightSamplesFreq(); if (userSettings.analyserHanning) { - this._hanningWindow(flightSamples.samples, flightSamples.count); + this._hanningWindow(flightSamples.samples, flightSamples.count); // Apply Hann function to actual flightSamples.count values only } - //calculate fft + //calculate fft for the all samples const fftOutput = this._fft(flightSamples.samples); // Normalize the result - const fftData = this._normalizeFft(fftOutput, flightSamples.samples.length); + const fftData = this._normalizeFft(fftOutput); return fftData; }; +GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { + const flightSamples = this._getFlightSamplesFreq(false); + + let pointsPerSegment = 512; + const multipiler = Math.floor(1 / analyserZoomY); // 0. ... 10 + if (multipiler == 0) { + pointsPerSegment = 256; + } else if (multipiler > 1) { + pointsPerSegment *= 2 ** Math.floor(multipiler / 2); + } + + // Use power 2 fft size what is not bigger flightSamples.samples.length + if (pointsPerSegment > flightSamples.samples.length) { + pointsPerSegment = Math.pow(2, Math.floor(Math.log2(flightSamples.samples.length))); + } + + const overlapCount = Math.floor(pointsPerSegment / 2); + + const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount); + + const psdData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + psdLength : psd.psdOutput.length, + psdOutput : psd.psdOutput, + blackBoxRate : this._blackBoxRate, + minimum: psd.min, + maximum: psd.max, + maxNoiseIdx: psd.maxNoiseIdx, + }; + return psdData; +}; GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { @@ -115,20 +147,28 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi // 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. - const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; + const fftChunkLength = Math.round(this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000); const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); + const fftBufferSize = Math.pow(2, Math.ceil(Math.log2(fftChunkLength))); let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies - const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftChunkLength * 2)); + const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftBufferSize * 2)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. - const fft = new FFT.complex(fftChunkLength, false); + + const fft = new FFT.complex(fftBufferSize, false); for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { - const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); - let fftOutput = new Float64Array(fftChunkLength * 2); + const fftInput = new Float64Array(fftBufferSize); + let fftOutput = new Float64Array(fftBufferSize * 2); + + //TODO: to find method to just resize samples array to fftBufferSize + const samples = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); + for (let i = 0; i < fftChunkLength; i++) { + fftInput[i] = samples[i]; + } // Hanning window applied to input data if (userSettings.analyserHanning) { @@ -137,10 +177,12 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi fft.simple(fftOutput, fftInput, 'real'); - fftOutput = fftOutput.slice(0, fftChunkLength); + fftOutput = fftOutput.slice(0, fftBufferSize); // The fft output contains two side spectrum, we use the first part only to get one side + // TODO: This is wrong spectrum magnitude calculation as abs of separate complex Re, Im values. + // We should use hypot(Re, Im) instead of and return divide by 2 (fftOutput.length / 4) arrays size // Use only abs values - for (let i = 0; i < fftChunkLength; i++) { + for (let i = 0; i < fftBufferSize; i++) { fftOutput[i] = Math.abs(fftOutput[i]); maxNoise = Math.max(fftOutput[i], maxNoise); } @@ -154,7 +196,7 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi } // Translate the average vs value to a bin index const avgVsValue = sumVsValues / fftChunkLength; - let vsBinIndex = Math.floor(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); + let vsBinIndex = Math.round(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } numberSamples[vsBinIndex]++; @@ -180,13 +222,13 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi // blur algorithm to the heat map image const fftData = { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - fftLength : fftChunkLength, - fftOutput : matrixFftOutput, - maxNoise : maxNoise, - blackBoxRate : this._blackBoxRate, - vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + fftLength : fftBufferSize, + fftOutput : matrixFftOutput, + maxNoise : maxNoise, + blackBoxRate : this._blackBoxRate, + vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, }; return fftData; @@ -300,8 +342,10 @@ GraphSpectrumCalc._getFlightSamplesFreq = function() { } } + // The FFT input size is power 2 to get maximal performance + const fftBufferSize = Math.pow(2, Math.ceil(Math.log2(samplesCount))); return { - samples : samples, + samples : samples.slice(0, fftBufferSize), count : samplesCount, }; }; @@ -377,7 +421,7 @@ GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = I } } - let slicedVsValues = []; + const slicedVsValues = []; for (const vsValueArray of vsValues) { slicedVsValues.push(vsValueArray.slice(0, samplesCount)); } @@ -453,18 +497,16 @@ GraphSpectrumCalc._fft = function(samples, type) { /** * Makes all the values absolute and returns the index of maxFrequency found */ -GraphSpectrumCalc._normalizeFft = function(fftOutput, fftLength) { - - if (!fftLength) { - fftLength = fftOutput.length; - } - +GraphSpectrumCalc._normalizeFft = function(fftOutput) { + // The fft output contains two side spectrum, we use the first part only to get one side + const fftLength = fftOutput.length / 2; // Make all the values absolute, and calculate some useful values (max noise, etc.) const maxFrequency = (this._blackBoxRate / 2.0); const noiseLowEndIdx = 100 / maxFrequency * fftLength; let maxNoiseIdx = 0; let maxNoise = 0; - + // TODO: This is wrong spectrum magnitude calculation as abs of separate complex Re, Im values. + // We should use hypot(Re, Im) instead of and return divide by 2 (fftOutput.length / 4) arrays size for (let i = 0; i < fftLength; i++) { fftOutput[i] = Math.abs(fftOutput[i]); if (i > noiseLowEndIdx && fftOutput[i] > maxNoise) { From 1ade10270283143d6a597933dac44488ed838420 Mon Sep 17 00:00:00 2001 From: demvlad Date: Tue, 13 May 2025 22:06:29 +0300 Subject: [PATCH 02/63] Added Power spectral density curves chart --- index.html | 1 + src/graph_spectrum.js | 9 +++ src/graph_spectrum_calc.js | 138 ++++++++++++++++++++++++++++++++++--- src/graph_spectrum_plot.js | 116 +++++++++++++++++++++++++++---- 4 files changed, 242 insertions(+), 22 deletions(-) diff --git a/index.html b/index.html index 5218a8d4..3f41083f 100644 --- a/index.html +++ b/index.html @@ -459,6 +459,7 @@

Workspace

+ diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 843fa284..be5b024a 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -113,6 +113,10 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { fftData = GraphSpectrumCalc.dataLoadPidErrorVsSetpoint(); break; + case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: + fftData = GraphSpectrumCalc.dataLoadPSD(analyserZoomY); + break; + case SPECTRUM_TYPE.FREQUENCY: default: fftData = GraphSpectrumCalc.dataLoadFrequency(); @@ -177,6 +181,11 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { debounce(100, function () { analyserZoomY = 1 / (analyserZoomYElem.val() / 100); GraphSpectrumPlot.setZoom(analyserZoomX, analyserZoomY); + // Recalculate PSD with updated samples per segment count + if (userSettings.spectrumType == SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) { + dataLoad(); + GraphSpectrumPlot.setData(fftData, userSettings.spectrumType); + } that.refresh(); }) ) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 0c2b8fe2..a218a090 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -327,7 +327,7 @@ GraphSpectrumCalc._getFlightChunks = function() { return allChunks; }; -GraphSpectrumCalc._getFlightSamplesFreq = function() { +GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { const allChunks = this._getFlightChunks(); @@ -337,7 +337,11 @@ GraphSpectrumCalc._getFlightSamplesFreq = function() { let samplesCount = 0; for (const chunk of allChunks) { for (const frame of chunk.frames) { - samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex])); + if (scaled) { + samples[samplesCount] = this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]); + } else { + samples[samplesCount] = frame[this._dataBuffer.fieldIndex]; + } samplesCount++; } } @@ -425,13 +429,14 @@ GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = I for (const vsValueArray of vsValues) { slicedVsValues.push(vsValueArray.slice(0, samplesCount)); } + return { - samples : samples.slice(0, samplesCount), - vsValues : slicedVsValues, - count : samplesCount, - minValue : minValue, - maxValue : maxValue, - }; + samples : samples.slice(0, samplesCount), + vsValues : slicedVsValues, + count : samplesCount, + minValue : minValue, + maxValue : maxValue, + }; }; GraphSpectrumCalc._getFlightSamplesPidErrorVsSetpoint = function(axisIndex) { @@ -460,8 +465,8 @@ GraphSpectrumCalc._getFlightSamplesPidErrorVsSetpoint = function(axisIndex) { } return { - piderror, - setpoint, + piderror: piderror.slice(0, samplesCount), + setpoint: setpoint.slice(0, samplesCount), maxSetpoint, count: samplesCount, }; @@ -528,3 +533,116 @@ GraphSpectrumCalc._normalizeFft = function(fftOutput) { return fftData; }; + +/** + * Compute PSD for data samples by Welch method follow Python code + */ +GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scaling = 'density') { +// Compute FFT for samples segments + const fftOutput = this._fft_segmented(samples, pointsPerSegment, overlapCount); + + const dataCount = fftOutput[0].length; + const segmentsCount = fftOutput.length; + const psdOutput = new Float64Array(dataCount); + +// Compute power scale coef + let scale = 1; + if (userSettings.analyserHanning) { + const window = Array(pointsPerSegment).fill(1); + this._hanningWindow(window, pointsPerSegment); + if (scaling == 'density') { + let skSum = 0; + for (const value of window) { + skSum += value ** 2; + } + scale = 1 / (this._blackBoxRate * skSum); + } else if (scaling == 'spectrum') { + let sum = 0; + for (const value of window) { + sum += value; + } + scale = 1 / sum ** 2; + } + } else if (scaling == 'density') { + scale = 1 / pointsPerSegment; + } else if (scaling == 'spectrum') { + scale = 1 / pointsPerSegment ** 2; + } + +// Compute average for scaled power + let min = 1e6, + max = -1e6; + // Early exit if no segments were processed + if (segmentsCount === 0) { + return { + psdOutput: new Float64Array(0), + min: 0, + max: 0, + maxNoiseIdx: 0, + }; + } + const maxFrequency = (this._blackBoxRate / 2.0); + const noise50HzIdx = 50 / maxFrequency * dataCount; + const noise3HzIdx = 3 / maxFrequency * dataCount; + let maxNoiseIdx = 0; + let maxNoise = -100; + for (let i = 0; i < dataCount; i++) { + psdOutput[i] = 0.0; + for (let j = 0; j < segmentsCount; j++) { + let p = scale * fftOutput[j][i] ** 2; + if (i != dataCount - 1) { + p *= 2; + } + psdOutput[i] += p; + } + + const min_avg = 1e-7; // limit min value for -70db + let avg = psdOutput[i] / segmentsCount; + avg = Math.max(avg, min_avg); + psdOutput[i] = 10 * Math.log10(avg); + if (i > noise3HzIdx) { // Miss big zero freq magnitude + min = Math.min(psdOutput[i], min); + max = Math.max(psdOutput[i], max); + } + if (i > noise50HzIdx && psdOutput[i] > maxNoise) { + maxNoise = psdOutput[i]; + maxNoiseIdx = i; + } + } + + const maxNoiseFrequency = maxNoiseIdx / dataCount * maxFrequency; + + return { + psdOutput: psdOutput, + min: min, + max: max, + maxNoiseIdx: maxNoiseFrequency, + }; +}; + + +/** + * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count + */ +GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { + const samplesCount = samples.length; + let output = []; + for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { + const fftInput = samples.slice(i, i + pointsPerSegment); + + if (userSettings.analyserHanning) { + this._hanningWindow(fftInput, pointsPerSegment); + } + + const fftComplex = this._fft(fftInput); + const magnitudes = new Float64Array(pointsPerSegment / 2); + for (let i = 0; i < pointsPerSegment / 2; i++) { + const re = fftComplex[2 * i]; + const im = fftComplex[2 * i + 1]; + magnitudes[i] = Math.hypot(re, im); + } + output.push(magnitudes); + } + + return output; +}; diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index b43d4aea..2ff35470 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -17,6 +17,7 @@ export const SPECTRUM_TYPE = { FREQ_VS_THROTTLE: 1, PIDERROR_VS_SETPOINT: 2, FREQ_VS_RPM: 3, + POWER_SPECTRAL_DENSITY: 4, }; export const SPECTRUM_OVERDRAW_TYPE = { @@ -171,6 +172,10 @@ GraphSpectrumPlot._drawGraph = function (canvasCtx) { case SPECTRUM_TYPE.PIDERROR_VS_SETPOINT: this._drawPidErrorVsSetpointGraph(canvasCtx); break; + + case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: + this._drawPowerSpectralDensityGraph(canvasCtx); + break; } }; @@ -294,7 +299,7 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { this._fftData.fieldName, WIDTH - 4, HEIGHT - 6, - "right" + "right", ); this._drawHorizontalGridLines( canvasCtx, @@ -304,10 +309,92 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { WIDTH, HEIGHT, MARGIN, - "Hz" + "Hz", ); }; +GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { + const HEIGHT = canvasCtx.canvas.height - MARGIN; + const ACTUAL_MARGIN_LEFT = this._getActualMarginLeft(); + const WIDTH = canvasCtx.canvas.width - ACTUAL_MARGIN_LEFT; + const LEFT = canvasCtx.canvas.offsetLeft + ACTUAL_MARGIN_LEFT; + const TOP = canvasCtx.canvas.offsetTop; + + const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; + + canvasCtx.save(); + canvasCtx.translate(LEFT, TOP); + this._drawGradientBackground(canvasCtx, WIDTH, HEIGHT); + + const pointsCount = this._fftData.psdLength; + const scaleX = 2 * WIDTH / PLOTTED_BLACKBOX_RATE * this._zoomX; + canvasCtx.beginPath(); + canvasCtx.lineWidth = 1; + canvasCtx.strokeStyle = "white"; + + // Allign y axis range by 10db + const dbStep = 10; + const minY = Math.floor(this._fftData.minimum / dbStep) * dbStep; + const maxY = (Math.floor(this._fftData.maximum / dbStep) + 1) * dbStep; + const ticksCount = (maxY - minY) / dbStep; + const scaleY = HEIGHT / (maxY - minY); + //Store vsRange for _drawMousePosition + this._fftData.vsRange = { + min: minY, + max: maxY, + }; + canvasCtx.moveTo(0, 0); + for (let pointNum = 0; pointNum < pointsCount; pointNum++) { + const freq = PLOTTED_BLACKBOX_RATE / 2 * pointNum / pointsCount; + const y = HEIGHT - (this._fftData.psdOutput[pointNum] - minY) * scaleY; + canvasCtx.lineTo(freq * scaleX, y); + } + canvasCtx.stroke(); + + this._drawAxisLabel( + canvasCtx, + this._fftData.fieldName, + WIDTH - 4, + HEIGHT - 6, + "right", + ); + this._drawHorizontalGridLines( + canvasCtx, + PLOTTED_BLACKBOX_RATE / 2, + LEFT, + TOP, + WIDTH, + HEIGHT, + MARGIN, + "Hz", + ); + this._drawVerticalGridLines( + canvasCtx, + LEFT, + TOP, + WIDTH, + HEIGHT, + minY, + maxY, + "dBm/Hz", + ticksCount, + ); + const offset = 1; + this._drawInterestFrequency( + canvasCtx, + this._fftData.maxNoiseIdx, + PLOTTED_BLACKBOX_RATE, + "Max noise", + WIDTH, + HEIGHT, + 15 * offset + MARGIN, + "rgba(255,0,0,0.50)", + 3, + ); + + canvasCtx.restore(); +}; + GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx) { const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; @@ -862,7 +949,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { canvasCtx, this._fftData.maxNoiseIdx, PLOTTED_BLACKBOX_RATE, - "Max motor noise", + "Max noise", WIDTH, HEIGHT, 15 * offset + MARGIN, @@ -993,22 +1080,22 @@ GraphSpectrumPlot._drawVerticalGridLines = function ( HEIGHT, minValue, maxValue, - label + label, + ticks = 5, ) { - const TICKS = 5; - for (let i = 0; i <= TICKS; i++) { + for (let i = 0; i <= ticks; i++) { canvasCtx.beginPath(); canvasCtx.lineWidth = 1; canvasCtx.strokeStyle = "rgba(255,255,255,0.25)"; - const verticalPosition = i * (HEIGHT / TICKS); + const verticalPosition = i * (HEIGHT / ticks); canvasCtx.moveTo(0, verticalPosition); canvasCtx.lineTo(WIDTH, verticalPosition); canvasCtx.stroke(); const verticalAxisValue = ( - (maxValue - minValue) * ((TICKS - i) / TICKS) + + (maxValue - minValue) * ((ticks - i) / ticks) + minValue ).toFixed(0); let textBaseline; @@ -1016,7 +1103,7 @@ GraphSpectrumPlot._drawVerticalGridLines = function ( case 0: textBaseline = "top"; break; - case TICKS: + case ticks: textBaseline = "bottom"; break; default: @@ -1129,8 +1216,8 @@ GraphSpectrumPlot._drawGradientBackground = function ( ); if (this._isFullScreen) { - backgroundGradient.addColorStop(1, "rgba(0,0,0,0.9)"); - backgroundGradient.addColorStop(0, "rgba(0,0,0,0.7)"); + backgroundGradient.addColorStop(1, "rgba(0,0,0,1)"); + backgroundGradient.addColorStop(0, "rgba(0,0,0,0.9)"); } else { backgroundGradient.addColorStop(1, "rgba(255,255,255,0.25)"); backgroundGradient.addColorStop(0, "rgba(255,255,255,0)"); @@ -1359,7 +1446,8 @@ GraphSpectrumPlot._drawMousePosition = function ( if ( this._spectrumType === SPECTRUM_TYPE.FREQUENCY || this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || - this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM + this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM || + this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY ) { // Calculate frequency at mouse const sampleRate = this._fftData.blackBoxRate / this._zoomX; @@ -1391,6 +1479,9 @@ GraphSpectrumPlot._drawMousePosition = function ( case SPECTRUM_TYPE.FREQ_VS_RPM: unitLabel = "Hz"; break; + case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: + unitLabel = "dBm/Hz"; + break; default: unitLabel = null; break; @@ -1466,6 +1557,7 @@ GraphSpectrumPlot._getActualMarginLeft = function () { switch (this._spectrumType) { case SPECTRUM_TYPE.FREQ_VS_THROTTLE: case SPECTRUM_TYPE.FREQ_VS_RPM: + case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: actualMarginLeft = this._isFullScreen ? MARGIN_LEFT_FULLSCREEN : MARGIN_LEFT; From e19b7090ec77dfb655341e9ba2e628063c5dd4a3 Mon Sep 17 00:00:00 2001 From: demvlad Date: Tue, 13 May 2025 22:58:47 +0300 Subject: [PATCH 03/63] The spectrum magnitude is computed by complex Re and Im value --- src/graph_spectrum_calc.js | 54 +++++++++++++++++++++----------------- src/graph_spectrum_plot.js | 9 ++++--- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index a218a090..9e7da07c 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -150,7 +150,7 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi const fftChunkLength = Math.round(this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000); const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); const fftBufferSize = Math.pow(2, Math.ceil(Math.log2(fftChunkLength))); - + const magnitudeLength = Math.floor(fftBufferSize / 2); let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftBufferSize * 2)); @@ -178,13 +178,14 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi fft.simple(fftOutput, fftInput, 'real'); fftOutput = fftOutput.slice(0, fftBufferSize); // The fft output contains two side spectrum, we use the first part only to get one side - - // TODO: This is wrong spectrum magnitude calculation as abs of separate complex Re, Im values. - // We should use hypot(Re, Im) instead of and return divide by 2 (fftOutput.length / 4) arrays size - // Use only abs values - for (let i = 0; i < fftBufferSize; i++) { - fftOutput[i] = Math.abs(fftOutput[i]); - maxNoise = Math.max(fftOutput[i], maxNoise); + const magnitudes = new Float64Array(magnitudeLength); + +// Compute magnitude + for (let i = 0; i < magnitudeLength; i++) { + const re = fftOutput[2 * i], + im = fftOutput[2 * i + 1]; + magnitudes[i] = Math.hypot(re, im); + maxNoise = Math.max(magnitudes[i], maxNoise); } // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately @@ -198,12 +199,12 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi const avgVsValue = sumVsValues / fftChunkLength; let vsBinIndex = Math.round(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access - if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } + if (vsBinIndex >= NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } numberSamples[vsBinIndex]++; // add the output from the fft to the row given by the vs value bin index - for (let i = 0; i < fftOutput.length; i++) { - matrixFftOutput[vsBinIndex][i] += fftOutput[i]; + for (let i = 0; i < magnitudeLength; i++) { + matrixFftOutput[vsBinIndex][i] += magnitudes[i]; } } } @@ -224,7 +225,7 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi const fftData = { fieldIndex : this._dataBuffer.fieldIndex, fieldName : this._dataBuffer.fieldName, - fftLength : fftBufferSize, + fftLength : magnitudeLength, fftOutput : matrixFftOutput, maxNoise : maxNoise, blackBoxRate : this._blackBoxRate, @@ -395,7 +396,7 @@ GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = I } // Calculate min max average of the VS values in the chunk what will used by spectrum data definition - const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; + const fftChunkLength = Math.round(this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000); const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < samplesCount; fftChunkIndex += fftChunkWindow) { for (const vsValueArray of vsValues) { @@ -505,28 +506,33 @@ GraphSpectrumCalc._fft = function(samples, type) { GraphSpectrumCalc._normalizeFft = function(fftOutput) { // The fft output contains two side spectrum, we use the first part only to get one side const fftLength = fftOutput.length / 2; - // Make all the values absolute, and calculate some useful values (max noise, etc.) + + // The fft output contains complex values (re, im pairs) of two-side spectrum + // Compute magnitudes for one spectrum side + const magnitudeLength = Math.floor(fftLength / 2); const maxFrequency = (this._blackBoxRate / 2.0); - const noiseLowEndIdx = 100 / maxFrequency * fftLength; + const noiseLowEndIdx = 100 / maxFrequency * magnitudeLength; + const magnitudes = new Float64Array(magnitudeLength); let maxNoiseIdx = 0; let maxNoise = 0; - // TODO: This is wrong spectrum magnitude calculation as abs of separate complex Re, Im values. - // We should use hypot(Re, Im) instead of and return divide by 2 (fftOutput.length / 4) arrays size - for (let i = 0; i < fftLength; i++) { - fftOutput[i] = Math.abs(fftOutput[i]); - if (i > noiseLowEndIdx && fftOutput[i] > maxNoise) { - maxNoise = fftOutput[i]; + + for (let i = 0; i < magnitudeLength; i++) { + const re = fftOutput[2 * i], + im = fftOutput[2 * i + 1]; + magnitudes[i] = Math.hypot(re, im); + if (i > noiseLowEndIdx && magnitudes[i] > maxNoise) { + maxNoise = magnitudes[i]; maxNoiseIdx = i; } } - maxNoiseIdx = maxNoiseIdx / fftLength * maxFrequency; + maxNoiseIdx = maxNoiseIdx / magnitudeLength * maxFrequency; const fftData = { fieldIndex : this._dataBuffer.fieldIndex, fieldName : this._dataBuffer.fieldName, - fftLength : fftLength, - fftOutput : fftOutput, + fftLength : magnitudeLength, + fftOutput : magnitudes, maxNoiseIdx : maxNoiseIdx, blackBoxRate : this._blackBoxRate, }; diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 2ff35470..d0782640 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -193,7 +193,7 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { this._drawGradientBackground(canvasCtx, WIDTH, HEIGHT); - const barWidth = WIDTH / (PLOTTED_BUFFER_LENGTH / 10) - 1; + const barWidth = WIDTH / (PLOTTED_BUFFER_LENGTH / 5) - 1; let x = 0; const barGradient = canvasCtx.createLinearGradient(0, HEIGHT, 0, 0); @@ -217,7 +217,7 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { canvasCtx.fillStyle = barGradient; const fftScale = HEIGHT / (this._zoomY * 100); - for (let i = 0; i < PLOTTED_BUFFER_LENGTH; i += 10) { + for (let i = 0; i < PLOTTED_BUFFER_LENGTH; i += 5) { const barHeight = this._fftData.fftOutput[i] * fftScale; canvasCtx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight); x += barWidth + 1; @@ -469,7 +469,8 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx) { 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 + //The magnitude is greate then seperete Re or Im value up to 1.4=sqrt(2). Therefore the SCALE_HEATMAP is decreased from 1.3 to 1.1 + const SCALE_HEATMAP = 1.1; // Value decided after some tests to be similar to the s // This value will be maximum color const heatMapCanvas = document.createElement("canvas"); @@ -482,7 +483,7 @@ GraphSpectrumPlot._drawHeatMap = function () { const fftColorScale = 100 / (this._zoomY * SCALE_HEATMAP); // Loop for throttle - for (let j = 0; j < 100; j++) { + for (let j = 0; j < THROTTLE_VALUES_SIZE; j++) { // Loop for frequency for (let i = 0; i < this._fftData.fftLength; i++) { const valuePlot = Math.round( From 97b6ed1c30d747a3447c3478b13ef1f2a453a81e Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 20:47:36 +0300 Subject: [PATCH 04/63] Added functions to compute PSD by RPM and Throttle --- src/graph_spectrum_calc.js | 77 +++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 9e7da07c..61b175d8 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -236,6 +236,75 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi }; +GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { + + const flightSamples = this._getFlightSamplesFreqVsX(vsFieldNames, minValue, maxValue, false); + + // 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. + const fftChunkLength = Math.round(this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000); + const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); + + let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks + let psdLength = 0; + // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies + const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => (new Float64Array(fftChunkLength * 2)).fill(-70)); + + const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. + + for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { + + const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); + const psd = this._psd(fftInput, fftChunkLength, 0, 'density'); + psdLength = psd.psdOutput.length; + maxNoise = Math.max(psd.max, maxNoise); + // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately + for (const vsValueArray of flightSamples.vsValues) { + // Calculate average of the VS values in the chunk + let sumVsValues = 0; + for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { + sumVsValues += vsValueArray[indexVs]; + } + // Translate the average vs value to a bin index + const avgVsValue = sumVsValues / fftChunkLength; + let vsBinIndex = Math.floor(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); + // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access + if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } + numberSamples[vsBinIndex]++; + + // add the output from the fft to the row given by the vs value bin index + for (let i = 0; i < psd.psdOutput.length; i++) { + matrixFftOutput[vsBinIndex][i] += psd.psdOutput[i]; + } + } + } + + // Divide the values from the fft in each row (vs value bin) by the number of samples in the bin + for (let i = 0; i < NUM_VS_BINS; i++) { + if (numberSamples[i] > 1) { + for (let j = 0; j < matrixFftOutput[i].length; j++) { + matrixFftOutput[i][j] /= numberSamples[i]; + } + } + } + + // 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 + + const psdData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + fftLength : psdLength, + fftOutput : matrixFftOutput, + maxNoise : maxNoise, + blackBoxRate : this._blackBoxRate, + vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, + }; + + return psdData; + +}; GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); }; @@ -365,7 +434,7 @@ GraphSpectrumCalc._getVsIndexes = function(vsFieldNames) { return fieldIndexes; }; -GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { +GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity, scaled = true) { const allChunks = this._getFlightChunks(); const vsIndexes = this._getVsIndexes(vsFieldNames); @@ -376,7 +445,11 @@ GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = I let samplesCount = 0; for (const chunk of allChunks) { for (let frameIndex = 0; frameIndex < chunk.frames.length; frameIndex++) { - samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][this._dataBuffer.fieldIndex])); + if (scaled) { + samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][this._dataBuffer.fieldIndex])); + } else { + samples[samplesCount] = chunk.frames[frameIndex][this._dataBuffer.fieldIndex]; + } for (let i = 0; i < vsIndexes.length; i++) { let vsFieldIx = vsIndexes[i]; let value = chunk.frames[frameIndex][vsFieldIx]; From d0ea63f0a2f7c5a3affd439b6ad6f6d13da6a9b6 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 20:49:10 +0300 Subject: [PATCH 05/63] Added drawing heat map for PSD values --- src/graph_spectrum_plot.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index d0782640..d341d17d 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -395,7 +395,7 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { canvasCtx.restore(); }; -GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx) { +GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) { const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; const ACTUAL_MARGIN_LEFT = this._getActualMarginLeft(); @@ -407,7 +407,7 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx) { canvasCtx.translate(LEFT, TOP); if (this._cachedDataCanvas == null) { - this._cachedDataCanvas = this._drawHeatMap(); + this._cachedDataCanvas = this._drawHeatMap(drawPSD); } canvasCtx.drawImage(this._cachedDataCanvas, 0, 0, WIDTH, HEIGHT); @@ -467,7 +467,7 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx) { } }; -GraphSpectrumPlot._drawHeatMap = function () { +GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { const THROTTLE_VALUES_SIZE = 100; //The magnitude is greate then seperete Re or Im value up to 1.4=sqrt(2). Therefore the SCALE_HEATMAP is decreased from 1.3 to 1.1 const SCALE_HEATMAP = 1.1; // Value decided after some tests to be similar to the s @@ -486,9 +486,16 @@ GraphSpectrumPlot._drawHeatMap = function () { for (let j = 0; j < THROTTLE_VALUES_SIZE; j++) { // Loop for frequency for (let i = 0; i < this._fftData.fftLength; i++) { - const valuePlot = Math.round( - Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100) - ); + if (drawPSD) { + const min = -40, max = 10; //limit values dBm + let valuePlot = Math.max(this._fftData.fftOutput[j][i], min); + valuePlot = Math.min(this._fftData.fftOutput[j][i], max); + valuePlot = Math.round((valuePlot - min) * 100 / (max - min)); + } else { + const 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}%)`; From 4d36d7f3667101d2ae0bfdb08d47ef6f7f050934 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 21:11:20 +0300 Subject: [PATCH 06/63] Added switch to select PSD by throttle or RPM charts --- index.html | 4 +- src/graph_spectrum.js | 8 + src/graph_spectrum_calc.js | 11 +- src/graph_spectrum_calc.js.bak | 631 +++++++++++++++++++++++++++++++++ src/graph_spectrum_plot.js | 31 +- 5 files changed, 674 insertions(+), 11 deletions(-) create mode 100644 src/graph_spectrum_calc.js.bak diff --git a/index.html b/index.html index 3f41083f..71b5d086 100644 --- a/index.html +++ b/index.html @@ -458,8 +458,10 @@

Workspace

- + + + diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index be5b024a..bcc19151 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -109,6 +109,14 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { fftData = GraphSpectrumCalc.dataLoadFrequencyVsRpm(); break; + case SPECTRUM_TYPE.PSD_VS_THROTTLE: + fftData = GraphSpectrumCalc.dataLoadFrequencyVsThrottle(true); + break; + + case SPECTRUM_TYPE.PSD_VS_RPM: + fftData = GraphSpectrumCalc.dataLoadFrequencyVsRpm(true); + break; + case SPECTRUM_TYPE.PIDERROR_VS_SETPOINT: fftData = GraphSpectrumCalc.dataLoadPidErrorVsSetpoint(); break; diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 61b175d8..e5106d02 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -305,12 +305,15 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV return psdData; }; -GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { - return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); + +GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function(drawPSD = false) { + return drawPSD ? this._dataLoadPowerSpectralDensityVsX(FIELD_THROTTLE_NAME, 0, 100) : + this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); }; -GraphSpectrumCalc.dataLoadFrequencyVsRpm = function() { - const fftData = this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); +GraphSpectrumCalc.dataLoadFrequencyVsRpm = function(drawPSD = false) { + const fftData = drawPSD ? this._dataLoadPowerSpectralDensityVsX(FIELD_RPM_NAMES, 0) : + this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); fftData.vsRange.max *= 3.333 / this._motorPoles; fftData.vsRange.min *= 3.333 / this._motorPoles; return fftData; diff --git a/src/graph_spectrum_calc.js.bak b/src/graph_spectrum_calc.js.bak new file mode 100644 index 00000000..f29989aa --- /dev/null +++ b/src/graph_spectrum_calc.js.bak @@ -0,0 +1,631 @@ +import { FlightLogFieldPresenter } from "./flightlog_fields_presenter"; + +const + FIELD_THROTTLE_NAME = ['rcCommands[3]'], + FIELD_RPM_NAMES = [ + "eRPM[0]", + "eRPM[1]", + "eRPM[2]", + "eRPM[3]", + "eRPM[4]", + "eRPM[5]", + "eRPM[6]", + "eRPM[7]", + ], + FREQ_VS_THR_CHUNK_TIME_MS = 300, + FREQ_VS_THR_WINDOW_DIVISOR = 6, + MAX_ANALYSER_LENGTH = 300 * 1000 * 1000, // 5min + NUM_VS_BINS = 100, + WARNING_RATE_DIFFERENCE = 0.05, + MAX_RPM_HZ_VALUE = 800, + MAX_RPM_AXIS_GAP = 1.05; + + +export const GraphSpectrumCalc = { + _analyserTimeRange : { + in: 0, + out: MAX_ANALYSER_LENGTH, + }, + _blackBoxRate : 0, + _dataBuffer : { + fieldIndex: 0, + curve: 0, + fieldName: null, + }, + _flightLog : null, + _sysConfig : null, + _motorPoles : null, +}; + +GraphSpectrumCalc.initialize = function(flightLog, sysConfig) { + + this._flightLog = flightLog; + this._sysConfig = sysConfig; + + const gyroRate = (1000000 / this._sysConfig['looptime']).toFixed(0); + this._motorPoles = flightLog.getSysConfig()['motor_poles']; + this._blackBoxRate = gyroRate * this._sysConfig['frameIntervalPNum'] / this._sysConfig['frameIntervalPDenom']; + if (this._sysConfig.pid_process_denom != null) { + this._blackBoxRate = this._blackBoxRate / this._sysConfig.pid_process_denom; + } + this._BetaflightRate = this._blackBoxRate; + + const actualLoggedTime = this._flightLog.getActualLoggedTime(), + length = flightLog.getCurrentLogRowsCount(); + + this._actualeRate = 1e6 * length / actualLoggedTime; + if (Math.abs(this._BetaflightRate - this._actualeRate) / this._actualeRate > WARNING_RATE_DIFFERENCE) + this._blackBoxRate = Math.round(this._actualeRate); + + if (this._BetaflightRate !== this._blackBoxRate) { + $('.actual-lograte').text(this._actualeRate.toFixed(0) + "/" + this._BetaflightRate.toFixed(0)+"Hz"); + return { + actualRate: this._actualeRate, + betaflightRate: this._BetaflightRate, + }; + } else { + $('.actual-lograte').text(""); + } + + return undefined; +}; + +GraphSpectrumCalc.setInTime = function(time) { + this._analyserTimeRange.in = time; + return this._analyserTimeRange.in; +}; + +GraphSpectrumCalc.setOutTime = function(time) { + if ((time - this._analyserTimeRange.in) <= MAX_ANALYSER_LENGTH) { + this._analyserTimeRange.out = time; + } else { + this._analyserTimeRange.out = analyserTimeRange.in + MAX_ANALYSER_LENGTH; + } + return this._analyserTimeRange.out; +}; + +GraphSpectrumCalc.setDataBuffer = function(dataBuffer) { + this._dataBuffer = dataBuffer; + return undefined; +}; + +GraphSpectrumCalc.dataLoadFrequency = function() { + + const flightSamples = this._getFlightSamplesFreq(); + + if (userSettings.analyserHanning) { + this._hanningWindow(flightSamples.samples, flightSamples.count); + } + + //calculate fft + const fftOutput = this._fft(flightSamples.samples); + + // Normalize the result + const fftData = this._normalizeFft(fftOutput, flightSamples.samples.length); + + return fftData; +}; + +GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { + const flightSamples = this._getFlightSamplesFreq(false); + + let pointsPerSegment = 512; + const multipiler = Math.floor(1 / analyserZoomY); // 0. ... 10 + if (multipiler == 0) { + pointsPerSegment = 256; + } else if (multipiler > 1) { + pointsPerSegment *= 2 ** Math.floor(multipiler / 2); + } + pointsPerSegment = Math.min(pointsPerSegment, flightSamples.samples.length); + const overlapCount = Math.floor(pointsPerSegment / 2); + + const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount); + + const psdData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + psdLength : psd.psdOutput.length, + psdOutput : psd.psdOutput, + blackBoxRate : this._blackBoxRate, + minimum: psd.min, + maximum: psd.max, + maxNoiseIdx: psd.maxNoiseIdx, + }; + return psdData; +}; + +GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { + + const flightSamples = this._getFlightSamplesFreqVsX(vsFieldNames, minValue, maxValue); + + // 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. + const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; + const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); + + let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks + // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies + const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftChunkLength * 2)); + + const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. + + const fft = new FFT.complex(fftChunkLength, false); + for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { + + const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); + let fftOutput = new Float64Array(fftChunkLength * 2); + + // Hanning window applied to input data + if (userSettings.analyserHanning) { + this._hanningWindow(fftInput, fftChunkLength); + } + + fft.simple(fftOutput, fftInput, 'real'); + + fftOutput = fftOutput.slice(0, fftChunkLength); + + // Use only abs values + for (let i = 0; i < fftChunkLength; i++) { + fftOutput[i] = Math.abs(fftOutput[i]); + maxNoise = Math.max(fftOutput[i], maxNoise); + } + + // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately + for (const vsValueArray of flightSamples.vsValues) { + // Calculate average of the VS values in the chunk + let sumVsValues = 0; + for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { + sumVsValues += vsValueArray[indexVs]; + } + // Translate the average vs value to a bin index + const avgVsValue = sumVsValues / fftChunkLength; + let vsBinIndex = Math.floor(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); + // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access + if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } + numberSamples[vsBinIndex]++; + + // add the output from the fft to the row given by the vs value bin index + for (let i = 0; i < fftOutput.length; i++) { + matrixFftOutput[vsBinIndex][i] += fftOutput[i]; + } + } + } + + // Divide the values from the fft in each row (vs value bin) by the number of samples in the bin + for (let i = 0; i < NUM_VS_BINS; i++) { + if (numberSamples[i] > 1) { + for (let j = 0; j < matrixFftOutput[i].length; j++) { + matrixFftOutput[i][j] /= numberSamples[i]; + } + } + } + + // 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 + + const fftData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + fftLength : fftChunkLength, + fftOutput : matrixFftOutput, + maxNoise : maxNoise, + blackBoxRate : this._blackBoxRate, + vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, + }; + + return fftData; + +}; + +GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { + return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); +}; + +GraphSpectrumCalc.dataLoadFrequencyVsRpm = function() { + const fftData = this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); + fftData.vsRange.max *= 3.333 / this._motorPoles; + fftData.vsRange.min *= 3.333 / this._motorPoles; + return fftData; +}; + +GraphSpectrumCalc.dataLoadPidErrorVsSetpoint = function() { + + // Detect the axis + let axisIndex; + if (this._dataBuffer.fieldName.indexOf('[roll]') >= 0) { + axisIndex = 0; + } else if (this._dataBuffer.fieldName.indexOf('[pitch]') >= 0) { + axisIndex = 1; + } else if (this._dataBuffer.fieldName.indexOf('[yaw]') >= 0) { + axisIndex = 2; + } + + const flightSamples = this._getFlightSamplesPidErrorVsSetpoint(axisIndex); + + // Add the total error by absolute position + const errorBySetpoint = Array.from({length: flightSamples.maxSetpoint + 1}); + const numberOfSamplesBySetpoint = Array.from({length: flightSamples.maxSetpoint + 1}); + + // Initialize + for (let i = 0; i <= flightSamples.maxSetpoint; i++) { + errorBySetpoint[i] = 0; + numberOfSamplesBySetpoint[i] = 0; + } + + // Sum by position + for (let i = 0; i < flightSamples.count; i++) { + + const pidErrorValue = Math.abs(flightSamples.piderror[i]); + const setpointValue = Math.abs(flightSamples.setpoint[i]); + + errorBySetpoint[setpointValue] += pidErrorValue; + numberOfSamplesBySetpoint[setpointValue]++; + } + + // Calculate the media and max values + let maxErrorBySetpoint = 0; + for (let i = 0; i <= flightSamples.maxSetpoint; i++) { + if (numberOfSamplesBySetpoint[i] > 0) { + errorBySetpoint[i] = errorBySetpoint[i] / numberOfSamplesBySetpoint[i]; + if (errorBySetpoint[i] > maxErrorBySetpoint) { + maxErrorBySetpoint = errorBySetpoint[i]; + } + } else { + errorBySetpoint[i] = null; + } + } + + return { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + axisName : FlightLogFieldPresenter.fieldNameToFriendly(`axisError[${axisIndex}]`), + fftOutput : errorBySetpoint, + fftMaxOutput : maxErrorBySetpoint, + }; + +}; + +GraphSpectrumCalc._getFlightChunks = function() { + + let logStart = 0; + if (this._analyserTimeRange.in) { + logStart = this._analyserTimeRange.in; + } else { + logStart = this._flightLog.getMinTime(); + } + + let logEnd = 0; + if (this._analyserTimeRange.out) { + logEnd = this._analyserTimeRange.out; + } else { + logEnd = this._flightLog.getMaxTime(); + } + + // Limit size + logEnd = (logEnd - logStart <= MAX_ANALYSER_LENGTH)? logEnd : logStart + MAX_ANALYSER_LENGTH; + + const allChunks = this._flightLog.getChunksInTimeRange(logStart, logEnd); + + return allChunks; +}; + +GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { + + const allChunks = this._getFlightChunks(); + + const samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); + + // Loop through all the samples in the chunks and assign them to a sample array ready to pass to the FFT. + let samplesCount = 0; + for (const chunk of allChunks) { + for (const frame of chunk.frames) { + if (scaled) { + samples[samplesCount] = this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]); + } else { + samples[samplesCount] = frame[this._dataBuffer.fieldIndex]; + } + samplesCount++; + } + } + + return { + samples : samples.slice(0, samplesCount), + count : samplesCount, + }; +}; + +GraphSpectrumCalc._getVsIndexes = function(vsFieldNames) { + const fieldIndexes = []; + for (const fieldName of vsFieldNames) { + if (Object.hasOwn(this._flightLog.getMainFieldIndexes(), fieldName)) { + fieldIndexes.push(this._flightLog.getMainFieldIndexByName(fieldName)); + } + } + return fieldIndexes; +}; + +GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { + + const allChunks = this._getFlightChunks(); + const vsIndexes = this._getVsIndexes(vsFieldNames); + + const samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); + const vsValues = new Array(vsIndexes.length).fill(null).map(() => new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate)); + + let samplesCount = 0; + for (const chunk of allChunks) { + for (let frameIndex = 0; frameIndex < chunk.frames.length; frameIndex++) { + samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][this._dataBuffer.fieldIndex])); + for (let i = 0; i < vsIndexes.length; i++) { + let vsFieldIx = vsIndexes[i]; + let value = chunk.frames[frameIndex][vsFieldIx]; + if (vsFieldNames == FIELD_RPM_NAMES) { + const maxRPM = MAX_RPM_HZ_VALUE * this._motorPoles / 3.333; + if (value > maxRPM) { + value = maxRPM; + } + else if (value < 0) { + value = 0; + } + } + vsValues[i][samplesCount] = value; + } + samplesCount++; + } + } + + // Calculate min max average of the VS values in the chunk what will used by spectrum data definition + const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; + const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); + for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < samplesCount; fftChunkIndex += fftChunkWindow) { + for (const vsValueArray of vsValues) { + // Calculate average of the VS values in the chunk + let sumVsValues = 0; + for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { + sumVsValues += vsValueArray[indexVs]; + } + // Find min max average of the VS values in the chunk + const avgVsValue = sumVsValues / fftChunkLength; + maxValue = Math.max(maxValue, avgVsValue); + minValue = Math.min(minValue, avgVsValue); + } + } + + maxValue *= MAX_RPM_AXIS_GAP; + + if (minValue > maxValue) { + if (minValue == Infinity) { // this should never happen + minValue = 0; + maxValue = 100; + console.log("Invalid minimum value"); + } else { + console.log("Maximum value %f smaller than minimum value %d", maxValue, minValue); + minValue = 0; + maxValue = 100; + } + } + + let slicedVsValues = []; + for (const vsValueArray of vsValues) { + slicedVsValues.push(vsValueArray.slice(0, samplesCount)); + } + + return { + samples : samples.slice(0, samplesCount), + vsValues : slicedVsValues, + count : samplesCount, + minValue : minValue, + maxValue : maxValue, + }; +}; + +GraphSpectrumCalc._getFlightSamplesPidErrorVsSetpoint = function(axisIndex) { + + const allChunks = this._getFlightChunks(); + + // Get the PID Error field + const FIELD_PIDERROR_INDEX = this._flightLog.getMainFieldIndexByName(`axisError[${axisIndex}]`); + const FIELD_SETPOINT_INDEX = this._flightLog.getMainFieldIndexByName(`setpoint[${axisIndex}]`); + + const piderror = new Int16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); + const setpoint = new Int16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); + + // Loop through all the samples in the chunks and assign them to a sample array. + let samplesCount = 0; + let maxSetpoint = 0; + for (const chunk of allChunks) { + for (const frame of chunk.frames) { + piderror[samplesCount] = frame[FIELD_PIDERROR_INDEX]; + setpoint[samplesCount] = frame[FIELD_SETPOINT_INDEX]; + if (setpoint[samplesCount] > maxSetpoint) { + maxSetpoint = setpoint[samplesCount]; + } + samplesCount++; + } + } + + return { + piderror: piderror.slice(0, samplesCount), + setpoint: setpoint.slice(0, samplesCount), + maxSetpoint, + count: samplesCount, + }; +}; + +GraphSpectrumCalc._hanningWindow = function(samples, size) { + + if (!size) { + size = samples.length; + } + + for(let i=0; i < size; i++) { + samples[i] *= 0.5 * (1-Math.cos((2*Math.PI*i)/(size - 1))); + } +}; + +GraphSpectrumCalc._fft = function(samples, type) { + + if (!type) { + type = 'real'; + } + + const fftLength = samples.length; + const fftOutput = new Float64Array(fftLength * 2); + const fft = new FFT.complex(fftLength, false); + + fft.simple(fftOutput, samples, type); + + return fftOutput; +}; + + +/** + * Makes all the values absolute and returns the index of maxFrequency found + */ +GraphSpectrumCalc._normalizeFft = function(fftOutput, fftLength) { + + if (!fftLength) { + fftLength = fftOutput.length; + } + + // Make all the values absolute, and calculate some useful values (max noise, etc.) + const maxFrequency = (this._blackBoxRate / 2.0); + const noiseLowEndIdx = 100 / maxFrequency * fftLength; + let maxNoiseIdx = 0; + let maxNoise = 0; + + for (let i = 0; i < fftLength; i++) { + fftOutput[i] = Math.abs(fftOutput[i]); + if (i > noiseLowEndIdx && fftOutput[i] > maxNoise) { + maxNoise = fftOutput[i]; + maxNoiseIdx = i; + } + } + + maxNoiseIdx = maxNoiseIdx / fftLength * maxFrequency; + + const fftData = { + fieldIndex : this._dataBuffer.fieldIndex, + fieldName : this._dataBuffer.fieldName, + fftLength : fftLength, + fftOutput : fftOutput, + maxNoiseIdx : maxNoiseIdx, + blackBoxRate : this._blackBoxRate, + }; + + return fftData; +}; + +/** + * Compute PSD for data samples by Welch method follow Python code + */ +GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scaling = 'density') { +// Compute FFT for samples segments + const fftOutput = this._fft_segmented(samples, pointsPerSegment, overlapCount); + + const dataCount = fftOutput[0].length; + const segmentsCount = fftOutput.length; + const psdOutput = new Float64Array(dataCount); + +// Compute power scale coef + let scale = 1; + if (userSettings.analyserHanning) { + const window = Array(pointsPerSegment).fill(1); + this._hanningWindow(window, pointsPerSegment); + if (scaling == 'density') { + let skSum = 0; + for (const value of window) { + skSum += value ** 2; + } + scale = 1 / (this._blackBoxRate * skSum); + } else if (scaling == 'spectrum') { + let sum = 0; + for (const value of window) { + sum += value; + } + scale = 1 / sum ** 2; + } + } else if (scaling == 'density') { + scale = 1 / pointsPerSegment; + } else if (scaling == 'spectrum') { + scale = 1 / pointsPerSegment ** 2; + } + +// Compute average for scaled power + let min = 1e6, + max = -1e6; + // Early exit if no segments were processed + if (segmentsCount === 0) { + return { + psdOutput: new Float64Array(0), + min: 0, + max: 0, + maxNoiseIdx: 0, + }; + } + const maxFrequency = (this._blackBoxRate / 2.0); + const noise50HzIdx = 50 / maxFrequency * dataCount; + const noise3HzIdx = 3 / maxFrequency * dataCount; + let maxNoiseIdx = 0; + let maxNoise = -100; + for (let i = 0; i < dataCount; i++) { + psdOutput[i] = 0.0; + for (let j = 0; j < segmentsCount; j++) { + let p = scale * fftOutput[j][i] ** 2; + if (i != dataCount - 1) { + p *= 2; + } + psdOutput[i] += p; + } + + const min_avg = 1e-7; // limit min value for -70db + let avg = psdOutput[i] / segmentsCount; + avg = Math.max(avg, min_avg); + psdOutput[i] = 10 * Math.log10(avg); + if (i > noise3HzIdx) { // Miss big zero freq magnitude + min = Math.min(psdOutput[i], min); + max = Math.max(psdOutput[i], max); + } + if (i > noise50HzIdx && psdOutput[i] > maxNoise) { + maxNoise = psdOutput[i]; + maxNoiseIdx = i; + } + } + + const maxNoiseFrequency = maxNoiseIdx / dataCount * maxFrequency; + + return { + psdOutput: psdOutput, + min: min, + max: max, + maxNoiseIdx: maxNoiseFrequency, + }; +}; + + +/** + * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count + */ +GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { + const samplesCount = samples.length; + let output = []; + for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { + const fftInput = samples.slice(i, i + pointsPerSegment); + + if (userSettings.analyserHanning) { + this._hanningWindow(fftInput, pointsPerSegment); + } + + const fftComplex = this._fft(fftInput); + const magnitudes = new Float64Array(pointsPerSegment / 2); + for (let i = 0; i < pointsPerSegment / 2; i++) { + const re = fftComplex[2 * i]; + const im = fftComplex[2 * i + 1]; + magnitudes[i] = Math.hypot(re, im); + } + output.push(magnitudes); + } + + return output; +}; diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index d341d17d..c6b14ca1 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -18,6 +18,8 @@ export const SPECTRUM_TYPE = { PIDERROR_VS_SETPOINT: 2, FREQ_VS_RPM: 3, POWER_SPECTRAL_DENSITY: 4, + PSD_VS_THROTTLE: 5, + PSD_VS_RPM: 6, }; export const SPECTRUM_OVERDRAW_TYPE = { @@ -169,6 +171,14 @@ GraphSpectrumPlot._drawGraph = function (canvasCtx) { this._drawFrequencyVsXGraph(canvasCtx); break; + case SPECTRUM_TYPE.PSD_VS_THROTTLE: + this._drawFrequencyVsXGraph(canvasCtx, true); + break; + + case SPECTRUM_TYPE.PSD_VS_RPM: + this._drawFrequencyVsXGraph(canvasCtx, true); + break; + case SPECTRUM_TYPE.PIDERROR_VS_SETPOINT: this._drawPidErrorVsSetpointGraph(canvasCtx); break; @@ -441,8 +451,9 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) MARGIN_BOTTOM, "Hz" ); - - if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE) { + + if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE) { this._drawVerticalGridLines( canvasCtx, LEFT, @@ -453,7 +464,8 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) this._fftData.vsRange.max, "%" ); - } else if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM) { + } else if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM) { this._drawVerticalGridLines( canvasCtx, LEFT, @@ -486,13 +498,14 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { for (let j = 0; j < THROTTLE_VALUES_SIZE; j++) { // Loop for frequency for (let i = 0; i < this._fftData.fftLength; i++) { + let valuePlot; if (drawPSD) { const min = -40, max = 10; //limit values dBm - let valuePlot = Math.max(this._fftData.fftOutput[j][i], min); + valuePlot = Math.max(this._fftData.fftOutput[j][i], min); valuePlot = Math.min(this._fftData.fftOutput[j][i], max); valuePlot = Math.round((valuePlot - min) * 100 / (max - min)); } else { - const valuePlot = Math.round( + valuePlot = Math.round( Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100) ); } @@ -1455,7 +1468,9 @@ GraphSpectrumPlot._drawMousePosition = function ( this._spectrumType === SPECTRUM_TYPE.FREQUENCY || this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM || - this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY + this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM ) { // Calculate frequency at mouse const sampleRate = this._fftData.blackBoxRate / this._zoomX; @@ -1482,9 +1497,11 @@ GraphSpectrumPlot._drawMousePosition = function ( let unitLabel; switch (this._spectrumType) { case SPECTRUM_TYPE.FREQ_VS_THROTTLE: + case SPECTRUM_TYPE.PSD_VS_THROTTLE: unitLabel = "%"; break; case SPECTRUM_TYPE.FREQ_VS_RPM: + case SPECTRUM_TYPE.PSD_VS_RPM: unitLabel = "Hz"; break; case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: @@ -1565,6 +1582,8 @@ GraphSpectrumPlot._getActualMarginLeft = function () { switch (this._spectrumType) { case SPECTRUM_TYPE.FREQ_VS_THROTTLE: case SPECTRUM_TYPE.FREQ_VS_RPM: + case SPECTRUM_TYPE.PSD_VS_THROTTLE: + case SPECTRUM_TYPE.PSD_VS_RPM: case SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY: actualMarginLeft = this._isFullScreen ? MARGIN_LEFT_FULLSCREEN From 2ae153f99bbf5eba0f7758d8b1e789af73d49f65 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 22:04:00 +0300 Subject: [PATCH 07/63] Resolved issue of limit drawing psd value --- src/graph_spectrum_calc.js.bak | 631 --------------------------------- src/graph_spectrum_plot.js | 2 +- 2 files changed, 1 insertion(+), 632 deletions(-) delete mode 100644 src/graph_spectrum_calc.js.bak diff --git a/src/graph_spectrum_calc.js.bak b/src/graph_spectrum_calc.js.bak deleted file mode 100644 index f29989aa..00000000 --- a/src/graph_spectrum_calc.js.bak +++ /dev/null @@ -1,631 +0,0 @@ -import { FlightLogFieldPresenter } from "./flightlog_fields_presenter"; - -const - FIELD_THROTTLE_NAME = ['rcCommands[3]'], - FIELD_RPM_NAMES = [ - "eRPM[0]", - "eRPM[1]", - "eRPM[2]", - "eRPM[3]", - "eRPM[4]", - "eRPM[5]", - "eRPM[6]", - "eRPM[7]", - ], - FREQ_VS_THR_CHUNK_TIME_MS = 300, - FREQ_VS_THR_WINDOW_DIVISOR = 6, - MAX_ANALYSER_LENGTH = 300 * 1000 * 1000, // 5min - NUM_VS_BINS = 100, - WARNING_RATE_DIFFERENCE = 0.05, - MAX_RPM_HZ_VALUE = 800, - MAX_RPM_AXIS_GAP = 1.05; - - -export const GraphSpectrumCalc = { - _analyserTimeRange : { - in: 0, - out: MAX_ANALYSER_LENGTH, - }, - _blackBoxRate : 0, - _dataBuffer : { - fieldIndex: 0, - curve: 0, - fieldName: null, - }, - _flightLog : null, - _sysConfig : null, - _motorPoles : null, -}; - -GraphSpectrumCalc.initialize = function(flightLog, sysConfig) { - - this._flightLog = flightLog; - this._sysConfig = sysConfig; - - const gyroRate = (1000000 / this._sysConfig['looptime']).toFixed(0); - this._motorPoles = flightLog.getSysConfig()['motor_poles']; - this._blackBoxRate = gyroRate * this._sysConfig['frameIntervalPNum'] / this._sysConfig['frameIntervalPDenom']; - if (this._sysConfig.pid_process_denom != null) { - this._blackBoxRate = this._blackBoxRate / this._sysConfig.pid_process_denom; - } - this._BetaflightRate = this._blackBoxRate; - - const actualLoggedTime = this._flightLog.getActualLoggedTime(), - length = flightLog.getCurrentLogRowsCount(); - - this._actualeRate = 1e6 * length / actualLoggedTime; - if (Math.abs(this._BetaflightRate - this._actualeRate) / this._actualeRate > WARNING_RATE_DIFFERENCE) - this._blackBoxRate = Math.round(this._actualeRate); - - if (this._BetaflightRate !== this._blackBoxRate) { - $('.actual-lograte').text(this._actualeRate.toFixed(0) + "/" + this._BetaflightRate.toFixed(0)+"Hz"); - return { - actualRate: this._actualeRate, - betaflightRate: this._BetaflightRate, - }; - } else { - $('.actual-lograte').text(""); - } - - return undefined; -}; - -GraphSpectrumCalc.setInTime = function(time) { - this._analyserTimeRange.in = time; - return this._analyserTimeRange.in; -}; - -GraphSpectrumCalc.setOutTime = function(time) { - if ((time - this._analyserTimeRange.in) <= MAX_ANALYSER_LENGTH) { - this._analyserTimeRange.out = time; - } else { - this._analyserTimeRange.out = analyserTimeRange.in + MAX_ANALYSER_LENGTH; - } - return this._analyserTimeRange.out; -}; - -GraphSpectrumCalc.setDataBuffer = function(dataBuffer) { - this._dataBuffer = dataBuffer; - return undefined; -}; - -GraphSpectrumCalc.dataLoadFrequency = function() { - - const flightSamples = this._getFlightSamplesFreq(); - - if (userSettings.analyserHanning) { - this._hanningWindow(flightSamples.samples, flightSamples.count); - } - - //calculate fft - const fftOutput = this._fft(flightSamples.samples); - - // Normalize the result - const fftData = this._normalizeFft(fftOutput, flightSamples.samples.length); - - return fftData; -}; - -GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { - const flightSamples = this._getFlightSamplesFreq(false); - - let pointsPerSegment = 512; - const multipiler = Math.floor(1 / analyserZoomY); // 0. ... 10 - if (multipiler == 0) { - pointsPerSegment = 256; - } else if (multipiler > 1) { - pointsPerSegment *= 2 ** Math.floor(multipiler / 2); - } - pointsPerSegment = Math.min(pointsPerSegment, flightSamples.samples.length); - const overlapCount = Math.floor(pointsPerSegment / 2); - - const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount); - - const psdData = { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - psdLength : psd.psdOutput.length, - psdOutput : psd.psdOutput, - blackBoxRate : this._blackBoxRate, - minimum: psd.min, - maximum: psd.max, - maxNoiseIdx: psd.maxNoiseIdx, - }; - return psdData; -}; - -GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { - - const flightSamples = this._getFlightSamplesFreqVsX(vsFieldNames, minValue, maxValue); - - // 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. - const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; - const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); - - let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks - // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies - const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftChunkLength * 2)); - - const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. - - const fft = new FFT.complex(fftChunkLength, false); - for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { - - const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); - let fftOutput = new Float64Array(fftChunkLength * 2); - - // Hanning window applied to input data - if (userSettings.analyserHanning) { - this._hanningWindow(fftInput, fftChunkLength); - } - - fft.simple(fftOutput, fftInput, 'real'); - - fftOutput = fftOutput.slice(0, fftChunkLength); - - // Use only abs values - for (let i = 0; i < fftChunkLength; i++) { - fftOutput[i] = Math.abs(fftOutput[i]); - maxNoise = Math.max(fftOutput[i], maxNoise); - } - - // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately - for (const vsValueArray of flightSamples.vsValues) { - // Calculate average of the VS values in the chunk - let sumVsValues = 0; - for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { - sumVsValues += vsValueArray[indexVs]; - } - // Translate the average vs value to a bin index - const avgVsValue = sumVsValues / fftChunkLength; - let vsBinIndex = Math.floor(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); - // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access - if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } - numberSamples[vsBinIndex]++; - - // add the output from the fft to the row given by the vs value bin index - for (let i = 0; i < fftOutput.length; i++) { - matrixFftOutput[vsBinIndex][i] += fftOutput[i]; - } - } - } - - // Divide the values from the fft in each row (vs value bin) by the number of samples in the bin - for (let i = 0; i < NUM_VS_BINS; i++) { - if (numberSamples[i] > 1) { - for (let j = 0; j < matrixFftOutput[i].length; j++) { - matrixFftOutput[i][j] /= numberSamples[i]; - } - } - } - - // 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 - - const fftData = { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - fftLength : fftChunkLength, - fftOutput : matrixFftOutput, - maxNoise : maxNoise, - blackBoxRate : this._blackBoxRate, - vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, - }; - - return fftData; - -}; - -GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { - return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); -}; - -GraphSpectrumCalc.dataLoadFrequencyVsRpm = function() { - const fftData = this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); - fftData.vsRange.max *= 3.333 / this._motorPoles; - fftData.vsRange.min *= 3.333 / this._motorPoles; - return fftData; -}; - -GraphSpectrumCalc.dataLoadPidErrorVsSetpoint = function() { - - // Detect the axis - let axisIndex; - if (this._dataBuffer.fieldName.indexOf('[roll]') >= 0) { - axisIndex = 0; - } else if (this._dataBuffer.fieldName.indexOf('[pitch]') >= 0) { - axisIndex = 1; - } else if (this._dataBuffer.fieldName.indexOf('[yaw]') >= 0) { - axisIndex = 2; - } - - const flightSamples = this._getFlightSamplesPidErrorVsSetpoint(axisIndex); - - // Add the total error by absolute position - const errorBySetpoint = Array.from({length: flightSamples.maxSetpoint + 1}); - const numberOfSamplesBySetpoint = Array.from({length: flightSamples.maxSetpoint + 1}); - - // Initialize - for (let i = 0; i <= flightSamples.maxSetpoint; i++) { - errorBySetpoint[i] = 0; - numberOfSamplesBySetpoint[i] = 0; - } - - // Sum by position - for (let i = 0; i < flightSamples.count; i++) { - - const pidErrorValue = Math.abs(flightSamples.piderror[i]); - const setpointValue = Math.abs(flightSamples.setpoint[i]); - - errorBySetpoint[setpointValue] += pidErrorValue; - numberOfSamplesBySetpoint[setpointValue]++; - } - - // Calculate the media and max values - let maxErrorBySetpoint = 0; - for (let i = 0; i <= flightSamples.maxSetpoint; i++) { - if (numberOfSamplesBySetpoint[i] > 0) { - errorBySetpoint[i] = errorBySetpoint[i] / numberOfSamplesBySetpoint[i]; - if (errorBySetpoint[i] > maxErrorBySetpoint) { - maxErrorBySetpoint = errorBySetpoint[i]; - } - } else { - errorBySetpoint[i] = null; - } - } - - return { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - axisName : FlightLogFieldPresenter.fieldNameToFriendly(`axisError[${axisIndex}]`), - fftOutput : errorBySetpoint, - fftMaxOutput : maxErrorBySetpoint, - }; - -}; - -GraphSpectrumCalc._getFlightChunks = function() { - - let logStart = 0; - if (this._analyserTimeRange.in) { - logStart = this._analyserTimeRange.in; - } else { - logStart = this._flightLog.getMinTime(); - } - - let logEnd = 0; - if (this._analyserTimeRange.out) { - logEnd = this._analyserTimeRange.out; - } else { - logEnd = this._flightLog.getMaxTime(); - } - - // Limit size - logEnd = (logEnd - logStart <= MAX_ANALYSER_LENGTH)? logEnd : logStart + MAX_ANALYSER_LENGTH; - - const allChunks = this._flightLog.getChunksInTimeRange(logStart, logEnd); - - return allChunks; -}; - -GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { - - const allChunks = this._getFlightChunks(); - - const samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); - - // Loop through all the samples in the chunks and assign them to a sample array ready to pass to the FFT. - let samplesCount = 0; - for (const chunk of allChunks) { - for (const frame of chunk.frames) { - if (scaled) { - samples[samplesCount] = this._dataBuffer.curve.lookupRaw(frame[this._dataBuffer.fieldIndex]); - } else { - samples[samplesCount] = frame[this._dataBuffer.fieldIndex]; - } - samplesCount++; - } - } - - return { - samples : samples.slice(0, samplesCount), - count : samplesCount, - }; -}; - -GraphSpectrumCalc._getVsIndexes = function(vsFieldNames) { - const fieldIndexes = []; - for (const fieldName of vsFieldNames) { - if (Object.hasOwn(this._flightLog.getMainFieldIndexes(), fieldName)) { - fieldIndexes.push(this._flightLog.getMainFieldIndexByName(fieldName)); - } - } - return fieldIndexes; -}; - -GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { - - const allChunks = this._getFlightChunks(); - const vsIndexes = this._getVsIndexes(vsFieldNames); - - const samples = new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); - const vsValues = new Array(vsIndexes.length).fill(null).map(() => new Float64Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate)); - - let samplesCount = 0; - for (const chunk of allChunks) { - for (let frameIndex = 0; frameIndex < chunk.frames.length; frameIndex++) { - samples[samplesCount] = (this._dataBuffer.curve.lookupRaw(chunk.frames[frameIndex][this._dataBuffer.fieldIndex])); - for (let i = 0; i < vsIndexes.length; i++) { - let vsFieldIx = vsIndexes[i]; - let value = chunk.frames[frameIndex][vsFieldIx]; - if (vsFieldNames == FIELD_RPM_NAMES) { - const maxRPM = MAX_RPM_HZ_VALUE * this._motorPoles / 3.333; - if (value > maxRPM) { - value = maxRPM; - } - else if (value < 0) { - value = 0; - } - } - vsValues[i][samplesCount] = value; - } - samplesCount++; - } - } - - // Calculate min max average of the VS values in the chunk what will used by spectrum data definition - const fftChunkLength = this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000; - const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); - for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < samplesCount; fftChunkIndex += fftChunkWindow) { - for (const vsValueArray of vsValues) { - // Calculate average of the VS values in the chunk - let sumVsValues = 0; - for (let indexVs = fftChunkIndex; indexVs < fftChunkIndex + fftChunkLength; indexVs++) { - sumVsValues += vsValueArray[indexVs]; - } - // Find min max average of the VS values in the chunk - const avgVsValue = sumVsValues / fftChunkLength; - maxValue = Math.max(maxValue, avgVsValue); - minValue = Math.min(minValue, avgVsValue); - } - } - - maxValue *= MAX_RPM_AXIS_GAP; - - if (minValue > maxValue) { - if (minValue == Infinity) { // this should never happen - minValue = 0; - maxValue = 100; - console.log("Invalid minimum value"); - } else { - console.log("Maximum value %f smaller than minimum value %d", maxValue, minValue); - minValue = 0; - maxValue = 100; - } - } - - let slicedVsValues = []; - for (const vsValueArray of vsValues) { - slicedVsValues.push(vsValueArray.slice(0, samplesCount)); - } - - return { - samples : samples.slice(0, samplesCount), - vsValues : slicedVsValues, - count : samplesCount, - minValue : minValue, - maxValue : maxValue, - }; -}; - -GraphSpectrumCalc._getFlightSamplesPidErrorVsSetpoint = function(axisIndex) { - - const allChunks = this._getFlightChunks(); - - // Get the PID Error field - const FIELD_PIDERROR_INDEX = this._flightLog.getMainFieldIndexByName(`axisError[${axisIndex}]`); - const FIELD_SETPOINT_INDEX = this._flightLog.getMainFieldIndexByName(`setpoint[${axisIndex}]`); - - const piderror = new Int16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); - const setpoint = new Int16Array(MAX_ANALYSER_LENGTH / (1000 * 1000) * this._blackBoxRate); - - // Loop through all the samples in the chunks and assign them to a sample array. - let samplesCount = 0; - let maxSetpoint = 0; - for (const chunk of allChunks) { - for (const frame of chunk.frames) { - piderror[samplesCount] = frame[FIELD_PIDERROR_INDEX]; - setpoint[samplesCount] = frame[FIELD_SETPOINT_INDEX]; - if (setpoint[samplesCount] > maxSetpoint) { - maxSetpoint = setpoint[samplesCount]; - } - samplesCount++; - } - } - - return { - piderror: piderror.slice(0, samplesCount), - setpoint: setpoint.slice(0, samplesCount), - maxSetpoint, - count: samplesCount, - }; -}; - -GraphSpectrumCalc._hanningWindow = function(samples, size) { - - if (!size) { - size = samples.length; - } - - for(let i=0; i < size; i++) { - samples[i] *= 0.5 * (1-Math.cos((2*Math.PI*i)/(size - 1))); - } -}; - -GraphSpectrumCalc._fft = function(samples, type) { - - if (!type) { - type = 'real'; - } - - const fftLength = samples.length; - const fftOutput = new Float64Array(fftLength * 2); - const fft = new FFT.complex(fftLength, false); - - fft.simple(fftOutput, samples, type); - - return fftOutput; -}; - - -/** - * Makes all the values absolute and returns the index of maxFrequency found - */ -GraphSpectrumCalc._normalizeFft = function(fftOutput, fftLength) { - - if (!fftLength) { - fftLength = fftOutput.length; - } - - // Make all the values absolute, and calculate some useful values (max noise, etc.) - const maxFrequency = (this._blackBoxRate / 2.0); - const noiseLowEndIdx = 100 / maxFrequency * fftLength; - let maxNoiseIdx = 0; - let maxNoise = 0; - - for (let i = 0; i < fftLength; i++) { - fftOutput[i] = Math.abs(fftOutput[i]); - if (i > noiseLowEndIdx && fftOutput[i] > maxNoise) { - maxNoise = fftOutput[i]; - maxNoiseIdx = i; - } - } - - maxNoiseIdx = maxNoiseIdx / fftLength * maxFrequency; - - const fftData = { - fieldIndex : this._dataBuffer.fieldIndex, - fieldName : this._dataBuffer.fieldName, - fftLength : fftLength, - fftOutput : fftOutput, - maxNoiseIdx : maxNoiseIdx, - blackBoxRate : this._blackBoxRate, - }; - - return fftData; -}; - -/** - * Compute PSD for data samples by Welch method follow Python code - */ -GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scaling = 'density') { -// Compute FFT for samples segments - const fftOutput = this._fft_segmented(samples, pointsPerSegment, overlapCount); - - const dataCount = fftOutput[0].length; - const segmentsCount = fftOutput.length; - const psdOutput = new Float64Array(dataCount); - -// Compute power scale coef - let scale = 1; - if (userSettings.analyserHanning) { - const window = Array(pointsPerSegment).fill(1); - this._hanningWindow(window, pointsPerSegment); - if (scaling == 'density') { - let skSum = 0; - for (const value of window) { - skSum += value ** 2; - } - scale = 1 / (this._blackBoxRate * skSum); - } else if (scaling == 'spectrum') { - let sum = 0; - for (const value of window) { - sum += value; - } - scale = 1 / sum ** 2; - } - } else if (scaling == 'density') { - scale = 1 / pointsPerSegment; - } else if (scaling == 'spectrum') { - scale = 1 / pointsPerSegment ** 2; - } - -// Compute average for scaled power - let min = 1e6, - max = -1e6; - // Early exit if no segments were processed - if (segmentsCount === 0) { - return { - psdOutput: new Float64Array(0), - min: 0, - max: 0, - maxNoiseIdx: 0, - }; - } - const maxFrequency = (this._blackBoxRate / 2.0); - const noise50HzIdx = 50 / maxFrequency * dataCount; - const noise3HzIdx = 3 / maxFrequency * dataCount; - let maxNoiseIdx = 0; - let maxNoise = -100; - for (let i = 0; i < dataCount; i++) { - psdOutput[i] = 0.0; - for (let j = 0; j < segmentsCount; j++) { - let p = scale * fftOutput[j][i] ** 2; - if (i != dataCount - 1) { - p *= 2; - } - psdOutput[i] += p; - } - - const min_avg = 1e-7; // limit min value for -70db - let avg = psdOutput[i] / segmentsCount; - avg = Math.max(avg, min_avg); - psdOutput[i] = 10 * Math.log10(avg); - if (i > noise3HzIdx) { // Miss big zero freq magnitude - min = Math.min(psdOutput[i], min); - max = Math.max(psdOutput[i], max); - } - if (i > noise50HzIdx && psdOutput[i] > maxNoise) { - maxNoise = psdOutput[i]; - maxNoiseIdx = i; - } - } - - const maxNoiseFrequency = maxNoiseIdx / dataCount * maxFrequency; - - return { - psdOutput: psdOutput, - min: min, - max: max, - maxNoiseIdx: maxNoiseFrequency, - }; -}; - - -/** - * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count - */ -GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { - const samplesCount = samples.length; - let output = []; - for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { - const fftInput = samples.slice(i, i + pointsPerSegment); - - if (userSettings.analyserHanning) { - this._hanningWindow(fftInput, pointsPerSegment); - } - - const fftComplex = this._fft(fftInput); - const magnitudes = new Float64Array(pointsPerSegment / 2); - for (let i = 0; i < pointsPerSegment / 2; i++) { - const re = fftComplex[2 * i]; - const im = fftComplex[2 * i + 1]; - magnitudes[i] = Math.hypot(re, im); - } - output.push(magnitudes); - } - - return output; -}; diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index c6b14ca1..ce558108 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -502,7 +502,7 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { if (drawPSD) { const min = -40, max = 10; //limit values dBm valuePlot = Math.max(this._fftData.fftOutput[j][i], min); - valuePlot = Math.min(this._fftData.fftOutput[j][i], max); + valuePlot = Math.min(valuePlot, max); valuePlot = Math.round((valuePlot - min) * 100 / (max - min)); } else { valuePlot = Math.round( From 654236418941d440090041c3bf05f735e1ca2296 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 22:17:47 +0300 Subject: [PATCH 08/63] Code style improvement --- src/graph_spectrum_calc.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index e5106d02..17c4dc63 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -112,11 +112,11 @@ GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { const flightSamples = this._getFlightSamplesFreq(false); let pointsPerSegment = 512; - const multipiler = Math.floor(1 / analyserZoomY); // 0. ... 10 - if (multipiler == 0) { + const multiplier = Math.floor(1 / analyserZoomY); // 0. ... 10 + if (multiplier == 0) { pointsPerSegment = 256; - } else if (multipiler > 1) { - pointsPerSegment *= 2 ** Math.floor(multipiler / 2); + } else if (multiplier > 1) { + pointsPerSegment *= 2 ** Math.floor(multiplier / 2); } // Use power 2 fft size what is not bigger flightSamples.samples.length From 19baa2f55b8f38725d89ace43d5058fcde7a9a13 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 22:30:13 +0300 Subject: [PATCH 09/63] Code refactoring in src/graph_spectrum_calc.js --- src/graph_spectrum_calc.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 17c4dc63..14cd0696 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -248,7 +248,9 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks let psdLength = 0; // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies - const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => (new Float64Array(fftChunkLength * 2)).fill(-70)); + const matrixFftOutput = new Array(NUM_VS_BINS) + .fill(null) + .map(() => (new Float64Array(Math.floor(fftChunkLength / 2))).fill(-70)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. From 4d77b48ac5e0c8a09a26d64ec3be9b1a6c549677 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 10 May 2025 22:39:00 +0300 Subject: [PATCH 10/63] Resolved missing this. issue --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 14cd0696..cb17c044 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -79,7 +79,7 @@ GraphSpectrumCalc.setOutTime = function(time) { if ((time - this._analyserTimeRange.in) <= MAX_ANALYSER_LENGTH) { this._analyserTimeRange.out = time; } else { - this._analyserTimeRange.out = analyserTimeRange.in + MAX_ANALYSER_LENGTH; + this._analyserTimeRange.out = this._analyserTimeRange.in + MAX_ANALYSER_LENGTH; } return this._analyserTimeRange.out; }; From 3a82b42d3e2cf79b853be679b874e932127d9002 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sun, 11 May 2025 14:14:49 +0300 Subject: [PATCH 11/63] Added PSD values label at mouse position cursor by Shift key --- src/graph_spectrum_calc.js | 1 - src/graph_spectrum_plot.js | 36 ++++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index cb17c044..868ffa23 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -704,7 +704,6 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal }; }; - /** * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count */ diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index ce558108..73a34144 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -451,7 +451,7 @@ GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) MARGIN_BOTTOM, "Hz" ); - + if (this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE) { this._drawVerticalGridLines( @@ -524,6 +524,17 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { return heatMapCanvas; }; +GraphSpectrumPlot.getValueFromMatrixFFT = function(frequency, vsArgument) { + const NUM_VS_BINS = 100; // redefinition of value from graph_spectrum_calc.js module! + const matrixFFT = this._fftData; + let vsArgumentIndex = Math.round(NUM_VS_BINS * (vsArgument - matrixFFT.vsRange.min) / (matrixFFT.vsRange.max - matrixFFT.vsRange.min)); + if (vsArgumentIndex === NUM_VS_BINS) { + vsArgumentIndex = NUM_VS_BINS - 1; + } + const freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftOutput[0].length - 1) ); + return matrixFFT.fftOutput[vsArgumentIndex][freqIndex]; +}; + GraphSpectrumPlot._drawPidErrorVsSetpointGraph = function (canvasCtx) { const ACTUAL_MARGIN_LEFT = this._getActualMarginLeft(); @@ -1464,6 +1475,7 @@ GraphSpectrumPlot._drawMousePosition = function ( lineWidth ) { // X axis + let mouseFrequency = 0; if ( this._spectrumType === SPECTRUM_TYPE.FREQUENCY || this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || @@ -1476,7 +1488,7 @@ GraphSpectrumPlot._drawMousePosition = function ( const sampleRate = this._fftData.blackBoxRate / this._zoomX; const marginLeft = this._getActualMarginLeft(); - const mouseFrequency = + mouseFrequency = ((mouseX - marginLeft) / WIDTH) * (this._fftData.blackBoxRate / this._zoomX / 2); if (mouseFrequency >= 0 && mouseFrequency <= sampleRate) { @@ -1514,12 +1526,12 @@ GraphSpectrumPlot._drawMousePosition = function ( if (unitLabel !== null) { const val_min = this._fftData.vsRange.min; const val_max = this._fftData.vsRange.max; - const mouseValue = (1 - mouseY / HEIGHT) * (val_max - val_min) + val_min; - if (mouseValue >= val_min && mouseValue <= val_max) { - const valueLabel = `${mouseValue.toFixed(0)}${unitLabel}`; + const vsArgValue = (1 - mouseY / HEIGHT) * (val_max - val_min) + val_min; + if (vsArgValue >= val_min && vsArgValue <= val_max) { + const valueLabel = `${vsArgValue.toFixed(0)}${unitLabel}`; this._drawHorizontalMarkerLine( canvasCtx, - mouseValue, + vsArgValue, val_min, val_max, valueLabel, @@ -1529,6 +1541,18 @@ GraphSpectrumPlot._drawMousePosition = function ( stroke, lineWidth ); + + if (this._spectrumType == SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType == SPECTRUM_TYPE.PSD_VS_RPM) { + const label = Math.round(this.getValueFromMatrixFFT(mouseFrequency, vsArgValue)).toString() + "dBm/Hz"; + this._drawAxisLabel( + canvasCtx, + label, + mouseX - 25, + mouseY - 4, + "left", + ); + } } } } else if (this._spectrumType === SPECTRUM_TYPE.PIDERROR_VS_SETPOINT) { From 28c010730877e2621738cb3e6aaca8871e82adc7 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sun, 11 May 2025 18:52:52 +0300 Subject: [PATCH 12/63] Added PSD values label on the mouse position cursor by Shift key at the power specral density chart --- src/graph_spectrum_plot.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 73a34144..9f26f20a 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -405,6 +405,11 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { canvasCtx.restore(); }; +GraphSpectrumPlot.getPSDbyFreq = function(frequency) { + const freqIndex = Math.round(2 * frequency / this._fftData.blackBoxRate * (this._fftData.psdOutput.length - 1) ); + return this._fftData.psdOutput[freqIndex]; +}; + GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) { const PLOTTED_BLACKBOX_RATE = this._fftData.blackBoxRate / this._zoomX; @@ -1505,6 +1510,17 @@ GraphSpectrumPlot._drawMousePosition = function ( ); } + if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) { + const psdLabel = Math.round(this.getPSDbyFreq(mouseFrequency)).toString() + "dBm/Hz"; + this._drawAxisLabel( + canvasCtx, + psdLabel, + mouseX - 30, + mouseY - 4, + "left", + ); + } + // Y axis let unitLabel; switch (this._spectrumType) { @@ -1548,7 +1564,7 @@ GraphSpectrumPlot._drawMousePosition = function ( this._drawAxisLabel( canvasCtx, label, - mouseX - 25, + mouseX - 30, mouseY - 4, "left", ); From 54e75aa37d8195d73abc1939f6de8c177d78f1c7 Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 12:11:00 +0300 Subject: [PATCH 13/63] Code refactoring --- src/graph_spectrum_calc.js | 38 +++++++++++++++++++------------------- src/graph_spectrum_plot.js | 6 +++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 868ffa23..ca4a33ec 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -136,7 +136,7 @@ GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { blackBoxRate : this._blackBoxRate, minimum: psd.min, maximum: psd.max, - maxNoiseIdx: psd.maxNoiseIdx, + maxNoiseFrequency: psd.maxNoiseFrequency, }; return psdData; }; @@ -246,11 +246,11 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks - let psdLength = 0; + const psdLength = Math.floor(fftChunkLength / 2); // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies - const matrixFftOutput = new Array(NUM_VS_BINS) + const matrixPsdOutput = new Array(NUM_VS_BINS) .fill(null) - .map(() => (new Float64Array(Math.floor(fftChunkLength / 2))).fill(-70)); + .map(() => (new Float64Array(psdLength)).fill(-70)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. @@ -258,7 +258,6 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); const psd = this._psd(fftInput, fftChunkLength, 0, 'density'); - psdLength = psd.psdOutput.length; maxNoise = Math.max(psd.max, maxNoise); // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately for (const vsValueArray of flightSamples.vsValues) { @@ -275,8 +274,8 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV numberSamples[vsBinIndex]++; // add the output from the fft to the row given by the vs value bin index - for (let i = 0; i < psd.psdOutput.length; i++) { - matrixFftOutput[vsBinIndex][i] += psd.psdOutput[i]; + for (let i = 0; i < psdLength; i++) { + matrixPsdOutput[vsBinIndex][i] += psd.psdOutput[i]; } } } @@ -284,8 +283,8 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV // Divide the values from the fft in each row (vs value bin) by the number of samples in the bin for (let i = 0; i < NUM_VS_BINS; i++) { if (numberSamples[i] > 1) { - for (let j = 0; j < matrixFftOutput[i].length; j++) { - matrixFftOutput[i][j] /= numberSamples[i]; + for (let j = 0; j < psdLength; j++) { + matrixPsdOutput[i][j] /= numberSamples[i]; } } } @@ -298,7 +297,7 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV fieldIndex : this._dataBuffer.fieldIndex, fieldName : this._dataBuffer.fieldName, fftLength : psdLength, - fftOutput : matrixFftOutput, + fftOutput : matrixPsdOutput, maxNoise : maxNoise, blackBoxRate : this._blackBoxRate, vsRange : { min: flightSamples.minValue, max: flightSamples.maxValue}, @@ -604,14 +603,14 @@ GraphSpectrumCalc._normalizeFft = function(fftOutput) { } } - maxNoiseIdx = maxNoiseIdx / magnitudeLength * maxFrequency; + const maxNoiseFrequency = maxNoiseIdx / fftLength * maxFrequency; const fftData = { fieldIndex : this._dataBuffer.fieldIndex, fieldName : this._dataBuffer.fieldName, - fftLength : magnitudeLength, - fftOutput : magnitudes, - maxNoiseIdx : maxNoiseIdx, + fftLength : fftLength, + fftOutput : fftOutput, + maxNoiseFrequency : maxNoiseFrequency, blackBoxRate : this._blackBoxRate, }; @@ -662,7 +661,7 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal psdOutput: new Float64Array(0), min: 0, max: 0, - maxNoiseIdx: 0, + maxNoiseFrequency: 0, }; } const maxFrequency = (this._blackBoxRate / 2.0); @@ -700,7 +699,7 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal psdOutput: psdOutput, min: min, max: max, - maxNoiseIdx: maxNoiseFrequency, + maxNoiseFrequency: maxNoiseFrequency, }; }; @@ -708,7 +707,8 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count */ GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { - const samplesCount = samples.length; + const samplesCount = samples.length, + fftLength = Math.floor(pointsPerSegment / 2); let output = []; for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { const fftInput = samples.slice(i, i + pointsPerSegment); @@ -718,8 +718,8 @@ GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapC } const fftComplex = this._fft(fftInput); - const magnitudes = new Float64Array(pointsPerSegment / 2); - for (let i = 0; i < pointsPerSegment / 2; i++) { + const magnitudes = new Float64Array(fftLength); + for (let i = 0; i < fftLength; i++) { const re = fftComplex[2 * i]; const im = fftComplex[2 * i + 1]; magnitudes[i] = Math.hypot(re, im); diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 9f26f20a..27fca69c 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -392,7 +392,7 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { const offset = 1; this._drawInterestFrequency( canvasCtx, - this._fftData.maxNoiseIdx, + this._fftData.maxNoiseFrequency, PLOTTED_BLACKBOX_RATE, "Max noise", WIDTH, @@ -536,7 +536,7 @@ GraphSpectrumPlot.getValueFromMatrixFFT = function(frequency, vsArgument) { if (vsArgumentIndex === NUM_VS_BINS) { vsArgumentIndex = NUM_VS_BINS - 1; } - const freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftOutput[0].length - 1) ); + const freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftLength - 1) ); return matrixFFT.fftOutput[vsArgumentIndex][freqIndex]; }; @@ -984,7 +984,7 @@ GraphSpectrumPlot._drawFiltersAndMarkers = function (canvasCtx) { if (this._spectrumType === SPECTRUM_TYPE.FREQUENCY) { this._drawInterestFrequency( canvasCtx, - this._fftData.maxNoiseIdx, + this._fftData.maxNoiseFrequency, PLOTTED_BLACKBOX_RATE, "Max noise", WIDTH, From 49b077689fcbc6dbd8f0c84165c827c397317edc Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 14:39:50 +0300 Subject: [PATCH 14/63] The background heatmap PSD value is changed from -70 to -100dBm --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index ca4a33ec..05716023 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -250,7 +250,7 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies const matrixPsdOutput = new Array(NUM_VS_BINS) .fill(null) - .map(() => (new Float64Array(psdLength)).fill(-70)); + .map(() => (new Float64Array(psdLength)).fill(-100)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. From 1fa0e648ea6a71c5eb4d98b110043c756e8ba0bd Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 19:57:24 +0300 Subject: [PATCH 15/63] Added PSD value to maximal noise PSD marker --- src/graph_spectrum_plot.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 27fca69c..c4c17255 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -406,8 +406,10 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { }; GraphSpectrumPlot.getPSDbyFreq = function(frequency) { - const freqIndex = Math.round(2 * frequency / this._fftData.blackBoxRate * (this._fftData.psdOutput.length - 1) ); - return this._fftData.psdOutput[freqIndex]; + let freqIndex = Math.round(2 * frequency / this._fftData.blackBoxRate * (this._fftData.psdOutput.length - 1) ); + freqIndex = Math.min(freqIndex, this._fftData.psdOutput.length - 1); + freqIndex = Math.max(freqIndex, 0); + return this._fftData.psdOutput.length ? this._fftData.psdOutput[freqIndex] : 0; }; GraphSpectrumPlot._drawFrequencyVsXGraph = function (canvasCtx, drawPSD = false) { @@ -536,7 +538,9 @@ GraphSpectrumPlot.getValueFromMatrixFFT = function(frequency, vsArgument) { if (vsArgumentIndex === NUM_VS_BINS) { vsArgumentIndex = NUM_VS_BINS - 1; } - const freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftLength - 1) ); + let freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftLength - 1)); + freqIndex = Math.max(freqIndex, 0); + freqIndex = Math.min(freqIndex, matrixFFT.fftLength - 1); return matrixFFT.fftOutput[vsArgumentIndex][freqIndex]; }; @@ -1275,7 +1279,14 @@ GraphSpectrumPlot._drawInterestFrequency = function ( stroke, lineWidth ) { - const interestLabel = `${label} ${frequency.toFixed(0)}Hz`; + + let interestLabel = ""; + if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY && label != "" ) { + const psdValue = this.getPSDbyFreq(frequency); + interestLabel = `${label}: (${frequency.toFixed(0)}Hz, ${psdValue.toFixed(0)}dBm/Hz)`; + } else { + interestLabel = `${label} ${frequency.toFixed(0)}Hz`; + } return this._drawVerticalMarkerLine( canvasCtx, frequency, From b0442a194702ebd53de785861511a49a79a01c98 Mon Sep 17 00:00:00 2001 From: Vladimir Demidov Date: Mon, 12 May 2025 21:16:16 +0300 Subject: [PATCH 16/63] Removed needless spaces Co-authored-by: Mark Haslinghuis --- src/graph_spectrum_plot.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index c4c17255..fc59d7bd 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -1524,11 +1524,11 @@ GraphSpectrumPlot._drawMousePosition = function ( if (this._spectrumType === SPECTRUM_TYPE.POWER_SPECTRAL_DENSITY) { const psdLabel = Math.round(this.getPSDbyFreq(mouseFrequency)).toString() + "dBm/Hz"; this._drawAxisLabel( - canvasCtx, - psdLabel, - mouseX - 30, - mouseY - 4, - "left", + canvasCtx, + psdLabel, + mouseX - 30, + mouseY - 4, + "left", ); } From 9fff07bc931e40bf9f1933edaf04997cbbbf6aa8 Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 12 May 2025 21:30:01 +0300 Subject: [PATCH 17/63] The SPECTRUM_TYPE enum members are reordered --- index.html | 10 +++++----- src/graph_spectrum_plot.js | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/index.html b/index.html index 71b5d086..94cf99f8 100644 --- a/index.html +++ b/index.html @@ -457,11 +457,11 @@

Workspace

diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index fc59d7bd..fd91160a 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -15,11 +15,11 @@ const BLUR_FILTER_PIXEL = 1, export const SPECTRUM_TYPE = { FREQUENCY: 0, FREQ_VS_THROTTLE: 1, - PIDERROR_VS_SETPOINT: 2, - FREQ_VS_RPM: 3, - POWER_SPECTRAL_DENSITY: 4, - PSD_VS_THROTTLE: 5, - PSD_VS_RPM: 6, + FREQ_VS_RPM: 2, + POWER_SPECTRAL_DENSITY: 3, + PSD_VS_THROTTLE: 4, + PSD_VS_RPM: 5, + PIDERROR_VS_SETPOINT: 6, }; export const SPECTRUM_OVERDRAW_TYPE = { From 822bcbc39575cf7362b22fad8534fc54b8dabedf Mon Sep 17 00:00:00 2001 From: Vladimir Demidov Date: Mon, 12 May 2025 21:51:53 +0300 Subject: [PATCH 18/63] Code style improvement Co-authored-by: Mark Haslinghuis --- src/graph_spectrum_plot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index fd91160a..cd2ec64d 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -1569,8 +1569,8 @@ GraphSpectrumPlot._drawMousePosition = function ( lineWidth ); - if (this._spectrumType == SPECTRUM_TYPE.PSD_VS_THROTTLE || - this._spectrumType == SPECTRUM_TYPE.PSD_VS_RPM) { + if (this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM) { const label = Math.round(this.getValueFromMatrixFFT(mouseFrequency, vsArgValue)).toString() + "dBm/Hz"; this._drawAxisLabel( canvasCtx, From 37911456ced73928ed27e36e54aa47313ab1cc2d Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 14 May 2025 09:36:26 +0300 Subject: [PATCH 19/63] Code refactoring --- src/graph_spectrum_calc.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 05716023..738d34bf 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -91,6 +91,10 @@ GraphSpectrumCalc.setDataBuffer = function(fieldIndex, curve, fieldName) { return undefined; }; +GraphSpectrumCalc.getNearPower2Value = function(size) { + return Math.pow(2, Math.ceil(Math.log2(size))); +}; + GraphSpectrumCalc.dataLoadFrequency = function() { const flightSamples = this._getFlightSamplesFreq(); @@ -110,21 +114,15 @@ GraphSpectrumCalc.dataLoadFrequency = function() { GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { const flightSamples = this._getFlightSamplesFreq(false); - - let pointsPerSegment = 512; const multiplier = Math.floor(1 / analyserZoomY); // 0. ... 10 - if (multiplier == 0) { - pointsPerSegment = 256; - } else if (multiplier > 1) { - pointsPerSegment *= 2 ** Math.floor(multiplier / 2); - } + let pointsPerSegment = 2 ** (8 + multiplier); //256, 512, 1024 ... // Use power 2 fft size what is not bigger flightSamples.samples.length if (pointsPerSegment > flightSamples.samples.length) { - pointsPerSegment = Math.pow(2, Math.floor(Math.log2(flightSamples.samples.length))); + pointsPerSegment = this.getNearPower2Value(flightSamples.samples.length); } - const overlapCount = Math.floor(pointsPerSegment / 2); + const overlapCount = pointsPerSegment / 2; const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount); @@ -144,21 +142,18 @@ GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infinity, maxValue = -Infinity) { const flightSamples = this._getFlightSamplesFreqVsX(vsFieldNames, minValue, maxValue); - // 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. const fftChunkLength = Math.round(this._blackBoxRate * FREQ_VS_THR_CHUNK_TIME_MS / 1000); const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); - const fftBufferSize = Math.pow(2, Math.ceil(Math.log2(fftChunkLength))); + const fftBufferSize = this.getNearPower2Value(fftChunkLength); const magnitudeLength = Math.floor(fftBufferSize / 2); let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftBufferSize * 2)); - const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. - - const fft = new FFT.complex(fftBufferSize, false); + for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { const fftInput = new Float64Array(fftBufferSize); @@ -170,7 +165,7 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi fftInput[i] = samples[i]; } - // Hanning window applied to input data + // Hanning window applied to input data, without padding zeros if (userSettings.analyserHanning) { this._hanningWindow(fftInput, fftChunkLength); } @@ -179,7 +174,7 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi fftOutput = fftOutput.slice(0, fftBufferSize); // The fft output contains two side spectrum, we use the first part only to get one side const magnitudes = new Float64Array(magnitudeLength); - + // Compute magnitude for (let i = 0; i < magnitudeLength; i++) { const re = fftOutput[2 * i], @@ -248,9 +243,10 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks const psdLength = Math.floor(fftChunkLength / 2); // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies + const backgroundValue = -200; const matrixPsdOutput = new Array(NUM_VS_BINS) .fill(null) - .map(() => (new Float64Array(psdLength)).fill(-100)); + .map(() => (new Float64Array(psdLength)).fill(backgroundValue)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. @@ -421,7 +417,7 @@ GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { } // The FFT input size is power 2 to get maximal performance - const fftBufferSize = Math.pow(2, Math.ceil(Math.log2(samplesCount))); + const fftBufferSize = this.getNearPower2Value(samplesCount); return { samples : samples.slice(0, fftBufferSize), count : samplesCount, From 257d694e5c20dce30874d02c117f5883eb884c97 Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 14 May 2025 09:42:23 +0300 Subject: [PATCH 20/63] Resolved issue of wrong simple frequency spectrum data --- src/graph_spectrum_calc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 738d34bf..49ce50a8 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -604,8 +604,8 @@ GraphSpectrumCalc._normalizeFft = function(fftOutput) { const fftData = { fieldIndex : this._dataBuffer.fieldIndex, fieldName : this._dataBuffer.fieldName, - fftLength : fftLength, - fftOutput : fftOutput, + fftLength : magnitudeLength, + fftOutput : magnitudes, maxNoiseFrequency : maxNoiseFrequency, blackBoxRate : this._blackBoxRate, }; From b5e3b083691837e787e91f60ad622fdb5e93ddb4 Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 14 May 2025 10:01:25 +0300 Subject: [PATCH 21/63] Code refactoring --- src/graph_spectrum_calc.js | 5 ++--- src/graph_spectrum_plot.js | 15 ++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 49ce50a8..88895516 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -15,11 +15,10 @@ const FREQ_VS_THR_CHUNK_TIME_MS = 300, FREQ_VS_THR_WINDOW_DIVISOR = 6, MAX_ANALYSER_LENGTH = 300 * 1000 * 1000, // 5min - NUM_VS_BINS = 100, WARNING_RATE_DIFFERENCE = 0.05, MAX_RPM_HZ_VALUE = 800, MAX_RPM_AXIS_GAP = 1.05; - +export const NUM_VS_BINS = 100; export const GraphSpectrumCalc = { _analyserTimeRange : { @@ -705,7 +704,7 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { const samplesCount = samples.length, fftLength = Math.floor(pointsPerSegment / 2); - let output = []; + const output = []; for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { const fftInput = samples.slice(i, i + pointsPerSegment); diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index cd2ec64d..9f1f1e8a 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -1,5 +1,6 @@ import { FILTER_TYPE } from "./flightlog_fielddefs"; import { constrain } from "./tools"; +import { NUM_VS_BINS } from "./graph_spectrum_calc"; const BLUR_FILTER_PIXEL = 1, DEFAULT_FONT_FACE = "Verdana, Arial, sans-serif", @@ -10,7 +11,9 @@ const BLUR_FILTER_PIXEL = 1, MARGIN_LEFT_FULLSCREEN = 35, MAX_SETPOINT_DEFAULT = 100, PID_ERROR_VERTICAL_CHUNK = 5, - ZOOM_X_MAX = 5; + ZOOM_X_MAX = 5, + MIN_DBM_VALUE = -40, + MAX_DBM_VALUE = 10; export const SPECTRUM_TYPE = { FREQUENCY: 0, @@ -507,10 +510,9 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { for (let i = 0; i < this._fftData.fftLength; i++) { let valuePlot; if (drawPSD) { - const min = -40, max = 10; //limit values dBm - valuePlot = Math.max(this._fftData.fftOutput[j][i], min); - valuePlot = Math.min(valuePlot, max); - valuePlot = Math.round((valuePlot - min) * 100 / (max - min)); + valuePlot = Math.max(this._fftData.fftOutput[j][i], MIN_DBM_VALUE); + valuePlot = Math.min(valuePlot, MAX_DBM_VALUE); + valuePlot = Math.round((valuePlot - MIN_DBM_VALUE) * 100 / (MAX_DBM_VALUE - MIN_DBM_VALUE)); } else { valuePlot = Math.round( Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100) @@ -532,10 +534,9 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { }; GraphSpectrumPlot.getValueFromMatrixFFT = function(frequency, vsArgument) { - const NUM_VS_BINS = 100; // redefinition of value from graph_spectrum_calc.js module! const matrixFFT = this._fftData; let vsArgumentIndex = Math.round(NUM_VS_BINS * (vsArgument - matrixFFT.vsRange.min) / (matrixFFT.vsRange.max - matrixFFT.vsRange.min)); - if (vsArgumentIndex === NUM_VS_BINS) { + if (vsArgumentIndex >= NUM_VS_BINS) { vsArgumentIndex = NUM_VS_BINS - 1; } let freqIndex = Math.round(2 * frequency / matrixFFT.blackBoxRate * (matrixFFT.fftLength - 1)); From 5e596660e0976a96ab16f80380d1ad443cdfbc71 Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 14 May 2025 11:01:46 +0300 Subject: [PATCH 22/63] Improved fill data into fft input array --- src/graph_spectrum_calc.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 88895516..1533d8ff 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -158,11 +158,8 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi const fftInput = new Float64Array(fftBufferSize); let fftOutput = new Float64Array(fftBufferSize * 2); - //TODO: to find method to just resize samples array to fftBufferSize const samples = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); - for (let i = 0; i < fftChunkLength; i++) { - fftInput[i] = samples[i]; - } + fftInput.set(samples); // Hanning window applied to input data, without padding zeros if (userSettings.analyserHanning) { From 75f36de7f11bddb16732535d743259c29f2f51c4 Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 14 May 2025 14:37:10 +0300 Subject: [PATCH 23/63] Improved computing of PSD --- src/graph_spectrum_calc.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 1533d8ff..0156bae7 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -665,8 +665,11 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal psdOutput[i] = 0.0; for (let j = 0; j < segmentsCount; j++) { let p = scale * fftOutput[j][i] ** 2; - if (i != dataCount - 1) { - p *= 2; + if (i != 0) { + const even = dataCount % 2 == 0; + if (!even || even && i != dataCount - 1) { + p *= 2; + } } psdOutput[i] += p; } From 7c673a1fed553da00037be8226b99469dbe5f7ab Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 14 May 2025 16:47:15 +0300 Subject: [PATCH 24/63] Code refactoring --- src/graph_spectrum.js | 2 +- src/graph_spectrum_calc.js | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index bcc19151..d60c278e 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -110,7 +110,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { break; case SPECTRUM_TYPE.PSD_VS_THROTTLE: - fftData = GraphSpectrumCalc.dataLoadFrequencyVsThrottle(true); + fftData = GraphSpectrumCalc.dataLoadPowerSpectralDensityVsThrottle(); break; case SPECTRUM_TYPE.PSD_VS_RPM: diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 0156bae7..fb613454 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -299,9 +299,12 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV }; -GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function(drawPSD = false) { - return drawPSD ? this._dataLoadPowerSpectralDensityVsX(FIELD_THROTTLE_NAME, 0, 100) : - this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); +GraphSpectrumCalc.dataLoadFrequencyVsThrottle = function() { + return this._dataLoadFrequencyVsX(FIELD_THROTTLE_NAME, 0, 100); +}; + +GraphSpectrumCalc.dataLoadPowerSpectralDensityVsThrottle = function() { + return this._dataLoadPowerSpectralDensityVsX(FIELD_THROTTLE_NAME, 0, 100); }; GraphSpectrumCalc.dataLoadFrequencyVsRpm = function(drawPSD = false) { From 1fe73937710bf1925d6a0109d4d70a7dd941ee23 Mon Sep 17 00:00:00 2001 From: Vladimir Demidov Date: Thu, 15 May 2025 09:06:52 +0300 Subject: [PATCH 25/63] Resolved issue computing frequency with maximal noise at the simple spectrum chart --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index fb613454..dfddcfe2 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -598,7 +598,7 @@ GraphSpectrumCalc._normalizeFft = function(fftOutput) { } } - const maxNoiseFrequency = maxNoiseIdx / fftLength * maxFrequency; + const maxNoiseFrequency = maxNoiseIdx / magnitudeLength * maxFrequency; const fftData = { fieldIndex : this._dataBuffer.fieldIndex, From 4ec3c4626355bf0885a4edcdb270986b8b962e3a Mon Sep 17 00:00:00 2001 From: Vladimir Demidov Date: Thu, 15 May 2025 09:53:45 +0300 Subject: [PATCH 26/63] Improved computing of vsBinIndex values --- src/graph_spectrum_calc.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index dfddcfe2..d8c3b62e 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -188,9 +188,7 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi } // Translate the average vs value to a bin index const avgVsValue = sumVsValues / fftChunkLength; - let vsBinIndex = Math.round(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); - // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access - if (vsBinIndex >= NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } + const vsBinIndex = Math.round((NUM_VS_BINS - 1) * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); numberSamples[vsBinIndex]++; // add the output from the fft to the row given by the vs value bin index @@ -260,9 +258,7 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV } // Translate the average vs value to a bin index const avgVsValue = sumVsValues / fftChunkLength; - let vsBinIndex = Math.floor(NUM_VS_BINS * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); - // ensure that avgVsValue == flightSamples.maxValue does not result in an out of bounds access - if (vsBinIndex === NUM_VS_BINS) { vsBinIndex = NUM_VS_BINS - 1; } + const vsBinIndex = Math.round((NUM_VS_BINS - 1) * (avgVsValue - flightSamples.minValue) / (flightSamples.maxValue - flightSamples.minValue)); numberSamples[vsBinIndex]++; // add the output from the fft to the row given by the vs value bin index From 6fac4f835da6a007846f930efe197c65b292ebb7 Mon Sep 17 00:00:00 2001 From: demvlad Date: Thu, 15 May 2025 17:28:53 +0300 Subject: [PATCH 27/63] Code refactoring: using of named constant --- src/graph_spectrum_calc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index d8c3b62e..f369d4eb 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -237,10 +237,10 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks const psdLength = Math.floor(fftChunkLength / 2); // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies - const backgroundValue = -200; + const BACKGROUND_PSD_VALUE = -200; const matrixPsdOutput = new Array(NUM_VS_BINS) .fill(null) - .map(() => (new Float64Array(psdLength)).fill(backgroundValue)); + .map(() => (new Float64Array(psdLength)).fill(BACKGROUND_PSD_VALUE)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. From 5e1edf0b4f12bd51728e61b092c1add2464fb066 Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 16 May 2025 17:14:52 +0300 Subject: [PATCH 28/63] The top margin at heatmaps charts applies for RPM axis only --- src/graph_spectrum_calc.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index f369d4eb..9a08207d 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -17,7 +17,7 @@ const MAX_ANALYSER_LENGTH = 300 * 1000 * 1000, // 5min WARNING_RATE_DIFFERENCE = 0.05, MAX_RPM_HZ_VALUE = 800, - MAX_RPM_AXIS_GAP = 1.05; + RPM_AXIS_TOP_MARGIN_PERCENT = 2; export const NUM_VS_BINS = 100; export const GraphSpectrumCalc = { @@ -480,7 +480,10 @@ GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = I } } - maxValue *= MAX_RPM_AXIS_GAP; +// Use small top margin for RPM axis only. Because it has bad axis view for throttle + if (vsFieldNames == FIELD_RPM_NAMES) { + maxValue *= 1 + RPM_AXIS_TOP_MARGIN_PERCENT / 100; + } if (minValue > maxValue) { if (minValue == Infinity) { // this should never happen From 91fd9c43ba0ca1cfdd0987c90f1a6bc9f1b04c0f Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 16 May 2025 17:18:02 +0300 Subject: [PATCH 29/63] Resolved issue of filters drawing at the PSD charts --- src/graph_spectrum_plot.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 9f1f1e8a..2b1316ac 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -1357,7 +1357,9 @@ GraphSpectrumPlot._drawLowpassDynFilter = function ( // frequency2 line const offsetByType = this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || - this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM + this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM ? 0 : OFFSET; const x2 = this._drawVerticalMarkerLine( @@ -1379,7 +1381,9 @@ GraphSpectrumPlot._drawLowpassDynFilter = function ( if ( this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || - this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM + this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM ) { /* * It draws a curve: @@ -1445,7 +1449,9 @@ GraphSpectrumPlot._drawNotchFilter = function ( if ( this._spectrumType === SPECTRUM_TYPE.FREQ_VS_THROTTLE || - this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM + this._spectrumType === SPECTRUM_TYPE.FREQ_VS_RPM || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_THROTTLE || + this._spectrumType === SPECTRUM_TYPE.PSD_VS_RPM ) { canvasCtx.moveTo(cutoffX, 0); canvasCtx.lineTo(centerX * 2 - cutoffX, HEIGHT); From dcfd8235b51faf4f4c05de97370a26a00d418bbf Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 16 May 2025 19:08:38 +0300 Subject: [PATCH 30/63] Improved computing of top margin value at vs RPM charts --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 9a08207d..59b1e3aa 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -482,7 +482,7 @@ GraphSpectrumCalc._getFlightSamplesFreqVsX = function(vsFieldNames, minValue = I // Use small top margin for RPM axis only. Because it has bad axis view for throttle if (vsFieldNames == FIELD_RPM_NAMES) { - maxValue *= 1 + RPM_AXIS_TOP_MARGIN_PERCENT / 100; + maxValue += (maxValue - minValue) * RPM_AXIS_TOP_MARGIN_PERCENT / 100; } if (minValue > maxValue) { From 39ff24007421b1d02daf20f836a60e2b6e31c5aa Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 17 May 2025 21:58:46 +0300 Subject: [PATCH 31/63] Set properly value for overdrawSpectrumType select element position --- src/css/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/main.css b/src/css/main.css index 37a37bfd..34dfd1c4 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -616,7 +616,7 @@ html.has-analyser-fullscreen.has-analyser height: 0; overflow: hidden; opacity: 0; - left: 120px; + left: 130px; float: left; z-index: 9; position: absolute; From f357404d87c5567829ab908f1a77f29fabb655bb Mon Sep 17 00:00:00 2001 From: demvlad Date: Sat, 17 May 2025 23:39:02 +0300 Subject: [PATCH 32/63] The vertical slider shifts the PSD charts dBm range --- src/graph_spectrum_plot.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 2b1316ac..bc756ebf 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -504,15 +504,24 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { const fftColorScale = 100 / (this._zoomY * SCALE_HEATMAP); + //Compute the dbm range shift from zoomY as[-30, -20, -10, 0, +10, +20] + let dBmRangeShift = Math.floor(1 / this._zoomY) - 1; // -1 ... 9 + if (dBmRangeShift > 0) { + dBmRangeShift = -10 * Math.round(dBmRangeShift / 3); //-10, -20, -30 + } else if (dBmRangeShift < 0) { + dBmRangeShift = -10 * Math.round(dBmRangeShift * 2); //+20 + } + const dBmValueMin = MIN_DBM_VALUE + dBmRangeShift, + dBmValueMax = MAX_DBM_VALUE + dBmRangeShift; // Loop for throttle for (let j = 0; j < THROTTLE_VALUES_SIZE; j++) { // Loop for frequency for (let i = 0; i < this._fftData.fftLength; i++) { let valuePlot; if (drawPSD) { - valuePlot = Math.max(this._fftData.fftOutput[j][i], MIN_DBM_VALUE); - valuePlot = Math.min(valuePlot, MAX_DBM_VALUE); - valuePlot = Math.round((valuePlot - MIN_DBM_VALUE) * 100 / (MAX_DBM_VALUE - MIN_DBM_VALUE)); + valuePlot = Math.max(this._fftData.fftOutput[j][i], dBmValueMin); + valuePlot = Math.min(valuePlot, dBmValueMax); + valuePlot = Math.round((valuePlot - dBmValueMin) * 100 / (dBmValueMax - dBmValueMin)); } else { valuePlot = Math.round( Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100) From 4cdc2f49f7ba13618ecdbefb5c604feb7f7e84d6 Mon Sep 17 00:00:00 2001 From: demvlad Date: Tue, 20 May 2025 14:23:02 +0300 Subject: [PATCH 33/63] Improved draw simple spectrum code --- src/graph_spectrum_plot.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index bc756ebf..073617fd 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -13,7 +13,8 @@ const BLUR_FILTER_PIXEL = 1, PID_ERROR_VERTICAL_CHUNK = 5, ZOOM_X_MAX = 5, MIN_DBM_VALUE = -40, - MAX_DBM_VALUE = 10; + MAX_DBM_VALUE = 10, + MAX_SPECTRUM_LINE_COUNT = 30000; export const SPECTRUM_TYPE = { FREQUENCY: 0, @@ -206,9 +207,6 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { this._drawGradientBackground(canvasCtx, WIDTH, HEIGHT); - const barWidth = WIDTH / (PLOTTED_BUFFER_LENGTH / 5) - 1; - let x = 0; - const barGradient = canvasCtx.createLinearGradient(0, HEIGHT, 0, 0); barGradient.addColorStop( constrain(0 / this._zoomY, 0, 1), @@ -230,13 +228,17 @@ GraphSpectrumPlot._drawFrequencyGraph = function (canvasCtx) { canvasCtx.fillStyle = barGradient; const fftScale = HEIGHT / (this._zoomY * 100); - for (let i = 0; i < PLOTTED_BUFFER_LENGTH; i += 5) { + // Limit maximal count of drawing line to get good performance + const stepData = Math.floor(PLOTTED_BUFFER_LENGTH / MAX_SPECTRUM_LINE_COUNT) + 1; + const stepX = WIDTH / (PLOTTED_BUFFER_LENGTH / stepData); + const barWidth = Math.max(stepX, 1); + let x = 0; + for (let i = 0; i < PLOTTED_BUFFER_LENGTH; i += stepData) { const barHeight = this._fftData.fftOutput[i] * fftScale; canvasCtx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight); - x += barWidth + 1; + x += stepX; } - //Draw imported spectrums const curvesColors = [ "Blue", From 27e9873a9609d9aa8f6a6159393f8c9a123411ea Mon Sep 17 00:00:00 2001 From: demvlad Date: Tue, 20 May 2025 14:23:56 +0300 Subject: [PATCH 34/63] Limit fft input count for simple spectrum chart to get normal charts plot quality --- src/graph_spectrum_calc.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 59b1e3aa..c1e96f9c 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -17,7 +17,8 @@ const MAX_ANALYSER_LENGTH = 300 * 1000 * 1000, // 5min WARNING_RATE_DIFFERENCE = 0.05, MAX_RPM_HZ_VALUE = 800, - RPM_AXIS_TOP_MARGIN_PERCENT = 2; + RPM_AXIS_TOP_MARGIN_PERCENT = 2, + MIN_SPECTRUM_SAMPLES_COUNT = 2048; export const NUM_VS_BINS = 100; export const GraphSpectrumCalc = { @@ -412,7 +413,14 @@ GraphSpectrumCalc._getFlightSamplesFreq = function(scaled = true) { } // The FFT input size is power 2 to get maximal performance - const fftBufferSize = this.getNearPower2Value(samplesCount); + // Limit fft input count for simple spectrum chart to get normal charts plot quality + let fftBufferSize; + if (scaled && samplesCount < MIN_SPECTRUM_SAMPLES_COUNT) { + fftBufferSize = MIN_SPECTRUM_SAMPLES_COUNT; + } else { + fftBufferSize = this.getNearPower2Value(samplesCount); + } + return { samples : samples.slice(0, fftBufferSize), count : samplesCount, From 062a353762a4ef5cfe41ad23ad8af57226877633 Mon Sep 17 00:00:00 2001 From: demvlad Date: Tue, 20 May 2025 15:08:31 +0300 Subject: [PATCH 35/63] Using power at 2 fft inputsize to compute PSD ws throttle and RPM --- src/graph_spectrum_calc.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index c1e96f9c..f068bb48 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -248,7 +248,7 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); - const psd = this._psd(fftInput, fftChunkLength, 0, 'density'); + const psd = this._psd(fftInput, fftChunkLength, 0, 'density'); // Using the one segment with all chunks fftChunkLength size, it will extended at power at 2 size inside _psd() - _fft_segmented() maxNoise = Math.max(psd.max, maxNoise); // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately for (const vsValueArray of flightSamples.vsValues) { @@ -712,16 +712,27 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count */ GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { - const samplesCount = samples.length, - fftLength = Math.floor(pointsPerSegment / 2); + const samplesCount = samples.length; const output = []; + for (let i = 0; i <= samplesCount - pointsPerSegment; i += pointsPerSegment - overlapCount) { - const fftInput = samples.slice(i, i + pointsPerSegment); + let fftInput = samples.slice(i, i + pointsPerSegment); if (userSettings.analyserHanning) { this._hanningWindow(fftInput, pointsPerSegment); } + let fftLength; + if (pointsPerSegment != samplesCount) { + fftLength = Math.floor(pointsPerSegment / 2); + } else { // Extend the one segment input on power at 2 size + const fftSize = this.getNearPower2Value(pointsPerSegment); + const power2Input = new Float64Array(fftSize); + power2Input.set(fftInput); + fftInput = power2Input; + fftLength = fftSize / 2; + } + const fftComplex = this._fft(fftInput); const magnitudes = new Float64Array(fftLength); for (let i = 0; i < fftLength; i++) { From e1e522d6d48315919edd08794875f2ba3ec74898 Mon Sep 17 00:00:00 2001 From: demvlad Date: Tue, 20 May 2025 18:52:00 +0300 Subject: [PATCH 36/63] Resolved issues of using power at 2 values --- src/graph_spectrum_calc.js | 9 +++++---- src/graph_spectrum_plot.js | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index f068bb48..f7f1e981 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -117,13 +117,14 @@ GraphSpectrumCalc.dataLoadPSD = function(analyserZoomY) { const multiplier = Math.floor(1 / analyserZoomY); // 0. ... 10 let pointsPerSegment = 2 ** (8 + multiplier); //256, 512, 1024 ... - // Use power 2 fft size what is not bigger flightSamples.samples.length + let overlapCount; if (pointsPerSegment > flightSamples.samples.length) { - pointsPerSegment = this.getNearPower2Value(flightSamples.samples.length); + pointsPerSegment = flightSamples.samples.length; // Use actual sample length. It will transform to power at 2 value inside the _psd() - fft_segmented + overlapCount = 0; + } else { + overlapCount = pointsPerSegment / 2; } - const overlapCount = pointsPerSegment / 2; - const psd = this._psd(flightSamples.samples, pointsPerSegment, overlapCount); const psdData = { diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 073617fd..62e6d76b 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -350,7 +350,10 @@ GraphSpectrumPlot._drawPowerSpectralDensityGraph = function (canvasCtx) { // Allign y axis range by 10db const dbStep = 10; const minY = Math.floor(this._fftData.minimum / dbStep) * dbStep; - const maxY = (Math.floor(this._fftData.maximum / dbStep) + 1) * dbStep; + let maxY = (Math.floor(this._fftData.maximum / dbStep) + 1) * dbStep; + if (minY == maxY) { + maxY = minY + 1; // prevent divide by zero + } const ticksCount = (maxY - minY) / dbStep; const scaleY = HEIGHT / (maxY - minY); //Store vsRange for _drawMousePosition From 25c94ec0eb46e8311f2c6a0b180fadd41646a9dc Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 21 May 2025 11:11:56 +0300 Subject: [PATCH 37/63] Resolved issue wrong PSD values matrix size --- src/graph_spectrum_calc.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index f7f1e981..9e7c749c 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -237,12 +237,10 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV const fftChunkWindow = Math.round(fftChunkLength / FREQ_VS_THR_WINDOW_DIVISOR); let maxNoise = 0; // Stores the maximum amplitude of the fft over all chunks - const psdLength = Math.floor(fftChunkLength / 2); + let psdLength = 0; // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies const BACKGROUND_PSD_VALUE = -200; - const matrixPsdOutput = new Array(NUM_VS_BINS) - .fill(null) - .map(() => (new Float64Array(psdLength)).fill(BACKGROUND_PSD_VALUE)); + let matrixPsdOutput = undefined; const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. @@ -251,6 +249,13 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV const fftInput = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); const psd = this._psd(fftInput, fftChunkLength, 0, 'density'); // Using the one segment with all chunks fftChunkLength size, it will extended at power at 2 size inside _psd() - _fft_segmented() maxNoise = Math.max(psd.max, maxNoise); + // The _psd() can extend fft data size. Set psdLength and create matrix by first using + if (matrixPsdOutput == undefined) { + psdLength = psd.psdOutput.length; + matrixPsdOutput = new Array(NUM_VS_BINS) + .fill(null) + .map(() => (new Float64Array(psdLength)).fill(BACKGROUND_PSD_VALUE)); + } // calculate a bin index and put the fft value in that bin for each field (e.g. eRPM[0], eRPM[1]..) sepparately for (const vsValueArray of flightSamples.vsValues) { // Calculate average of the VS values in the chunk From 518ee5912edff92de0be26a2f124bbd71166ecee Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 21 May 2025 12:04:16 +0300 Subject: [PATCH 38/63] Impoved PSD heat map charts vertical slider control: the avaiable dBm range shift value are -30, -20, -10, 0, 10, 20 dBm --- src/graph_spectrum_calc.js | 4 ++++ src/graph_spectrum_plot.js | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 9e7c749c..e4b22f5a 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -627,6 +627,8 @@ GraphSpectrumCalc._normalizeFft = function(fftOutput) { /** * Compute PSD for data samples by Welch method follow Python code + * It is good to use power at 2 values for pointsPerSegment. + * For short data length, set pointsPerSegment same samples.length to extend samples count for power at 2 value inside _fft_segmented */ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scaling = 'density') { // Compute FFT for samples segments @@ -716,6 +718,8 @@ GraphSpectrumCalc._psd = function(samples, pointsPerSegment, overlapCount, scal /** * Compute FFT for samples segments by lenghts as pointsPerSegment with overlapCount overlap points count + * It is good to use power at 2 values for pointsPerSegment. + * For short data length, set pointsPerSegment same samples.length to extend samples count for power at 2 value inside _fft_segmented */ GraphSpectrumCalc._fft_segmented = function(samples, pointsPerSegment, overlapCount) { const samplesCount = samples.length; diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 62e6d76b..5c0ea8ff 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -510,11 +510,11 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { const fftColorScale = 100 / (this._zoomY * SCALE_HEATMAP); //Compute the dbm range shift from zoomY as[-30, -20, -10, 0, +10, +20] - let dBmRangeShift = Math.floor(1 / this._zoomY) - 1; // -1 ... 9 - if (dBmRangeShift > 0) { - dBmRangeShift = -10 * Math.round(dBmRangeShift / 3); //-10, -20, -30 - } else if (dBmRangeShift < 0) { - dBmRangeShift = -10 * Math.round(dBmRangeShift * 2); //+20 + let dBmRangeShift = 0; + if (this._zoomY <= 1) { // [10 ... 0.1] + dBmRangeShift = -10 * Math.round((1 / this._zoomY - 1) / 3); //0, -10, -20, -30 + } else { + dBmRangeShift = 10 * Math.round((this._zoomY - 1) / 9 * 2); // 0, 10, 20 } const dBmValueMin = MIN_DBM_VALUE + dBmRangeShift, dBmValueMax = MAX_DBM_VALUE + dBmRangeShift; From 427688ae0d93b7b9e6978e8577245390a6d7228e Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 21 May 2025 17:36:48 +0300 Subject: [PATCH 39/63] Code style improvement --- src/graph_spectrum_calc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index e4b22f5a..f938ea54 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -240,7 +240,7 @@ GraphSpectrumCalc._dataLoadPowerSpectralDensityVsX = function(vsFieldNames, minV let psdLength = 0; // Matrix where each row represents a bin of vs values, and the columns are amplitudes at frequencies const BACKGROUND_PSD_VALUE = -200; - let matrixPsdOutput = undefined; + let matrixPsdOutput; const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. From 989ff7307df20ccbc7bb6df78f2ed06f266d2784 Mon Sep 17 00:00:00 2001 From: demvlad Date: Thu, 22 May 2025 14:02:27 +0300 Subject: [PATCH 40/63] Code refactoring --- src/graph_spectrum_calc.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index f938ea54..4ad77bd9 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -154,12 +154,11 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi const matrixFftOutput = new Array(NUM_VS_BINS).fill(null).map(() => new Float64Array(fftBufferSize * 2)); const numberSamples = new Uint32Array(NUM_VS_BINS); // Number of samples in each vs value, used to average them later. const fft = new FFT.complex(fftBufferSize, false); + const fftInput = new Float64Array(fftBufferSize); + const fftOutput = new Float64Array(fftBufferSize * 2); for (let fftChunkIndex = 0; fftChunkIndex + fftChunkLength < flightSamples.samples.length; fftChunkIndex += fftChunkWindow) { - const fftInput = new Float64Array(fftBufferSize); - let fftOutput = new Float64Array(fftBufferSize * 2); - const samples = flightSamples.samples.slice(fftChunkIndex, fftChunkIndex + fftChunkLength); fftInput.set(samples); @@ -170,10 +169,9 @@ GraphSpectrumCalc._dataLoadFrequencyVsX = function(vsFieldNames, minValue = Infi fft.simple(fftOutput, fftInput, 'real'); - fftOutput = fftOutput.slice(0, fftBufferSize); // The fft output contains two side spectrum, we use the first part only to get one side - const magnitudes = new Float64Array(magnitudeLength); - // Compute magnitude +// The fftOutput contains two side spectrum, we use the first part only to get one side + const magnitudes = new Float64Array(magnitudeLength); for (let i = 0; i < magnitudeLength; i++) { const re = fftOutput[2 * i], im = fftOutput[2 * i + 1]; From f3f0f4eb87ad9b82988bcbd28e3e8839231d5288 Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 23 May 2025 11:08:19 +0300 Subject: [PATCH 41/63] Added separated own vertical slider for PSD ranges shift --- index.html | 3 +++ src/css/main.css | 9 +++++++++ src/graph_spectrum.js | 31 ++++++++++++++++++++++++++++--- src/graph_spectrum_plot.js | 21 ++++++++++++--------- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index 94cf99f8..aad0034d 100644 --- a/index.html +++ b/index.html @@ -493,6 +493,9 @@

Workspace

/> + + diff --git a/src/css/main.css b/src/css/main.css index 34dfd1c4..2c3bb152 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -670,6 +670,7 @@ html.has-analyser-fullscreen.has-analyser top: 10px; float: right; } + .analyser input#analyserZoomY { width: 10px; height: 100px; @@ -678,6 +679,14 @@ html.has-analyser-fullscreen.has-analyser top: 30px; } +.analyser input#analyserShiftPSD { + width: 10px; + height: 100px; + -webkit-appearance: slider-vertical; + left: 0px; + top: 30px; +} + .analyser input.onlyFullScreen { display: none; padding: 3px; diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index d60c278e..49052ca0 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -30,6 +30,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { GraphSpectrumPlot.setLogRateWarningInfo(logRateInfo); let analyserZoomXElem = $("#analyserZoomX"); let analyserZoomYElem = $("#analyserZoomY"); + const analyserShiftPSDElem = $("#analyserShiftPSD"); let spectrumToolbarElem = $("#spectrumToolbar"); let spectrumTypeElem = $("#spectrumTypeSelect"); @@ -84,10 +85,13 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { top: newSize.top, // (canvas.height * getSize().top ) + "px" }); // place the sliders. - $("input:first-of-type", parentElem).css({ + $("#analyserZoomX", parentElem).css({ left: `${newSize.width - 130}px`, }); - $("input:last-of-type", parentElem).css({ + $("#analyserZoomY", parentElem).css({ + left: `${newSize.width - 20}px`, + }); + $("#analyserShiftPSD", parentElem).css({ left: `${newSize.width - 20}px`, }); $("#analyserResize", parentElem).css({ @@ -202,6 +206,20 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { }) .val(DEFAULT_ZOOM); + analyserShiftPSDElem + .on( + "input", + debounce(100, function () { + const shift = -parseInt(analyserShiftPSDElem.val()); + GraphSpectrumPlot.setShiftPSD(shift); + that.refresh(); + }) + ) + .dblclick(function () { + $(this).val(0).trigger("input"); + }) + .val(0); + // Spectrum type to show userSettings.spectrumType = userSettings.spectrumType || SPECTRUM_TYPE.FREQUENCY; @@ -223,10 +241,17 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { // Hide overdraw and zoomY if needed const pidErrorVsSetpointSelected = optionSelected === SPECTRUM_TYPE.PIDERROR_VS_SETPOINT; + const psdHeatMapSelected = + optionSelected === SPECTRUM_TYPE.PSD_VS_THROTTLE || + optionSelected === SPECTRUM_TYPE.PSD_VS_RPM; overdrawSpectrumTypeElem.toggle(!pidErrorVsSetpointSelected); analyserZoomYElem.toggleClass( "onlyFullScreenException", - pidErrorVsSetpointSelected + pidErrorVsSetpointSelected || psdHeatMapSelected + ); + analyserShiftPSDElem.toggleClass( + "onlyFullScreenException", + !psdHeatMapSelected ); $("#spectrumComparison").css("visibility", (optionSelected == 0 ? "visible" : "hidden")); diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 5c0ea8ff..7db88499 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -50,6 +50,7 @@ export const GraphSpectrumPlot = window.GraphSpectrumPlot || { _sysConfig: null, _zoomX: 1.0, _zoomY: 1.0, + _shiftPSD: 0, _drawingParams: { fontSizeFrameLabel: "6", fontSizeFrameLabelFullscreen: "9", @@ -79,6 +80,15 @@ GraphSpectrumPlot.setZoom = function (zoomX, zoomY) { } }; +GraphSpectrumPlot.setShiftPSD = function (shift) { + const modifiedShift = this._shiftPSD !== shift; + if (modifiedShift) { + this._shiftPSD = shift; + this._invalidateCache(); + this._invalidateDataCache(); + } +}; + GraphSpectrumPlot.setSize = function (width, height) { this._canvasCtx.canvas.width = width; this._canvasCtx.canvas.height = height; @@ -509,15 +519,8 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { const fftColorScale = 100 / (this._zoomY * SCALE_HEATMAP); - //Compute the dbm range shift from zoomY as[-30, -20, -10, 0, +10, +20] - let dBmRangeShift = 0; - if (this._zoomY <= 1) { // [10 ... 0.1] - dBmRangeShift = -10 * Math.round((1 / this._zoomY - 1) / 3); //0, -10, -20, -30 - } else { - dBmRangeShift = 10 * Math.round((this._zoomY - 1) / 9 * 2); // 0, 10, 20 - } - const dBmValueMin = MIN_DBM_VALUE + dBmRangeShift, - dBmValueMax = MAX_DBM_VALUE + dBmRangeShift; + const dBmValueMin = MIN_DBM_VALUE + this._shiftPSD, + dBmValueMax = MAX_DBM_VALUE + this._shiftPSD; // Loop for throttle for (let j = 0; j < THROTTLE_VALUES_SIZE; j++) { // Loop for frequency From 74f03d62195aead3e73aa4e52af0b06cfb636a85 Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 23 May 2025 12:18:29 +0300 Subject: [PATCH 42/63] Added vertical slider to control drawing PSD low level --- index.html | 2 ++ src/css/main.css | 8 ++++++++ src/graph_spectrum.js | 23 +++++++++++++++++++++++ src/graph_spectrum_plot.js | 23 +++++++++++++++++++---- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index aad0034d..9344add7 100644 --- a/index.html +++ b/index.html @@ -495,6 +495,8 @@

Workspace

/> + diff --git a/src/css/main.css b/src/css/main.css index 2c3bb152..8d2fced3 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -687,6 +687,14 @@ html.has-analyser-fullscreen.has-analyser top: 30px; } +.analyser input#analyserLowLevelPSD { + width: 10px; + height: 100px; + -webkit-appearance: slider-vertical; + left: 0px; + top: 30px; +} + .analyser input.onlyFullScreen { display: none; padding: 3px; diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 49052ca0..9de8a3e7 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -31,6 +31,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { let analyserZoomXElem = $("#analyserZoomX"); let analyserZoomYElem = $("#analyserZoomY"); const analyserShiftPSDElem = $("#analyserShiftPSD"); + const analyserLowLevelPSDElem = $("#analyserLowLevelPSD"); let spectrumToolbarElem = $("#spectrumToolbar"); let spectrumTypeElem = $("#spectrumTypeSelect"); @@ -94,6 +95,9 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { $("#analyserShiftPSD", parentElem).css({ left: `${newSize.width - 20}px`, }); + $("#analyserLowLevelPSD", parentElem).css({ + left: `${newSize.width - 130}px`, + }); $("#analyserResize", parentElem).css({ left: `${newSize.width - 20}px`, }); @@ -212,6 +216,21 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { debounce(100, function () { const shift = -parseInt(analyserShiftPSDElem.val()); GraphSpectrumPlot.setShiftPSD(shift); + analyserLowLevelPSDElem.val(0).trigger("input"); + that.refresh(); + }) + ) + .dblclick(function () { + $(this).val(0).trigger("input"); + }) + .val(0); + + analyserLowLevelPSDElem + .on( + "input", + debounce(100, function () { + const lowLevel = analyserLowLevelPSDElem.val() / 100; + GraphSpectrumPlot.setLowLevelPSD(lowLevel); that.refresh(); }) ) @@ -253,6 +272,10 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { "onlyFullScreenException", !psdHeatMapSelected ); + analyserLowLevelPSDElem.toggleClass( + "onlyFullScreenException", + !psdHeatMapSelected + ); $("#spectrumComparison").css("visibility", (optionSelected == 0 ? "visible" : "hidden")); }) diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index 7db88499..fab9f3d4 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -51,6 +51,7 @@ export const GraphSpectrumPlot = window.GraphSpectrumPlot || { _zoomX: 1.0, _zoomY: 1.0, _shiftPSD: 0, + _lowLevelPSD: 0, _drawingParams: { fontSizeFrameLabel: "6", fontSizeFrameLabelFullscreen: "9", @@ -89,6 +90,15 @@ GraphSpectrumPlot.setShiftPSD = function (shift) { } }; +GraphSpectrumPlot.setLowLevelPSD = function (lowLevel) { + const modifiedLevel = this._lowLevelPSD !== lowLevel; + if (modifiedLevel) { + this._lowLevelPSD = lowLevel; + this._invalidateCache(); + this._invalidateDataCache(); + } +}; + GraphSpectrumPlot.setSize = function (width, height) { this._canvasCtx.canvas.width = width; this._canvasCtx.canvas.height = height; @@ -520,19 +530,24 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { const fftColorScale = 100 / (this._zoomY * SCALE_HEATMAP); const dBmValueMin = MIN_DBM_VALUE + this._shiftPSD, - dBmValueMax = MAX_DBM_VALUE + this._shiftPSD; + dBmValueMax = MAX_DBM_VALUE + this._shiftPSD, + lowLevel = dBmValueMin + (dBmValueMax - dBmValueMin) * this._lowLevelPSD; // Loop for throttle for (let j = 0; j < THROTTLE_VALUES_SIZE; j++) { // Loop for frequency for (let i = 0; i < this._fftData.fftLength; i++) { - let valuePlot; + let valuePlot = this._fftData.fftOutput[j][i]; if (drawPSD) { - valuePlot = Math.max(this._fftData.fftOutput[j][i], dBmValueMin); + if (valuePlot < lowLevel) { + valuePlot = dBmValueMin; // Filter low values + } else { + valuePlot = Math.max(valuePlot, dBmValueMin); + } valuePlot = Math.min(valuePlot, dBmValueMax); valuePlot = Math.round((valuePlot - dBmValueMin) * 100 / (dBmValueMax - dBmValueMin)); } else { valuePlot = Math.round( - Math.min(this._fftData.fftOutput[j][i] * fftColorScale, 100) + Math.min(valuePlot * fftColorScale, 100) ); } From 47660a94f4314896863df43335070935223910ac Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 23 May 2025 12:29:54 +0300 Subject: [PATCH 43/63] Code refactoring --- src/graph_spectrum.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 9de8a3e7..82d1b007 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -24,18 +24,18 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { try { let isFullscreen = false; - let sysConfig = flightLog.getSysConfig(); + const sysConfig = flightLog.getSysConfig(); const logRateInfo = GraphSpectrumCalc.initialize(flightLog, sysConfig); GraphSpectrumPlot.initialize(analyserCanvas, sysConfig); GraphSpectrumPlot.setLogRateWarningInfo(logRateInfo); - let analyserZoomXElem = $("#analyserZoomX"); - let analyserZoomYElem = $("#analyserZoomY"); + const analyserZoomXElem = $("#analyserZoomX"); + const analyserZoomYElem = $("#analyserZoomY"); const analyserShiftPSDElem = $("#analyserShiftPSD"); const analyserLowLevelPSDElem = $("#analyserLowLevelPSD"); - let spectrumToolbarElem = $("#spectrumToolbar"); - let spectrumTypeElem = $("#spectrumTypeSelect"); - let overdrawSpectrumTypeElem = $("#overdrawSpectrumTypeSelect"); + const spectrumToolbarElem = $("#spectrumToolbar"); + const spectrumTypeElem = $("#spectrumTypeSelect"); + const overdrawSpectrumTypeElem = $("#overdrawSpectrumTypeSelect"); this.setFullscreen = function (size) { isFullscreen = size == true; @@ -73,13 +73,13 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { }; this.resize = function () { - let newSize = getSize(); + const newSize = getSize(); // Determine the analyserCanvas location GraphSpectrumPlot.setSize(newSize.width, newSize.height); // Recenter the analyser canvas in the bottom left corner - let parentElem = $(analyserCanvas).parent(); + const parentElem = $(analyserCanvas).parent(); $(parentElem).css({ left: newSize.left, // (canvas.width * getSize().left) + "px", @@ -288,7 +288,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { GraphSpectrumPlot.setOverdraw(userSettings.overdrawSpectrumType); overdrawSpectrumTypeElem.change(function () { - let optionSelected = parseInt(overdrawSpectrumTypeElem.val(), 10); + const optionSelected = parseInt(overdrawSpectrumTypeElem.val(), 10); if (optionSelected != userSettings.overdrawSpectrumType) { userSettings.overdrawSpectrumType = optionSelected; @@ -312,9 +312,9 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { // Hide the combo and maximize buttons spectrumToolbarElem.removeClass("non-shift"); - let rect = analyserCanvas.getBoundingClientRect(); - let mouseX = e.clientX - rect.left; - let mouseY = e.clientY - rect.top; + const rect = analyserCanvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; if (mouseX != lastMouseX || mouseY != lastMouseY) { lastMouseX = mouseX; lastMouseY = mouseY; From 76cbc3c1e51784937e50eb6ae90fa8a175e450b6 Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 23 May 2025 15:04:50 +0300 Subject: [PATCH 44/63] Added text label for the PSD vertical sliders --- index.html | 6 +++++ src/css/main.css | 21 +++++++++++++++ src/graph_spectrum.js | 53 +++++++++++++++++++++++++++++++------- src/graph_spectrum_plot.js | 7 ++--- 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index 9344add7..c3f81869 100644 --- a/index.html +++ b/index.html @@ -497,6 +497,12 @@

Workspace

/> + + + diff --git a/src/css/main.css b/src/css/main.css index 8d2fced3..27b9986e 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -695,6 +695,27 @@ html.has-analyser-fullscreen.has-analyser top: 30px; } +.analyser input#analyserMaxPSD { + width: 30px; + height: 18px; + left: 0px; + top: 30px; +} + +.analyser input#analyserMinPSD { + width: 30px; + height: 18px; + left: 0px; + top: 110px; +} + +.analyser input#analyserLowPSD { + width: 40px; + height: 18px; + left: 0px; + top: 70px; +} + .analyser input.onlyFullScreen { display: none; padding: 3px; diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 82d1b007..8ba40f50 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -4,6 +4,8 @@ import { GraphSpectrumPlot, SPECTRUM_TYPE, SPECTRUM_OVERDRAW_TYPE, + MIN_DBM_VALUE, + MAX_DBM_VALUE, } from "./graph_spectrum_plot"; import { PrefStorage } from "./pref_storage"; import { SpectrumExporter } from "./spectrum-exporter"; @@ -30,8 +32,11 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { GraphSpectrumPlot.setLogRateWarningInfo(logRateInfo); const analyserZoomXElem = $("#analyserZoomX"); const analyserZoomYElem = $("#analyserZoomY"); - const analyserShiftPSDElem = $("#analyserShiftPSD"); - const analyserLowLevelPSDElem = $("#analyserLowLevelPSD"); + const analyserShiftPSDSlider = $("#analyserShiftPSD"); + const analyserLowLevelPSDSlider = $("#analyserLowLevelPSD"); + const analyserMinPSDText = $("#analyserMinPSD"); + const analyserMaxPSDText = $("#analyserMaxPSD"); + const analyserLowPSDText = $("#analyserLowPSD"); const spectrumToolbarElem = $("#spectrumToolbar"); const spectrumTypeElem = $("#spectrumTypeSelect"); @@ -101,6 +106,15 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { $("#analyserResize", parentElem).css({ left: `${newSize.width - 20}px`, }); + $("#analyserMaxPSD", parentElem).css({ + left: `${newSize.width - 50}px`, + }); + $("#analyserMinPSD", parentElem).css({ + left: `${newSize.width - 50}px`, + }); + $("#analyserLowPSD", parentElem).css({ + left: `${newSize.width - 120}px`, + }); }; const dataLoad = function (fieldIndex, curve, fieldName) { @@ -210,13 +224,15 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { }) .val(DEFAULT_ZOOM); - analyserShiftPSDElem + analyserShiftPSDSlider .on( "input", debounce(100, function () { - const shift = -parseInt(analyserShiftPSDElem.val()); + const shift = -parseInt(analyserShiftPSDSlider.val()); GraphSpectrumPlot.setShiftPSD(shift); - analyserLowLevelPSDElem.val(0).trigger("input"); + analyserLowLevelPSDSlider.val(0).trigger("input"); + analyserMinPSDText.val(-40 + shift); + analyserMaxPSDText.val(10 + shift); that.refresh(); }) ) @@ -225,12 +241,17 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { }) .val(0); - analyserLowLevelPSDElem + analyserLowLevelPSDSlider .on( "input", debounce(100, function () { - const lowLevel = analyserLowLevelPSDElem.val() / 100; - GraphSpectrumPlot.setLowLevelPSD(lowLevel); + const lowLevelPercent = analyserLowLevelPSDSlider.val(); + GraphSpectrumPlot.setLowLevelPSD(lowLevelPercent); + const shift = -parseInt(analyserShiftPSDSlider.val()); + const dBmValueMin = MIN_DBM_VALUE + shift, + dBmValueMax = MAX_DBM_VALUE + shift, + lowLevel = dBmValueMin + (dBmValueMax - dBmValueMin) * lowLevelPercent / 100; + analyserLowPSDText.val(lowLevel); that.refresh(); }) ) @@ -268,11 +289,23 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { "onlyFullScreenException", pidErrorVsSetpointSelected || psdHeatMapSelected ); - analyserShiftPSDElem.toggleClass( + analyserShiftPSDSlider.toggleClass( + "onlyFullScreenException", + !psdHeatMapSelected + ); + analyserLowLevelPSDSlider.toggleClass( + "onlyFullScreenException", + !psdHeatMapSelected + ); + analyserMinPSDText.toggleClass( + "onlyFullScreenException", + !psdHeatMapSelected + ); + analyserMaxPSDText.toggleClass( "onlyFullScreenException", !psdHeatMapSelected ); - analyserLowLevelPSDElem.toggleClass( + analyserLowPSDText.toggleClass( "onlyFullScreenException", !psdHeatMapSelected ); diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index fab9f3d4..bb5808c5 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -12,10 +12,11 @@ const BLUR_FILTER_PIXEL = 1, MAX_SETPOINT_DEFAULT = 100, PID_ERROR_VERTICAL_CHUNK = 5, ZOOM_X_MAX = 5, - MIN_DBM_VALUE = -40, - MAX_DBM_VALUE = 10, MAX_SPECTRUM_LINE_COUNT = 30000; +export const MIN_DBM_VALUE = -40, + MAX_DBM_VALUE = 10; + export const SPECTRUM_TYPE = { FREQUENCY: 0, FREQ_VS_THROTTLE: 1, @@ -531,7 +532,7 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { const dBmValueMin = MIN_DBM_VALUE + this._shiftPSD, dBmValueMax = MAX_DBM_VALUE + this._shiftPSD, - lowLevel = dBmValueMin + (dBmValueMax - dBmValueMin) * this._lowLevelPSD; + lowLevel = dBmValueMin + (dBmValueMax - dBmValueMin) * this._lowLevelPSD / 100; // Loop for throttle for (let j = 0; j < THROTTLE_VALUES_SIZE; j++) { // Loop for frequency From 0e50e64dbc16f4741faa46b671eb4ad094bb5742 Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 23 May 2025 17:27:49 +0300 Subject: [PATCH 45/63] Code refactoring --- src/graph_spectrum.js | 12 ++++-------- src/graph_spectrum_plot.js | 4 +--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 8ba40f50..77e10d9c 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -235,11 +235,9 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { analyserMaxPSDText.val(10 + shift); that.refresh(); }) - ) - .dblclick(function () { + ).dblclick(function () { $(this).val(0).trigger("input"); - }) - .val(0); + }).val(0); analyserLowLevelPSDSlider .on( @@ -254,11 +252,9 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { analyserLowPSDText.val(lowLevel); that.refresh(); }) - ) - .dblclick(function () { + ).dblclick(function () { $(this).val(0).trigger("input"); - }) - .val(0); + }).val(0); // Spectrum type to show userSettings.spectrumType = diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index bb5808c5..d666aa48 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -547,9 +547,7 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { valuePlot = Math.min(valuePlot, dBmValueMax); valuePlot = Math.round((valuePlot - dBmValueMin) * 100 / (dBmValueMax - dBmValueMin)); } else { - valuePlot = Math.round( - Math.min(valuePlot * fftColorScale, 100) - ); + valuePlot = Math.round(Math.min(valuePlot * fftColorScale, 100)); } // The fillStyle is slow, but I haven't found a way to do this faster... From 7024a547ea5285da37f93875a1f8d4298fa40e52 Mon Sep 17 00:00:00 2001 From: demvlad Date: Sun, 25 May 2025 17:37:21 +0300 Subject: [PATCH 46/63] replaced PSD dBm values labels --- src/css/main.css | 8 ++++---- src/graph_spectrum.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/css/main.css b/src/css/main.css index 27b9986e..13029117 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -684,7 +684,7 @@ html.has-analyser-fullscreen.has-analyser height: 100px; -webkit-appearance: slider-vertical; left: 0px; - top: 30px; + top: 48px; } .analyser input#analyserLowLevelPSD { @@ -692,7 +692,7 @@ html.has-analyser-fullscreen.has-analyser height: 100px; -webkit-appearance: slider-vertical; left: 0px; - top: 30px; + top: 48px; } .analyser input#analyserMaxPSD { @@ -706,14 +706,14 @@ html.has-analyser-fullscreen.has-analyser width: 30px; height: 18px; left: 0px; - top: 110px; + top: 150px; } .analyser input#analyserLowPSD { width: 40px; height: 18px; left: 0px; - top: 70px; + top: 150px; } .analyser input.onlyFullScreen { diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 77e10d9c..918493ee 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -107,13 +107,13 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { left: `${newSize.width - 20}px`, }); $("#analyserMaxPSD", parentElem).css({ - left: `${newSize.width - 50}px`, + left: `${newSize.width - 30}px`, }); $("#analyserMinPSD", parentElem).css({ - left: `${newSize.width - 50}px`, + left: `${newSize.width - 30}px`, }); $("#analyserLowPSD", parentElem).css({ - left: `${newSize.width - 120}px`, + left: `${newSize.width - 145}px`, }); }; From 25af58f68312e652ba265305d2df1b63b11966bf Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 26 May 2025 09:32:06 +0300 Subject: [PATCH 47/63] Replaced dBm range shift slider at PSD heat map charts --- src/graph_spectrum.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 918493ee..15dfd68c 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -98,22 +98,22 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { left: `${newSize.width - 20}px`, }); $("#analyserShiftPSD", parentElem).css({ - left: `${newSize.width - 20}px`, + left: `${newSize.width - 60}px`, }); $("#analyserLowLevelPSD", parentElem).css({ - left: `${newSize.width - 130}px`, + left: `${newSize.width - 110}px`, }); $("#analyserResize", parentElem).css({ left: `${newSize.width - 20}px`, }); $("#analyserMaxPSD", parentElem).css({ - left: `${newSize.width - 30}px`, + left: `${newSize.width - 70}px`, }); $("#analyserMinPSD", parentElem).css({ - left: `${newSize.width - 30}px`, + left: `${newSize.width - 70}px`, }); $("#analyserLowPSD", parentElem).css({ - left: `${newSize.width - 145}px`, + left: `${newSize.width - 125}px`, }); }; From 1a8e681f8ad8eebf715e9bf818363a3e5a3fc061 Mon Sep 17 00:00:00 2001 From: demvlad Date: Mon, 26 May 2025 19:14:54 +0300 Subject: [PATCH 48/63] Code refactoring: use two different function to compute frequency by RPM and PSD by RPM --- src/graph_spectrum.js | 2 +- src/graph_spectrum_calc.js | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 15dfd68c..d9625759 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -136,7 +136,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { break; case SPECTRUM_TYPE.PSD_VS_RPM: - fftData = GraphSpectrumCalc.dataLoadFrequencyVsRpm(true); + fftData = GraphSpectrumCalc.dataLoadPowerSpectralDensityVsRpm(); break; case SPECTRUM_TYPE.PIDERROR_VS_SETPOINT: diff --git a/src/graph_spectrum_calc.js b/src/graph_spectrum_calc.js index 4ad77bd9..4f2b2026 100644 --- a/src/graph_spectrum_calc.js +++ b/src/graph_spectrum_calc.js @@ -308,9 +308,15 @@ GraphSpectrumCalc.dataLoadPowerSpectralDensityVsThrottle = function() { return this._dataLoadPowerSpectralDensityVsX(FIELD_THROTTLE_NAME, 0, 100); }; -GraphSpectrumCalc.dataLoadFrequencyVsRpm = function(drawPSD = false) { - const fftData = drawPSD ? this._dataLoadPowerSpectralDensityVsX(FIELD_RPM_NAMES, 0) : - this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); +GraphSpectrumCalc.dataLoadFrequencyVsRpm = function() { + const fftData = this._dataLoadFrequencyVsX(FIELD_RPM_NAMES, 0); + fftData.vsRange.max *= 3.333 / this._motorPoles; + fftData.vsRange.min *= 3.333 / this._motorPoles; + return fftData; +}; + +GraphSpectrumCalc.dataLoadPowerSpectralDensityVsRpm = function() { + const fftData = this._dataLoadPowerSpectralDensityVsX(FIELD_RPM_NAMES, 0); fftData.vsRange.max *= 3.333 / this._motorPoles; fftData.vsRange.min *= 3.333 / this._motorPoles; return fftData; From d3de155f779a88dad77349cc2c797e8830f0e0e1 Mon Sep 17 00:00:00 2001 From: Vladimir Demidov Date: Wed, 28 May 2025 21:11:06 +0300 Subject: [PATCH 49/63] Resolved Missing trailing comma issue --- src/graph_spectrum.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index d9625759..9874aceb 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -283,27 +283,27 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { overdrawSpectrumTypeElem.toggle(!pidErrorVsSetpointSelected); analyserZoomYElem.toggleClass( "onlyFullScreenException", - pidErrorVsSetpointSelected || psdHeatMapSelected + pidErrorVsSetpointSelected || psdHeatMapSelected, ); analyserShiftPSDSlider.toggleClass( "onlyFullScreenException", - !psdHeatMapSelected + !psdHeatMapSelected, ); analyserLowLevelPSDSlider.toggleClass( "onlyFullScreenException", - !psdHeatMapSelected + !psdHeatMapSelected, ); analyserMinPSDText.toggleClass( "onlyFullScreenException", - !psdHeatMapSelected + !psdHeatMapSelected, ); analyserMaxPSDText.toggleClass( "onlyFullScreenException", - !psdHeatMapSelected + !psdHeatMapSelected, ); analyserLowPSDText.toggleClass( "onlyFullScreenException", - !psdHeatMapSelected + !psdHeatMapSelected, ); $("#spectrumComparison").css("visibility", (optionSelected == 0 ? "visible" : "hidden")); From 1e4f619eab89781c03a8775b83faba62300417e5 Mon Sep 17 00:00:00 2001 From: Vladimir Demidov Date: Wed, 28 May 2025 21:16:54 +0300 Subject: [PATCH 50/63] Resolved missing trailing coma issue --- src/graph_spectrum.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index 9874aceb..dcb9336f 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -234,7 +234,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { analyserMinPSDText.val(-40 + shift); analyserMaxPSDText.val(10 + shift); that.refresh(); - }) + }), ).dblclick(function () { $(this).val(0).trigger("input"); }).val(0); @@ -251,7 +251,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { lowLevel = dBmValueMin + (dBmValueMax - dBmValueMin) * lowLevelPercent / 100; analyserLowPSDText.val(lowLevel); that.refresh(); - }) + }), ).dblclick(function () { $(this).val(0).trigger("input"); }).val(0); From 86a23fcb65a34c5694affc3ed86bfa2cff187e05 Mon Sep 17 00:00:00 2001 From: demvlad Date: Wed, 28 May 2025 14:55:45 +0300 Subject: [PATCH 51/63] Added full dBm range min and max values control by using spin number input fields --- index.html | 19 +++++--- src/css/main.css | 71 +++++++++++++++++++++-------- src/graph_spectrum.js | 93 ++++++++++++++++++++++---------------- src/graph_spectrum_plot.js | 42 +++++++++-------- 4 files changed, 141 insertions(+), 84 deletions(-) diff --git a/index.html b/index.html index c3f81869..4437528e 100644 --- a/index.html +++ b/index.html @@ -493,16 +493,21 @@

Workspace

/> - - - - - + + + diff --git a/src/css/main.css b/src/css/main.css index 13029117..c807e6b5 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -543,6 +543,12 @@ html.has-analyser-fullscreen.has-analyser display: block; } +html.has-analyser-fullscreen.has-analyser + .analyser + label:not(.onlyFullScreenException) { + display: block; +} + #analyser, #log-seek-bar { z-index: 10; @@ -679,41 +685,60 @@ html.has-analyser-fullscreen.has-analyser top: 30px; } -.analyser input#analyserShiftPSD { - width: 10px; - height: 100px; - -webkit-appearance: slider-vertical; - left: 0px; - top: 48px; +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; + opacity: 1; } .analyser input#analyserLowLevelPSD { - width: 10px; - height: 100px; - -webkit-appearance: slider-vertical; + width: 50px; + height: 20px; left: 0px; - top: 48px; + top: 90px; } .analyser input#analyserMaxPSD { - width: 30px; - height: 18px; + width: 50px; + height: 20px; left: 0px; top: 30px; } .analyser input#analyserMinPSD { - width: 30px; - height: 18px; + width: 50px; + height: 20px; left: 0px; - top: 150px; + top: 60px; } -.analyser input#analyserLowPSD { - width: 40px; - height: 18px; +.analyser label#analyserMaxPSDLabel { + position:absolute; + color:gray; + + left: 0px; + top: 30px; + font-size: 12px; +} + +.analyser label#analyserMinPSDLabel { + position:absolute; + color:gray; + + left: 0px; + top: 60px; + font-size: 12px; +} + +.analyser label#analyserLowLevelPSDLabel { + position:absolute; + color:gray; + left: 0px; - top: 150px; + top: 90px; + font-size: 12px; } .analyser input.onlyFullScreen { @@ -724,6 +749,14 @@ html.has-analyser-fullscreen.has-analyser position: absolute; } +.analyser label.onlyFullScreen { + display: none; + padding: 3px; + margin-right: 3px; + z-index: 9; + position: absolute; +} + .analyser, .map-container, .log-seek-bar { diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index dcb9336f..ae6656bb 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -4,8 +4,8 @@ import { GraphSpectrumPlot, SPECTRUM_TYPE, SPECTRUM_OVERDRAW_TYPE, - MIN_DBM_VALUE, - MAX_DBM_VALUE, + DEFAULT_MIN_DBM_VALUE, + DEFAULT_MAX_DBM_VALUE, } from "./graph_spectrum_plot"; import { PrefStorage } from "./pref_storage"; import { SpectrumExporter } from "./spectrum-exporter"; @@ -32,11 +32,10 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { GraphSpectrumPlot.setLogRateWarningInfo(logRateInfo); const analyserZoomXElem = $("#analyserZoomX"); const analyserZoomYElem = $("#analyserZoomY"); - const analyserShiftPSDSlider = $("#analyserShiftPSD"); - const analyserLowLevelPSDSlider = $("#analyserLowLevelPSD"); - const analyserMinPSDText = $("#analyserMinPSD"); - const analyserMaxPSDText = $("#analyserMaxPSD"); - const analyserLowPSDText = $("#analyserLowPSD"); + const analyserMinPSD = $("#analyserMinPSD"); + const analyserMaxPSD = $("#analyserMaxPSD"); + const analyserLowLevelPSD = $("#analyserLowLevelPSD"); + const spectrumToolbarElem = $("#spectrumToolbar"); const spectrumTypeElem = $("#spectrumTypeSelect"); @@ -97,23 +96,26 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { $("#analyserZoomY", parentElem).css({ left: `${newSize.width - 20}px`, }); - $("#analyserShiftPSD", parentElem).css({ - left: `${newSize.width - 60}px`, - }); - $("#analyserLowLevelPSD", parentElem).css({ - left: `${newSize.width - 110}px`, - }); $("#analyserResize", parentElem).css({ left: `${newSize.width - 20}px`, }); $("#analyserMaxPSD", parentElem).css({ - left: `${newSize.width - 70}px`, + left: `${newSize.width - 90}px`, }); $("#analyserMinPSD", parentElem).css({ - left: `${newSize.width - 70}px`, + left: `${newSize.width - 90}px`, }); - $("#analyserLowPSD", parentElem).css({ - left: `${newSize.width - 125}px`, + $("#analyserLowLevelPSD", parentElem).css({ + left: `${newSize.width - 90}px`, + }); + $("#analyserMaxPSDLabel", parentElem).css({ + left: `${newSize.width - 150}px`, + }); + $("#analyserMinPSDLabel", parentElem).css({ + left: `${newSize.width - 150}px`, + }); + $("#analyserLowLevelPSDLabel", parentElem).css({ + left: `${newSize.width - 155}px`, }); }; @@ -224,37 +226,44 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { }) .val(DEFAULT_ZOOM); - analyserShiftPSDSlider + analyserMinPSD .on( "input", debounce(100, function () { - const shift = -parseInt(analyserShiftPSDSlider.val()); - GraphSpectrumPlot.setShiftPSD(shift); - analyserLowLevelPSDSlider.val(0).trigger("input"); - analyserMinPSDText.val(-40 + shift); - analyserMaxPSDText.val(10 + shift); + const min = parseInt(analyserMinPSD.val()); + GraphSpectrumPlot.setMinPSD(min); + analyserLowLevelPSD.val(min).trigger("input"); + analyserLowLevelPSD.prop("min", min); that.refresh(); }), ).dblclick(function () { - $(this).val(0).trigger("input"); - }).val(0); + $(this).val(DEFAULT_MIN_DBM_VALUE).trigger("input"); + }).val(DEFAULT_MIN_DBM_VALUE); - analyserLowLevelPSDSlider + analyserMaxPSD .on( "input", debounce(100, function () { - const lowLevelPercent = analyserLowLevelPSDSlider.val(); - GraphSpectrumPlot.setLowLevelPSD(lowLevelPercent); - const shift = -parseInt(analyserShiftPSDSlider.val()); - const dBmValueMin = MIN_DBM_VALUE + shift, - dBmValueMax = MAX_DBM_VALUE + shift, - lowLevel = dBmValueMin + (dBmValueMax - dBmValueMin) * lowLevelPercent / 100; - analyserLowPSDText.val(lowLevel); + const max = parseInt(analyserMaxPSD.val()); + GraphSpectrumPlot.setMaxPSD(max); + analyserLowLevelPSD.prop("max", max); that.refresh(); }), ).dblclick(function () { - $(this).val(0).trigger("input"); - }).val(0); + $(this).val(DEFAULT_MAX_DBM_VALUE).trigger("input"); + }).val(DEFAULT_MAX_DBM_VALUE); + + analyserLowLevelPSD + .on( + "input", + debounce(100, function () { + const lowLevel = analyserLowLevelPSD.val(); + GraphSpectrumPlot.setLowLevelPSD(lowLevel); + that.refresh(); + }) + ).dblclick(function () { + $(this).val(analyserMinPSD.val()).trigger("input"); + }).val(analyserMinPSD.val()); // Spectrum type to show userSettings.spectrumType = @@ -285,23 +294,27 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { "onlyFullScreenException", pidErrorVsSetpointSelected || psdHeatMapSelected, ); - analyserShiftPSDSlider.toggleClass( + analyserLowLevelPSD.toggleClass( "onlyFullScreenException", !psdHeatMapSelected, ); - analyserLowLevelPSDSlider.toggleClass( + analyserMinPSD.toggleClass( "onlyFullScreenException", !psdHeatMapSelected, ); - analyserMinPSDText.toggleClass( + analyserMaxPSD.toggleClass( "onlyFullScreenException", !psdHeatMapSelected, ); - analyserMaxPSDText.toggleClass( + $("#analyserMaxPSDLabel").toggleClass( "onlyFullScreenException", !psdHeatMapSelected, ); - analyserLowPSDText.toggleClass( + $("#analyserMinPSDLabel").toggleClass( + "onlyFullScreenException", + !psdHeatMapSelected + ); + $("#analyserLowLevelPSDLabel").toggleClass( "onlyFullScreenException", !psdHeatMapSelected, ); diff --git a/src/graph_spectrum_plot.js b/src/graph_spectrum_plot.js index d666aa48..bd53efc9 100644 --- a/src/graph_spectrum_plot.js +++ b/src/graph_spectrum_plot.js @@ -14,8 +14,8 @@ const BLUR_FILTER_PIXEL = 1, ZOOM_X_MAX = 5, MAX_SPECTRUM_LINE_COUNT = 30000; -export const MIN_DBM_VALUE = -40, - MAX_DBM_VALUE = 10; +export const DEFAULT_MIN_DBM_VALUE = -40, + DEFAULT_MAX_DBM_VALUE = 10; export const SPECTRUM_TYPE = { FREQUENCY: 0, @@ -51,8 +51,9 @@ export const GraphSpectrumPlot = window.GraphSpectrumPlot || { _sysConfig: null, _zoomX: 1.0, _zoomY: 1.0, - _shiftPSD: 0, - _lowLevelPSD: 0, + _minPSD: DEFAULT_MIN_DBM_VALUE, + _maxPSD: DEFAULT_MAX_DBM_VALUE, + _lowLevelPSD: DEFAULT_MIN_DBM_VALUE, _drawingParams: { fontSizeFrameLabel: "6", fontSizeFrameLabelFullscreen: "9", @@ -82,10 +83,19 @@ GraphSpectrumPlot.setZoom = function (zoomX, zoomY) { } }; -GraphSpectrumPlot.setShiftPSD = function (shift) { - const modifiedShift = this._shiftPSD !== shift; - if (modifiedShift) { - this._shiftPSD = shift; +GraphSpectrumPlot.setMinPSD = function (min) { + const modified = this._minPSD !== min; + if (modified) { + this._minPSD = min; + this._invalidateCache(); + this._invalidateDataCache(); + } +}; + +GraphSpectrumPlot.setMaxPSD = function (max) { + const modified = this._maxPSD !== max; + if (modified) { + this._maxPSD = max; this._invalidateCache(); this._invalidateDataCache(); } @@ -528,25 +538,21 @@ GraphSpectrumPlot._drawHeatMap = function (drawPSD = false) { canvasCtx.canvas.width = this._fftData.fftLength; canvasCtx.canvas.height = THROTTLE_VALUES_SIZE; - const fftColorScale = 100 / (this._zoomY * SCALE_HEATMAP); - - const dBmValueMin = MIN_DBM_VALUE + this._shiftPSD, - dBmValueMax = MAX_DBM_VALUE + this._shiftPSD, - lowLevel = dBmValueMin + (dBmValueMax - dBmValueMin) * this._lowLevelPSD / 100; // Loop for throttle for (let j = 0; j < THROTTLE_VALUES_SIZE; j++) { // Loop for frequency for (let i = 0; i < this._fftData.fftLength; i++) { let valuePlot = this._fftData.fftOutput[j][i]; if (drawPSD) { - if (valuePlot < lowLevel) { - valuePlot = dBmValueMin; // Filter low values + if (valuePlot < this._lowLevelPSD) { + valuePlot = this._minPSD; // Filter low values } else { - valuePlot = Math.max(valuePlot, dBmValueMin); + valuePlot = Math.max(valuePlot, this._minPSD); } - valuePlot = Math.min(valuePlot, dBmValueMax); - valuePlot = Math.round((valuePlot - dBmValueMin) * 100 / (dBmValueMax - dBmValueMin)); + valuePlot = Math.min(valuePlot, this._maxPSD); + valuePlot = Math.round((valuePlot - this._minPSD) * 100 / (this._maxPSD - this._minPSD)); } else { + const fftColorScale = 100 / (this._zoomY * SCALE_HEATMAP); valuePlot = Math.round(Math.min(valuePlot * fftColorScale, 100)); } From 6f9c1ffc51c69b96b57d68ce1da0517b30a0a3f0 Mon Sep 17 00:00:00 2001 From: demvlad Date: Thu, 29 May 2025 17:07:37 +0300 Subject: [PATCH 52/63] Resolved missing trailing coma issues --- src/graph_spectrum.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index ae6656bb..cf58042c 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -260,7 +260,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { const lowLevel = analyserLowLevelPSD.val(); GraphSpectrumPlot.setLowLevelPSD(lowLevel); that.refresh(); - }) + }), ).dblclick(function () { $(this).val(analyserMinPSD.val()).trigger("input"); }).val(analyserMinPSD.val()); @@ -312,7 +312,7 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { ); $("#analyserMinPSDLabel").toggleClass( "onlyFullScreenException", - !psdHeatMapSelected + !psdHeatMapSelected, ); $("#analyserLowLevelPSDLabel").toggleClass( "onlyFullScreenException", From 7b034ac08e6c095f244f2de32cf1af10ce97a515 Mon Sep 17 00:00:00 2001 From: demvlad Date: Thu, 29 May 2025 17:34:00 +0300 Subject: [PATCH 53/63] The number input fields spin buttons are visible now --- src/css/main.css | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/css/main.css b/src/css/main.css index c807e6b5..805365f0 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -685,12 +685,18 @@ html.has-analyser-fullscreen.has-analyser top: 30px; } -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: textfield; - -moz-appearance: textfield; - appearance: textfield; - opacity: 1; +.analyser input#analyserMinPSD::-webkit-inner-spin-button, +.analyser input#analyserMinPSD::-webkit-outer-spin-button, +.analyser input#analyserMaxPSD::-webkit-inner-spin-button, +.analyser input#analyserMaxPSD::-webkit-outer-spin-button, +.analyser input#analyserLowLevelPSD::-webkit-inner-spin-button, +.analyser input#analyserLowLevelPSD::-webkit-outer-spin-button { + -webkit-appearance: auto !important; + -moz-appearance: auto !important; + appearance: auto !important; + opacity: 1 !important; + height: auto !important; + width: auto !important; } .analyser input#analyserLowLevelPSD { From d7dbd9135df2ff634bda1bba714866f5d2e6c857 Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 30 May 2025 10:43:59 +0300 Subject: [PATCH 54/63] Added Ctrl key for double mouse click to set PSD range values as default --- src/graph_spectrum.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/graph_spectrum.js b/src/graph_spectrum.js index cf58042c..8486baf3 100644 --- a/src/graph_spectrum.js +++ b/src/graph_spectrum.js @@ -236,8 +236,10 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { analyserLowLevelPSD.prop("min", min); that.refresh(); }), - ).dblclick(function () { - $(this).val(DEFAULT_MIN_DBM_VALUE).trigger("input"); + ).dblclick(function (e) { + if (e.ctrlKey) { + $(this).val(DEFAULT_MIN_DBM_VALUE).trigger("input"); + } }).val(DEFAULT_MIN_DBM_VALUE); analyserMaxPSD @@ -249,8 +251,10 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { analyserLowLevelPSD.prop("max", max); that.refresh(); }), - ).dblclick(function () { - $(this).val(DEFAULT_MAX_DBM_VALUE).trigger("input"); + ).dblclick(function (e) { + if (e.ctrlKey) { + $(this).val(DEFAULT_MAX_DBM_VALUE).trigger("input"); + } }).val(DEFAULT_MAX_DBM_VALUE); analyserLowLevelPSD @@ -261,8 +265,10 @@ export function FlightLogAnalyser(flightLog, canvas, analyserCanvas) { GraphSpectrumPlot.setLowLevelPSD(lowLevel); that.refresh(); }), - ).dblclick(function () { - $(this).val(analyserMinPSD.val()).trigger("input"); + ).dblclick(function (e) { + if (e.ctrlKey) { + $(this).val(analyserMinPSD.val()).trigger("input"); + } }).val(analyserMinPSD.val()); // Spectrum type to show From 734fca8e75a4cec3175f5e0a20ae39c98709faae Mon Sep 17 00:00:00 2001 From: demvlad Date: Fri, 30 May 2025 10:52:06 +0300 Subject: [PATCH 55/63] Improved PSD range values validation logic --- index.html | 4 ++-- src/graph_spectrum.js | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 4437528e..c7db87d9 100644 --- a/index.html +++ b/index.html @@ -495,9 +495,9 @@

Workspace

/> - -