From 335083c37ddf0d795588089ad4878a4f9b917483 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Fri, 28 Jan 2022 19:56:55 +0100 Subject: [PATCH] Improvement of the startup behaviour and external stroke definition (#46) * Improvement of the startup behaviour These changes improve the startup behaviour: ORM will now always start with a detected drive-phase, which will trigger recording etc. This changes several initialisations of variables to make sure they enable the triggering of a "Drive" at the beginning of a stroke. Also workoutHandler.handleStrokeEnd has been renamed to workoutHandler.handleDriveEnd, as this is the clear end of that stroke-phase. * Renamed handleStrokeEnd to handleDriveEnd Renamed handleStrokeEnd to handleDriveEnd, as this is the clear end of that stroke-phase, as the current model alternates between Drive+Recovery and Recovery+Drive strokes to keep feeding the metrics, there isn't a very clear "End of the Stroke". * Redefinition of a stroke Traditionally, a stroke is defined as a Drive phase, followed by a Recovery Phase. This is also expected by Garmin and EXR as Concept2 implements it this way as well. This implementation makes it more consistent with that interpretation, while leaving room for intermediate updates. This should make Bluetooth behaviour and metrics recording more consistent. --- app/engine/MovingFlankDetector.js | 2 +- app/engine/RowingEngine.js | 27 +++++++++++++++------------ app/engine/RowingEngine.test.js | 8 ++++---- app/engine/RowingStatistics.js | 8 ++++---- app/server.js | 16 +++++++++------- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/app/engine/MovingFlankDetector.js b/app/engine/MovingFlankDetector.js index 8fe5ac9..3454eb7 100644 --- a/app/engine/MovingFlankDetector.js +++ b/app/engine/MovingFlankDetector.js @@ -20,7 +20,7 @@ function createMovingFlankDetector (rowerSettings) { const angularVelocity = new Array(rowerSettings.flankLength + 1) angularVelocity.fill(angularDisplacementPerImpulse / rowerSettings.minimumTimeBetweenImpulses) const angularAcceleration = new Array(rowerSettings.flankLength + 1) - angularAcceleration.fill(0) + angularAcceleration.fill(0.1) const movingAverage = createMovingAverager(rowerSettings.smoothing, rowerSettings.maximumTimeBetweenImpulses) let numberOfSequentialCorrections = 0 const maxNumberOfSequentialCorrections = (rowerSettings.smoothing >= 2 ? rowerSettings.smoothing : 2) diff --git a/app/engine/RowingEngine.js b/app/engine/RowingEngine.js index d0f7a9e..308563c 100644 --- a/app/engine/RowingEngine.js +++ b/app/engine/RowingEngine.js @@ -19,23 +19,23 @@ const log = loglevel.getLogger('RowingEngine') function createRowingEngine (rowerSettings) { let workoutHandler const flankDetector = createMovingFlankDetector(rowerSettings) - let cyclePhase = 'Drive' + let cyclePhase = 'Recovery' let totalTime = 0.0 let totalNumberOfImpulses = 0.0 let strokeNumber = 0.0 const angularDisplacementPerImpulse = (2.0 * Math.PI) / rowerSettings.numOfImpulsesPerRevolution let drivePhaseStartTime = 0.0 let drivePhaseStartAngularDisplacement = 0.0 - let drivePhaseLength = rowerSettings.minimumDriveTime + let drivePhaseLength = 2.0 * rowerSettings.minimumDriveTime let drivePhaseAngularDisplacement = rowerSettings.numOfImpulsesPerRevolution // let driveStartAngularVelocity = 0 // let driveEndAngularVelocity = angularDisplacementPerImpulse / rowerSettings.minimumTimeBetweenImpulses let driveLinearDistance = 0.0 // let drivePhaseEnergyProduced = 0.0 - let recoveryPhaseStartTime = 0.0 - let recoveryPhaseStartAngularDisplacement = 0.0 + let recoveryPhaseStartTime = -2 * rowerSettings.minimumRecoveryTime // Make sure that the first CurrentDt will trigger a detected stroke by faking a recovery phase that is long enough + let recoveryPhaseStartAngularDisplacement = -1.0 * rowerSettings.numOfImpulsesPerRevolution let recoveryPhaseAngularDisplacement = rowerSettings.numOfImpulsesPerRevolution - let recoveryPhaseLength = rowerSettings.minimumRecoveryTime + let recoveryPhaseLength = 2.0 * rowerSettings.minimumRecoveryTime let recoveryStartAngularVelocity = angularDisplacementPerImpulse / rowerSettings.minimumTimeBetweenImpulses let recoveryEndAngularVelocity = angularDisplacementPerImpulse / rowerSettings.maximumTimeBetweenImpulses let recoveryLinearDistance = 0.0 @@ -233,7 +233,7 @@ function createRowingEngine (rowerSettings) { // Update the metrics if (workoutHandler) { - workoutHandler.handleStrokeEnd({ + workoutHandler.handleDriveEnd({ timeSinceStart: totalTime, power: averagedCyclePower, duration: cycleLength, @@ -296,32 +296,35 @@ function createRowingEngine (rowerSettings) { } function reset () { - cyclePhase = 'Drive' + cyclePhase = 'Recovery' totalTime = 0.0 totalNumberOfImpulses = 0.0 strokeNumber = 0.0 drivePhaseStartTime = 0.0 drivePhaseStartAngularDisplacement = 0.0 - drivePhaseLength = 0.0 + drivePhaseLength = 2.0 * rowerSettings.minimumDriveTime drivePhaseAngularDisplacement = rowerSettings.numOfImpulsesPerRevolution // driveStartAngularVelocity = 0 // driveEndAngularVelocity = angularDisplacementPerImpulse / rowerSettings.minimumTimeBetweenImpulses driveLinearDistance = 0.0 // drivePhaseEnergyProduced = 0.0 - recoveryPhaseStartTime = 0.0 - recoveryPhaseStartAngularDisplacement = 0.0 + recoveryPhaseStartTime = -2 * rowerSettings.minimumRecoveryTime // Make sure that the first CurrentDt will trigger a detected stroke by faking a recovery phase that is long enough + recoveryPhaseStartAngularDisplacement = -1.0 * rowerSettings.numOfImpulsesPerRevolution recoveryPhaseAngularDisplacement = rowerSettings.numOfImpulsesPerRevolution - recoveryPhaseLength = rowerSettings.minimumRecoveryTime + recoveryPhaseLength = 2.0 * rowerSettings.minimumRecoveryTime recoveryStartAngularVelocity = angularDisplacementPerImpulse / rowerSettings.minimumTimeBetweenImpulses recoveryEndAngularVelocity = angularDisplacementPerImpulse / rowerSettings.maximumTimeBetweenImpulses recoveryLinearDistance = 0.0 currentDragFactor = rowerSettings.dragFactor / 1000000 movingDragAverage.reset() dragFactor = movingDragAverage.getAverage() - cycleLength = 0.0 + cycleLength = minimumCycleLength linearCycleVelocity = 0.0 totalLinearDistance = 0.0 averagedCyclePower = 0.0 + currentTorque = 0.0 + previousAngularVelocity = 0.0 + currentAngularVelocity = 0.0 } function notify (receiver) { diff --git a/app/engine/RowingEngine.test.js b/app/engine/RowingEngine.test.js index 13fe2e7..0685bbf 100644 --- a/app/engine/RowingEngine.test.js +++ b/app/engine/RowingEngine.test.js @@ -17,7 +17,7 @@ log.setLevel('warn') const createWorkoutEvaluator = function () { const strokes = [] - function handleStrokeEnd (stroke) { + function handleDriveEnd (stroke) { strokes.push(stroke) log.info(`stroke: ${strokes.length}, power: ${Math.round(stroke.power)}w, duration: ${stroke.duration.toFixed(2)}s, ` + ` drivePhase: ${stroke.durationDrivePhase.toFixed(2)}s, distance: ${stroke.distance.toFixed(2)}m`) @@ -42,7 +42,7 @@ const createWorkoutEvaluator = function () { } return { - handleStrokeEnd, + handleDriveEnd, handleRecoveryEnd, updateKeyMetrics, handlePause, @@ -61,7 +61,7 @@ test('sample data for WRX700 should produce plausible results with rower profile await replayRowingSession(rowingEngine.handleRotationImpulse, { filename: 'recordings/WRX700_2magnets.csv' }) assert.is(workoutEvaluator.getNumOfStrokes(), 16, 'number of strokes does not meet expectation') assertPowerRange(workoutEvaluator, 50, 220) - assertDistanceRange(workoutEvaluator, 158, 162) + assertDistanceRange(workoutEvaluator, 159, 163) assertStrokeDistanceSumMatchesTotal(workoutEvaluator) }) @@ -72,7 +72,7 @@ test('sample data for DKNR320 should produce plausible results with rower profil await replayRowingSession(rowingEngine.handleRotationImpulse, { filename: 'recordings/DKNR320.csv' }) assert.is(workoutEvaluator.getNumOfStrokes(), 10, 'number of strokes does not meet expectation') assertPowerRange(workoutEvaluator, 75, 200) - assertDistanceRange(workoutEvaluator, 64, 67) + assertDistanceRange(workoutEvaluator, 65, 68) assertStrokeDistanceSumMatchesTotal(workoutEvaluator) }) diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index 9179c0b..ec98113 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -50,8 +50,8 @@ function createRowingStatistics (config) { } } - function handleStrokeEnd (stroke) { - // if we do not get a stroke for timeBetweenStrokesBeforePause milliseconds we treat this as a rowing pause + function handleDriveEnd (stroke) { + // if we do not get a drive for timeBetweenStrokesBeforePause milliseconds we treat this as a rowing pause if (rowingPausedTimer)clearInterval(rowingPausedTimer) rowingPausedTimer = setTimeout(() => pauseRowing(), timeBetweenStrokesBeforePause) @@ -77,7 +77,7 @@ function createRowingStatistics (config) { lastStrokeState = stroke.strokeState lastStrokeSpeed = stroke.speed instantaneousTorque = stroke.instantaneousTorque - emitter.emit('strokeFinished', getMetrics()) + emitter.emit('driveFinished', getMetrics()) } // initiated by the rowing engine in case an impulse was not considered @@ -209,7 +209,7 @@ function createRowingStatistics (config) { } return Object.assign(emitter, { - handleStrokeEnd, + handleDriveEnd, handlePause, handleHeartrateMeasurement, handleRecoveryEnd, diff --git a/app/server.js b/app/server.js index 81f36c5..fb20c64 100644 --- a/app/server.js +++ b/app/server.js @@ -78,19 +78,21 @@ const rowingStatistics = createRowingStatistics(config) rowingEngine.notify(rowingStatistics) const workoutRecorder = createWorkoutRecorder() -rowingStatistics.on('strokeFinished', (metrics) => { +rowingStatistics.on('driveFinished', (metrics) => { + webServer.notifyClients(metrics) + peripheralManager.notifyMetrics('strokeStateChanged', metrics) +}) + +rowingStatistics.on('recoveryFinished', (metrics) => { log.info(`stroke: ${metrics.strokesTotal}, dur: ${metrics.strokeTime.toFixed(2)}s, power: ${Math.round(metrics.power)}w` + `, split: ${metrics.splitFormatted}, ratio: ${metrics.powerRatio.toFixed(2)}, dist: ${metrics.distanceTotal.toFixed(1)}m` + `, cal: ${metrics.caloriesTotal.toFixed(1)}kcal, SPM: ${metrics.strokesPerMinute.toFixed(1)}, speed: ${metrics.speed.toFixed(2)}km/h` + `, cal/hour: ${metrics.caloriesPerHour.toFixed(1)}kcal, cal/minute: ${metrics.caloriesPerMinute.toFixed(1)}kcal`) webServer.notifyClients(metrics) peripheralManager.notifyMetrics('strokeFinished', metrics) - workoutRecorder.recordStroke(metrics) -}) - -rowingStatistics.on('recoveryFinished', (metrics) => { - webServer.notifyClients(metrics) - peripheralManager.notifyMetrics('strokeStateChanged', metrics) + if (metrics.sessionStatus === 'rowing' && metrics.strokesTotal > 0) { + workoutRecorder.recordStroke(metrics) + } }) rowingStatistics.on('metricsUpdate', (metrics) => {