openrowingmonitor/app/engine/RowingStatistics.js

158 lines
5.6 KiB
JavaScript

'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
This Module calculates the training specific metrics.
*/
import { EventEmitter } from 'events'
import { createMovingIntervalAverager } from './MovingIntervalAverager.js'
import { createWeightedAverager } from './WeightedAverager.js'
// The number of strokes that are considered when averaging the calculated metrics
// Higher values create more stable metrics but make them less responsive
const numOfDataPointsForAveraging = 3
function createRowingStatistics () {
const emitter = new EventEmitter()
const strokeAverager = createWeightedAverager(numOfDataPointsForAveraging)
const powerAverager = createWeightedAverager(numOfDataPointsForAveraging)
const speedAverager = createWeightedAverager(numOfDataPointsForAveraging)
const powerRatioAverager = createWeightedAverager(numOfDataPointsForAveraging)
const caloriesAveragerMinute = createMovingIntervalAverager(60)
const caloriesAveragerHour = createMovingIntervalAverager(60 * 60)
let trainingRunning = false
let durationTimer
let rowingPausedTimer
let distanceTotal = 0.0
let durationTotal = 0
let strokesTotal = 0
let caloriesTotal = 0.0
let lastStrokeDuration = 0.0
let lastStrokeState = 'RECOVERY'
function handleStroke (stroke) {
if (!trainingRunning) startTraining()
// if we do not get a stroke for 6 seconds we treat this as a rowing pause
if (rowingPausedTimer)clearInterval(rowingPausedTimer)
rowingPausedTimer = setTimeout(() => pauseRowing(), 6000)
// based on: http://eodg.atm.ox.ac.uk/user/dudhia/rowing/physics/ergometer.html#section11
const calories = (4 * powerAverager.weightedAverage() + 350) * (stroke.duration) / 4200
powerAverager.pushValue(stroke.power)
speedAverager.pushValue(stroke.distance / stroke.duration)
powerRatioAverager.pushValue(stroke.durationDrivePhase / stroke.duration)
strokeAverager.pushValue(stroke.duration)
caloriesAveragerMinute.pushValue(calories, stroke.duration)
caloriesAveragerHour.pushValue(calories, stroke.duration)
caloriesTotal += calories
distanceTotal += stroke.distance
strokesTotal++
lastStrokeDuration = stroke.duration
lastStrokeState = stroke.strokeState
emitter.emit('strokeFinished', getMetrics())
}
// initiated by the rowing engine in case an impulse was not considered
// because it was too large
function handlePause (duration) {
caloriesAveragerMinute.pushValue(0, duration)
caloriesAveragerHour.pushValue(0, duration)
}
// initiated when the stroke state changes
function handleStrokeStateChanged (state) {
// todo: wee need a better mechanism to communicate strokeState updates
// this is an initial hacky attempt to see if we can use it for the C2-pm5 protocol
lastStrokeState = state.strokeState
emitter.emit('strokeStateChanged', getMetrics())
}
function getMetrics () {
const splitTime = speedAverager.weightedAverage() !== 0 ? (500.0 / speedAverager.weightedAverage()) : Infinity
return {
durationTotal,
durationTotalFormatted: secondsToTimeString(durationTotal),
strokesTotal,
distanceTotal: Math.round(distanceTotal), // meters
caloriesTotal: Math.round(caloriesTotal), // kcal
caloriesPerMinute: Math.round(caloriesAveragerMinute.average()),
caloriesPerHour: Math.round(caloriesAveragerHour.average()),
strokeTime: lastStrokeDuration.toFixed(2), // seconds
power: Math.round(powerAverager.weightedAverage()), // watts
split: splitTime, // seconds/500m
splitFormatted: secondsToTimeString(splitTime),
powerRatio: powerRatioAverager.weightedAverage().toFixed(2),
strokesPerMinute: strokeAverager.weightedAverage() !== 0 ? (60.0 / strokeAverager.weightedAverage()).toFixed(1) : 0,
speed: (speedAverager.weightedAverage() * 3.6).toFixed(2), // km/h
strokeState: lastStrokeState
}
}
function startTraining () {
trainingRunning = true
startDurationTimer()
}
function stopTraining () {
trainingRunning = false
stopDurationTimer()
if (rowingPausedTimer)clearInterval(rowingPausedTimer)
}
function resetTraining () {
stopTraining()
distanceTotal = 0.0
strokesTotal = 0
caloriesTotal = 0.0
durationTotal = 0
caloriesAveragerMinute.reset()
caloriesAveragerHour.reset()
strokeAverager.reset()
powerAverager.reset()
speedAverager.reset()
powerRatioAverager.reset()
}
// clear the displayed metrics in case the user pauses rowing
function pauseRowing () {
emitter.emit('rowingPaused', getMetrics())
}
function startDurationTimer () {
durationTimer = setInterval(() => {
durationTotal++
emitter.emit('durationUpdate', {
durationTotal,
durationTotalFormatted: secondsToTimeString(durationTotal)
})
}, 1000)
}
function stopDurationTimer () {
clearInterval(durationTimer)
durationTimer = undefined
}
// converts a timeStamp in seconds to a human readable hh:mm:ss format
function secondsToTimeString (secondsTimeStamp) {
if (secondsTimeStamp === Infinity) return '∞'
const hours = Math.floor(secondsTimeStamp / 60 / 60)
const minutes = Math.floor(secondsTimeStamp / 60) - (hours * 60)
const seconds = Math.floor(secondsTimeStamp % 60)
let timeString = hours > 0 ? ` ${hours.toString().padStart(2, '0')}:` : ''
timeString += `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
return timeString
}
return Object.assign(emitter, {
handleStroke,
handlePause,
handleStrokeStateChanged,
reset: resetTraining
})
}
export { createRowingStatistics }