From 37edd0290b7bcf99d49e0702bb85c814f24169dd Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 17:29:58 +0100 Subject: [PATCH] Update to facilitate more complex workouts Update to facilitate more complex workouts. RowingStatistics will now manage the intervals completely independently and alert server.js only when the session has ended. --- app/engine/RowingStatistics.js | 101 +++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index de4d10c..dc2b0e8 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -13,7 +13,7 @@ import { createCurveAligner } from './utils/CurveAligner.js' import loglevel from 'loglevel' const log = loglevel.getLogger('RowingEngine') -function createRowingStatistics (config, session) { +function createRowingStatistics (config) { const numOfDataPointsForAveraging = config.numOfPhasesForAveragingScreenData const webUpdateInterval = config.webUpdateInterval const peripheralUpdateInterval = config.peripheralUpdateInterval @@ -26,6 +26,12 @@ function createRowingStatistics (config, session) { const cyclePower = createStreamFilter(numOfDataPointsForAveraging, 0) const cycleLinearVelocity = createStreamFilter(numOfDataPointsForAveraging, 0) let sessionStatus = 'WaitingForStart' + let intervalSettings = [] + let currentIntervalNumber = -1 + let intervalTargetDistance = 0 + let intervalTargetTime = 0 + let intervalPrevAccumulatedDistance = 0 + let intervalPrevAccumulatedTime = 0 let heartrateResetTimer let totalLinearDistance = 0.0 let totalMovingTime = 0 @@ -91,7 +97,7 @@ function createRowingStatistics (config, session) { updateContinousMetrics() updateCycleMetrics() handleRecoveryEnd() - emitMetrics('intervalTargetReached') + handleIntervalEnd() break case (sessionStatus === 'Rowing' && lastStrokeState === 'Recovery' && rower.strokeState() === 'Drive'): updateContinousMetrics() @@ -103,7 +109,7 @@ function createRowingStatistics (config, session) { updateContinousMetrics() updateCycleMetrics() handleDriveEnd() - emitMetrics('intervalTargetReached') + handleIntervalEnd() break case (sessionStatus === 'Rowing' && lastStrokeState === 'Drive' && rower.strokeState() === 'Recovery'): updateContinousMetrics() @@ -113,7 +119,7 @@ function createRowingStatistics (config, session) { break case (sessionStatus === 'Rowing' && intervalTargetReached()): updateContinousMetrics() - emitMetrics('intervalTargetReached') + handleIntervalEnd() break case (sessionStatus === 'Rowing'): updateContinousMetrics() @@ -181,6 +187,12 @@ function createRowingStatistics (config, session) { rower.allowMovement() totalMovingTime = 0 totalLinearDistance = 0.0 + intervalSettings = [] + currentIntervalNumber = -1 + intervalTargetDistance = 0 + intervalTargetTime = 0 + intervalPrevAccumulatedDistance = 0 + intervalPrevAccumulatedTime = 0 totalNumberOfStrokes = -1 driveLastStartTime = 0 distanceOverTime.reset() @@ -242,6 +254,63 @@ function createRowingStatistics (config, session) { calories.push(totalMovingTime, totalCalories) } + function setIntervalParameters (intervalParameters) { + intervalSettings = intervalParameters + currentIntervalNumber = -1 + if (intervalSettings.length > 0) { + log.info(`Workout recieved with ${intervalSettings.length} interval(s)`) + activateNextIntervalParameters() + } else { + // intervalParameters were empty, lets log this odd situation + log.error('Recieved workout containing no intervals') + } + } + + function intervalTargetReached () { + // This tests wether the end of the current interval is reached + if ((intervalTargetDistance > 0 && rower.totalLinearDistanceSinceStart() >= intervalTargetDistance) || (intervalTargetTime > 0 && rower.totalMovingTimeSinceStart() >= intervalTargetTime)) { + return true + } else { + return false + } + } + + function handleIntervalEnd () { + // initiated when the state machine has concluded the interval has ended + if (intervalSettings.length > 0 && intervalSettings.length > (currentIntervalNumber + 1)) { + // There is a next interval available + emitMetrics('intervalTargetReached') + activateNextIntervalParameters() + } else { + // There is no additional interval available + stopTraining () + } + } + + function activateNextIntervalParameters () { + if (intervalSettings.length > 0 && intervalSettings.length > (currentIntervalNumber + 1)) { + // This function sets the interval parameters in absolute distances/times + // Thus the interval target always is a projected "finishline" from the current position + intervalPrevAccumulatedTime = rower.totalMovingTimeSinceStart() + intervalPrevAccumulatedDistance = rower.totalLinearDistanceSinceStart() + + currentIntervalNumber++ + if (intervalSettings[currentIntervalNumber].targetDistance > 0) { + // A target distance is set + intervalTargetTime = 0 + intervalTargetDistance = intervalPrevAccumulatedDistance + intervalSettings[currentIntervalNumber].targetDistance + log.info(`Interval settings for interval ${currentIntervalNumber + 1} of ${intervalSettings.length}: Distance target ${intervalSettings[currentIntervalNumber].targetDistance} meters`) + } else { + // A target time is set + intervalTargetTime = intervalPrevAccumulatedTime + intervalSettings[currentIntervalNumber].targetTime + intervalTargetDistance = 0 + log.info(`Interval settings for interval ${currentIntervalNumber + 1} of ${intervalSettings.length}: time target ${secondsToTimeString(intervalSettings[currentIntervalNumber].targetTime)} minutes`) + } + } else { + log.error('Interval error: there is no next interval!') + } + } + // initiated when a new heart rate value is received from heart rate sensor function handleHeartrateMeasurement (value) { // set the heart rate to zero if we did not receive a value for some time @@ -254,14 +323,6 @@ function createRowingStatistics (config, session) { heartrateBatteryLevel = value.batteryLevel } - function intervalTargetReached () { - if ((session.targetDistance > 0 && rower.totalLinearDistanceSinceStart() >= session.targetDistance) || (session.targetTime > 0 && rower.totalMovingTimeSinceStart() >= session.targetTime)) { - return true - } else { - return false - } - } - function measureRecoveryHR () { // This function is called when the rowing session is stopped. postExerciseHR[0] is the last measured excercise HR // Thus postExerciseHR[1] is Recovery HR after 1 min, etc.. @@ -298,11 +359,13 @@ function createRowingStatistics (config, session) { sessionStatus, strokeState: rower.strokeState(), totalMovingTime: totalMovingTime > 0 ? totalMovingTime : 0, - driveLastStartTime: driveLastStartTime > 0 ? driveLastStartTime : 0, - totalMovingTimeFormatted: session.targetTime > 0 ? secondsToTimeString(Math.round(Math.max(session.targetTime - totalMovingTime, 0))) : secondsToTimeString(Math.round(totalMovingTime)), + totalMovingTimeFormatted: intervalTargetTime > 0 ? secondsToTimeString(Math.round(Math.max(intervalTargetTime - totalMovingTime, 0))) : secondsToTimeString(Math.round(totalMovingTime - intervalPrevAccumulatedTime)), totalNumberOfStrokes: totalNumberOfStrokes > 0 ? totalNumberOfStrokes : 0, totalLinearDistance: totalLinearDistance > 0 ? totalLinearDistance : 0, // meters - totalLinearDistanceFormatted: session.targetDistance > 0 ? Math.max(session.targetDistance - totalLinearDistance, 0) : totalLinearDistance, + totalLinearDistanceFormatted: intervalTargetDistance > 0 ? Math.max(intervalTargetDistance - totalLinearDistance, 0) : totalLinearDistance - intervalPrevAccumulatedDistance, + intervalNumber: Math.max(currentIntervalNumber + 1, 0), // Interval number + intervalMovingTime: totalMovingTime - intervalPrevAccumulatedTime, + intervalLinearDistance: totalLinearDistance - intervalPrevAccumulatedDistance, strokeCalories: strokeCalories > 0 ? strokeCalories : 0, // kCal strokeWork: strokeWork > 0 ? strokeWork : 0, // Joules totalCalories: calories.yAtSeriesEnd() > 0 ? calories.yAtSeriesEnd() : 0, // kcal @@ -312,11 +375,12 @@ function createRowingStatistics (config, session) { cycleStrokeRate: cycleDuration.clean() > minimumStrokeTime && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? (60.0 / cycleDuration.clean()) : 0, // strokeRate in SPM cycleDistance: cycleDistance.raw() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cycleDistance.clean() : 0, // meters cycleLinearVelocity: cycleLinearVelocity.clean() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cycleLinearVelocity.clean() : 0, // m/s - cyclePace: cycleLinearVelocity.raw() > 0 ? cyclePace : Infinity, // seconds/500m + cyclePace: cycleLinearVelocity.raw() > 0 ? cyclePace : Infinity, // seconds/50 0m cyclePaceFormatted: cycleLinearVelocity.raw() > 0 ? secondsToTimeString(Math.round(cyclePace)) : Infinity, cyclePower: cyclePower.clean() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cyclePower.clean() : 0, // watts - cycleProjectedEndTime: session.targetDistance > 0 ? distanceOverTime.projectY(session.targetDistance) : session.targetTime, - cycleProjectedEndLinearDistance: session.targetTime > 0 ? distanceOverTime.projectX(session.targetTime) : session.targetDistance, + cycleProjectedEndTime: intervalTargetDistance > 0 ? distanceOverTime.projectY(intervalTargetDistance) : intervalTargetTime, + cycleProjectedEndLinearDistance: intervalTargetTime > 0 ? distanceOverTime.projectX(intervalTargetTime) : intervalTargetDistance, + driveLastStartTime: driveLastStartTime > 0 ? driveLastStartTime : 0, driveDuration: driveDuration.clean() >= config.rowerSettings.minimumDriveTime && totalNumberOfStrokes > 0 && sessionStatus === 'Rowing' ? driveDuration.clean() : NaN, // seconds driveLength: driveLength.clean() > 0 && sessionStatus === 'Rowing' ? driveLength.clean() : NaN, // meters of chain movement driveDistance: driveDistance.clean() >= 0 && sessionStatus === 'Rowing' ? driveDistance.clean() : NaN, // meters @@ -355,6 +419,7 @@ function createRowingStatistics (config, session) { return Object.assign(emitter, { handleHeartrateMeasurement, handleRotationImpulse, + setIntervalParameters, pause: pauseTraining, stop: stopTraining, resume: allowResumeTraining,