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) => {