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:
parent
426f982683
commit
335083c37d
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
if (metrics.sessionStatus === 'rowing' && metrics.strokesTotal > 0) {
|
||||
workoutRecorder.recordStroke(metrics)
|
||||
})
|
||||
|
||||
rowingStatistics.on('recoveryFinished', (metrics) => {
|
||||
webServer.notifyClients(metrics)
|
||||
peripheralManager.notifyMetrics('strokeStateChanged', metrics)
|
||||
}
|
||||
})
|
||||
|
||||
rowingStatistics.on('metricsUpdate', (metrics) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue