Code Cleanup and Documentation Update (#30)

* Moved .doc to .md

Moved the Word document to a more open format

* Update of the file based on the new settings

Update of the settings documentation due to the introduction of the new RowingEngine

* Moved .doc to .md

Moved the information to the .md file, updated with the design decissions made in the new RowingEngine.

* Splitting the hardware set-ups for specific rowers

Split off the WRX700 hardware set-up from the generic setup, to allow for other rower-specific set-ups to be created (easier to read and better for search engines).

* Removal of generic text

* SPlit the generic from the WRX700

* Fixed a very rare bug

A very rare condition can cause the noise correction filter to get stuck and dismiss all subsequent values. Now the number of corrections is maximised preventing that situation.

* Code refactoring

Refactoring of the code (added calculateLinearVelocity(), calculateCyclePower(), calculateTorque() functions) to reduce code duplication across phases.

* Fixed a very rare bug

A very rare condition can cause the noise correction filter to get stuck and dismiss all subsequent values. Now the number of corrections is maximised preventing that situation.

* Code refactoring

Refactoring of the code (added calculateLinearVelocity(), calculateCyclePower(), calculateTorque() functions) to reduce code duplication across phases.

* fixes some typos, fixes some links, renames some files

Co-authored-by: Lars Berning <151194+laberning@users.noreply.github.com>
This commit is contained in:
Jaap van Ekris 2021-12-13 20:01:28 +01:00 committed by GitHub
parent 2b7baf8f28
commit 19ec4f28bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 247 additions and 104 deletions

View File

@ -22,6 +22,8 @@ function createMovingFlankDetector (rowerSettings) {
const angularAcceleration = new Array(rowerSettings.flankLength + 1)
angularAcceleration.fill(0)
const movingAverage = createMovingAverager(rowerSettings.smoothing, rowerSettings.maximumTimeBetweenImpulses)
let numberOfSequentialCorrections = 0
const maxNumberOfSequentialCorrections = (rowerSettings.smoothing >= 2 ? rowerSettings.smoothing : 2)
function pushValue (dataPoint) {
// add the new dataPoint to the array, we have to move data points starting at the oldest ones
@ -47,10 +49,19 @@ function createMovingFlankDetector (rowerSettings) {
// lets test if pushing this value would fit the curve we are looking for
movingAverage.pushValue(dataPoint)
if (movingAverage.getAverage() < (rowerSettings.maximumDownwardChange * cleanDataPoints[1]) || movingAverage.getAverage() > (rowerSettings.maximumUpwardChange * cleanDataPoints[1])) {
// impulses are outside plausible ranges, so we assume it is close to the previous one
log.debug(`noise filter corrected currentDt, ${dataPoint} was too much of an accelleration/decelleration with respect to ${movingAverage.getAverage()}, changed to previous value, ${cleanDataPoints[1]}`)
movingAverage.replaceLastPushedValue(cleanDataPoints[1])
if (movingAverage.getAverage() > (rowerSettings.maximumDownwardChange * cleanDataPoints[1]) && movingAverage.getAverage() < (rowerSettings.maximumUpwardChange * cleanDataPoints[1])) {
numberOfSequentialCorrections = 0
} else {
// impulses are outside plausible ranges
if (numberOfSequentialCorrections <= maxNumberOfSequentialCorrections) {
// We haven't made too many corrections, so we assume it is close to the previous one
log.debug(`noise filter corrected currentDt, ${dataPoint} was too much of an accelleration/decelleration with respect to ${movingAverage.getAverage()}, changed to previous value, ${cleanDataPoints[1]}`)
movingAverage.replaceLastPushedValue(cleanDataPoints[1])
} else {
// We made too many corrections (typically, one currentDt is too long, the next is to short or vice versa), let's allow the algorithm to pick it up otherwise we might get stuck
log.debug(`noise filter wanted to corrected currentDt (${dataPoint} sec), but it had already made ${numberOfSequentialCorrections} corrections, filter temporarily disabled`)
}
numberOfSequentialCorrections = numberOfSequentialCorrections + 1
}
// determine the moving average, to reduce noise

View File

@ -154,13 +154,8 @@ function createRowingEngine (rowerSettings) {
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
currentTorque = rowerSettings.flywheelInertia * ((currentAngularVelocity - previousAngularVelocity) / currentDt) + dragFactor * Math.pow(currentAngularVelocity, 2)
}
if (cycleLength >= minimumCycleLength) {
// There is no division by zero and the data data is plausible
linearCycleVelocity = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * ((recoveryPhaseAngularDisplacement + drivePhaseAngularDisplacement) / cycleLength)
averagedCyclePower = dragFactor * Math.pow((recoveryPhaseAngularDisplacement + drivePhaseAngularDisplacement) / cycleLength, 3.0)
} else {
log.error(`Time: ${totalTime.toFixed(4)} sec, impuls ${totalNumberOfImpulses}: cycle length was not plausible, CycleLength = ${cycleLength} sec`)
}
linearCycleVelocity = calculateLinearVelocity()
averagedCyclePower = calculateCyclePower()
// Next, we start the "Drive" Phase
log.debug(`*** DRIVE phase started at time: ${totalTime.toFixed(4)} sec, impuls number ${totalNumberOfImpulses}`)
@ -194,7 +189,7 @@ function createRowingEngine (rowerSettings) {
if (currentDt > 0) {
previousAngularVelocity = currentAngularVelocity
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
currentTorque = rowerSettings.flywheelInertia * ((currentAngularVelocity - previousAngularVelocity) / currentDt) + dragFactor * Math.pow(currentAngularVelocity, 2)
currentTorque = calculateTorque(currentDt)
}
if (workoutHandler) {
workoutHandler.updateKeyMetrics({
@ -221,17 +216,11 @@ function createRowingEngine (rowerSettings) {
if (currentDt > 0) {
previousAngularVelocity = currentAngularVelocity
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
currentTorque = rowerSettings.flywheelInertia * ((currentAngularVelocity - previousAngularVelocity) / currentDt) + dragFactor * Math.pow(currentAngularVelocity, 2)
currentTorque = calculateTorque(currentDt)
}
// We display the AVERAGE speed in the display, NOT the top speed of the stroke
if (drivePhaseLength > rowerSettings.minimumDriveTime && cycleLength > minimumCycleLength) {
// let's prevent division's by zero and make sure data is plausible
linearCycleVelocity = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * ((drivePhaseAngularDisplacement + recoveryPhaseAngularDisplacement) / cycleLength)
// drivePhaseEnergyProduced = rowerSettings.flywheelInertia * ((driveEndAngularVelocity - driveStartAngularVelocity) / drivePhaseLength) * drivePhaseAngularDisplacement + dragFactor * Math.pow(driveEndAngularVelocity, 2) * drivePh$
averagedCyclePower = dragFactor * Math.pow((recoveryPhaseAngularDisplacement + drivePhaseAngularDisplacement) / cycleLength, 3.0)
} else {
log.error(`Time: ${totalTime.toFixed(4)} sec, impuls ${totalNumberOfImpulses}: cycle length was not plausible, drivePhaseLength = ${drivePhaseLength.toFixed(4)} sec, cycleLength = ${cycleLength.toFixed(4)} sec`)
}
linearCycleVelocity = calculateLinearVelocity()
averagedCyclePower = calculateCyclePower()
// Next, we start the "Recovery" Phase
log.debug(`*** RECOVERY phase started at time: ${totalTime.toFixed(4)} sec, impuls number ${totalNumberOfImpulses}`)
@ -266,7 +255,7 @@ function createRowingEngine (rowerSettings) {
if (currentDt > 0) {
previousAngularVelocity = currentAngularVelocity
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
currentTorque = rowerSettings.flywheelInertia * ((currentAngularVelocity - previousAngularVelocity) / currentDt) + dragFactor * Math.pow(currentAngularVelocity, 2)
currentTorque = calculateTorque(currentDt)
}
if (workoutHandler) {
workoutHandler.updateKeyMetrics({
@ -277,6 +266,40 @@ function createRowingEngine (rowerSettings) {
}
}
function calculateLinearVelocity () {
// Here we calculate the AVERAGE speed for the displays, NOT the topspeed of the stroke
let tempLinearVelocity = linearCycleVelocity
if (drivePhaseLength > rowerSettings.minimumDriveTime && cycleLength > minimumCycleLength) {
// There is no division by zero and the data data is plausible
tempLinearVelocity = Math.pow((dragFactor / rowerSettings.magicConstant), 1.0 / 3.0) * ((recoveryPhaseAngularDisplacement + drivePhaseAngularDisplacement) / cycleLength)
} else {
log.error(`Time: ${totalTime.toFixed(4)} sec, impuls ${totalNumberOfImpulses}: cycle length was not plausible, CycleLength = ${cycleLength} sec`)
}
return tempLinearVelocity
}
function calculateCyclePower () {
// Here we calculate the AVERAGE power for the displays, NOT the top power of the stroke
let cyclePower = averagedCyclePower
if (drivePhaseLength > rowerSettings.minimumDriveTime && cycleLength > minimumCycleLength) {
// There is no division by zero and the data data is plausible
cyclePower = dragFactor * Math.pow((recoveryPhaseAngularDisplacement + drivePhaseAngularDisplacement) / cycleLength, 3.0)
} else {
log.error(`Time: ${totalTime.toFixed(4)} sec, impuls ${totalNumberOfImpulses}: cycle length was not plausible, CycleLength = ${cycleLength} sec`)
}
return cyclePower
}
function calculateTorque (currentDt) {
let torque = currentTorque
if (currentDt > 0) {
previousAngularVelocity = currentAngularVelocity
currentAngularVelocity = angularDisplacementPerImpulse / currentDt
torque = rowerSettings.flywheelInertia * ((currentAngularVelocity - previousAngularVelocity) / currentDt) + dragFactor * Math.pow(currentAngularVelocity, 2)
}
return torque
}
function reset () {
cyclePhase = 'Drive'
totalTime = 0.0

View File

@ -1,30 +0,0 @@
# Set Up of the Open Rowing Monitor settings for a specific rower
This guide helps you to adjust the rowing monitor specifically for your rower or even for you
## Why have settings
No rowing machine is the same, and their physical construction is important for the Rowing Monitor to understand to be able to understand your rowing. The easiest way is to select your rower profile from `config/rowerProfiles.js` and put its name in `config.js` (i.e. `rowerSettings: rowerProfiles.WRX700`).
If your rower isn't in there, this guide will help you set it up (please send in the data and settings, so we can add it to the OpenRowingMonitor).
Settings important for Open Rowing Monitor:
* numOfImpulsesPerRevolution: tells Open Rowing Monitor how many impulses per rotation of the flywheel to expect. Although sometimes not easy to detect, you can sometimes find it in the manual under the parts-list
* liquidFlywheel: tells OpenRowingMonitor if you are using a water rower (true) or a solid flywheel with magnetic or air-resistance (false)
* dragFactor: tells OpenRowingMonitor how much damping and thus resistance your flywheel is offering. This is typically also dependent on your damper-setting (if present). To measure it for your rowing machine, comment in the logging at the end of "startDrivePhase" function. Then do some strokes on the rower and estimate a value based on the logging.
* flywheelInertia: The inertia of the flywheel, which in practice influences your power values and distance. This typically is set by rowing and see what kind of power is displayed on the monitor. Typical ranges are weight dependent (see [this explanation](https://www.rowingmachine-guide.com/tabata-rowing-workouts.html)).
* Noise reduction settings. You should only change these settings if you experience issues.
* minimumTimeBetweenImpulses
* maximumTimeBetweenImpulses
* maximumDownwardChange
* maximumUpwardChange
* Stroke detection settings.
* minimumDriveTime
* minimumRecoveryTime
For the noise reduction settings and stroke detection settings, you can use the Excel tool. When OpenRowingMonitor records a log (comment out the line in `server.js`), you can paste the values in the first column of the "Raw Data" tab (please observe that the Raspberry uses a point as separator, and your version of Excel might expect a comma). From there, the Excel file simulates the calculations the OpenRowingMonitor makes, allowing you to play with these settings.
By changing the noise reduction settings, you can remove any obvious errors. You don't need to filter everything: it is just to remove obvious errors that might frustrate the stroke detection, but in the end you can't prevent every piece of noise out there. Begin with the noise filtering, when you are satisfied, you can adjust the stroke detection.
Please note that changing the noise filtering and stroke detection settings will affect your dragFactor and flywheelInertia. So it is best to start with rowing a few strokes to determine settings for noise filtering and stroke detection, and then move on to the other settings.

View File

@ -0,0 +1,24 @@
# Hardware set up of Open Rowing Monitor on a Sportstech WRX700
This guide roughly explains how to set up the hardware.
After the software installation, basically all that's left to do is hook up your sensor to the GPIO pins of the Raspberry Pi and configure the rowing machine specific parameters of the software.
Open Rowing Monitor reads the sensor signal from GPIO port 17 and expects it to pull on GND if the sensor is closed. To get a stable reading you should add a pull-up resistor to that pin. I prefer to use the internal resistor of the Raspberry Pi to keep the wiring simple but of course you can also go with an external circuit.
![Internal wiring of Raspberry Pi](img/raspberrypi_internal_wiring.jpg)
*Internal wiring of Raspberry Pi*
The internal pull-up can be enabled as described [here](https://www.raspberrypi.org/documentation/configuration/config-txt/gpio.md). So its as simple as adding the following to `/boot/config.txt` and then rebooting the device.
``` Properties
# configure GPIO 17 as input and enable the pull-up resistor
gpio=17=pu,ip
```
How to connect this to your rowing machine is specific to your device. You need some kind of mechanism to convert the rotation of the flywheel into impulses. The WRX700 has a reed sensor for this built-in so hooking it up is as simple as connecting the cables. This sensor had one magnet on the wheel, which gives one impulse per rotation. I simply plugged a second magnet to the opposite side of the wheel to double the resolution for more precision.
![Connecting the reed sensor](img/raspberrypi_reedsensor_wiring.jpg)
*Connecting the reed sensor*
You should now adjust the rower specific parameters in `config/config.js` to suit your rowing machine. Have a look at [config/default.config.js](../config/default.config.js) to see what config parameters are available.

View File

@ -6,11 +6,12 @@ This guide roughly explains how to set up the rowing software and hardware.
* A Raspberry Pi that supports Bluetooth Low Energy. Probably this also runs on other devices.
* Raspberry Pi Zero W or WH
* Raspberry Pi Zero 2 W or WH
* Raspberry Pi 3 Model A+, B or B+
* Raspberry Pi 4 Model B
* An SD Card, any size above 4GB should be fine
* A rowing machine (obviously) with some way to measure the rotation of the flywheel
* the WRX700 has a build in reed sensor that I can directly connect to the GPIO pins of the Raspberry Pi
* with a build in reed sensor that you can directly connect to the GPIO pins of the Raspberry Pi
* if your machine doesn't have a sensor, it should be easy to build something similar (magnetically or optical)
* Some Dupont cables to connect the GPIO pins to the sensor
@ -39,7 +40,7 @@ Open Rowing Monitor does not provide proper releases (yet), but you can update t
updateopenrowingmonitor.sh
```
### Running Open Rowing Monitor without root permissions
### Running Open Rowing Monitor without root permissions (optional)
The default installation will run Open Rowing Monitor with root permissions. You can also run it as normal user by modifying the following system services:
@ -76,15 +77,22 @@ The internal pull-up can be enabled as described [here](https://www.raspberrypi.
gpio=17=pu,ip
```
How to connect this to your rowing machine is specific to your device. You need some kind of mechanism to convert the rotation of the flywheel into impulses. The WRX700 has a reed sensor for this built-in so hooking it up is as simple as connecting the cables. This sensor had one magnet on the wheel, which gives one impulse per rotation. I simply plugged a second magnet to the opposite side of the wheel to double the resolution for more precision.
How to connect this to your rowing machine is specific to your device. You need some kind of mechanism to convert the rotation of the flywheel into impulses. Some rowers have a reed sensor for this built-in so hooking it up is as simple as connecting the cables. Such sensor has one or more magnet on the wheel, which give impulses per rotation. If this is the case, you can simply plug in a second magnet to the opposite side of the wheel to double the resolution for more precision.
![Connecting the reed sensor](img/raspberrypi_reedsensor_wiring.jpg)
*Connecting the reed sensor*
If your machine does not have something like this or if the sensor is not accessible, you can still build something similar quite easily. Some ideas on what to use:
For a specific hardware-setup, please look at:
* [Sportstech WRX700](hardware_setup_WRX700.md)
If your machine isn't listed and does not have something like this or if the sensor is not accessible, you can still build something similar quite easily. Some ideas on what to use:
* Reed sensor (i.e. of an old bike tachometer)
* PAS sensor (i.e. from an E-bike)
* Optical chopper wheel
## Rower Settings
You should now adjust the rower specific parameters in `config/config.js` to suit your rowing machine. Have a look at [config/default.config.js](../config/default.config.js) to see what config parameters are available.
Also check the [Guide for rower specific settings](rower_settings.md).

View File

@ -1,16 +1,20 @@
# The physics behind Open Rowing Monitor
<!-- markdownlint-disable no-inline-html -->
In this document we explain the physics behind the OpenRowing Monitor, to allow for independent review and software maintenance. This work wouldn't have been possible without some solid physics, described by some people with real knowledge of the subject matter. Please note that any errors in our implementation probably is on us, not them. When appropriate, we link to these sources. When possible, we also link to the source code.
## Leading principles
In this model, we try to:
The physics engine is the core of Open Rowing Monitor. In our design of the physics engine, we try to:
* stay as close to the original data as possible (thus depend on direct measurements as much as possible) instead of depending on derived data. This means that there are two absolute values: **time** and **Number of Impulses**;
* stay as close to the original data as possible (thus depend on direct measurements as much as possible) instead of depending on derived data. This means that there are two absolute values we try to stay close to as much as possible: the **time between an impulse** and the **Number of Impulses** (their origin and meaning is later explained);
* use robust calculations as possible (i.e. not depend on a single measurements to reduce effects of measurement errors);
* use robust calculations wherever possible (i.e. not depend on a single measurements, extrapolations or derivative functions, etc.) to reduce effects of measurement errors.
## Phases, properties and concepts in the rowing cycle
Before we analyze the physics of a rowing engine, we first need to define the basic concepts.
<img src="img/physics/indoorrower.png" width="700">
<!-- markdownlint-disable-next-line no-emphasis-as-header -->
@ -18,7 +22,7 @@ In this model, we try to:
A rowing machine effectively has two fundamental movements: a **linear** (the rower moving up and down, or a boat moving forward) and a **rotational** where the energy that the rower inputs in the system is absorbed through a flywheel (either a solid one, or a liquid one).
The linear and rotational speeds are related: the stronger/faster you pull in the linear direction, the faster the flywheel will rotate. The rotation of the flywheel simulates the effect of a boat in the water: after the stroke, the boat will continue to glide, so does the flywheel.
The linear and rotational speeds are related: the stronger/faster you pull in the linear direction, the faster the flywheel will rotate. The rotation of the flywheel simulates the effect of a boat in the water: after the stroke, the boat will continue to glide only be dampened by the drag of the boat, so does the flywheel.
There are several types of rowers:
@ -28,23 +32,23 @@ There are several types of rowers:
* **Magnetic resistance**: where the resistance is constant
Currently, we treat all these rowers as identical as air rowers, although differences are significant.
There are also hybrid rowers, which combine air resistance and magnetic resistance. The differences in physical behavior can be significant, for example a magnetic rower has a constant resistance while a air/water rower's resistance is dependent on the flywheel's speed. As the key principle is the same for all these rowers (some mass is made to spin and drag brings its speed down), we treat them the same.
Typically, measurements are done in the rotational part of the rower, on the flywheel. There is a reed sensor or optical sensor that will measure time between either magnets or reflective stripes, which gives an **Impulse** each time a magnet or stripe passes. Depending on the **number of impulse providers** (i.e. the number of magnets or stripes), the number of impulses per rotation increases, increasing the resolution of the measurement. By measuring the **time between impulses**, deductions about speed and acceleration of the flywheel can be made, and thus the effort of the rower.
### Key physical concepts
## What a rowing machine actually measures
Here, we distinguish the following concepts:
As mentioned above, most rowers depend on measuring the **time between impulses**, triggered by some impulse giver (magnet or light) on the flywheel. For example, when the flywheel rotates on a NordicTrack RX800, the passing of a magnet on the flywheel triggers a reed-switch, that delivers a pulse to our Raspberry Pi. We measure the time between two subsequent impulses and call this *currentDt*: the time between two impulses. *currentDt* is the basis for all our calculations.
* The **Angular Displacement** of the flywheel in Radians: in essence the number of rotations the flywheel has made. As the impulse-givers are evenly spread over the flywheel, the **angular displacement** between two **impulses** is 2π/(*number of impulse providers*);
The following picture shows the time between impulses through time:
![Measurements of flywheel](img/physics/flywheelmeasurement.png)
*Measurements of flywheel*
* The **Angular Velocity** of the flywheel in Radians/second: in essence the number of rotations of the flywheel per second. As the *Angular Displacement* is fixed, the *Angular Velocity* is (*angular displacement between impulses*) / (time between impulses);
Here, it is clear that the flywheel first accelerates and then decelerates, which is typical for rowing.
* The **Angular Acceleration** of the flywheel in Radians/second^2^: the acceleration/deceleration of the flywheel;
Using *currentDt* means we can't measure anything directly aside from *angular displacement*, and that we have to accept some noise in measurements. For example, as we don't measure torque on the flywheel directly, we can't determine where the flywheel exactly accelerates/decelerates, we can only detect a change in the times between impulses. In essence, we only can conclude that an acceleration has taken place somewhere near a specific impulse, but we can't be certain about where the acceleration exactly has taken place and we can only estimate how big the force must have been. Additionally, small vibrations in the chassis due to tiny unbalance in the flywheel can lead to small deviations in measurements. This kind of noise in our measurement can make many subsequent derived calculation on this measurement too volatile, This is why we explicitly distinguish between *measurements* and *estimates* based on these measurements, to clearly indicate their potential volatility.
* 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.
Dealing with noise is an dominant issue, especially because we have to deal with many types of machines. Aside from implementing a lot of noise reduction, we also focus on using robust calculations: calculations that don't deliver radically different results when a small measurement error is introduced in the measurement of *currentDt*. We typically avoid things like derived functions when possible, as deriving over small values of *currentDt* typically produce huge deviations in the resulting estimate. We sometimes even do this at the cost of inaccuracy with respect to the perfect theoretical model, as long as the deviation is systematic in one direction, to prevent estimates to become too unstable for practical use.
## The rowing cycle and detecting the stroke and recovery phase
@ -53,25 +57,51 @@ On an indoor rower, the rowing cycle will always start with a stroke, followed b
![Impulses, impulse lengths and rowing cycle phases](img/physics/rowingcycle.png)
*Impulses, impulse lengths and rowing cycle phases*
Here, we plot the time between impulses against its sequence number. So, a high number means a long time between impulses, and a low number means that there is a short time between impulses. As this figure also shows, we split the rowing cycle in two distinct phases:
Here, we plot the *currentDt* (time between impulses) against its sequence number. So, a high *currentDt* means a long time between impulses (so a low *angular velocity*), and a low *currentDt* means that there is a short time between impulses (so a high *angular velocity*). As this figure also shows, we split the rowing cycle in two distinct phases:
* The **Drive phase**, where the rower pulls on the handle
* The **Recovery Phase**, where the rower returns to his starting position
Given that the *Angular Displacement* between impulses is fixed, we can deduct some things simply from looking at the subsequent *time between impulses*. When the *time between impulses* shortens, *Angular Velocity* is increasing, and thus the flywheel is accelerating (i.e. we are in the drive phase of the rowing cycle). When times between subsequent impulses become longer, the *Angular Velocity* is decreasing and thus the flywheel is decelerating (i.e. we are the recovery phase of the rowing cycle).
As the rowing cycle always follows this fixed schema, Open Rowing Monitor models it as a finite state machine:
As the rowing cycle always follows this fixed schema, Open Rowing Monitor models it as a finite state machine (implemented in [RowingEngine's handleRotationImpulse](https://github.com/laberning/openrowingmonitor/blob/main/app/engine/RowingEngine.js)):
![Finite state machine of rowing cycle](img/physics/finitestatemachine.png)
*Finite state machine of rowing cycle*
Please note: in essence, you can only measure that the flywheel is accelerating/decelerating (thus after it is going on some time), and as we lack the _measurement_ of force on the flywheel, we can't measure the exact moment of acceleration.
### Basic stroke detection
![Measurements of flywheel](img/physics/flywheelmeasurement.png)
*Measurements of flywheel*
Given that the *Angular Displacement* between impulses is fixed, we can deduct some things simply from looking at the subsequent *time between impulses*, *currentDt*. When the *currentDt* shortens, *Angular Velocity* is increasing, and thus the flywheel is accelerating (i.e. we are in the drive phase of the rowing cycle). When times between subsequent impulses become longer, the *Angular Velocity* is decreasing and thus the flywheel is decelerating (i.e. we are the recovery phase of the rowing cycle). This is the robust implementation of a stroke (implemented in [MovingFlankDetector's implementation of isFlywheelPowered and isFlywheelUnpowered for naturalDeceleration = 0](https://github.com/laberning/openrowingmonitor/blob/main/app/engine/MovingFlankDetector.js)), which is similar to the implementation used by industry leaders like Concept2. Concept2 are generally considered the golden standard when it comes to metrics, and they state (see [this Concept2 FAQ](https://www.concept2.com/service/software/ergdata/faqs): 'Drive time is measured by the amount of time the flywheel is accelerating. Note: It is possible that the athlete may pull on the handle and not accelerate the flywheel due to no energy being put into it and therefore no effective effort. This portion of the handle pull is not measured.')
As above picture shows, we can only detect a change in the times between impulses, but where the flywheel exactly accelerates/decelerates is difficult to assess. In essence we only can conclude that an acceleration has taken place somewhere near an impulse, but we can't be certain about where the acceleration exactly takes place. This makes many measurements that depend on this accurate estimate, but not measurements.
### Advanced stroke detection
Looking at the average curve of an actual rowing machine (this example is based on averaging 300 strokes), we see the following:
![Average curves of a rowing machine](img/physics/currentdtandacceleration.png)
*Average currentDt (red) and Acceleration (blue) of a single stroke on a rowing machine*
In this graph, we plot *currentDt* against the time in the stroke, averaged over 300 strokes. As *currentDt* is (reversely) related to angular velocity, we can calculate the angular acceleration/deceleration. In essence, as soon as the acceleration becomes below the 0, the currentDt begins to lengthen again (i.e. the flywheel is decelerating). As indicated earlier, this is the trigger for the robust stroke detection algorithm (i.e. the one used when naturalDeceleration is set to 0): when the *currentDt* starts to lengthen, the drive-phase is considered complete.
However, from the acceleration/deceleration curve it is also clear that despite the deceleration, there is still a force present: the deceleration-curve hasn't reached its stable minimum despite crossing 0. This is due to the pull still continuing through the arms: the net force is negative due to a part drive-phase (the arm-movement) delivering weaker forces than the drag-forces of the flywheel. Despite being weaker than the other forces on the flywheel, the rower is still working. In this specific example, at around 0.52 sec the rower's force was weaker than all drag-forces combined. However, only at 0,67 seconds (thus approx. 150 ms later) the net force reaches its stable bottom: the only force present is the drag from the flywheel. Getting closer to this moment is a goal.
By specifying the expected natural deceleration of the flywheel (naturalDeceleration, which in this case is around 8 Rad/S^2) in the configuration of the rower, the stroke starts earlier and ends later (implemented in [MovingFlankDetector's implementation of isFlywheelPowered and isFlywheelUnpowered for naturalDeceleration < 0](../app/engine/MovingFlankDetector.js)). Please note: as several derived metrics depend on the length of the drive phase or the exact timing of that moment (like the drag factor when calculated dynamically), these are likely to change when this setting is changed. For a more in-depth explanation, see [here for more details](physics_openrowingmonitor.md#a-closer-look-at-the-effects-of-the-various-drive-and-recovery-phase-detection).
Testing shows that setting a value close to the natural deceleration provides more accurate results very reliably. However, some rowers might contain a lot of noise in their data, making this approach infeasible (hence the fallback option of naturalDeceleration = 0)
This approach is a better approximation than the acceleration/deceleration approach, but still is not perfect. For example, drag-force of the the rower presented in the above graph slowly reduces. This is expected, as the drag-force is speed dependent. For a pure air-rower, the best theoretical approach would be to see if the drag-force is the only force present by calculating the expected drag-force using the current speed and the drag factor (making the stroke detection completely independent on speed). Testing has shown that this approach is too prone to errors, as it requires another derivation with respect to *currentDt*, making it too volatile. Above this, hybrid rower behave differently as well: dependent on the speed, the balance shifts between the speed-dependent air-resistance drag-force and the speed-independent magnetic resistance force. To make the solution robust and broadly applicable, this approach has been abandoned.
## Key physical metrics during the rowing cycle
There are several key metrics that underpin the performance measurement of a rowing stroke. Here, we distinguish the following concepts:
* The **Angular Displacement** of the flywheel in Radians: in essence the distance the flywheel has traveled (i.e. the number of Radians the flywheel has rotated). As the impulse-givers are evenly spread over the flywheel, the **angular displacement** between two **impulses** is 2π/(*number of impulse providers on the flywheel*). This can easily be measured by counting the number of 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 *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.
## Measurements during the recovery phase
@ -161,7 +191,7 @@ Although this is an easy implementable algorithm by calculating a running sum of
> <img src="https://render.githubusercontent.com/render/math?math=\textrm{AngularVelocity}=\frac{2\pi}{\textrm{NumberOfImpulsegivers}*\textrm{TimeBetweenImpulses}}">
As Time Between Impulses tends to be small (typically much smaller than 1, typically between 0,1 and 0,0015 seconds), small errors tend to increase the Angular Velocity significantly, enlarging the effect of an error and potentially causing this volatility. Currently, this effected is remedied by using a running average on the presentation layer (in RowingStatistics.js). However, when this is bypassed, data shows significant spikes of 20Watts in quite stable cycles due to small changes in the data.
As *currentDt* tends to be small (typically much smaller than 1, typically between 0,1 and 0,0015 seconds), small errors tend to increase the Angular Velocity significantly, enlarging the effect of an error and potentially causing this volatility. An approach is to use a running average on the presentation layer (in `RowingStatistics.js`). However, when this is bypassed, data shows significant spikes of 20Watts in quite stable cycles due to small changes in the data.
An alternative approach is given by [[3]](#3), which proposes
@ -169,62 +199,65 @@ An alternative approach is given by [[3]](#3), which proposes
Where P is the average power and ω is the average speed during the stroke. Here, the average speed can be determined in a robust manner (i.e. Angular Displacement of the Drive Phase / DriveLength).
As Dave Venrooy indicates this is accurate with a 5% margin. Testing this on live data confirms this behaviour (tested with a autoAdjustDragFactor = true, to maximize noise-effects), with two added observations:
As Dave Venrooy indicates this is accurate with a 5% margin. Testing this on live data confirms this behavior (tested with a *autoAdjustDragFactor* = true, to maximize noise-effects), with three added observations:
* The simpler measurement is structurally below the more precise measurement when it comes to total power produced in a 30 minutes or 15 minutes row on a RX800 with any damper setting;
* The robust algorithm is structurally below the more precise measurement when it comes to total power produced in a 30 minutes or 15 minutes row on a RX800 with any damper setting;
* The simpler algorithm is indeed much less volatile: spikes found in the current algorithm are much smaller in the simple algorithm
* The robust algorithm is indeed much less volatile: the spikes found in the more precise algorithm are much bigger than the ones found in the robust algorithm
Following that, I bypassed the running average in the presentation layer and plugged the data in directly. The effects show that the monitor is a bit more responsive but doesn't fluctuate unexpectedly.
* A test with *numOfPhasesForAveragingScreenData* = 1 (practically bypassing the running average in the presentation layer) combined with the robust algorithm shows that the monitor is a bit more responsive but doesn't fluctuate unexpectedly.
As the flywheel inertia is mostly guessed based on its effects on the Power outcome anyway (as nobody is willing to take his rower apart for this), the 5% error wouldn't matter much anyway: the Inertia will simply become 5% more to get to the same power. Using the simpler more robust algorithm has some advantages:
As the *flywheelinertia* is mostly guessed based on its effects on the Power outcome anyway (as nobody is willing to take his rower apart for this), the 5% error wouldn't matter much anyway: the *flywheelinertia* will simply become 5% more to get to the same power in the display. Therefore, we choose to use the simpler more robust algorithm, as it has some advantages:
* In essence the instantaneous angular velocities at the flanks are removed from the power measurement, making it more robust against "unexpected" behavior of the rowers (like the cavitation-like effects found in LiquidFlywheel Rowers). Regardless of settings, only instantaneous angular velocities that affect displayed data are the start and begin of each phase;
* Setting autoAdjustDampingConstant to "false" effectively removes/disables all calculations with instantaneous angular velocities (only average velocity is calculated over the entire flank, which typically is not on a single measurement), making Open Rowing Monitor an option for rowers with noisy data or otherwise unstable/unreliable measurements;
* Setting *autoAdjustDragFactor* to "false" effectively removes/disables all calculations with instantaneous angular velocities (only average velocity is calculated over the entire phase, which typically is not on a single measurement), making Open Rowing Monitor an option for rowers with noisy data or otherwise unstable/unreliable measurements;
* Given the stability of the measurements, it might be an option to remove the filter in the presentation layer completely, making the monitor more responsive to user actions.
* Given the stability of the measurements, it might be a realistic option for users to remove the filter in the presentation layer completely by setting *numOfPhasesForAveragingScreenData* to 1, making the monitor much more responsive to user actions.
Given these advantages and that in practice it won't have a practical implications for users, we think it is best to use the robust implementation.
Given these advantages and that in practice it won't have a practical implications for users, we have chosen to use the robust implementation.
## Additional options and considerations
## Additional considerations for the frequency of the metrics calculations
There are some additional options and considerations:
There are some additional options for the frequency of metric calculations:
* Currently, the metrics are only updated at the end of the Recovery Phase, which is once every 2 to 3 seconds. An option would be to update the metrics at the end of the Drive Phase as well.
* An option would be to update the metrics only updated at the end of stroke, which is once every 2 to 3 seconds. This is undesirable as a typical stroke takes around 2.5 seconds to complete and covers around 10 meters. It is very desirable to update typical end-criteria for trainings that change quite quickly (i.e. absolute distance, elapsed time) more frequently than that;
* An additional alternative would be to update typical end-criteria for trainings that change quite quickly (i.e. absolute distance, elapsed time) every complete rotation;
* We additionally update the metrics (when dependent on the stroke dependent parameters, like stroke length) both at the end of the Drive and Recovery Phases, as Marinus van Holst [[2]](#2) suggests that both are valid perspectives on the stroke. This allows for a recalculation of these metrics twice per stroke;
* Due to reviewing the drag factor at the end of the recovery phase and (retrospectively) applying it to the realized linear distance of that same recovery phase, it would be simpler to report absolute distance from the RowingEngine, instead of added distance;
* To allow for a very frequent update of the monitor, and allow for testing for typical end-criteria for trainings that change quite quickly (i.e. absolute distance, elapsed time), we calculate these for each new *currentDt*;
## A closer look at the Drive and Recovery phases
* As we can only calculate the drag factor at the end of the recovery phase, we can only (retrospectively) apply it to the realized linear distance of that same recovery phase. Therefore, we we need to report absolute time and distance from the [RowingEngine](../app/engine/RowingEngine.js));
Looking at the average curves of an actual rowing machine, we see the following:
## A closer look at the effects of the various Drive and Recovery phase detection
![Average curves of a rowing machine](img/physics/currentdtandacceleration.png)
*Average curves of a rowing machine*
In this section, we will answer the question whether Concept2 made a big error in their stroke detection, and thus that using *naturalDeceleration* is set to 0 is inferior to actually setting it to a different value. The short answer is that Concept2 has made a perfectly acceptable tradeoff between reliability of the stroke detection and precision of some metrics.
In this graph, we plot the time between impulses (CurrentDt) against the time in the stroke. As CurrentDt is reversely related to angular velocity, we can calculate the angular acceleration/deceleration. In essence, as soon as the acceleration becomes below the 0, the CurrentDt begins to lengthen again (i.e. the flywheel is decelerating). However, from the acceleration/deceleration curve it is also clear that despite the deceleration, there is still a force present: the deceleration-curve hasn't reached its minimum despite crossing 0. This is due to the pull still continuing through the arms: the Netto force is negative due to a part of the arm-moment being weaker than the drag-force of the flywheel. Only approx. 150 ms later the force reaches its stable bottom (and thus the only force is the drag from the flywheel).
Effectively, Open Rowing Monitor can use two different methods of stroke detection. When *naturalDeceleration* is set to 0, it will detect an acceleration/deceleration directly based on *currentDt*, similar to Concept2. When *naturalDeceleration* is set to a negative number, it will consider that number as the minimum level of deceleration (in Rad/S^2). The later is more volatile, as described above, but some consider this desirable when possible.
Question is if erroneously detecting the recovery-phase too early can affect measurements. The most important measurement here is the calculation of the drag factor. The drag factor can be pinned down if needed by setting autoAdjustDragFactor to "false". If used dynamically, it might affect measurements of both distance and power. In itself the calculation of power is measured based on the power during the drive phase, and thus depends on its correct detection. Distance does not depend directly on phase detection (it just depends on the total number of impulses and the drag factor which is already discussed).
Our practical experiments show that assuming the recovery-phase started too early doesn't affect measurements per se. In theory, the calculation of speed and power do not depend directly on phase detection, they do depend on the total number of impulses and the drag factor. It is in fact the automatic update of the drag factor that is dependent on the correct detection of the stroke. The drag factor can be pinned down if needed by setting *autoAdjustDragFactor* to "false". If set to true, it might affect measurements of both distance and power, where we will discuss the knock-on effects.
### Effects on the drag factor
### Effects on the automatically calculated drag factor
The most important measurement that is affected by stroke detection errors is the calculation of the drag factor.
Our robust implementation of the drag factor is:
> <img src="https://render.githubusercontent.com/render/math?math=\textrm{DragFactor}=\textrm{FlywheelInertia}*(\frac{1}{\textrm{AngularVelocity}_{start}}-\frac{1}{\textrm{AngularVelocity}_{end}}*\textrm{RecoveryLength})">
Looking at the effect of erroneously starting the recovery early, it affects two variables:
Looking at the effect of erroneously starting the recovery early and ending it late, it affects two variables:
* Recovery length will _systematically_ become too long (approx. 1,5 sec instead of 1,3 sec)
* Recovery length will _systematically_ become too long (approx. 200 ms from our experiments)
* The Angular Velocity~Start~ will _systematically_ become too high as the flywheel already starts to decelerate at the end of the drive phase, which we mistakenly consider the start of the recovery (approx. 83,2 Rad/sec instead of 82,7 Rad/sec).
* The Angular Velocity will _systematically_ become too high as the flywheel already starts to decelerate at the end of the drive phase, which we mistakenly consider the start of the recovery (in our tests this was approx. 83,2 Rad/sec instead of 82,7 Rad/sec). A similar thing can happen at the begin of the recovery phase when the rower doesn't have an explosive Drive.
Example calculations show that this results in a systematically too high estimate of the drag factor. As these errors are systematic, it is safe to assume these will be fixed by the calibration of the power and distance corrections (i.e. the estimate of the Flywheel Inertia and the MagicConstant).
Example calculations based on several tests show that this results in a systematically too high estimate of the drag factor. As these errors are systematic, it is safe to assume these will be fixed by the calibration of the power and distance corrections (i.e. the estimate of the *FlywheelInertia* and the *MagicConstant*). Thus, as long as the user calibrates the rower to provide credible data for his setting of *naturalDeceleration*, there will be no issues.
### Effects on the Power calculation
Please note that this does imply that changing the *naturalDeceleration* when the *autoAdjustDragFactor* is set to true (thus drag is automatically calculated) will require a recalibration of the power and distance measurements.
The power calculation is as follows:
### Knock-on Effects on the Power calculation
Question is what the effect of this deviation of the drag factor is on the power calculation. The power is calculated as follows:
> <img src="https://render.githubusercontent.com/render/math?math=P=k\omega^3">

74
docs/rower_settings.md Normal file
View File

@ -0,0 +1,74 @@
# Guide for rower specific settings
This guide helps you to adjust the rowing monitor specifically for a new type of rower or even for your specific use, when the default rowers don't suffice.
## Why we need rower specific settings
No rowing machine is the same, and some physical construction parameters are important for the Rowing Monitor to be known to be able to understand your rowing stroke. By far, the easiest way is to select your rower profile from `config/rowerProfiles.js` and put its name in `config.js` (i.e. `rowerSettings: rowerProfiles.WRX700`). The rowers mentioned there are maintained by us for OpenRowingMonitor and are structurally tested with changes typically automatically implemented.
If you want something special, or if your rower isn't in there, this guide will help you set it up. Please note that determining these settings is quite labor-intensive, and typically some hard rowing is involved. If you find suitable settings for a new type of rower, please send in the data and settings, so we can add it to OpenRowingMonitor and make other users happy as well.
## Settings you must change for a new rower
The key feature for Open Rowing Monitor is to reliably produce metrics you see on the monitor, share via Bluetooth with games and share with Strava and the like. Typically, these metrics are reported on a per-stroke basis. So, to be able to use these metrics for these goals, you need two key parts of the settings right:
* Stroke detection
* Physical metrics (like distance, power and speed)
### Getting stroke detection right
A key element in getting rowing data right is getting the stroke detection right, as we report many metrics on a per-stroke basis. The **Impulse Noise reduction settings** reduce the level of noise on the level of individual impulses. You should change these settings if you experience issues with stroke detection or the stability of the drag factor calculation. The stroke detection consists out of three types of filters:
* A smoothing filter, using a running average. The **smoothing** setting determines the length of the running average for the impulses, which removes the height of the peaks, removes noise to a certain level but keeps the stroke detection responsive. Smoothing typically varies between 1 to 4, where 1 effectively turns it off.
* A high-pass/low-pass filter, based on **minimumTimeBetweenImpulses** (the shortest allowable time between impulses) and **maximumTimeBetweenImpulses** (the longest allowed time between impulses). Combined, they remove any obvious errors in the duration between impulses (in seconds) during _active_ rowing. Measurements outside of this range are filtered out to prevent the stroke detection algorithm to get distracted. This setting is highly dependent on the physical construction of your rower, so you have to determine it yourself without any hints. The easiest way to determine this is by visualizing your raw recordings in Excel.
* A maximum change filter, which based on the the previous impulse determines the maximum amount of change (percentage) through the **maximumDownwardChange** and **maximumUpwardChange** settings.
By changing the noise reduction settings, you can remove any obvious errors. You don't need to filter everything: it is just to remove obvious errors that might frustrate the stroke detection, but in the end you can't prevent every piece of noise out there. Begin with the noise filtering, when you are satisfied, you can adjust the rest of the stroke detection settings.
Another set of settings are the **flankLength** and **numberOfErrorsAllowed** setting, which determine the condition when the stroke detection is sufficiently confident that the stroke has started/ended. In essence, the stroke detection looks for a consecutive increasing/decreasing impulse lengths, and the **flankLength** determines how many consecutive flanks have to be seen before the stroke detection considers a stroke to begin or end. Please note that making the flank longer does _NOT_ change your measurement in any way: the algorithms always rely on the beginning of the flank, not at the current end. Generally, a **flankLength** of 2 to 3 typically works. Sometimes, a measurement is too noisy, which requires some errors in the flanks to be ignored, which can be done through the **numberOfErrorsAllowed** setting. For example, the NordicTrack RX-800 successfully uses a **flankLength** of 9 and a **numberOfErrorsAllowed** of 2, which allows quite some noise but forces quite a long flank. This setting requires a lot of tweaking and rowing.
At the level of the stroke detection, there is some additional noise filtering, preventing noise to start a drive- or recovery-phase too early. The settings **minimumDriveTime** and **minimumRecoveryTime** determine the minimum times (in seconds) for the drive and recovery phases. Generally, the drive phase lasts at least 0.500 second, and the recovery phase 1.250 second for recreational rowers.
For the noise reduction settings and stroke detection settings, you can use the Excel tool. When OpenRowingMonitor records a log (set setting recordRawData to true), you can paste the values in the first column of the "Raw Data" tab (please observe that the Raspberry uses a point as separator, and your version of Excel might expect a comma). From there, the Excel file simulates the calculations the OpenRowingMonitor makes, allowing you to play with these settings.
Please note that changing the noise filtering and stroke detection settings will affect your calculated dragFactor. So it is best to start with rowing a few strokes to determine settings for noise filtering and stroke detection, and then move on to the other settings.
### Getting the metrics right
There are some parameters you must change to get Open Rowing Monitor to calculate the real physics with a rower. These are:
* **numOfImpulsesPerRevolution**: this tells Open Rowing Monitor how many impulses per rotation of the flywheel to expect. An inspection of the flywheel could reveal how many magnets it uses (typically a rower has 2 to 4 magnets). Although sometimes it is well-hidden, you can sometimes find it in the manual under the parts-list of your rower.
* **dragFactor**: tells Open Rowing Monitor how much damping and thus resistance your flywheel is offering. This is typically also dependent on your damper-setting (if present). Regardless if you use a static or dynamically calculated drag factor, this setting is needed as the first stroke also needs it to calculate distance, speed and power. Just as a frame of reference: the Concept2 can display this factor from the menu. Please note that the drag factor is much dependent on the physical construction of the flywheel and mechanical properties of the transmission of power to the flywheel. For a new Concept2, the Drag Factor ranges between 80 (Damper setting 1) and 220 (Damper setting 10). The NordicTrack RX-800 ranges from 150 to 450, where the 150 feels much lighter than a 150 on the Concept2.
Here, some rowing and some knowledge about your rowing gets involved. Setting your damping factor is done by rowing a certain number of strokes and then seeing how much you have rowed and at what pace. If you know these metrics by hart, it just requires some rowing and adjusting to get them right. If you aren't that familiar with rowing, a good starting point is that a typical distance covered by a single stroke at 20 strokes per minute (SPM) is around 10 meters. So when you row a minute, you will have 20 strokes recorded and around 200 meters rowed. When possible, we use the [Concept Model D (or RowerErg)](https://www.concept2.com/indoor-rowers/concept2-rowerg) as a "Golden standard": when you know your pace on that machine, you can try to mimic that pace on your machine. Most gym's have one, so trying one can help you a lot in finding the right settings for your machine.
## Settings you COULD change for a new rower
In the previous section, we've guided you to set up a real robust working rower, but it will result in more crude data. To improve the accuracy of many measurements, you could switch to a more accurate and dynamic rower. This does require a more sophisticated rower: you need quite a few data points per stroke, with much accuracy, to get this working reliably. And a lot of rowing to get these settings right is involved.
### More accurate stroke detection
The **naturalDeceleration** setting determines the natural deceleration. This setting is used to distinguish between a powered and unpowered flywheel. This must be a negative number and indicates the level of deceleration required to interpret it as a free spinning flywheel. The best way to find the correct value for your rowing machine is a try and error approach. You can also set this to zero (or positive), to use the more robust, but not so precise acceleration-based stroke detection algorithm. Setting it to -0.1 enables the alternative less robust algorithm. By seeing how your stroke detection behaves during a row, you can slowly lower this number until you start missing strokes.
Please note that changing this stroke detection will affect your calculated dragFactor.
### Dynamically adapting the drag factor
In reality, the drag factor of a rowing machine isn't static: it depends on air temperature, moisture, dust, (air)obstructions of the flywheel cage and sometimes even speed of the flywheel. So using a static drag factor is reliable, it isn't accurate. Open Rowing Monitor can automatically calculate the drag factor on-the-fly based on the recovery phase (see [this description of the underlying physics](physics_openrowingmonitor.md)). To do this, you need to set the following settings:
* **autoAdjustDragFactor**: the Drag Factor can be calculated automatically. Setting it to true, will allow Open Rowing Monitor to automatically calculate the drag factor based on the **flywheelInertia** and the on the measured values in the stroke recovery phase.
* **flywheelInertia**: The moment of inertia of the flywheel (in kg\*m^2), which in practice influences your power values and distance. A formal way to measure it is outlined in [Flywheel moment of inertia](https://dvernooy.github.io/projects/ergware/). However, the most practical way to set it is by rowing and see what kind of power is displayed on the monitor. Typical ranges are weight dependent (see [this explanation](https://www.rowingmachine-guide.com/tabata-rowing-workouts.html)), and it helps if you know your times on a reliable machine like the Concept2.
Please note that you don't need to use the dynamic drag factor to test your settings. To see the calculated drag factor for your rowing machine, please ensure that the logging level of the RowingEngine is set to 'info' or higher. Then do some strokes on the rower and observe the calculated drag factor in the logging.
It must be noted that you have to make sure that your machine's measurements are sufficiently free of noise: noise in the drag calculation can have a strong influence on your speed and distance calculations and thus your results. If your rower produces stable damping values, then this could be a good option to dynamically adjust your measurements to the damper setting of your rower as it takes in account environmental conditions. When your machine's power and speed readings are too volatile it is wise to turn it off
## Settings you can tweak
Some people want it all, and we're happy to give to you when your rower and your Raspberry Pi can handle the pain. Some interesting settings:
* **maximumImpulseTimeBeforePause** determines the maximum time between impulses before the rowing engine considers it a pause.
* **magicConstant** is a constant that is commonly used to convert flywheel revolutions to a rowed distance and speed (see [the physics of ergometers](http://eodg.atm.ox.ac.uk/user/dudhia/rowing/physics/ergometer.html#section9)). Concept2 seems to use 2.8, which they admit is an arbitrary number which came close to their expectations of a competetion boat. As this setting only affects speed/distance, this setting typically is used to change the power needed to row a certain distance or reach a certain speed. So changing this can make your rower's metrics act as sluggish as an oil tanker (much power needed for little speed), or more like a smooth eight (less power needed for more speed). So for your rower, you could set your own plausible distance for the effort you put in. Please note that the rowed distance also depends on **flywheelInertia**, so please calibrate that before changing this constant. Another note: increasing this number decreases your rowed meters, but not in a linear fashion.
* **screenUpdateInterval**: normally set at 1000 milliseconds, but for a more smoother experience on your monitor you can go as low as 100 ms. This makes the transition of the distance and time quite smooth, but at the price of some more CPU-load.
* **numOfPhasesForAveragingScreenData**: we average the data from several stroke phases to prevent the monitor and Bluetooth devices to become fidgety. Typically, we set this value to 6, which means 3 strokes (there are two phases in each stroke). However, some Bluetooth devices do their own calculations. And sometimes you really want the feedback on your individual strokes without any punches hold back. Setting this to 1 will result in a very volatile, but direct feedback mechanism on your stroke.
* **recordRawData**: This is as raw as it gets, as setting this to `true` makes Open Rowing Monitor dump the raw impulse-lengths to a file (see [how we interpret this data](physics_openrowingmonitor.md)).