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)
|
||||
bufferBuilder.writeUInt8(255)
|
||||
// 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
|
||||
bufferBuilder.writeUInt8(1)
|
||||
bufferBuilder.writeUInt8(data.sessionState === 'rowing' ? 1 : 0)
|
||||
// strokeState: UInt8 2 DRIVING, 4 RECOVERY
|
||||
bufferBuilder.writeUInt8(data.strokeState === 'DRIVING' ? 2 : 4)
|
||||
// totalWorkDistance: UInt24LE in 1 m
|
||||
|
|
|
|||
|
|
@ -149,11 +149,7 @@ function createRowingEngine (rowerSettings) {
|
|||
// Calculate the key metrics
|
||||
recoveryLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * recoveryPhaseAngularDisplacement
|
||||
totalLinearDistance += recoveryLinearDistance
|
||||
if (currentDt > 0) {
|
||||
previousAngularVelocity = currentAngularVelocity
|
||||
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
|
||||
currentTorque = rowerSettings.flywheelInertia * ((currentAngularVelocity - previousAngularVelocity) / currentDt) + dragFactor * Math.pow(currentAngularVelocity, 2)
|
||||
}
|
||||
currentTorque = calculateTorque(currentDt)
|
||||
linearCycleVelocity = calculateLinearVelocity()
|
||||
averagedCyclePower = calculateCyclePower()
|
||||
|
||||
|
|
@ -186,11 +182,7 @@ function createRowingEngine (rowerSettings) {
|
|||
// Update the key metrics on each impulse
|
||||
drivePhaseAngularDisplacement = ((totalNumberOfImpulses - flankDetector.noImpulsesToBeginFlank()) - drivePhaseStartAngularDisplacement) * angularDisplacementPerImpulse
|
||||
driveLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * drivePhaseAngularDisplacement
|
||||
if (currentDt > 0) {
|
||||
previousAngularVelocity = currentAngularVelocity
|
||||
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
|
||||
currentTorque = calculateTorque(currentDt)
|
||||
}
|
||||
currentTorque = calculateTorque(currentDt)
|
||||
if (workoutHandler) {
|
||||
workoutHandler.updateKeyMetrics({
|
||||
timeSinceStart: totalTime,
|
||||
|
|
@ -213,11 +205,7 @@ function createRowingEngine (rowerSettings) {
|
|||
// driveEndAngularVelocity = angularDisplacementPerImpulse / flankDetector.impulseLengthAtBeginFlank()
|
||||
driveLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * drivePhaseAngularDisplacement
|
||||
totalLinearDistance += driveLinearDistance
|
||||
if (currentDt > 0) {
|
||||
previousAngularVelocity = currentAngularVelocity
|
||||
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
|
||||
currentTorque = calculateTorque(currentDt)
|
||||
}
|
||||
currentTorque = calculateTorque(currentDt)
|
||||
// We display the AVERAGE speed in the display, NOT the top speed of the stroke
|
||||
linearCycleVelocity = calculateLinearVelocity()
|
||||
averagedCyclePower = calculateCyclePower()
|
||||
|
|
@ -252,11 +240,7 @@ function createRowingEngine (rowerSettings) {
|
|||
// Update the key metrics on each impulse
|
||||
recoveryPhaseAngularDisplacement = ((totalNumberOfImpulses - flankDetector.noImpulsesToBeginFlank()) - recoveryPhaseStartAngularDisplacement) * angularDisplacementPerImpulse
|
||||
recoveryLinearDistance = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * recoveryPhaseAngularDisplacement
|
||||
if (currentDt > 0) {
|
||||
previousAngularVelocity = currentAngularVelocity
|
||||
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
|
||||
currentTorque = calculateTorque(currentDt)
|
||||
}
|
||||
currentTorque = calculateTorque(currentDt)
|
||||
if (workoutHandler) {
|
||||
workoutHandler.updateKeyMetrics({
|
||||
timeSinceStart: totalTime,
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ function createRowingStatistics (config) {
|
|||
const powerRatioAverager = createWeightedAverager(numOfDataPointsForAveraging)
|
||||
const caloriesAveragerMinute = createMovingIntervalAverager(60)
|
||||
const caloriesAveragerHour = createMovingIntervalAverager(60 * 60)
|
||||
let trainingRunning = false
|
||||
let sessionState = 'waitingForStart'
|
||||
let rowingPausedTimer
|
||||
let heartrateResetTimer
|
||||
let distanceTotal = 0.0
|
||||
let durationTotal = 0
|
||||
let strokesTotal = 0
|
||||
let caloriesTotal = 0.0
|
||||
let heartrate
|
||||
let heartrate = 0
|
||||
let heartrateBatteryLevel = 0
|
||||
let lastStrokeDuration = 0.0
|
||||
let instantaneousTorque = 0.0
|
||||
|
|
@ -51,8 +51,6 @@ function createRowingStatistics (config) {
|
|||
}
|
||||
|
||||
function handleStrokeEnd (stroke) {
|
||||
if (!trainingRunning) startTraining()
|
||||
|
||||
// if we do not get a stroke for timeBetweenStrokesBeforePause milliseconds we treat this as a rowing pause
|
||||
if (rowingPausedTimer)clearInterval(rowingPausedTimer)
|
||||
rowingPausedTimer = setTimeout(() => pauseRowing(), timeBetweenStrokesBeforePause)
|
||||
|
|
@ -85,6 +83,7 @@ function createRowingStatistics (config) {
|
|||
// initiated by the rowing engine in case an impulse was not considered
|
||||
// because it was too large
|
||||
function handlePause (duration) {
|
||||
sessionState = 'paused'
|
||||
caloriesAveragerMinute.pushValue(0, duration)
|
||||
caloriesAveragerHour.pushValue(0, duration)
|
||||
emitter.emit('rowingPaused')
|
||||
|
|
@ -94,6 +93,7 @@ function createRowingStatistics (config) {
|
|||
function handleRecoveryEnd (stroke) {
|
||||
// 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
|
||||
if (sessionState !== 'rowing') startTraining()
|
||||
durationTotal = stroke.timeSinceStart
|
||||
powerAverager.pushValue(stroke.power)
|
||||
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
|
||||
// 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.
|
||||
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 {
|
||||
sessionState,
|
||||
durationTotal,
|
||||
durationTotalFormatted: secondsToTimeString(durationTotal),
|
||||
strokesTotal,
|
||||
|
|
@ -147,14 +148,14 @@ function createRowingStatistics (config) {
|
|||
caloriesPerMinute: caloriesAveragerMinute.getAverage() > 0 ? caloriesAveragerMinute.getAverage() : 0,
|
||||
caloriesPerHour: caloriesAveragerHour.getAverage() > 0 ? caloriesAveragerHour.getAverage() : 0,
|
||||
strokeTime: lastStrokeDuration, // seconds
|
||||
distance: lastStrokeDistance > 0 && lastStrokeSpeed > 0 ? lastStrokeDistance : 0, // meters
|
||||
power: powerAverager.getAverage() > 0 && lastStrokeSpeed > 0 ? powerAverager.getAverage() : 0, // watts
|
||||
distance: lastStrokeDistance > 0 && lastStrokeSpeed > 0 && sessionState === 'rowing' ? lastStrokeDistance : 0, // meters
|
||||
power: powerAverager.getAverage() > 0 && lastStrokeSpeed > 0 && sessionState === 'rowing' ? powerAverager.getAverage() : 0, // watts
|
||||
split: splitTime, // seconds/500m
|
||||
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,
|
||||
strokesPerMinute: averagedStrokeTime !== 0 ? (60.0 / averagedStrokeTime) : 0,
|
||||
speed: speedAverager.getAverage() > 0 && lastStrokeSpeed > 0 ? (speedAverager.getAverage() * 3.6) : 0, // km/h
|
||||
strokesPerMinute: averagedStrokeTime !== 0 && sessionState === 'rowing' ? (60.0 / averagedStrokeTime) : 0,
|
||||
speed: speedAverager.getAverage() > 0 && lastStrokeSpeed > 0 && sessionState === 'rowing' ? (speedAverager.getAverage() * 3.6) : 0, // km/h
|
||||
strokeState: lastStrokeState,
|
||||
heartrate,
|
||||
heartrateBatteryLevel
|
||||
|
|
@ -162,11 +163,11 @@ function createRowingStatistics (config) {
|
|||
}
|
||||
|
||||
function startTraining () {
|
||||
trainingRunning = true
|
||||
sessionState = 'rowing'
|
||||
}
|
||||
|
||||
function stopTraining () {
|
||||
trainingRunning = false
|
||||
sessionState = 'stopped'
|
||||
if (rowingPausedTimer)clearInterval(rowingPausedTimer)
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +183,7 @@ function createRowingStatistics (config) {
|
|||
powerAverager.reset()
|
||||
speedAverager.reset()
|
||||
powerRatioAverager.reset()
|
||||
sessionState = 'waitingForStart'
|
||||
}
|
||||
|
||||
// clear the metrics in case the user pauses rowing
|
||||
|
|
@ -191,6 +193,7 @@ function createRowingStatistics (config) {
|
|||
speedAverager.reset()
|
||||
powerRatioAverager.reset()
|
||||
lastStrokeState = 'RECOVERY'
|
||||
sessionState = 'paused'
|
||||
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.
|
||||
|
||||
* 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/).
|
||||
|
||||
|
|
|
|||
|
|
@ -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 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
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
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">
|
||||
|
||||
|
|
@ -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="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