More refined session state (#33)
* A more refined rowing session state (PM5) Introduces a more refined session state needed for the PM5 rower session simulation, as well as a cleaner setup for the metrics to stop when the rower is stopped. * Added the WorkoutState and rowingState Added the WorkoutState and rowingState to the PM5 interface, to prevent EXR to start too early. * Added a link to the brilliant work of Nomath Added a link to Nomath's work, as it is a brilliant piece of work that independently verifies our calculations and provides a basis for support of the Concept2 rower. * Bugfix in Instantanous Torque calculation The Instantanous Torque calculation wasn't cleaned up sufficiently, causing issues in the powercurve. * unifies reset of heartrate value at different places * unifies naming with other variables status -> state * just some minor link adjustments in documentation Co-authored-by: Lars Berning <151194+laberning@users.noreply.github.com>
This commit is contained in:
parent
ab02153c2d
commit
e85491acb9
|
|
@ -46,9 +46,9 @@ export default class GeneralStatus extends bleno.Characteristic {
|
||||||
// intervalType: UInt8 will always use 255 (NONE)
|
// intervalType: UInt8 will always use 255 (NONE)
|
||||||
bufferBuilder.writeUInt8(255)
|
bufferBuilder.writeUInt8(255)
|
||||||
// workoutState: UInt8 0 WAITTOBEGIN, 1 WORKOUTROW, 10 WORKOUTEND
|
// workoutState: UInt8 0 WAITTOBEGIN, 1 WORKOUTROW, 10 WORKOUTEND
|
||||||
bufferBuilder.writeUInt8(1)
|
bufferBuilder.writeUInt8(data.sessionState === 'rowing' ? 1 : (data.sessionState === 'waitingForStart' ? 0 : 10))
|
||||||
// rowingState: UInt8 0 INACTIVE, 1 ACTIVE
|
// rowingState: UInt8 0 INACTIVE, 1 ACTIVE
|
||||||
bufferBuilder.writeUInt8(1)
|
bufferBuilder.writeUInt8(data.sessionState === 'rowing' ? 1 : 0)
|
||||||
// strokeState: UInt8 2 DRIVING, 4 RECOVERY
|
// strokeState: UInt8 2 DRIVING, 4 RECOVERY
|
||||||
bufferBuilder.writeUInt8(data.strokeState === 'DRIVING' ? 2 : 4)
|
bufferBuilder.writeUInt8(data.strokeState === 'DRIVING' ? 2 : 4)
|
||||||
// totalWorkDistance: UInt24LE in 1 m
|
// totalWorkDistance: UInt24LE in 1 m
|
||||||
|
|
|
||||||
|
|
@ -149,11 +149,7 @@ function createRowingEngine (rowerSettings) {
|
||||||
// Calculate the key metrics
|
// Calculate the key metrics
|
||||||
recoveryLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * recoveryPhaseAngularDisplacement
|
recoveryLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * recoveryPhaseAngularDisplacement
|
||||||
totalLinearDistance += recoveryLinearDistance
|
totalLinearDistance += recoveryLinearDistance
|
||||||
if (currentDt > 0) {
|
currentTorque = calculateTorque(currentDt)
|
||||||
previousAngularVelocity = currentAngularVelocity
|
|
||||||
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
|
|
||||||
currentTorque = rowerSettings.flywheelInertia * ((currentAngularVelocity - previousAngularVelocity) / currentDt) + dragFactor * Math.pow(currentAngularVelocity, 2)
|
|
||||||
}
|
|
||||||
linearCycleVelocity = calculateLinearVelocity()
|
linearCycleVelocity = calculateLinearVelocity()
|
||||||
averagedCyclePower = calculateCyclePower()
|
averagedCyclePower = calculateCyclePower()
|
||||||
|
|
||||||
|
|
@ -186,11 +182,7 @@ function createRowingEngine (rowerSettings) {
|
||||||
// Update the key metrics on each impulse
|
// Update the key metrics on each impulse
|
||||||
drivePhaseAngularDisplacement = ((totalNumberOfImpulses - flankDetector.noImpulsesToBeginFlank()) - drivePhaseStartAngularDisplacement) * angularDisplacementPerImpulse
|
drivePhaseAngularDisplacement = ((totalNumberOfImpulses - flankDetector.noImpulsesToBeginFlank()) - drivePhaseStartAngularDisplacement) * angularDisplacementPerImpulse
|
||||||
driveLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * drivePhaseAngularDisplacement
|
driveLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * drivePhaseAngularDisplacement
|
||||||
if (currentDt > 0) {
|
currentTorque = calculateTorque(currentDt)
|
||||||
previousAngularVelocity = currentAngularVelocity
|
|
||||||
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
|
|
||||||
currentTorque = calculateTorque(currentDt)
|
|
||||||
}
|
|
||||||
if (workoutHandler) {
|
if (workoutHandler) {
|
||||||
workoutHandler.updateKeyMetrics({
|
workoutHandler.updateKeyMetrics({
|
||||||
timeSinceStart: totalTime,
|
timeSinceStart: totalTime,
|
||||||
|
|
@ -213,11 +205,7 @@ function createRowingEngine (rowerSettings) {
|
||||||
// driveEndAngularVelocity = angularDisplacementPerImpulse / flankDetector.impulseLengthAtBeginFlank()
|
// driveEndAngularVelocity = angularDisplacementPerImpulse / flankDetector.impulseLengthAtBeginFlank()
|
||||||
driveLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * drivePhaseAngularDisplacement
|
driveLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * drivePhaseAngularDisplacement
|
||||||
totalLinearDistance += driveLinearDistance
|
totalLinearDistance += driveLinearDistance
|
||||||
if (currentDt > 0) {
|
currentTorque = calculateTorque(currentDt)
|
||||||
previousAngularVelocity = currentAngularVelocity
|
|
||||||
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
|
|
||||||
currentTorque = calculateTorque(currentDt)
|
|
||||||
}
|
|
||||||
// We display the AVERAGE speed in the display, NOT the top speed of the stroke
|
// We display the AVERAGE speed in the display, NOT the top speed of the stroke
|
||||||
linearCycleVelocity = calculateLinearVelocity()
|
linearCycleVelocity = calculateLinearVelocity()
|
||||||
averagedCyclePower = calculateCyclePower()
|
averagedCyclePower = calculateCyclePower()
|
||||||
|
|
@ -252,11 +240,7 @@ function createRowingEngine (rowerSettings) {
|
||||||
// Update the key metrics on each impulse
|
// Update the key metrics on each impulse
|
||||||
recoveryPhaseAngularDisplacement = ((totalNumberOfImpulses - flankDetector.noImpulsesToBeginFlank()) - recoveryPhaseStartAngularDisplacement) * angularDisplacementPerImpulse
|
recoveryPhaseAngularDisplacement = ((totalNumberOfImpulses - flankDetector.noImpulsesToBeginFlank()) - recoveryPhaseStartAngularDisplacement) * angularDisplacementPerImpulse
|
||||||
recoveryLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * recoveryPhaseAngularDisplacement
|
recoveryLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * recoveryPhaseAngularDisplacement
|
||||||
if (currentDt > 0) {
|
currentTorque = calculateTorque(currentDt)
|
||||||
previousAngularVelocity = currentAngularVelocity
|
|
||||||
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
|
|
||||||
currentTorque = calculateTorque(currentDt)
|
|
||||||
}
|
|
||||||
if (workoutHandler) {
|
if (workoutHandler) {
|
||||||
workoutHandler.updateKeyMetrics({
|
workoutHandler.updateKeyMetrics({
|
||||||
timeSinceStart: totalTime,
|
timeSinceStart: totalTime,
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,14 @@ function createRowingStatistics (config) {
|
||||||
const powerRatioAverager = createWeightedAverager(numOfDataPointsForAveraging)
|
const powerRatioAverager = createWeightedAverager(numOfDataPointsForAveraging)
|
||||||
const caloriesAveragerMinute = createMovingIntervalAverager(60)
|
const caloriesAveragerMinute = createMovingIntervalAverager(60)
|
||||||
const caloriesAveragerHour = createMovingIntervalAverager(60 * 60)
|
const caloriesAveragerHour = createMovingIntervalAverager(60 * 60)
|
||||||
let trainingRunning = false
|
let sessionState = 'waitingForStart'
|
||||||
let rowingPausedTimer
|
let rowingPausedTimer
|
||||||
let heartrateResetTimer
|
let heartrateResetTimer
|
||||||
let distanceTotal = 0.0
|
let distanceTotal = 0.0
|
||||||
let durationTotal = 0
|
let durationTotal = 0
|
||||||
let strokesTotal = 0
|
let strokesTotal = 0
|
||||||
let caloriesTotal = 0.0
|
let caloriesTotal = 0.0
|
||||||
let heartrate
|
let heartrate = 0
|
||||||
let heartrateBatteryLevel = 0
|
let heartrateBatteryLevel = 0
|
||||||
let lastStrokeDuration = 0.0
|
let lastStrokeDuration = 0.0
|
||||||
let instantaneousTorque = 0.0
|
let instantaneousTorque = 0.0
|
||||||
|
|
@ -51,8 +51,6 @@ function createRowingStatistics (config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStrokeEnd (stroke) {
|
function handleStrokeEnd (stroke) {
|
||||||
if (!trainingRunning) startTraining()
|
|
||||||
|
|
||||||
// if we do not get a stroke for timeBetweenStrokesBeforePause milliseconds we treat this as a rowing pause
|
// if we do not get a stroke for timeBetweenStrokesBeforePause milliseconds we treat this as a rowing pause
|
||||||
if (rowingPausedTimer)clearInterval(rowingPausedTimer)
|
if (rowingPausedTimer)clearInterval(rowingPausedTimer)
|
||||||
rowingPausedTimer = setTimeout(() => pauseRowing(), timeBetweenStrokesBeforePause)
|
rowingPausedTimer = setTimeout(() => pauseRowing(), timeBetweenStrokesBeforePause)
|
||||||
|
|
@ -85,6 +83,7 @@ function createRowingStatistics (config) {
|
||||||
// initiated by the rowing engine in case an impulse was not considered
|
// initiated by the rowing engine in case an impulse was not considered
|
||||||
// because it was too large
|
// because it was too large
|
||||||
function handlePause (duration) {
|
function handlePause (duration) {
|
||||||
|
sessionState = 'paused'
|
||||||
caloriesAveragerMinute.pushValue(0, duration)
|
caloriesAveragerMinute.pushValue(0, duration)
|
||||||
caloriesAveragerHour.pushValue(0, duration)
|
caloriesAveragerHour.pushValue(0, duration)
|
||||||
emitter.emit('rowingPaused')
|
emitter.emit('rowingPaused')
|
||||||
|
|
@ -94,6 +93,7 @@ function createRowingStatistics (config) {
|
||||||
function handleRecoveryEnd (stroke) {
|
function handleRecoveryEnd (stroke) {
|
||||||
// todo: we need a better mechanism to communicate strokeState updates
|
// todo: we 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
|
// this is an initial hacky attempt to see if we can use it for the C2-pm5 protocol
|
||||||
|
if (sessionState !== 'rowing') startTraining()
|
||||||
durationTotal = stroke.timeSinceStart
|
durationTotal = stroke.timeSinceStart
|
||||||
powerAverager.pushValue(stroke.power)
|
powerAverager.pushValue(stroke.power)
|
||||||
speedAverager.pushValue(stroke.speed)
|
speedAverager.pushValue(stroke.speed)
|
||||||
|
|
@ -137,8 +137,9 @@ function createRowingStatistics (config) {
|
||||||
// todo: due to sanitization we currently do not use a consistent time throughout the engine
|
// todo: due to sanitization we currently do not use a consistent time throughout the engine
|
||||||
// We will rework this section to use both absolute and sanitized time in the appropriate places.
|
// We will rework this section to use both absolute and sanitized time in the appropriate places.
|
||||||
// We will also polish up the events for the recovery and drive phase, so we get clean complete strokes from the first stroke onwards.
|
// We will also polish up the events for the recovery and drive phase, so we get clean complete strokes from the first stroke onwards.
|
||||||
const averagedStrokeTime = strokeAverager.getAverage() > minimumStrokeTime && strokeAverager.getAverage() < maximumStrokeTime && lastStrokeSpeed > 0 ? strokeAverager.getAverage() : 0 // seconds
|
const averagedStrokeTime = strokeAverager.getAverage() > minimumStrokeTime && strokeAverager.getAverage() < maximumStrokeTime && lastStrokeSpeed > 0 && sessionState === 'rowing' ? strokeAverager.getAverage() : 0 // seconds
|
||||||
return {
|
return {
|
||||||
|
sessionState,
|
||||||
durationTotal,
|
durationTotal,
|
||||||
durationTotalFormatted: secondsToTimeString(durationTotal),
|
durationTotalFormatted: secondsToTimeString(durationTotal),
|
||||||
strokesTotal,
|
strokesTotal,
|
||||||
|
|
@ -147,14 +148,14 @@ function createRowingStatistics (config) {
|
||||||
caloriesPerMinute: caloriesAveragerMinute.getAverage() > 0 ? caloriesAveragerMinute.getAverage() : 0,
|
caloriesPerMinute: caloriesAveragerMinute.getAverage() > 0 ? caloriesAveragerMinute.getAverage() : 0,
|
||||||
caloriesPerHour: caloriesAveragerHour.getAverage() > 0 ? caloriesAveragerHour.getAverage() : 0,
|
caloriesPerHour: caloriesAveragerHour.getAverage() > 0 ? caloriesAveragerHour.getAverage() : 0,
|
||||||
strokeTime: lastStrokeDuration, // seconds
|
strokeTime: lastStrokeDuration, // seconds
|
||||||
distance: lastStrokeDistance > 0 && lastStrokeSpeed > 0 ? lastStrokeDistance : 0, // meters
|
distance: lastStrokeDistance > 0 && lastStrokeSpeed > 0 && sessionState === 'rowing' ? lastStrokeDistance : 0, // meters
|
||||||
power: powerAverager.getAverage() > 0 && lastStrokeSpeed > 0 ? powerAverager.getAverage() : 0, // watts
|
power: powerAverager.getAverage() > 0 && lastStrokeSpeed > 0 && sessionState === 'rowing' ? powerAverager.getAverage() : 0, // watts
|
||||||
split: splitTime, // seconds/500m
|
split: splitTime, // seconds/500m
|
||||||
splitFormatted: secondsToTimeString(splitTime),
|
splitFormatted: secondsToTimeString(splitTime),
|
||||||
powerRatio: powerRatioAverager.getAverage() > 0 && lastStrokeSpeed > 0 ? powerRatioAverager.getAverage() : 0,
|
powerRatio: powerRatioAverager.getAverage() > 0 && lastStrokeSpeed > 0 && sessionState === 'rowing' ? powerRatioAverager.getAverage() : 0,
|
||||||
instantaneousTorque: instantaneousTorque,
|
instantaneousTorque: instantaneousTorque,
|
||||||
strokesPerMinute: averagedStrokeTime !== 0 ? (60.0 / averagedStrokeTime) : 0,
|
strokesPerMinute: averagedStrokeTime !== 0 && sessionState === 'rowing' ? (60.0 / averagedStrokeTime) : 0,
|
||||||
speed: speedAverager.getAverage() > 0 && lastStrokeSpeed > 0 ? (speedAverager.getAverage() * 3.6) : 0, // km/h
|
speed: speedAverager.getAverage() > 0 && lastStrokeSpeed > 0 && sessionState === 'rowing' ? (speedAverager.getAverage() * 3.6) : 0, // km/h
|
||||||
strokeState: lastStrokeState,
|
strokeState: lastStrokeState,
|
||||||
heartrate,
|
heartrate,
|
||||||
heartrateBatteryLevel
|
heartrateBatteryLevel
|
||||||
|
|
@ -162,11 +163,11 @@ function createRowingStatistics (config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTraining () {
|
function startTraining () {
|
||||||
trainingRunning = true
|
sessionState = 'rowing'
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopTraining () {
|
function stopTraining () {
|
||||||
trainingRunning = false
|
sessionState = 'stopped'
|
||||||
if (rowingPausedTimer)clearInterval(rowingPausedTimer)
|
if (rowingPausedTimer)clearInterval(rowingPausedTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,6 +183,7 @@ function createRowingStatistics (config) {
|
||||||
powerAverager.reset()
|
powerAverager.reset()
|
||||||
speedAverager.reset()
|
speedAverager.reset()
|
||||||
powerRatioAverager.reset()
|
powerRatioAverager.reset()
|
||||||
|
sessionState = 'waitingForStart'
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear the metrics in case the user pauses rowing
|
// clear the metrics in case the user pauses rowing
|
||||||
|
|
@ -191,6 +193,7 @@ function createRowingStatistics (config) {
|
||||||
speedAverager.reset()
|
speedAverager.reset()
|
||||||
powerRatioAverager.reset()
|
powerRatioAverager.reset()
|
||||||
lastStrokeState = 'RECOVERY'
|
lastStrokeState = 'RECOVERY'
|
||||||
|
sessionState = 'paused'
|
||||||
emitter.emit('rowingPaused')
|
emitter.emit('rowingPaused')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ Open Rowing Monitor uses some great work by others. Thank you for all the great
|
||||||
|
|
||||||
* Dave Vernooy's project description on [ErgWare](https://dvernooy.github.io/projects/ergware) has some good information on the maths involved in a rowing ergometer.
|
* Dave Vernooy's project description on [ErgWare](https://dvernooy.github.io/projects/ergware) has some good information on the maths involved in a rowing ergometer.
|
||||||
|
|
||||||
* Bluetooth is quite a complex beast, luckily the Bluetooth SIG releases all the [specifications here](https://www.bluetooth.com/specifications/specs).
|
* Nomath has done a very impressive [Reverse engineering of the actual workings of the Concept 2 PM5](https://www.c2forum.com/viewtopic.php?f=7&t=194719), including experimentally checking drag calculations.
|
||||||
|
|
||||||
* The app icon is based on this [image of a rowing machine](https://thenounproject.com/term/rowing-machine/659265) by [Gan Khoon Lay](https://thenounproject.com/leremy/) licensed under [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/).
|
* Bluetooth is quite a complex beast, luckily the Bluetooth SIG releases all the [Bluetooth Specifications](https://www.bluetooth.com/specifications/specs).
|
||||||
|
|
||||||
|
* The app icon is based on this [Image of a rowing machine](https://thenounproject.com/term/rowing-machine/659265) by [Gan Khoon Lay](https://thenounproject.com/leremy/) licensed under [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/).
|
||||||
|
|
||||||
* The frontend uses some icons from [Font Awesome](https://fontawesome.com/), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
* The frontend uses some icons from [Font Awesome](https://fontawesome.com/), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,11 @@ There are several key metrics that underpin the performance measurement of a row
|
||||||
|
|
||||||
* The **Angular Velocity** of the flywheel in Radians/second: in essence the number of (partial) rotations of the flywheel per second. As the *Angular Displacement* is fixed for a specific rowing machine, the *Angular Velocity* is (*angular displacement between impulses*) / (time between impulses);
|
* The **Angular Velocity** of the flywheel in Radians/second: in essence the number of (partial) rotations of the flywheel per second. As the *Angular Displacement* is fixed for a specific rowing machine, the *Angular Velocity* is (*angular displacement between impulses*) / (time between impulses);
|
||||||
|
|
||||||
* The **Angular Acceleration** of the flywheel in Radians/second^2^: the acceleration/deceleration of the flywheel;
|
* The **Angular Acceleration** of the flywheel (in Radians/second^2): the acceleration/deceleration of the flywheel;
|
||||||
|
|
||||||
* The *estimated* **Linear Distance** of the boat in Meters: the distance the boat is expected to travel;
|
* The *estimated* **Linear Distance** of the boat (in Meters): the distance the boat is expected to travel;
|
||||||
|
|
||||||
* _estimated_ **Linear Velocity** of the boat in Meters/Second: the speed at which the boat is expected to travel.
|
* _estimated_ **Linear Velocity** of the boat (in Meters/Second): the speed at which the boat is expected to travel.
|
||||||
|
|
||||||
## Measurements during the recovery phase
|
## Measurements during the recovery phase
|
||||||
|
|
||||||
|
|
@ -116,7 +116,7 @@ Although not the first phase in a cycle, it is an important phase as it deducts
|
||||||
|
|
||||||
In the recovery phase, the only force exerted on the flywheel is the (air/water/magnetic)resistance. Thus we can calculate the Drag factor of the Flywheel based on the entire phase.
|
In the recovery phase, the only force exerted on the flywheel is the (air/water/magnetic)resistance. Thus we can calculate the Drag factor of the Flywheel based on the entire phase.
|
||||||
|
|
||||||
As [[1]](#1) describes in formula 7.2:
|
As [[1]](#1) describes in formula 7.2, which is also experimentally verified by Nomath on a Concept 2 [[5]](#5):
|
||||||
|
|
||||||
> <img src="https://render.githubusercontent.com/render/math?math=k=Id(1/\omega)/dt">
|
> <img src="https://render.githubusercontent.com/render/math?math=k=Id(1/\omega)/dt">
|
||||||
|
|
||||||
|
|
@ -280,3 +280,5 @@ Again, this is a systematic (overestimation) of the power, which will be systema
|
||||||
<a id="3">[3]</a> Dave Vernooy, "Open Source Ergometer ErgWare" <https://dvernooy.github.io/projects/ergware/>
|
<a id="3">[3]</a> Dave Vernooy, "Open Source Ergometer ErgWare" <https://dvernooy.github.io/projects/ergware/>
|
||||||
|
|
||||||
<a id="4">[4]</a> <https://github.com/dvernooy/ErgWare/blob/master/v0.5/main/main.ino>
|
<a id="4">[4]</a> <https://github.com/dvernooy/ErgWare/blob/master/v0.5/main/main.ino>
|
||||||
|
|
||||||
|
<a id="5">[5]</a> Fan blade Physics and a Peek inside C2's Black Box, Nomath <https://www.c2forum.com/viewtopic.php?f=7&t=194719>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue