/**
* @file infoComputing.js
* @description This file contains every fonction needed to compute a result to display on-screen as numbers or as signals, etc.
*
* Functions included in this file :
* - **getTimeScale**
* - **getTimePerDiv**
* - **mapVoltageToRaw**
* - **mapRawToVoltage**
* - **getTimeBetweenCursors**
* - **getMillivoltsBetweenCursors**
* - **getMilliVoltForACursor**
* - **getMilliVoltsRelativeToTriggerCursor**
* - **getTimeForACursor**
* - **setScreenInformation**
* - **generatePoints**
* - **calculateAutoMeasures**
* - **getMilliVoltsPerDiv**
* - **updateGeneratedMathSignalsData**
* - **toggleMeasurement**
* - **resetMeasurements**
* - **getPositionRelativeToTriggerCursor**
* - **updateTriggerSettings**
* - **calculateZoomFactors**
*
* @version 1.0.0
* @since 2024-05-31
* @author Owen Pichot
*
* @license Public Domain
*/
/**
* @module Computing
*/
/**
* This function takes a time in seconds and scales it to something that makes more sense depending on the value.
*
* @function getTimeScale
* @memberof module:Computing
* @param {number} timeInSeconds Time period in seconds.
* @returns {object} An object with two attributes, 'value' & 'scale'.
* @example
* getTimeScale(0.05);
* //output : { value: 50, scale: "ms" }
* getTimeScale(0.0004);
* //output : { value: 400, scale: "µs" }
* getTimeScale(0.00000154);
* //output : { value: 1.54, scale: "µs" }
*/
function getTimeScale(timeInSeconds){
let scale;
let value;
//convert timeInSeconds to ms as a base unit to simplify the logic of the function
const microseconds = timeInSeconds * 1e6;
if (microseconds < 1) {// nanoseconds
scale = 'ns';
value = (microseconds * 1000).toFixed(2); // convert to nanoseconds
} else if (microseconds < 1000) {// microseconds
scale = 'µs';
value = microseconds.toFixed(2);
} else if (microseconds < 1e6) {// Milliseconds
scale = 'ms';
value = (microseconds / 1000).toFixed(2); // convert microseconds to milliseconds
} else { // Seconds
scale = 's';
value = (microseconds / 1e6).toFixed(2); // convert microseconds to seconds
}
return { value: parseFloat(value), scale: scale };
};
/**
* This function returns how much time represents 1 horizontal division on screen.
*
* @function getTimePerDiv
* @memberof module:Computing
* @returns {object} An object with two attributes, 'value' & 'scale'.
*/
function getTimePerDiv() {
const totalSamplingTime = config.samplesPerFrame * 1e-8;
const timePerDivision = (totalSamplingTime / config.horizontalDivisions) * (horizontalScale / 50);
//we have to divide by 50 because the default value of the input is 50 which corresponds to 1 : no scaling
const resultScaled = getTimeScale(timePerDivision);
return resultScaled;
};
/**
* This function converts a voltage to the equivalent absolute raw value of a 14-bit ADC
* @function mapVoltageToRaw
* @memberof module:Computing
* @param {number} voltage - Voltage to convert.
* @returns {number} The corresponding raw value to the voltage given.
*/
function mapVoltageToRaw(voltage) {
return (voltage + (config.voltage / 2)) / config.voltage * config.maxSampleValue;
};
/**
* This function converts a raw value from a 14-bit ADC to the equivalent voltage.
*
* @function mapRawToVoltage
* @memberof module:Computing
* @param {number} rawValue - Raw value to convert.
* @returns {number} The correponding voltage to the raw value given.
*/
function mapRawToVoltage(rawValue) {
return (rawValue / config.maxSampleValue * config.voltage) - (config.voltage / 2);
}
/**
* This function is used when the user uses the vertical measure cursors on the screen.
* It calculates the time represented between the two vertical measure cursors.
* @function getTimeBetweenCursors
* @memberof module:Computing
* @param {number} pixelsBetweenCursors Number of pixels between the two cursors.
* @returns {number|string} Returns the time value and the corresponding scale.
* @example
* // /!\ Without any horizontal scaling /!\
* getTimeBetweenCursors(542);
* //output : { value: 4.63, scale: "µs" }
* getTimeBetweenCursors(247):
* //output : { value: 2.11, scale: "µs" }
*/
function getTimeBetweenCursors(pixelsBetweenCursors){
const totalSamplingTime = config.samplesPerFrame * 1e-8;
const sizeOfOneDivisionInPixels = CANVAS.width / config.horizontalDivisions;
const DivisionsBetweenCursors = pixelsBetweenCursors / sizeOfOneDivisionInPixels;
const TimeBetweenCursors = totalSamplingTime * (DivisionsBetweenCursors / config.horizontalDivisions) * (horizontalScale / 50);
const resultScaled = getTimeScale(TimeBetweenCursors);
return resultScaled;
};
/**
* This function, similarly to 'getTimeBetweenCursors' gets the voltage represented between the two horizontal measure cursors.
* It takes into account which channel we are currently focused on and its current vertical scaling.
*
* @function getMillivoltsBetweenCursors
* @memberof module:Computing
* @param {number} pixelsBetweenCursors Number of pixels between the two cursors.
* @returns {number|string} Returns the voltage and its associated scale (mv, v, etc).
* @example
* getMillivoltsBetweenCursors(254);
* //output : { value: "698.5", scale: "mV" }
* getMillivoltsBetweenCursors(57);
* //output : { value: "156.8", scale: "mV" }
*/
function getMillivoltsBetweenCursors(pixelsBetweenCursors){
//If a channel is focused then we get the mv/div of this one.
let result = {value: "No channel", scale: "selected"};
Object.keys(channelData).forEach(key => {
if(channelData[key].focused){
const sizeOfOneDivisionInPixels = CANVAS.height / config.verticalDivisions;
const milliVoltsPerDivision = getMilliVoltsPerDiv(channelData[key].verticalScale);
const milliVoltsBetweenCursors = (pixelsBetweenCursors / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
result = {value: milliVoltsBetweenCursors.toFixed(1), scale: "mV"};
}
});
//If not, we just return "NA" until the user selects a certain channel.
return result;
};
/**
* This function returns the equivalent in mV relative to the position of a horizontal measure cursor.
* It also takes into account the vertical scaling of the current channel focused.
* If no channel is focused then the function will just return "No channel Selected".
*
* @function getMilliVoltForACursor
* @memberof module:Computing
* @param {number} cursorPosition Position, in pixels from the top of the screen to the cursor.
* @returns {number|string} Returns the voltage (in mv) equivalent to the position of the cursor.
* @example
* getMilliVoltForACursor(150);
* //output : { value: "687.5", scale: "mV" }
* getMilliVoltForACursor(540);
* //output : { value: -385, scale: "mV" }
* getMilliVoltForACursor(300);//While no channel is focused.
* //output : { value: "No channel", scale: "selected" }
*/
function getMilliVoltForACursor(cursorPosition){
let result = {value: "No channel", scale: "selected"};
Object.keys(channelData).forEach(key => {
if(channelData[key].focused){
const sizeOfOneDivisionInPixels = CANVAS.height / config.verticalDivisions;
const milliVoltsPerDivision = getMilliVoltsPerDiv(channelData[key].verticalScale);
if (cursorPosition == (CANVAS.height / 2)){
result = {value: "0", scale: "mV"};
}else if (cursorPosition > (CANVAS.height / 2)){//negative values
const cursorValue = ((cursorPosition - (CANVAS.height / 2)) / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
result = {value: -cursorValue.toFixed(1), scale: "mV"}
}else if (cursorPosition < (CANVAS.height / 2)){//positive values
const cursorValue = (((CANVAS.height / 2) - cursorPosition) / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
result = {value: cursorValue.toFixed(1), scale: "mV"}
}
}
});
return result;
};
/**
* This fonction returns how much mV represents the current position of the trigger cursor (right of the screen, T icon).
* This function is mostly used when the user moves the trigger cursor to change the current threshold.
*
* @function getMilliVoltsRelativeToTriggerCursor
* @memberof module:Computing
* @param {number} Position Position of the trigger cursor, in pixels, on its scrollbar.
* @returns {number|string} Returns the voltage (in mv) relative to the cursor's position.
* @example
* getMilliVoltsRelativeToTriggerCursor(150);
* //output : { value: 687.5, scale: "mV" }
* getMilliVoltsRelativeToTriggerCursor(430);
* //output : { value: -82.5, scale: "mV" }
*/
function getMilliVoltsRelativeToTriggerCursor(Position){
const triggerChannel = triggerOptions.triggerChannel;
const sizeOfOneDivisionInPixels = CANVAS.height / config.verticalDivisions;
const milliVoltsPerDivision = getMilliVoltsPerDiv(channelData[triggerChannel].verticalScale);
let result;
if (Position == (CANVAS.height / 2)){
result = {value: "0", scale: "mV"};
}else if (Position > (CANVAS.height / 2)){//negative values
const cursorValue = ((Position - (CANVAS.height / 2)) / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
result = {value: -cursorValue.toFixed(1), scale: "mV"}
}else if (Position < (CANVAS.height / 2)){//positive values
const cursorValue = (((CANVAS.height / 2) - Position) / sizeOfOneDivisionInPixels) * milliVoltsPerDivision;
result = {value: cursorValue.toFixed(1), scale: "mV"}
}
return result;
};
/**
* Similarly to 'getMilliVoltForACursor', this function returns the equivalent time relative to the position of a vertical measure cursor.
* It takes into account the global horizontal scaling of the oscilloscope.
*
* @function getTimeForACursor
* @memberof module:Computing
* @param {number} cursorPosition Position, in pixels, from the left of the screen to the cursor.
* @returns {number|string} Returns the time and time-scale relative to the cursor's position.
* @example
* getTimeForACursor(150);
* //output : { value: 1.28, scale: "µs" }
* getTimeForACursor(655);
* //output : { value: 5.59, scale: "µs" }
*/
function getTimeForACursor(cursorPosition){
const totalSamplingTime = config.samplesPerFrame * 1e-8; // Total time for all divisions.
const sizeOfOneDivisionInPixels = CANVAS.width / config.horizontalDivisions; // Pixel width of one division.
// Calculate how many divisions the cursor is from the left of the canvas.
const divisionsFromLeft = cursorPosition / sizeOfOneDivisionInPixels;
// Calculate the time at the cursor position using the proportion of the total time.
const timeAtCursor = totalSamplingTime * (divisionsFromLeft / config.horizontalDivisions) * (horizontalScale / 50);
const resultScaled = getTimeScale(timeAtCursor);
return resultScaled;
};
/**
* This function displays multiple informations below the screen like Mv/div, S/div, etc etc.
* It also displays all the 'auto-measures' selected by the user for a specific channel like vpp, rms, etc.
* This function's job is more about displaying the information on screen and not so much about doing the actual calculations for each value.
*
* @function setScreenInformation
* @memberof module:Computing
* @returns {void}
*/
function setScreenInformation(){
//insert time scale to the screen
const timePerDiv = getTimePerDiv();
document.getElementById('tpdiv-value').innerHTML = timePerDiv.value + ' ' + timePerDiv.scale + '/div';
Object.keys(channelData).forEach(key => {
const voltsPerDiv = getMilliVoltsPerDiv(channelData[key].verticalScale);
let channelNumber = parseInt(key.substring(2), 10);
if (voltsPerDiv > 1000){
document.getElementById('mes-CH' + channelNumber).innerHTML = (voltsPerDiv / 1000).toFixed(2) + ' V/dv';
}else{
document.getElementById('mes-CH' + channelNumber).innerHTML = voltsPerDiv + ' mv/dv';
}
document.getElementById('mes-CH' + channelNumber).style.color = channelData[key].colorDark;
});
if (triggerOptions.isTriggerOn == "on"){
if (triggered){
TRIGGER.style.color = "lightgreen"
}else{
TRIGGER.style.color = "chocolate"
}
}else{
TRIGGER.style.color = "#e2e2e2"//white
}
//This section is about the auto-measures display.
//These are put on screen below the voltages display.
valuesToDisplay = []; // format inside : {text: "textToDisplay", color: "colorOfTheChannel", value: "valueToDisplay"}
let min, max//we declare them here to reuse in case vpp and either min or max are set, it prevent another call to the function.
if (autoMeasureOptions.vpp.set){// get min, max, vpp
min = autoMeasures.getMinValue(channelData[autoMeasureOptions.associatedChannel].points);
max = autoMeasures.getMaxValue(channelData[autoMeasureOptions.associatedChannel].points);
let vpp = autoMeasures.getVppValue(channelData[autoMeasureOptions.associatedChannel].points);
valuesToDisplay.push({
text: "Vpp",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: vpp.toFixed(1) + "mV",
id: "vpp-measure",
});
};
if (autoMeasureOptions.min.set){// get only min
if (min == undefined){
min = autoMeasures.getMinValue(channelData[autoMeasureOptions.associatedChannel].points);
}
valuesToDisplay.push({
text: "Min",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: min.toFixed(1) + "mV",
id: "min-measure",
});
};
if (autoMeasureOptions.max.set){// get only max
if (max == undefined){
max = autoMeasures.getMaxValue(channelData[autoMeasureOptions.associatedChannel].points);
}
valuesToDisplay.push({
text: "Max",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: max.toFixed(1) + "mV",
id: "max-measure",
});
};
if (autoMeasureOptions.mean.set){// get avg value
let mean = autoMeasures.getMeanValue(channelData[autoMeasureOptions.associatedChannel].points);
valuesToDisplay.push({
text: "Avg",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: mean.toFixed(1) + "mV",
id: "avg-measure",
});
};
if (autoMeasureOptions.mid.set){// get middle value
let middle = autoMeasures.getMiddleValue(channelData[autoMeasureOptions.associatedChannel].points);
valuesToDisplay.push({
text: "Mid",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: middle.toFixed(1) + "mV",
id: "mid-measure",
});
};
if (autoMeasureOptions.rms.set){// get rms value
let rms = autoMeasures.getRMS(channelData[autoMeasureOptions.associatedChannel].points);
valuesToDisplay.push({
text: "RMS",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: rms.toFixed(1) + "mV",
id: "rms-measure",
});
};
function formatFrequency(frequency) {
if (frequency >= 1e9) { // gte to 1 Gigahertz
return (frequency / 1e9).toFixed(1) + " GHz";
} else if (frequency >= 1e6) { // gte to 1 Megahertz
return (frequency / 1e6).toFixed(1) + " MHz";
} else if (frequency >= 1e3) { // gte to 1 Kilohertz
return (frequency / 1e3).toFixed(1) + " kHz";
} else {
return frequency.toFixed(1) + " Hz"; // lt 1 Kilohertz, display in Hertz (Very very very unlikely but still, we never know)
}
}
if (autoMeasureOptions.freq.set){// get avg frequency
let freq = autoMeasures.getAverageFrequency(channelData[autoMeasureOptions.associatedChannel].points, config.frequency);
valuesToDisplay.push({
text: "Freq",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: formatFrequency(freq),
id: "avg-freq-measure",
});
};
if (autoMeasureOptions.highFreq.set || autoMeasureOptions.lowFreq.set){// get min and max frequencies
let freqs = autoMeasures.getFrequenciesMaxMin(channelData[autoMeasureOptions.associatedChannel].points, config.frequency);
if (autoMeasureOptions.highFreq.set){// high freq
valuesToDisplay.push({
text: "Max Freq",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: formatFrequency(freqs.highestFrequency),
id: "max-freq-measure",
});
};
if (autoMeasureOptions.lowFreq.set){//low freq
valuesToDisplay.push({
text: "Min Freq",
color: channelData[autoMeasureOptions.associatedChannel].colorDark,
value: formatFrequency(freqs.lowestFrequency),
id: "min-freq-measure",
});
};
};
//now we inject the calculated values to the html page.
const valuesContainer = document.getElementById('auto-measures-display');
valuesContainer.querySelectorAll('p').forEach(p => p.style.display = 'none');
for (let i = 0; i < valuesToDisplay.length; i++) {
let paragraph = document.getElementById(valuesToDisplay[i].id);
paragraph.style.display = 'block';
paragraph.style.color = valuesToDisplay[i].color;
paragraph.style.borderColor = valuesToDisplay[i].color;
paragraph.textContent = valuesToDisplay[i].text + " : " + valuesToDisplay[i].value;
};
};
/**
* This function is responsible for generating the points array for the signals not received directly but generated by the user.
* This goes for all maths functions like +, -, fft, integral, etc.
* Each time this function is called for a certain channel, we check the type of operation to execute, generate the points and return immediately.
* The drawSignal function will then draw the signal like any other.
*
* @function generatePoints
* @memberof module:Computing
* @param {string} channelKey Key to access this signal within 'channelData' (CH1, CH2, etc.).
* @returns {void}
*/
function generatePoints(channelKey){
const originChannel1 = channelData[channelKey].originChannel1;
const originChannel2 = channelData[channelKey].originChannel2;
const operation = channelData[channelKey].operation;
if (operation == "squared"){
const pointsSquared = channelData[originChannel1].points.map(point => {
const voltage = autoMeasures.voltage_from_raw(point);
const squaredVoltage = voltage * voltage;
return parseInt(mapVoltageToRaw(squaredVoltage).toFixed(0));
});
channelData[channelKey].points = pointsSquared;
return
};
if (operation == "deriv"){
function getDerivative(points) {
let derivative = [];
for (let i = 0; i < points.length - 1; i++) {
difference = points[i + 1] - points[i];
//Since the derivative show the difference between two points on the graph we need to add the equivalent of 1/2 the height of the graph
//so that it doesn't start being drawn at the bottom of the screen.
derivative[i] = parseInt((difference + (config.maxSampleValue / 2)).toFixed(0));
}
return derivative;
};
channelData[channelKey].points = getDerivative(channelData[originChannel1].points);
return
}
if (operation == "integral"){
function integrateSignal(points, deltaT) {
const integratedSignal = [];
let integralSum = 0;
for (let i = 0; i < points.length; i++) {
const pointV = mapRawToVoltage(points[i]);
integralSum += pointV * deltaT;
integratedSignal.push(mapVoltageToRaw(integralSum));
}
return integratedSignal;
}
channelData[channelKey].points = integrateSignal(channelData[originChannel1].points, 1);
return
};
if (operation == "fft") {
function fillArrayToNextPowerOfTwo(array) {
// check if the length of the array is already a power of two
function isPowerOfTwo(n) {
return n && (n & (n - 1)) === 0;
}
let targetLength = array.length;
if (!isPowerOfTwo(targetLength)) {
// if not a power of two, find the next power of two
targetLength = Math.pow(2, Math.ceil(Math.log2(targetLength)));
// fill the array with zeros until it reaches the target length
while (array.length < targetLength) {
array.push(0);
}
}
return array;
}
function calculateFFT(points, sampleRate) {
//===============================================================
// FFT calculation - credit : https://github.com/indutny/fft.js/
//===============================================================
const fft = new FFT(points.length);
const complexInput = fft.toComplexArray(points);
const complexOutput = fft.createComplexArray();
fft.realTransform(complexOutput, complexInput);
fft.completeSpectrum(complexOutput);
const magnitudes = complexOutput.map((value, index) => {
if (index % 2 === 0) {
// even index, real part
const re = value;
// odd index, imaginary part (since the array is [re, im, re, im, ...]) => see fft.js doc
const im = complexOutput[index + 1];
return Math.sqrt(re * re + im * im);
}
return null;
})
const displayData = magnitudes.map((magnitude, index) => {
const frequency = index * sampleRate / points.length;
return { frequency: frequency, magnitude: magnitude };
});
const halfDisplayData = displayData.slice(0, displayData.length / 2);
return halfDisplayData;
}
//we have to fill the gaps in the array until the next power of 2 (required by the library)
let tempOriginChannel = channelData[originChannel1];
channelData[channelKey].points = fillArrayToNextPowerOfTwo(tempOriginChannel.points);
channelData[channelKey].points = calculateFFT(channelData[channelKey].points, config.frequency);
channelData[channelKey].verticalScale = 2.5; // We set the vertical scale to 2.5 to make the fft thinner than other signals. This can still be modified by the user oc.
return
};
if (operation == "add"){
const points1 = channelData[originChannel1].points;
const points2 = channelData[originChannel2].points;
const pointsAdded = points1.map((point, index) => {
point = autoMeasures.voltage_from_raw(point);
let point2 = autoMeasures.voltage_from_raw(points2[index]);
return parseInt(mapVoltageToRaw(point + point2).toFixed(0));
});
channelData[channelKey].points = pointsAdded;
};
if (operation == "mult"){
const points1 = channelData[originChannel1].points;
const points2 = channelData[originChannel2].points;
const pointsMultiplied = points1.map((point, index) => {
point = autoMeasures.voltage_from_raw(point);
let point2 = autoMeasures.voltage_from_raw(points2[index]);
return parseInt(mapVoltageToRaw(point * point2).toFixed(0));
});
channelData[channelKey].points = pointsMultiplied;
};
if (operation == "sub"){
const points1 = channelData[originChannel1].points;
const points2 = channelData[originChannel2].points;
const pointsSubtracted = points1.map((point, index) => {
point = autoMeasures.voltage_from_raw(point);
let point2 = autoMeasures.voltage_from_raw(points2[index]);
return parseInt(mapVoltageToRaw(point - point2).toFixed(0));
});
channelData[channelKey].points = pointsSubtracted;
};
if (operation == "div"){
const points1 = channelData[originChannel1].points;
const points2 = channelData[originChannel2].points;
const pointsDivided = points1.map((point, index) => {
point = autoMeasures.voltage_from_raw(point);
let point2 = autoMeasures.voltage_from_raw(points2[index]);
if (point2 === 0){
return 8192;// = 0V
}else{
return parseInt(mapVoltageToRaw(point / point2).toFixed(0));
}
});
channelData[channelKey].points = pointsDivided;
};
};
/**
* This function regroups all sub-functions needed to calculate the values we display below the oscilloscope's screen.
* These sub-functions won't be entirely documented, most of them are pretty self-explanatory and comments are there already to clarify their use.
*
* @function calculateAutoMeasures
* @memberof module:Computing
* @returns {functions} Returns the following functions to use :
* - **voltage_from_raw**
* - **getMinValue**
* - **getMaxValue**
* - **getVppValue**
* - **getMeanValue**
* - **getMiddleValue**
* - **getRMS**
* - **getAverageFrequency**
* - **calculateThreshold**
* - **getFrequenciesMaxMin**
*
* @example
* const measures = calculateAutoMeasures();
* let minimumValue = measures.getMinValue(channelData["CH2"].points);
*/
function calculateAutoMeasures(){
//converts an absolute value (from 0 to 16383) to the corresponding voltage value (-1.1 to +1.1v or else if the config changes).
function voltage_from_raw(raw_value, max_raw=config.maxSampleValue){
let voltage_range = [-(config.voltage/2), config.voltage/2]
let span = voltage_range[1] - voltage_range[0]
normalized = raw_value / max_raw
return voltage_range[0] + (normalized * span)
};
function getMinValue(points){
let minAbsValue = Math.min(...points); // get smallest value from array
minAbsValue = voltage_from_raw(minAbsValue); //convert abs value to V
return minAbsValue * 1000;//convert V to mV
}
function getMaxValue(points){
let maxAbsValue = Math.max(...points); // get biggest value from array
maxAbsValue = voltage_from_raw(maxAbsValue); //convert abs value to V
return maxAbsValue * 1000;//convert V to mV
}
function getVppValue(points){
const maxVoltage = getMinValue(points);//mV
const minVoltage = getMaxValue(points);//mV
const vpp = maxVoltage - minVoltage;//mV
return vpp;
}
function getMeanValue(points){
const average = getMedian(points);//Get the average abs value of the array
const averageInVolts = voltage_from_raw(average); //convert abs value to V
return averageInVolts * 1000;//convert V to mV
}
function getMiddleValue(points){
let middleAbs = Math.round(points.length / 2); //get the middle of the point array
middleInVolts = voltage_from_raw(points[middleAbs]);//convert the value in volts
return middleInVolts * 1000; //convert V to mV
}
function getRMS(points){
let sum = 0;
points.forEach(point => {//add each point squared to the total sum
sum += point * point;
});
const rmsAbs = Math.sqrt(sum / points.length);//take the square root of the average of the points
const rmsInVolts = voltage_from_raw(rmsAbs);//convert the value in volts
return rmsInVolts * 1000;//convert final value to mV
}
function getAverageFrequency(points, sampleRate) {
const threshold = calculateThreshold(points); // Define a suitable threshold
let cycleCount = 0;
let isInCycle = false;
for (let i = 0; i < points.length; i++) {
if (points[i] > threshold && !isInCycle) {
cycleCount++; // start of a new cycle
isInCycle = true;
} else if (points[i] < threshold && isInCycle) {
isInCycle = false; // end of a cycle
}
}
const totalDuration = points.length / sampleRate; // total duration in seconds (sample rate will be at 1e8 Hz since agata has a sampling period of 10ns)
return cycleCount / totalDuration; // average frequency in Hz
}
function calculateThreshold(points) {
let mean = points.reduce((acc, val) => acc + val, 0) / points.length;
let sumOfSquares = points.reduce((acc, val) => acc + (val - mean) ** 2, 0);
let standardDeviation = Math.sqrt(sumOfSquares / points.length);
return mean + standardDeviation; // Threshold at mean + 1 SD
}
function getFrequenciesMaxMin(points, sampleRate) {
const threshold = calculateThreshold(points);
let cycleStart = null;
let frequencies = [];
for (let i = 0; i < points.length; i++) {
if (points[i] > threshold && cycleStart === null) {
cycleStart = i; //new cycle
} else if ((points[i] < threshold || i === points.length - 1) && cycleStart !== null) {
const cycleEnd = i;
const cycleDuration = (cycleEnd - cycleStart) / sampleRate; // one cycle in seconds
if (cycleDuration > 0) { // avoid division by zero
const frequency = 1 / cycleDuration;
frequencies.push(frequency);
}
cycleStart = null; // look for new cycle in the next loop
}
}
if (frequencies.length === 0) {
return {lowestFrequency: 0, highestFrequency: 0}; // no cycles found
}
const lowestFrequency = Math.min(...frequencies);
const highestFrequency = Math.max(...frequencies);
return {
lowestFrequency: lowestFrequency,
highestFrequency: highestFrequency
};
}
return {getMinValue, getMaxValue, getVppValue, getMeanValue, getMiddleValue, getRMS, getAverageFrequency, getFrequenciesMaxMin, voltage_from_raw};
};
/**
* This function returns how many mV are within a single vertical division.
*
* @function getMilliVoltsPerDiv
* @memberof module:Computing
* @param {number} channelVerticalScale Vertical scale of a specific channel we want to know the mv/div for.
* @returns {number} Returns the mv/div for the given channel.
*/
function getMilliVoltsPerDiv(channelVerticalScale) {
const totalVoltageRange = config.voltage;
const verticalDivisions = config.verticalDivisions;
const voltsPerDivision = totalVoltageRange / (verticalDivisions * channelVerticalScale);
return (voltsPerDivision * 1000).toFixed(1);
};
/**
* This function is called whenever a user selects a new signal to generate via a math function.
* We start by creating a new slot for a channel within channelData.
* We then update the config and the UI components related to this channel (button, scroller).
* We finish by setting up the interaction for the user with the offset cursor.
* The new signal will then be generated and drawned within the main loop.
*
* @function updateGeneratedMathSignalsData
* @memberof module:Computing
* @param {string} slotChannel Empty slot to use for the new signal.
* @param {string} channel1 Base signal for the generated new one.
* @param {string} channel2 Second base signal in case the operation needs two operands.
* @param {string} operation Which operation to use for the new signal.
* @returns {void}
* @example
* updateGeneratedMathSignalsData("CH6", "CH2", null, "squared");
* updateGeneratedMathSignalsData("CH8", "CH4", "CH1", "add");
*/
function updateGeneratedMathSignalsData(slotChannel, channel1, channel2, operation){
//console.log(`Updating math signal in slot ${slotChannel} for the operation : ${operation} between ${channel1} and ${channel2}`);
channelData[slotChannel] = {
points: [],
display: true,
type: "generatedData",
focused: false,
colorDark: channelsMetaData[slotChannel].colorDark,
colorLight: channelsMetaData[slotChannel].colorLight,
verticalOffset: 0,
verticalScale: 1,
verticalOffsetRelativeCursorPosition: 395,
//these attributes are specific to generated signals from a function
originChannel1: channel1,
originChannel2: channel2,//none if operation does not require 2 channels
operation: operation,
};
//Here we set the button styles to make it show up as available
channelButton = document.getElementById(slotChannel);
channelButton.classList.remove("channel-not-displayed");
channelButton.classList.add("channel-displayed");
channelButton.classList.add(channelData[slotChannel].colorDark);
//we update the global config to reflect the new channel
config.numChannels += 1;
//we add a cursor for this newly created channel
const scroller = document.getElementById("scroller-" + slotChannel);
scroller.style.display = "block";
scroller.style.backgroundColor = channelData[slotChannel].colorDark;
const channelID = slotChannel.split("CH")[1];
setScrollersEvents(channelID);
};
/**
* This function changes the status for the auto-measures, which ones to display or not.
* The values being changed are all stored within the global object 'autoMeasureOptions'.
*
* @function toggleMeasurement
* @memberof module:Computing
* @param {string} measureKey Key within the 'autoMeasureOptions' object representing the measure.
* @param {string} buttonId Id of the button associated to this measure.
* @returns {void}
*/
function toggleMeasurement(measureKey, buttonId) {
let measure = autoMeasureOptions[measureKey];
if (measure) {
measure.set = !measure.set; // Toggle the 'set' value
//change button class depending on wether the measure is set or not
document.getElementById(buttonId).className = measure.set ? 'modal-measure-button active-measure-button' : 'modal-measure-button inactive-measure-button';
}
};
/**
* This function resets all values within the object 'autoMeasureOptions'.
* It also resets the visual aspect of each button for the user to select the auto-measures.
*
* @function resetMeasurements
* @memberof module:Computing
* @returns {void}
*/
function resetMeasurements(){
try{
autoMeasureOptions.associatedChannel = "CH1";
Object.keys(autoMeasureOptions).forEach(key => {
if (key !== "associatedChannel") {
autoMeasureOptions[key].set = false;
autoMeasureOptions[key].value = null;
document.querySelectorAll('.modal-measure-button').forEach(button => {
button.className = 'modal-measure-button inactive-measure-button';
});
}
});
document.getElementById("selectChannel").value = "CH1";
showToast("Auto Measurements values reset !", "toast-info");
} catch (error) {
console.error('Failed to reset measurements', error);
showToast("Error while resetting the measurements..", "toast-error");
}
};
/**
* This function is used to get the position to set the trigger cursor at relative to the threshold set by the user.
*
* @function getPositionRelativeToTriggerCursor
* @memberof module:Computing
* @param {number} milliVolts Current treshold set for the trigger in mv.
* @param {string} channelKey Object key corresponding to a certain signal saved within 'channelData'.
* @returns {number} Position, in pixels, of the trigger cursor along the scrollbar.
*/
function getPositionRelativeToTriggerCursor(milliVolts, channelKey) {
const sizeOfOneDivisionInPixels = CANVAS.height / config.verticalDivisions;
const milliVoltsPerDivision = getMilliVoltsPerDiv(channelData[channelKey].verticalScale);
let position;
if (milliVolts === 0) {
position = CANVAS.height / 2;
} else if (milliVolts < 0) {
const cursorValue = Math.abs(milliVolts) / milliVoltsPerDivision;
position = (CANVAS.height / 2) + (cursorValue * sizeOfOneDivisionInPixels);
} else if (milliVolts > 0) {
const cursorValue = milliVolts / milliVoltsPerDivision;
position = (CANVAS.height / 2) - (cursorValue * sizeOfOneDivisionInPixels);
}
return position;
};
/**
* This function is used to update the current trigger settings set by the user.
* We had to add a few verifications in case the values entered by the user are outside the acceptable range.
*
* @function updateTriggerSettings
* @memberof module:Computing
* @param {DOMElement} modalElement Modal containing the inputs relative to the trigger options.
* @returns {void}
*/
function updateTriggerSettings(modalElement){
triggerOptions.isTriggerOn = modalElement.querySelector("#selectOnOffStatus").value;
triggerOptions.triggerMode = modalElement.querySelector("#selectTriggerMode").value;
triggerOptions.triggerChannel = modalElement.querySelector("#selectTriggerChannel").value;
if (modalElement.querySelector("#TriggerLevelInput").value > 1100){
triggerOptions.triggerLevel = 1100
}else if(modalElement.querySelector("#TriggerLevelInput").value < -1100){
triggerOptions.triggerLevel = -1100
}else{
triggerOptions.triggerLevel = modalElement.querySelector("#TriggerLevelInput").value;
}
if (modalElement.querySelector("#WindowLevelMinInput").value > 1100){
triggerOptions.windowLevelMin = 1100
}else if(modalElement.querySelector("#WindowLevelMinInput").value < -1100){
triggerOptions.windowLevelMin = -1100
}else{
triggerOptions.windowLevelMin = modalElement.querySelector("#WindowLevelMinInput").value;
}
if (modalElement.querySelector("#WindowLevelMaxInput").value > 1100){
triggerOptions.windowLevelMax = 1100
}else if(modalElement.querySelector("#WindowLevelMaxInput").value < -1100){
triggerOptions.windowLevelMax = -1100
}else{
triggerOptions.windowLevelMax = modalElement.querySelector("#WindowLevelMaxInput").value;
}
triggerOptions.triggerSlope = modalElement.querySelector("#selectTriggerSlope").value;
if (modalElement.querySelector("#holdOffInput").value > 3600){
triggerOptions.holdOff = 3600
}else if(modalElement.querySelector("#holdOffInput").value < 0){
triggerOptions.holdOff = 0
}else{
triggerOptions.holdOff = modalElement.querySelector("#holdOffInput").value;
}
showToast("Trigger settings updated !", "toast-info");
if (triggerOptions.isTriggerOn == "on"){
document.getElementById("trigger-cursor").style.display = "block";
const newCursorPosition = getPositionRelativeToTriggerCursor(parseFloat(triggerOptions.triggerLevel), triggerOptions.triggerChannel);
document.getElementById("trigger-cursor").style.top = newCursorPosition + 'px';
}else{
document.getElementById("trigger-cursor").style.display = "none";
}
};
/**
* This function calculates the zoom factors when a user selects an area to zoom on.
* It also update the global object 'zoomConfig' with the new values for the current zoom if any.
*
* @function calculateZoomFactors
* @memberof module:Computing
* @returns {void}
*/
function calculateZoomFactors() {
const selectedWidth = zoomConfig.finalX - zoomConfig.initX;
const selectedHeight = zoomConfig.finalY - zoomConfig.initY;
// Calculate scale factors to fit the selected area to the canvas size
zoomConfig.zoomX = CANVAS.width / selectedWidth;
zoomConfig.zoomY = CANVAS.height / selectedHeight;
};