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.
This commit is contained in:
Jaap van Ekris 2022-01-28 19:56:55 +01:00 committed by GitHub
parent 426f982683
commit 335083c37d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 33 additions and 28 deletions

View File

@ -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)

View File

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

View File

@ -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)
})

View File

@ -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,

View File

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