From 475f0f483598cd071e8b94e8da0930ff7c7bb68b Mon Sep 17 00:00:00 2001 From: Lars Berning <151194+laberning@users.noreply.github.com> Date: Sun, 2 May 2021 15:02:58 +0200 Subject: [PATCH] improves regression tests, renames some settings --- app/engine/RowingEngine.js | 6 ++-- app/engine/RowingEngine.test.js | 34 +++++++++++++------ app/gpio/GpioTimerService.js | 2 +- app/server.js | 2 +- app/tools/ConfigManager.js | 2 +- config/rowerProfiles.js | 56 ++++++++++++++++---------------- docs/Modifying_Rower_Settings.md | 12 +++---- 7 files changed, 64 insertions(+), 50 deletions(-) diff --git a/app/engine/RowingEngine.js b/app/engine/RowingEngine.js index a6d9ddc..5a3661a 100644 --- a/app/engine/RowingEngine.js +++ b/app/engine/RowingEngine.js @@ -56,8 +56,8 @@ function createRowingEngine (rowerSettings) { let workoutHandler const kDampEstimatorAverager = createWeightedAverager(3) - const flankDetector = createMovingFlankDetector(rowerSettings.numOfImpulsesPerRevolution, rowerSettings.maximumTimeBetweenMagnets, 0) - let prevDt = rowerSettings.maximumTimeBetweenMagnets + const flankDetector = createMovingFlankDetector(rowerSettings.numOfImpulsesPerRevolution, rowerSettings.maximumTimeBetweenImpulses, 0) + let prevDt = rowerSettings.maximumTimeBetweenImpulses let kPower = 0.0 let jPower = 0.0 let kDampEstimator = 0.0 @@ -92,7 +92,7 @@ function createRowingEngine (rowerSettings) { // STEP 1: reduce noise in the measurements by applying some sanity checks // noise filter on the value of currentDt: it should be within sane levels and should not deviate too much from the previous reading - if (currentDt < rowerSettings.minimumTimeBetweenMagnets || currentDt > rowerSettings.maximumTimeBetweenMagnets || currentDt < (rowerSettings.maximumDownwardChange * prevDt) || currentDt > (rowerSettings.maximumUpwardChange * prevDt)) { + if (currentDt < rowerSettings.minimumTimeBetweenImpulses || currentDt > rowerSettings.maximumTimeBetweenImpulses || currentDt < (rowerSettings.maximumDownwardChange * prevDt) || currentDt > (rowerSettings.maximumUpwardChange * prevDt)) { // impulses are outside plausible ranges, so we assume it is close to the previous one currentDt = prevDt log.debug(`noise filter corrected currentDt, ${currentDt} was dubious, changed to ${prevDt}`) diff --git a/app/engine/RowingEngine.test.js b/app/engine/RowingEngine.test.js index b1c4960..49ca2f8 100644 --- a/app/engine/RowingEngine.test.js +++ b/app/engine/RowingEngine.test.js @@ -9,6 +9,7 @@ import loglevel from 'loglevel' import rowerProfiles from '../../config/rowerProfiles.js' import { createRowingEngine } from './RowingEngine.js' import { replayRowingSession } from '../tools/RowingRecorder.js' +import { deepMerge } from '../tools/ConfigManager.js' const log = loglevel.getLogger('RowingEngine.test') log.setLevel('warn') @@ -32,44 +33,57 @@ const createWorkoutEvaluator = function () { function getMinStrokePower () { return strokes.map((stroke) => stroke.power).reduce((acc, power) => Math.max(acc, power)) } + function getDistance () { + return strokes.reduce((acc, stroke) => acc + stroke.distance, 0) + } return { handleStroke, handleStrokeStateChanged, handlePause, getNumOfStrokes, getMaxStrokePower, - getMinStrokePower + getMinStrokePower, + getDistance } } test('sample data for WRX700 should produce plausible results with rower profile', async () => { - const rowingEngine = createRowingEngine(rowerProfiles.WRX700) + const rowingEngine = createRowingEngine(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.WRX700)) const workoutEvaluator = createWorkoutEvaluator() rowingEngine.notify(workoutEvaluator) await replayRowingSession(rowingEngine.handleRotationImpulse, { filename: 'recordings/WRX700_2magnets.csv' }) assert.is(workoutEvaluator.getNumOfStrokes(), 16, 'number of strokes does not meet expectation') - assert.ok(workoutEvaluator.getMaxStrokePower() < 220, `maximum stroke power should be below 220w, but is ${workoutEvaluator.getMaxStrokePower()}w`) - assert.ok(workoutEvaluator.getMinStrokePower() > 50, `minimum stroke power should be above 50w, but is ${workoutEvaluator.getMinStrokePower()}w`) + assertPowerRange(workoutEvaluator, 50, 220) + assertDistanceRange(workoutEvaluator, 140, 144) }) test('sample data for DKNR320 should produce plausible results with rower profile', async () => { - const rowingEngine = createRowingEngine(rowerProfiles.DKNR320) + const rowingEngine = createRowingEngine(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.DKNR320)) const workoutEvaluator = createWorkoutEvaluator() rowingEngine.notify(workoutEvaluator) await replayRowingSession(rowingEngine.handleRotationImpulse, { filename: 'recordings/DKNR320.csv' }) assert.is(workoutEvaluator.getNumOfStrokes(), 10, 'number of strokes does not meet expectation') - assert.ok(workoutEvaluator.getMaxStrokePower() < 200, `maximum stroke power should be below 200w, but is ${workoutEvaluator.getMaxStrokePower()}w`) - assert.ok(workoutEvaluator.getMinStrokePower() > 75, `minimum stroke power should be above 75w, but is ${workoutEvaluator.getMinStrokePower()}w`) + assertPowerRange(workoutEvaluator, 75, 200) + assertDistanceRange(workoutEvaluator, 64, 67) }) test('sample data for RX800 should produce plausible results with rower profile', async () => { - const rowingEngine = createRowingEngine(rowerProfiles.RX800) + const rowingEngine = createRowingEngine(deepMerge(rowerProfiles.DEFAULT, rowerProfiles.RX800)) const workoutEvaluator = createWorkoutEvaluator() rowingEngine.notify(workoutEvaluator) await replayRowingSession(rowingEngine.handleRotationImpulse, { filename: 'recordings/RX800.csv' }) assert.is(workoutEvaluator.getNumOfStrokes(), 10, 'number of strokes does not meet expectation') - assert.ok(workoutEvaluator.getMaxStrokePower() < 260, `maximum stroke power should be below 260, but is ${workoutEvaluator.getMaxStrokePower()}w`) - assert.ok(workoutEvaluator.getMinStrokePower() > 160, `minimum stroke power should be above 160w, but is ${workoutEvaluator.getMinStrokePower()}w`) + assertPowerRange(workoutEvaluator, 160, 260) + assertDistanceRange(workoutEvaluator, 88, 92) }) +function assertPowerRange (evaluator, minPower, maxPower) { + assert.ok(evaluator.getMinStrokePower() > minPower, `minimum stroke power should be above ${minPower}w, but is ${evaluator.getMinStrokePower()}w`) + assert.ok(evaluator.getMaxStrokePower() < maxPower, `maximum stroke power should be below ${maxPower}w, but is ${evaluator.getMaxStrokePower()}w`) +} + +function assertDistanceRange (evaluator, minDistance, maxDistance) { + console.log(evaluator.getDistance().toFixed(2)) + assert.ok(evaluator.getDistance() >= minDistance && evaluator.getDistance() <= maxDistance, `distance should be between ${minDistance}m and ${maxDistance}m, but is ${evaluator.getDistance().toFixed(2)}m`) +} test.run() diff --git a/app/gpio/GpioTimerService.js b/app/gpio/GpioTimerService.js index 5f6b501..15827aa 100644 --- a/app/gpio/GpioTimerService.js +++ b/app/gpio/GpioTimerService.js @@ -21,7 +21,7 @@ export function createGpioTimerService () { // setting priority of current process os.setPriority(-20) } catch (err) { - log.error('error while setting priority of Gpio-Thread: ', err) + log.debug('need root permission to set priority of Gpio-Thread') } // mode can be rising, falling, both const reedSensor = new Gpio(17, 'in', 'rising') diff --git a/app/server.js b/app/server.js index 3b9c0ed..75aa610 100644 --- a/app/server.js +++ b/app/server.js @@ -135,7 +135,7 @@ webServer.on('clientConnected', () => { /* replayRowingSession(rowingEngine.handleRotationImpulse, { - filename: 'recordings/wrx700_2magnets.csv', + filename: 'recordings/WRX700_2magnets.csv', realtime: true, loop: true }) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index 794ac06..12742fc 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -15,7 +15,7 @@ async function getConfig () { return customConfig !== undefined ? deepMerge(defaultConfig, customConfig.default) : defaultConfig } -function deepMerge (...objects) { +export function deepMerge (...objects) { const isObject = obj => obj && typeof obj === 'object' return objects.reduce((prev, obj) => { diff --git a/config/rowerProfiles.js b/config/rowerProfiles.js index 0463e87..0179e47 100644 --- a/config/rowerProfiles.js +++ b/config/rowerProfiles.js @@ -10,24 +10,24 @@ */ export default { - // Profile for an example rower + // The default rower profile DEFAULT: { // How many impulses are triggered per revolution of the flywheel // i.e. the number of magnets if used with a reed sensor numOfImpulsesPerRevolution: 1, - // Filter values for sanity checks - // First are the sane minimum and maximum times between magnets during active rows - minimumTimeBetweenMagnets: 0.014, - maximumTimeBetweenMagnets: 0.5, - // Procentual change between successive intervals - maximumDownwardChange: 0.2, // effectively the maximum deceleration - maximumUpwardChange: 1.75, // effectively the maximum acceleration - // Settings for the phase detection + // Filter Settings to reduce noise in the measured data + // Minimum and maximum duration between impulses in seconds during active rowing. Measurements outside of this range + // will be replaced by a default value. + minimumTimeBetweenImpulses: 0.014, + maximumTimeBetweenImpulses: 0.5, + // Percentage change between successive intervals + maximumDownwardChange: 0.2, // effectively the maximum deceleration + maximumUpwardChange: 1.75, // effectively the maximum acceleration + // Settings for the rowing phase detection (in seconds) minimumDriveTime: 0.300, minimumRecoveryTime: 0.750, - // Needed to determine the damping constant of the rowing machine. This value can be measured in the recovery phase // of the stroke (some ergometers do this constantly). // However I still keep it constant here, as I still have to figure out the damping physics of a water rower (see below) @@ -46,9 +46,9 @@ export default { // Concept2 seems to use 2.8, which they admit is an arbitrary number which came close // to their expectations. So for your rower, you have to find a credible distance for your effort. // Also note that the rowed distance also depends on jMoment, so please calibrate that before changing this constant. - // PLEASE NOTE: INcreasing this number DEcreases your rowed meters + // PLEASE NOTE: Increasing this number decreases your rowed meters magicConstant: 2.8, - + // Set this to true if you are using a water rower // The mass of the water starts rotating, when you pull the handle, and therefore acts // like a massive flywheel @@ -62,8 +62,8 @@ export default { // Sportstech WRX700 WRX700: { numOfImpulsesPerRevolution: 2, - minimumTimeBetweenMagnets: 0.05, - maximumTimeBetweenMagnets: 1, + minimumTimeBetweenImpulses: 0.05, + maximumTimeBetweenImpulses: 1, maximumDownwardChange: 0.25, maximumUpwardChange: 2, minimumDriveTime: 0.500, @@ -76,8 +76,8 @@ export default { // DKN R-320 Air Rower DKNR320: { numOfImpulsesPerRevolution: 1, - minimumTimeBetweenMagnets: 0.15, - maximumTimeBetweenMagnets: 0.5, + minimumTimeBetweenImpulses: 0.15, + maximumTimeBetweenImpulses: 0.5, maximumDownwardChange: 0.25, maximumUpwardChange: 1.75, minimumDriveTime: 0.500, @@ -86,15 +86,15 @@ export default { jMoment: 0.4, liquidFlywheel: true }, - + // NordicTrack RX800 Air Rower RX800: { numOfImpulsesPerRevolution: 4, liquidFlywheel: false, - + // Damper setting 10 - minimumTimeBetweenMagnets: 0.018, - maximumTimeBetweenMagnets: 0.0338, + minimumTimeBetweenImpulses: 0.018, + maximumTimeBetweenImpulses: 0.0338, maximumDownwardChange: 0.69, maximumUpwardChange: 1.3, minimumDriveTime: 0.300, @@ -105,8 +105,8 @@ export default { // /* Damper setting 8 - minimumTimeBetweenMagnets: 0.017, - maximumTimeBetweenMagnets: 0.034, + minimumTimeBetweenImpulses: 0.017, + maximumTimeBetweenImpulses: 0.034, maximumDownwardChange: 0.8, maximumUpwardChange: 1.15, minimumDriveTime: 0.300, @@ -117,8 +117,8 @@ export default { */ /* Damper setting 6 - minimumTimeBetweenMagnets: 0.017, - maximumTimeBetweenMagnets: 0.034, + minimumTimeBetweenImpulses: 0.017, + maximumTimeBetweenImpulses: 0.034, maximumDownwardChange: 0.85, maximumUpwardChange: 1.15, minimumDriveTime: 0.300, @@ -129,8 +129,8 @@ export default { */ /* Damper setting 4 - minimumTimeBetweenMagnets: 0.019, - maximumTimeBetweenMagnets: 0.032, + minimumTimeBetweenImpulses: 0.019, + maximumTimeBetweenImpulses: 0.032, maximumDownwardChange: 0.70, maximumUpwardChange: 1.30, minimumDriveTime: 0.300, @@ -141,8 +141,8 @@ export default { */ /* Damper setting 2 - minimumTimeBetweenMagnets: 0.016, - maximumTimeBetweenMagnets: 0.033, + minimumTimeBetweenImpulses: 0.016, + maximumTimeBetweenImpulses: 0.033, maximumDownwardChange: 0.85, maximumUpwardChange: 1.15, minimumDriveTime: 0.300, diff --git a/docs/Modifying_Rower_Settings.md b/docs/Modifying_Rower_Settings.md index 2950c6a..584e380 100644 --- a/docs/Modifying_Rower_Settings.md +++ b/docs/Modifying_Rower_Settings.md @@ -2,18 +2,18 @@ This guide helps you to adjust the rowing monitor specifically for your rower or even for you -## Why have setings +## Why have settings -No rowingmachine is the same, and their physical construction is important for the Rowing Monitor to understand to be able to understand your rowing. Easiest way is to select your rower from owerProfiles.js and put its name in default.config.js instead of "rowerProfiles.DEFAULT". +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 there, this guide will help you set it up (please send in the data and settings, so we can add it to the OpenRowingMonitor). +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 waterrower (true) or a solid flywheel with magnetic or air-resistance (false) +* liquidFlywheel: tells OpenRowingMonitor if you are using a water rower (true) or a solid flywheel with magnetic or air-resistance (false) * omegaDotDivOmegaSquare: 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. -* jMoment: 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 weigth dependent (see [this explanation](https://www.rowingmachine-guide.com/tabata-rowing-workouts.html)). +* jMoment: 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 @@ -23,7 +23,7 @@ Settings important for Open Rowing Monitor: * 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 seperator, 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. +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.