'use strict' /* Open Rowing Monitor, https://github.com/laberning/openrowingmonitor A Detector used to test for up-going and down-going flanks Please note: The array contains flankLength + 1 measured currentDt's, thus flankLength number of flanks between them They are arranged that dataPoints[0] is the youngest, and dataPoints[flankLength] the oldest */ import loglevel from 'loglevel' import { createMovingAverager } from './averager/MovingAverager.js' const log = loglevel.getLogger('RowingEngine') function createMovingFlankDetector (rowerSettings) { const angularDisplacementPerImpulse = (2.0 * Math.PI) / rowerSettings.numOfImpulsesPerRevolution const dirtyDataPoints = new Array(rowerSettings.flankLength + 1) dirtyDataPoints.fill(rowerSettings.maximumTimeBetweenImpulses) const cleanDataPoints = new Array(rowerSettings.flankLength + 1) cleanDataPoints.fill(rowerSettings.maximumTimeBetweenImpulses) const angularVelocity = new Array(rowerSettings.flankLength + 1) angularVelocity.fill(angularDisplacementPerImpulse / rowerSettings.minimumTimeBetweenImpulses) const angularAcceleration = new Array(rowerSettings.flankLength + 1) angularAcceleration.fill(0) const movingAverage = createMovingAverager(rowerSettings.smoothing, rowerSettings.maximumTimeBetweenImpulses) function pushValue (dataPoint) { // add the new dataPoint to the array, we have to move data points starting at the oldest ones let i = rowerSettings.flankLength while (i > 0) { // older data points are moved toward the higher numbers dirtyDataPoints[i] = dirtyDataPoints[i - 1] cleanDataPoints[i] = cleanDataPoints[i - 1] angularVelocity[i] = angularVelocity[i - 1] angularAcceleration[i] = angularAcceleration[i - 1] i = i - 1 } dirtyDataPoints[0] = dataPoint // reduce noise in the measurements by applying some sanity checks // noise filter on the value of dataPoint: it should be within sane levels and should not deviate too much from the previous reading if (dataPoint < rowerSettings.minimumTimeBetweenImpulses || dataPoint > rowerSettings.maximumTimeBetweenImpulses) { // impulseTime is outside plausible ranges, so we assume it is close to the previous clean one log.debug(`noise filter corrected currentDt, ${dataPoint} was not between minimumTimeBetweenImpulses and maximumTimeBetweenImpulses, changed to ${cleanDataPoints[1]}`) dataPoint = cleanDataPoints[1] } // lets test if pushing this value would fit the curve we are looking for movingAverage.pushValue(dataPoint) if (movingAverage.getAverage() < (rowerSettings.maximumDownwardChange * cleanDataPoints[1]) || movingAverage.getAverage() > (rowerSettings.maximumUpwardChange * cleanDataPoints[1])) { // impulses are outside plausible ranges, so we assume it is close to the previous one log.debug(`noise filter corrected currentDt, ${dataPoint} was too much of an accelleration/decelleration with respect to ${movingAverage.getAverage()}, changed to previous value, ${cleanDataPoints[1]}`) movingAverage.replaceLastPushedValue(cleanDataPoints[1]) } // determine the moving average, to reduce noise cleanDataPoints[0] = movingAverage.getAverage() // determine the derived data if (cleanDataPoints[0] > 0) { angularVelocity[0] = angularDisplacementPerImpulse / cleanDataPoints[0] angularAcceleration[0] = (angularVelocity[0] - angularVelocity[1]) / cleanDataPoints[0] } else { log.error('Impuls of 0 seconds encountered, this should not be possible (division by 0 prevented)') angularVelocity[0] = 0 angularAcceleration[0] = 0 } } function isFlywheelUnpowered () { let numberOfErrors = 0 if (rowerSettings.naturalDeceleration < 0) { // A valid natural deceleration of the flywheel has been provided, this has to be maintained for a flank length // to count as an indication for an unpowered flywheel // Please note that angularAcceleration[] contains flank-information already, so we need to check from // rowerSettings.flankLength -1 until 0 flanks let i = rowerSettings.flankLength - 1 while (i >= 0) { if (angularAcceleration[i] > rowerSettings.naturalDeceleration) { // There seems to be some power present, so we detected an error numberOfErrors = numberOfErrors + 1 } i = i - 1 } } else { // No valid natural deceleration has been provided, we rely on pure deceleration for recovery detection let i = rowerSettings.flankLength while (i > 0) { if (cleanDataPoints[i] >= cleanDataPoints[i - 1]) { // Oldest interval (dataPoints[i]) is larger than the younger one (datapoint[i-1], as the distance is // fixed, we are accelerating numberOfErrors = numberOfErrors + 1 } i = i - 1 } } if (numberOfErrors > rowerSettings.numberOfErrorsAllowed) { return false } else { return true } } function isFlywheelPowered () { let numberOfErrors = 0 if (rowerSettings.naturalDeceleration < 0) { // A valid natural deceleration of the flywheel has been provided, this has to be consistently encountered // for a flank length to count as an indication of a powered flywheel // Please note that angularAcceleration[] contains flank-information already, so we need to check from // rowerSettings.flankLength -1 until 0 flanks let i = rowerSettings.flankLength - 1 while (i >= 0) { if (angularAcceleration[i] < rowerSettings.naturalDeceleration) { // Some deceleration is below the natural deceleration, so we detected an error numberOfErrors = numberOfErrors + 1 } i = i - 1 } } else { // No valid natural deceleration of the flywheel has been provided, we rely on pure acceleration for stroke detection let i = rowerSettings.flankLength while (i > 1) { if (cleanDataPoints[i] < cleanDataPoints[i - 1]) { // Oldest interval (dataPoints[i]) is shorter than the younger one (datapoint[i-1], as the distance is fixed, we // discovered a deceleration numberOfErrors = numberOfErrors + 1 } i = i - 1 } if (cleanDataPoints[1] <= cleanDataPoints[0]) { // We handle the last measurement more specifically: at least the youngest measurement must be really accelerating // This prevents when the currentDt "flatlines" (i.e. error correction kicks in) a ghost-stroke is detected numberOfErrors = numberOfErrors + 1 } } if (numberOfErrors > rowerSettings.numberOfErrorsAllowed) { return false } else { return true } } function timeToBeginOfFlank () { // We expect the curve to bend between dirtyDataPoints[rowerSettings.flankLength] and dirtyDataPoints[rowerSettings.flankLength+1], // as acceleration FOLLOWS the start of the pulling the handle, we assume it must have started before that let i = rowerSettings.flankLength let total = 0.0 while (i >= 0) { total += dirtyDataPoints[i] i = i - 1 } return total } function noImpulsesToBeginFlank () { return rowerSettings.flankLength } function impulseLengthAtBeginFlank () { // As this is fed into the speed calculation where small changes have big effects, and we typically use it when // the curve is in a plateau, we return the cleaned data and not the dirty data // Regardless of the way to determine the acceleration, cleanDataPoints[rowerSettings.flankLength] is always the // impulse at the beginning of the flank being investigated return cleanDataPoints[rowerSettings.flankLength] } function accelerationAtBeginOfFlank () { return angularAcceleration[rowerSettings.flankLength - 1] } return { pushValue, isFlywheelUnpowered, isFlywheelPowered, timeToBeginOfFlank, noImpulsesToBeginFlank, impulseLengthAtBeginFlank, accelerationAtBeginOfFlank } } export { createMovingFlankDetector }