From 51400f2c02b93b639bc48f0267e063a8a929fed4 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:14:37 +0100 Subject: [PATCH 001/231] Fixed a bug in tick rollover Fixed a bug in the tick rollover (which seems to happen every 60 minutes of processtime). --- app/gpio/GpioTimerService.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/gpio/GpioTimerService.js b/app/gpio/GpioTimerService.js index 368ab7ca73..093a89529e 100644 --- a/app/gpio/GpioTimerService.js +++ b/app/gpio/GpioTimerService.js @@ -52,9 +52,17 @@ export function createGpioTimerService () { let previousTick = 0 // Define the alert handler - sensor.on('alert', (level, currentTick) => { + sensor.on('alert', (level, rawCurrentTick) => { if ((triggeredFlank === 'Both') || (triggeredFlank === 'Down' && level === 0) || (triggeredFlank === 'Up' && level === 1)) { - const currentDt = ((currentTick >> 0) - (previousTick >> 0)) / 1e6 + const currentTick = (rawCurrentTick >> 0) / 1e6 + let currentDt + if (currentTick > previousTick) { + currentDt = currentTick - previousTick + } else { + // We had a rollover of the tick, so the current tick misses 4,294,967,295 us + log.debug('Gpio-service: tick rollover detected and corrected') + currentDt = (currentTick + 4294.967295) - previousTick + } previousTick = currentTick process.send(currentDt) } From 6623aa2f86e42cb1a7224bc19bc86863a198ea08 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:27:13 +0100 Subject: [PATCH 002/231] Several bugfixes and improvements Bugfix: Fixed a bug where a new year or month would crash the workoutrecorder due to a missing data directory Several improvements: * Added possibility of only creating RowingData file * Improved HRR data when restarting a workout --- app/engine/WorkoutRecorder.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/engine/WorkoutRecorder.js b/app/engine/WorkoutRecorder.js index 5370a1432f..05e59849d5 100644 --- a/app/engine/WorkoutRecorder.js +++ b/app/engine/WorkoutRecorder.js @@ -87,6 +87,14 @@ function createWorkoutRecorder () { `"${currentstroke.driveHandleVelocityCurve}","${currentstroke.driveHandlePowerCurve}"\n` i++ } + + try { + await fs.mkdir(directory, { recursive: true }) + } catch (error) { + if (error.code !== 'EEXIST') { + log.error(`can not create directory ${directory}`, error) + } + } await createFile(RowingData, `${filename}`, false) } @@ -270,7 +278,7 @@ function createWorkoutRecorder () { } async function createRecordings () { - if (!config.createRawDataFiles && !config.createTcxFiles) { + if (!config.createRawDataFiles && !config.createTcxFiles && !config.createRowingDataFiles) { return } @@ -279,6 +287,8 @@ function createWorkoutRecorder () { return } + postExerciseHR = [] + const parallelCalls = [] if (config.createRawDataFiles) { From 062faa0190d132c59e877c759bc2e8d9f7dff3e3 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:34:00 +0100 Subject: [PATCH 003/231] A bit more defensive programming A bit more defensive programming as one bad (NaN) measurement of CurrentDt will kill all metrics for the entire session. --- app/engine/Flywheel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/engine/Flywheel.js b/app/engine/Flywheel.js index b20b09c1e3..565db81ac1 100644 --- a/app/engine/Flywheel.js +++ b/app/engine/Flywheel.js @@ -58,7 +58,7 @@ function createFlywheel (rowerSettings) { reset() function pushValue (dataPoint) { - if (dataPoint > rowerSettings.maximumStrokeTimeBeforePause || dataPoint < 0) { + if (isNaN(dataPoint) || dataPoint < 0 || dataPoint > rowerSettings.maximumStrokeTimeBeforePause) { // This typicaly happends after a pause, we need to fix this as it throws off all time calculations log.debug(`*** WARNING: currentDt of ${dataPoint} sec isn't between 0 and maximumStrokeTimeBeforePause (${rowerSettings.maximumStrokeTimeBeforePause} sec)`) dataPoint = currentDt.clean() From 35ff3489a567577b6dcca2dc0eb6d7211098eb85 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:44:37 +0100 Subject: [PATCH 004/231] Fixed a typo in function name To add insult to injury: by a typo in the error logging function, the app would crash if there was a recoverable error --- app/engine/utils/FullTSLinearSeries.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/engine/utils/FullTSLinearSeries.js b/app/engine/utils/FullTSLinearSeries.js index b0e9c3d4b4..1eab396f0e 100644 --- a/app/engine/utils/FullTSLinearSeries.js +++ b/app/engine/utils/FullTSLinearSeries.js @@ -183,7 +183,7 @@ function createTSLinearSeries (maxSeriesLength = 0) { const mid = Math.floor(sortedArray.length / 2) return (sortedArray.length % 2 !== 0 ? sortedArray[mid] : ((sortedArray[mid - 1] + sortedArray[mid]) / 2)) } else { - log.eror('TS Linear Regressor, Median calculation on empty dataset attempted!') + log.error('TS Linear Regressor, Median calculation on empty dataset attempted!') return 0 } } From 733720fa44b8840e997d8daf7bfb585c918a58b8 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:56:37 +0100 Subject: [PATCH 005/231] Expansion of the Interval type Addittion of the interval type, as a preperation of the more complex workout schedules --- app/ble/pm5/characteristic/GeneralStatus.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/ble/pm5/characteristic/GeneralStatus.js b/app/ble/pm5/characteristic/GeneralStatus.js index c0116ee038..b4b7a5aa6a 100644 --- a/app/ble/pm5/characteristic/GeneralStatus.js +++ b/app/ble/pm5/characteristic/GeneralStatus.js @@ -41,10 +41,11 @@ export default class GeneralStatus extends bleno.Characteristic { bufferBuilder.writeUInt24LE(Math.round(data.totalMovingTime * 100)) // distance: UInt24LE in 0.1 m bufferBuilder.writeUInt24LE(Math.round(data.totalLinearDistance * 10)) - // workoutType: UInt8 0 WORKOUTTYPE_JUSTROW_NOSPLITS, 2 WORKOUTTYPE_FIXEDDIST_NOSPLITS, 4 WORKOUTTYPE_FIXEDTIME_NOSPLITS + // workoutType: UInt8: 0 WORKOUTTYPE_JUSTROW_NOSPLITS, 2 WORKOUTTYPE_FIXEDDIST_NOSPLITS, 4 WORKOUTTYPE_FIXEDTIME_NOSPLITS bufferBuilder.writeUInt8(data.sessiontype === 'Distance' ? 2 : (data.sessiontype === 'Time' ? 4 : 0)) - // intervalType: UInt8 will always use 255 (NONE) - bufferBuilder.writeUInt8(255) + // intervalType: UInt8: 1 INTERVALTYPE_TIME, 2 INTERVALTYPE_DIST, 255 NONE + // ToDo: split down further to allow rest intervals when the PM5 schedule dictates it + bufferBuilder.writeUInt8(data.sessiontype === 'Distance' ? 2 : (data.sessiontype === 'Time' ? 1 : 255)) // workoutState: UInt8 0 WAITTOBEGIN, 1 WORKOUTROW, 10 WORKOUTEND bufferBuilder.writeUInt8(data.sessionStatus === 'Rowing' ? 1 : (data.sessionStatus === 'WaitingForStart' ? 0 : 10)) // rowingState: UInt8 0 INACTIVE, 1 ACTIVE From b4e8f16b3c0caf9c0f35d6d70716d5a38a92ca62 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 11 Jan 2023 10:12:54 +0100 Subject: [PATCH 006/231] Improvement of code quality Replaced the horrible ested If...then....else with a much more readable case statement to implement the Concept 2 table --- app/engine/VO2max.js | 47 ++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/app/engine/VO2max.js b/app/engine/VO2max.js index 6d15189c2d..b26de3dcc8 100644 --- a/app/engine/VO2max.js +++ b/app/engine/VO2max.js @@ -96,36 +96,31 @@ function createVO2max (config) { log.debug(`--- VO2Max Interpolated 2K pace: ${Math.floor(projectedTwoKPace / 60)}:${(projectedTwoKPace % 60).toFixed(1)}`) // This implements the table with formulas found at https://www.concept2.com/indoor-rowers/training/calculators/vo2max-calculator - if (config.userSettings.highlyTrained) { - // Highly trained - if (config.userSettings.sex === 'male') { - // Highly trained male - if (config.userSettings.weight > 75) { - // Highly trained male, above 75 Kg - Y = 15.7 - (1.5 * projectedTwoKTimeInMinutes) - } else { - // Highly trained male, equal or below 75 Kg - Y = 15.1 - (1.5 * projectedTwoKTimeInMinutes) - } - } else { - // Highly trained female - if (config.userSettings.weight > 61.36) { - // Highly trained female, above 61.36 Kg - Y = 14.9 - (1.5 * projectedTwoKTimeInMinutes) - } else { - // Highly trained female, equal or below 61.36 Kg - Y = 14.6 - (1.5 * projectedTwoKTimeInMinutes) - } - } - } else { - // Not highly trained - if (config.userSettings.sex === 'male') { + switch (true) { + case (config.userSettings.sex === 'male' && config.userSettings.highlyTrained && config.userSettings.weight > 75): + // Highly trained male, above 75 Kg + Y = 15.7 - (1.5 * projectedTwoKTimeInMinutes + break + case (config.userSettings.sex === 'male' && config.userSettings.highlyTrained): + // Highly trained male, equal or below 75 Kg + Y = 15.1 - (1.5 * projectedTwoKTimeInMinutes) + break + case (config.userSettings.sex === 'male'): // Not highly trained male Y = 10.7 - (0.9 * projectedTwoKTimeInMinutes) - } else { + break + case (config.userSettings.sex === 'female' && config.userSettings.highlyTrained && config.userSettings.weight > 61.36): + // Highly trained female, above 61.36 Kg + Y = 14.9 - (1.5 * projectedTwoKTimeInMinutes) + break + case (config.userSettings.sex === 'female' && config.userSettings.highlyTrained): + // Highly trained female, equal or below 61.36 Kg + Y = 14.6 - (1.5 * projectedTwoKTimeInMinutes) + break + case (config.userSettings.sex === 'female'): // Not highly trained female Y = 10.26 - (0.93 * projectedTwoKTimeInMinutes) - } + break } return (Y * 1000) / config.userSettings.weight } From 11ff22eca1c29cde0ef3adc3a5018b3fe1fe8e16 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 11 Jan 2023 10:15:48 +0100 Subject: [PATCH 007/231] Fixed a missing bracket --- app/engine/VO2max.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/engine/VO2max.js b/app/engine/VO2max.js index b26de3dcc8..ee196f2e01 100644 --- a/app/engine/VO2max.js +++ b/app/engine/VO2max.js @@ -99,7 +99,7 @@ function createVO2max (config) { switch (true) { case (config.userSettings.sex === 'male' && config.userSettings.highlyTrained && config.userSettings.weight > 75): // Highly trained male, above 75 Kg - Y = 15.7 - (1.5 * projectedTwoKTimeInMinutes + Y = 15.7 - (1.5 * projectedTwoKTimeInMinutes) break case (config.userSettings.sex === 'male' && config.userSettings.highlyTrained): // Highly trained male, equal or below 75 Kg From e29d723776552369c1bfa29ad6b803fb80f33038 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 11 Jan 2023 23:06:34 +0100 Subject: [PATCH 008/231] Added sanity checks to the config manager Added a lot of sanity checks to the loading of the config. --- app/tools/ConfigManager.js | 199 ++++++++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 1 deletion(-) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index ed4575b523..98d5f762d7 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -1,4 +1,4 @@ -'use strict' + 'use strict' /* Open Rowing Monitor, https://github.com/laberning/openrowingmonitor @@ -6,6 +6,7 @@ */ import defaultConfig from '../../config/default.config.js' import { deepMerge } from './Helper.js' +import log from 'loglevel' async function getConfig () { let customConfig @@ -16,6 +17,202 @@ async function getConfig () { return customConfig !== undefined ? deepMerge(defaultConfig, customConfig.default) : defaultConfig } +function checkConfig (configToCheck) { + checkRangeValue(configToCheck.loglevel.default, 'loglevel.default',['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') + checkRangeValue(configToCheck.loglevel.RowingEngine, 'loglevel.RowingEngine', ['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') + checkIntegerValue(configToCheck.gpioPin, 'gpioPin', 1, 27, false, false, null) + checkIntegerValue(configToCheck.gpioPriority, 'gpioPriority', -7, 0, true, true, 0) + checkIntegerValue(configToCheck.gpioMinimumPulseLength, 'gpioMinimumPulseLength', 1, 100000, false, true, 0) + checkIntegerValue(configToCheck.gpioPollingInterval, 'gpioPollingInterval', 1, 10, false, true, 10) + checkRangeValue(configToCheck.gpioPollingInterval, 'gpioPollingInterval', [1, 2, 5, 10], true, 10) + checkRangeValue(configToCheck.gpioTriggeredFlank, 'gpioTriggeredFlank', ['Up', 'Down', 'Both'], false, null) + checkIntegerValue(configToCheck.appPriority, 'appPriority', configToCheck.gpioPriority, 0, true, true, 0) + checkIntegerValue(configToCheck.webUpdateInterval, 'webUpdateInterval', 80, 1000, false, true, 1000) + checkBinaryValue(configToCheck.heartrateMonitorBLE, 'heartrateMonitorBLE', true, true) + checkBinaryValue(configToCheck.heartrateMonitorANT, 'heartrateMonitorANT', true, false) + checkRangeValue(configToCheck.bluetoothMode, 'bluetoothMode', ['off', 'PM5', 'FTMS', 'FTMSBIKE', 'CPS', 'CSC'], true, 'FTMS') + checkIntegerValue(configToCheck.peripheralUpdateInterval, 'peripheralUpdateInterval', 80, 1000, false, true, 1000) + checkIntegerValue(configToCheck.numOfPhasesForAveragingScreenData, 'numOfPhasesForAveragingScreenData', 2, null, false, true, 4) + checkBinaryValue(configToCheck.createRowingDataFiles, 'createRowingDataFiles', true, true) + checkBinaryValue(configToCheck.createRawDataFiles, 'createRawDataFiles', true, true) + checkBinaryValue(configToCheck.gzipRawDataFiles, 'gzipRawDataFiles', true, false) + checkBinaryValue(configToCheck.createTcxFiles, 'createTcxFiles', true, true) + checkBinaryValue(configToCheck.gzipTcxFiles, 'gzipTcxFiles', true, false) + if (configToCheck.createRowingDataFiles || configToCheck.createRawDataFiles || configToCheck.createTcxFiles) { + } + checkFloatValue(configToCheck.userSettings.restingHR, 'userSettings.restingHR', 30, 220, false, true, 40) + checkFloatValue(configToCheck.userSettings.maxHR, 'userSettings.maxHR', configToCheck.userSettings.restingHR, 220, false, true, 220) + if (configToCheck.createTcxFiles) { + checkFloatValue(configToCheck.userSettings.minPower, 'userSettings.minPower', 1, 500, false, true, 50) + checkFloatValue(configToCheck.userSettings.maxPower, 'userSettings.maxPower', 100, 6000, false, true, 500) + checkFloatValue(configToCheck.userSettings.distanceCorrectionFactor, 'userSettings.distanceCorrectionFactor', 0, 50, false, true, 5) + checkFloatValue(configToCheck.userSettings.weight, 'userSettings.weight', 25, 500, false, true, 80) + checkRangeValue(configToCheck.userSettings.sex, 'userSettings.sex', ['male', 'female'], true, 'male') + checkBinaryValue(configToCheck.userSettings.highlyTrained, 'userSettings.highlyTrained', true, false) + } + checkIntegerValue(configToCheck.rowerSettings.numOfImpulsesPerRevolution, 'rowerSettings.numOfImpulsesPerRevolution', 1, null, false, false, null) + checkIntegerValue(configToCheck.rowerSettings.flankLength, 'rowerSettings.flankLength', 3, null, false, false, null) + checkFloatValue(configToCheck.rowerSettings.sprocketRadius, 'rowerSettings.sprocketRadius', 0, 20, false, true, 3) + checkFloatValue(configToCheck.rowerSettings.minimumTimeBetweenImpulses, 'rowerSettings.minimumTimeBetweenImpulses', 0, 3, false, false, null) + checkFloatValue(configToCheck.rowerSettings.maximumTimeBetweenImpulses, 'rowerSettings.maximumTimeBetweenImpulses', configToCheck.rowerSettings.minimumTimeBetweenImpulses, 3, false, false, null) + checkFloatValue(configToCheck.rowerSettings.smoothing, 'rowerSettings.smoothing', 1, 1000000, false, true, 1) + checkFloatValue(configToCheck.rowerSettings.dragFactor, 'rowerSettings.dragFactor', 1, 1000000, false, false, null) + checkBinaryValue(configToCheck.rowerSettings.autoAdjustDragFactor, 'rowerSettings.autoAdjustDragFactor', true, false) + checkIntegerValue(configToCheck.rowerSettings.dragFactorSmoothing, 'rowerSettings.dragFactorSmoothing', 1, 1000, false, true, 1) + if (configToCheck.rowerSettings.autoAdjustDragFactor) { + checkFloatValue(configToCheck.rowerSettings.minimumDragQuality, 'rowerSettings.minimumDragQuality', 0, 1, true, true, 0) + } + checkFloatValue(configToCheck.rowerSettings.flywheelInertia, 'rowerSettings.flywheelInertia', 0, 2, false, false, null) + checkFloatValue(configToCheck.rowerSettings.minumumForceBeforeStroke, 'rowerSettings.minumumForceBeforeStroke', 0, 500, true, true, 0) + checkFloatValue(configToCheck.rowerSettings.minumumRecoverySlope, 'rowerSettings.minumumRecoverySlope', 0, 2, true, true, 0) + checkFloatValue(configToCheck.rowerSettings.minimumStrokeQuality, 'rowerSettings.minimumStrokeQuality', 0, 1, true, true, 0) + checkBinaryValue(configToCheck.rowerSettings.autoAdjustRecoverySlope, 'rowerSettings.autoAdjustRecoverySlope', true, false) + if (!configToCheck.rowerSettings.autoAdjustDragFactor && configToCheck.rowerSettings.autoAdjustRecoverySlope) { + log.error('Configuration Error: rowerSettings.autoAdjustRecoverySlope can not be true when configToCheck.rowerSettings.autoAdjustDragFactor is false, ignoring request') + } + if (configToCheck.rowerSettings.autoAdjustDragFactor && configToCheck.rowerSettings.autoAdjustRecoverySlope) { + checkFloatValue(configToCheck.rowerSettings.autoAdjustRecoverySlopeMargin, 'autoAdjustRecoverySlopeMargin', 0, 1, false, true, 1) + } + checkFloatValue(configToCheck.rowerSettings.minimumDriveTime, 'minimumDriveTime', 0, 6, false, true, 0.001) + checkFloatValue(configToCheck.rowerSettings.minimumRecoveryTime, 'minimumRecoveryTime', 0, 6, false, true, 0.001) + checkFloatValue(configToCheck.rowerSettings.maximumStrokeTimeBeforePause, 'maximumStrokeTimeBeforePause', 3, 60, false, true, 6) + checkFloatValue(configToCheck.rowerSettings.magicConstant, 'magicConstant', 0, 28, false, true, 2.8) +} + +function checkIntegerValue (parameter, parameterName, minimumValue, maximumvalue, allowZero, allowRepair, defaultValue) { + let errors = 0 + switch (true) { + case (parameter == undefined): + log.error(`Configuration Error: ${parameterName} isn't defined`) + errors++ + break + case (!Number.isInteger(parameter)): + log.error(`Configuration Error: ${parameterName} should be an integer value, encountered ${parameter}`) + errors++ + break + case (minimumValue != null && parameter < minimumValue): + log.error(`Configuration Error: ${parameterName} should be at least ${minimumValue}, encountered ${parameter}`) + errors++ + break + case (maximumvalue != null && parameter > maximumvalue): + log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) + errors++ + break + case (!allowZero && parameter == 0): + log.error(`Configuration Error: ${parameterName} can't be zero`) + errors++ + break + default: + // No error detected :) + } + if (errors > 0) { + // Errors were made + if (allowRepair) { + log.error(` resolved by setting ${parameterName} to ${defaultValue}`) + // ToDo: fix this by making the function call a call by reference!!! + parameter = defaultValue + } else { + log.error(`As ${parameterName} is a fatal parameter, I'm exiting`) + process.exit(9) + } + } +} + +function checkFloatValue (parameter, parameterName, minimumValue, maximumvalue, allowZero, allowRepair, defaultValue) { + let errors = 0 + switch (true) { + case (parameter == undefined): + log.error(`Configuration Error: ${parameterName} isn't defined`) + errors++ + break + case (!typeof(parameter)==="number"): + log.error(`Configuration Error: ${parameterName} should be a numerical value, encountered ${parameter}`) + errors++ + break + case (minimumValue != null && parameter < minimumValue): + log.error(`Configuration Error: ${parameterName} should be at least ${minimumValue}, encountered ${parameter}`) + errors++ + break + case (maximumvalue != null && parameter > maximumvalue): + log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) + errors++ + break + case (!allowZero && parameter == 0): + log.error(`Configuration Error: ${parameterName} can't be zero`) + errors++ + break + default: + // No error detected :) + } + if (errors > 0) { + // Errors were made + if (allowRepair) { + log.error(` resolved by setting ${parameterName} to ${defaultValue}`) + // ToDo: fix this by making the function call a call by reference!!! + parameter = defaultValue + } else { + log.error(`As ${parameterName} is a fatal parameter, I'm exiting`) + process.exit(9) + } + } +} + +function checkBinaryValue (parameter, parameterName, allowRepair, defaultValue) { + let errors = 0 + switch (true) { + case (parameter == undefined): + log.error(`Configuration Error: ${parameterName} isn't defined`) + errors++ + break + case (!(parameter === true || parameter === false)): + log.error(`Configuration Error: ${parameterName} should be either false or true, encountered ${parameter}`) + errors++ + break + default: + // No error detected :) + } + if (errors > 0) { + // Errors were made + if (allowRepair) { + log.error(` resolved by setting ${parameterName} to ${defaultValue}`) + // ToDo: fix this by making the function call a call by reference!!! + parameter = defaultValue + } else { + log.error(`As ${parameterName} is a fatal parameter, I'm exiting`) + process.exit(9) + } + } +} + +function checkRangeValue (parameter, parameterName, range, allowRepair, defaultValue) { + let errors = 0 + switch (true) { + case (parameter == undefined): + log.error(`Configuration Error: ${parameterName} isn't defined`) + errors++ + break + case (!range.includes(parameter)): + log.error(`Configuration Error: ${parameterName} should be come from ${range}, encountered ${parameter}`) + errors++ + break + default: + // No error detected :) + } + if (errors > 0) { + // Errors were made + if (allowRepair) { + log.error(` resolved by setting ${parameterName} to ${defaultValue}`) + // ToDo: fix this by making the function call a call by reference!!! + parameter = defaultValue + } else { + log.error(`As ${parameterName} is a fatal parameter, I'm exiting`) + process.exit(9) + } + } +} + const config = await getConfig() +checkConfig(config) + export default config From dc43c38e16c25c355e59ba1baed81f2d822cb1c8 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 11 Jan 2023 23:17:56 +0100 Subject: [PATCH 009/231] Improvement of code quality Improvement of code quality --- app/tools/ConfigManager.js | 143 ++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 73 deletions(-) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index 98d5f762d7..c3bde9012d 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -1,4 +1,4 @@ - 'use strict' +'use strict' /* Open Rowing Monitor, https://github.com/laberning/openrowingmonitor @@ -13,12 +13,11 @@ async function getConfig () { try { customConfig = await import('../../config/config.js') } catch (exception) {} - return customConfig !== undefined ? deepMerge(defaultConfig, customConfig.default) : defaultConfig } function checkConfig (configToCheck) { - checkRangeValue(configToCheck.loglevel.default, 'loglevel.default',['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') + checkRangeValue(configToCheck.loglevel.default, 'loglevel.default', ['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') checkRangeValue(configToCheck.loglevel.RowingEngine, 'loglevel.RowingEngine', ['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') checkIntegerValue(configToCheck.gpioPin, 'gpioPin', 1, 27, false, false, null) checkIntegerValue(configToCheck.gpioPriority, 'gpioPriority', -7, 0, true, true, 0) @@ -38,8 +37,6 @@ function checkConfig (configToCheck) { checkBinaryValue(configToCheck.gzipRawDataFiles, 'gzipRawDataFiles', true, false) checkBinaryValue(configToCheck.createTcxFiles, 'createTcxFiles', true, true) checkBinaryValue(configToCheck.gzipTcxFiles, 'gzipTcxFiles', true, false) - if (configToCheck.createRowingDataFiles || configToCheck.createRawDataFiles || configToCheck.createTcxFiles) { - } checkFloatValue(configToCheck.userSettings.restingHR, 'userSettings.restingHR', 30, 220, false, true, 40) checkFloatValue(configToCheck.userSettings.maxHR, 'userSettings.maxHR', configToCheck.userSettings.restingHR, 220, false, true, 220) if (configToCheck.createTcxFiles) { @@ -73,37 +70,37 @@ function checkConfig (configToCheck) { if (configToCheck.rowerSettings.autoAdjustDragFactor && configToCheck.rowerSettings.autoAdjustRecoverySlope) { checkFloatValue(configToCheck.rowerSettings.autoAdjustRecoverySlopeMargin, 'autoAdjustRecoverySlopeMargin', 0, 1, false, true, 1) } - checkFloatValue(configToCheck.rowerSettings.minimumDriveTime, 'minimumDriveTime', 0, 6, false, true, 0.001) - checkFloatValue(configToCheck.rowerSettings.minimumRecoveryTime, 'minimumRecoveryTime', 0, 6, false, true, 0.001) - checkFloatValue(configToCheck.rowerSettings.maximumStrokeTimeBeforePause, 'maximumStrokeTimeBeforePause', 3, 60, false, true, 6) - checkFloatValue(configToCheck.rowerSettings.magicConstant, 'magicConstant', 0, 28, false, true, 2.8) + checkFloatValue(configToCheck.rowerSettings.minimumDriveTime, 'rowerSettings.minimumDriveTime', 0, 6, false, true, 0.001) + checkFloatValue(configToCheck.rowerSettings.minimumRecoveryTime, 'rowerSettings.minimumRecoveryTime', 0, 6, false, true, 0.001) + checkFloatValue(configToCheck.rowerSettings.maximumStrokeTimeBeforePause, 'rowerSettings.maximumStrokeTimeBeforePause', 3, 60, false, true, 6) + checkFloatValue(configToCheck.rowerSettings.magicConstant, 'rowerSettings.magicConstant', 0, 28, false, true, 2.8) } function checkIntegerValue (parameter, parameterName, minimumValue, maximumvalue, allowZero, allowRepair, defaultValue) { let errors = 0 switch (true) { - case (parameter == undefined): - log.error(`Configuration Error: ${parameterName} isn't defined`) - errors++ - break - case (!Number.isInteger(parameter)): - log.error(`Configuration Error: ${parameterName} should be an integer value, encountered ${parameter}`) - errors++ - break - case (minimumValue != null && parameter < minimumValue): - log.error(`Configuration Error: ${parameterName} should be at least ${minimumValue}, encountered ${parameter}`) - errors++ - break - case (maximumvalue != null && parameter > maximumvalue): - log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) - errors++ - break - case (!allowZero && parameter == 0): - log.error(`Configuration Error: ${parameterName} can't be zero`) - errors++ - break - default: - // No error detected :) + case (parameter === undefined): + log.error(`Configuration Error: ${parameterName} isn't defined`) + errors++ + break + case (!Number.isInteger(parameter)): + log.error(`Configuration Error: ${parameterName} should be an integer value, encountered ${parameter}`) + errors++ + break + case (minimumValue != null && parameter < minimumValue): + log.error(`Configuration Error: ${parameterName} should be at least ${minimumValue}, encountered ${parameter}`) + errors++ + break + case (maximumvalue != null && parameter > maximumvalue): + log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) + errors++ + break + case (!allowZero && parameter == 0): + log.error(`Configuration Error: ${parameterName} can't be zero`) + errors++ + break + default: + // No error detected :) } if (errors > 0) { // Errors were made @@ -121,28 +118,28 @@ function checkIntegerValue (parameter, parameterName, minimumValue, maximumvalue function checkFloatValue (parameter, parameterName, minimumValue, maximumvalue, allowZero, allowRepair, defaultValue) { let errors = 0 switch (true) { - case (parameter == undefined): - log.error(`Configuration Error: ${parameterName} isn't defined`) - errors++ - break - case (!typeof(parameter)==="number"): - log.error(`Configuration Error: ${parameterName} should be a numerical value, encountered ${parameter}`) - errors++ - break - case (minimumValue != null && parameter < minimumValue): - log.error(`Configuration Error: ${parameterName} should be at least ${minimumValue}, encountered ${parameter}`) - errors++ - break - case (maximumvalue != null && parameter > maximumvalue): - log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) - errors++ - break - case (!allowZero && parameter == 0): - log.error(`Configuration Error: ${parameterName} can't be zero`) - errors++ - break - default: - // No error detected :) + case (parameter === undefined): + log.error(`Configuration Error: ${parameterName} isn't defined`) + errors++ + break + case (!typeof (parameter) === 'number'): + log.error(`Configuration Error: ${parameterName} should be a numerical value, encountered ${parameter}`) + errors++ + break + case (minimumValue != null && parameter < minimumValue): + log.error(`Configuration Error: ${parameterName} should be at least ${minimumValue}, encountered ${parameter}`) + errors++ + break + case (maximumvalue != null && parameter > maximumvalue): + log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) + errors++ + break + case (!allowZero && parameter == 0): + log.error(`Configuration Error: ${parameterName} can't be zero`) + errors++ + break + default: + // No error detected :) } if (errors > 0) { // Errors were made @@ -160,16 +157,16 @@ function checkFloatValue (parameter, parameterName, minimumValue, maximumvalue, function checkBinaryValue (parameter, parameterName, allowRepair, defaultValue) { let errors = 0 switch (true) { - case (parameter == undefined): - log.error(`Configuration Error: ${parameterName} isn't defined`) - errors++ - break - case (!(parameter === true || parameter === false)): - log.error(`Configuration Error: ${parameterName} should be either false or true, encountered ${parameter}`) - errors++ - break - default: - // No error detected :) + case (parameter === undefined): + log.error(`Configuration Error: ${parameterName} isn't defined`) + errors++ + break + case (!(parameter === true || parameter === false)): + log.error(`Configuration Error: ${parameterName} should be either false or true, encountered ${parameter}`) + errors++ + break + default: + // No error detected :) } if (errors > 0) { // Errors were made @@ -187,16 +184,16 @@ function checkBinaryValue (parameter, parameterName, allowRepair, defaultValue) function checkRangeValue (parameter, parameterName, range, allowRepair, defaultValue) { let errors = 0 switch (true) { - case (parameter == undefined): - log.error(`Configuration Error: ${parameterName} isn't defined`) - errors++ - break - case (!range.includes(parameter)): - log.error(`Configuration Error: ${parameterName} should be come from ${range}, encountered ${parameter}`) - errors++ - break - default: - // No error detected :) + case (parameter === undefined): + log.error(`Configuration Error: ${parameterName} isn't defined`) + errors++ + break + case (!range.includes(parameter)): + log.error(`Configuration Error: ${parameterName} should be come from ${range}, encountered ${parameter}`) + errors++ + break + default: + // No error detected :) } if (errors > 0) { // Errors were made From 0378f27655c593c567eae2e1b7814e5c2083f06e Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 11 Jan 2023 23:19:48 +0100 Subject: [PATCH 010/231] Fixed Lint errors Fixed Lint errors --- app/tools/ConfigManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index c3bde9012d..12fe1d3c75 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -95,7 +95,7 @@ function checkIntegerValue (parameter, parameterName, minimumValue, maximumvalue log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) errors++ break - case (!allowZero && parameter == 0): + case (!allowZero && parameter === 0): log.error(`Configuration Error: ${parameterName} can't be zero`) errors++ break @@ -134,7 +134,7 @@ function checkFloatValue (parameter, parameterName, minimumValue, maximumvalue, log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) errors++ break - case (!allowZero && parameter == 0): + case (!allowZero && parameter === 0): log.error(`Configuration Error: ${parameterName} can't be zero`) errors++ break From ce9294dfb82fbb7f7d06196216c9c204df4efb1c Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 11 Jan 2023 23:29:52 +0100 Subject: [PATCH 011/231] Centralising parameter check Removed sanity checks in RowingStatistics, as the configmanager centralises the parameter check --- app/engine/RowingStatistics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index 8249b3d56f..de4d10c939 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -15,8 +15,8 @@ const log = loglevel.getLogger('RowingEngine') function createRowingStatistics (config, session) { const numOfDataPointsForAveraging = config.numOfPhasesForAveragingScreenData - const webUpdateInterval = Math.min(config.webUpdateInterval, 2000) - const peripheralUpdateInterval = Math.min(config.peripheralUpdateInterval, 1000) + const webUpdateInterval = config.webUpdateInterval + const peripheralUpdateInterval = config.peripheralUpdateInterval const emitter = new EventEmitter() const rower = createRower(config.rowerSettings) const minimumStrokeTime = config.rowerSettings.minimumRecoveryTime + config.rowerSettings.minimumDriveTime From 3364ef38a8624bc7fe765a17a1cd01ebf50c517b Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 11 Jan 2023 23:30:11 +0100 Subject: [PATCH 012/231] Centralising parameter check Removed sanity checks in RowingStatistics, as the configmanager centralises the parameter check --- app/engine/Flywheel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/engine/Flywheel.js b/app/engine/Flywheel.js index 565db81ac1..ee2e4d8448 100644 --- a/app/engine/Flywheel.js +++ b/app/engine/Flywheel.js @@ -29,7 +29,7 @@ const log = loglevel.getLogger('RowingEngine') function createFlywheel (rowerSettings) { const angularDisplacementPerImpulse = (2.0 * Math.PI) / rowerSettings.numOfImpulsesPerRevolution - const flankLength = Math.max(3, rowerSettings.flankLength) + const flankLength = rowerSettings.flankLength const minimumDragFactorSamples = Math.floor(rowerSettings.minimumRecoveryTime / rowerSettings.maximumTimeBetweenImpulses) const minumumTorqueBeforeStroke = rowerSettings.minumumForceBeforeStroke * (rowerSettings.sprocketRadius / 100) const currentDt = createStreamFilter(rowerSettings.smoothing, rowerSettings.maximumTimeBetweenImpulses) From fb64547e307890bb4a5c447fad032610871f3bc4 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Thu, 12 Jan 2023 12:25:06 +0100 Subject: [PATCH 013/231] Allowed the config check to repair settings Added the ability to automatically fix forgotten or implausible settings when possible. --- app/tools/ConfigManager.js | 195 +++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 97 deletions(-) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index 12fe1d3c75..3bc3a5b20e 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -3,6 +3,7 @@ Open Rowing Monitor, https://github.com/laberning/openrowingmonitor Merges the different config files and presents the configuration to the application + Checks the config for plausibilit, fixes the errors when needed */ import defaultConfig from '../../config/default.config.js' import { deepMerge } from './Helper.js' @@ -17,86 +18,87 @@ async function getConfig () { } function checkConfig (configToCheck) { - checkRangeValue(configToCheck.loglevel.default, 'loglevel.default', ['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') - checkRangeValue(configToCheck.loglevel.RowingEngine, 'loglevel.RowingEngine', ['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') - checkIntegerValue(configToCheck.gpioPin, 'gpioPin', 1, 27, false, false, null) - checkIntegerValue(configToCheck.gpioPriority, 'gpioPriority', -7, 0, true, true, 0) - checkIntegerValue(configToCheck.gpioMinimumPulseLength, 'gpioMinimumPulseLength', 1, 100000, false, true, 0) - checkIntegerValue(configToCheck.gpioPollingInterval, 'gpioPollingInterval', 1, 10, false, true, 10) - checkRangeValue(configToCheck.gpioPollingInterval, 'gpioPollingInterval', [1, 2, 5, 10], true, 10) - checkRangeValue(configToCheck.gpioTriggeredFlank, 'gpioTriggeredFlank', ['Up', 'Down', 'Both'], false, null) - checkIntegerValue(configToCheck.appPriority, 'appPriority', configToCheck.gpioPriority, 0, true, true, 0) - checkIntegerValue(configToCheck.webUpdateInterval, 'webUpdateInterval', 80, 1000, false, true, 1000) - checkBinaryValue(configToCheck.heartrateMonitorBLE, 'heartrateMonitorBLE', true, true) - checkBinaryValue(configToCheck.heartrateMonitorANT, 'heartrateMonitorANT', true, false) - checkRangeValue(configToCheck.bluetoothMode, 'bluetoothMode', ['off', 'PM5', 'FTMS', 'FTMSBIKE', 'CPS', 'CSC'], true, 'FTMS') - checkIntegerValue(configToCheck.peripheralUpdateInterval, 'peripheralUpdateInterval', 80, 1000, false, true, 1000) - checkIntegerValue(configToCheck.numOfPhasesForAveragingScreenData, 'numOfPhasesForAveragingScreenData', 2, null, false, true, 4) - checkBinaryValue(configToCheck.createRowingDataFiles, 'createRowingDataFiles', true, true) - checkBinaryValue(configToCheck.createRawDataFiles, 'createRawDataFiles', true, true) - checkBinaryValue(configToCheck.gzipRawDataFiles, 'gzipRawDataFiles', true, false) - checkBinaryValue(configToCheck.createTcxFiles, 'createTcxFiles', true, true) - checkBinaryValue(configToCheck.gzipTcxFiles, 'gzipTcxFiles', true, false) - checkFloatValue(configToCheck.userSettings.restingHR, 'userSettings.restingHR', 30, 220, false, true, 40) - checkFloatValue(configToCheck.userSettings.maxHR, 'userSettings.maxHR', configToCheck.userSettings.restingHR, 220, false, true, 220) + checkRangeValue(configToCheck.loglevel, 'default', ['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') + checkRangeValue(configToCheck.loglevel, 'RowingEngine', ['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') + checkIntegerValue(configToCheck, 'gpioPin', 1, 27, false, false, null) + checkIntegerValue(configToCheck, 'gpioPriority', -7, 0, true, true, 0) + checkIntegerValue(configToCheck, 'gpioMinimumPulseLength', 1, 100000, false, true, 0) + checkIntegerValue(configToCheck, 'gpioPollingInterval', 1, 10, false, true, 10) + checkRangeValue(configToCheck, 'gpioPollingInterval', [1, 2, 5, 10], true, 10) + checkRangeValue(configToCheck, 'gpioTriggeredFlank', ['Up', 'Down', 'Both'], false, null) + checkIntegerValue(configToCheck, 'appPriority', configToCheck.gpioPriority, 0, true, true, 0) + checkIntegerValue(configToCheck, 'webUpdateInterval', 80, 1000, false, true, 1000) + checkBinaryValue(configToCheck, 'heartrateMonitorBLE', true, true) + checkBinaryValue(configToCheck, 'heartrateMonitorANT', true, false) + checkRangeValue(configToCheck, 'bluetoothMode', ['off', 'PM5', 'FTMS', 'FTMSBIKE', 'CPS', 'CSC'], true, 'FTMS') + checkIntegerValue(configToCheck, 'peripheralUpdateInterval', 80, 1000, false, true, 1000) + checkIntegerValue(configToCheck, 'numOfPhasesForAveragingScreenData', 2, null, false, true, 4) + checkBinaryValue(configToCheck, 'createRowingDataFiles', true, true) + checkBinaryValue(configToCheck, 'createRawDataFiles', true, true) + checkBinaryValue(configToCheck, 'gzipRawDataFiles', true, false) + checkBinaryValue(configToCheck, 'createTcxFiles', true, true) + checkBinaryValue(configToCheck, 'gzipTcxFiles', true, false) + checkFloatValue(configToCheck.userSettings, 'restingHR', 30, 220, false, true, 40) + checkFloatValue(configToCheck.userSettings, 'maxHR', configToCheck.userSettings.restingHR, 220, false, true, 220) if (configToCheck.createTcxFiles) { - checkFloatValue(configToCheck.userSettings.minPower, 'userSettings.minPower', 1, 500, false, true, 50) - checkFloatValue(configToCheck.userSettings.maxPower, 'userSettings.maxPower', 100, 6000, false, true, 500) - checkFloatValue(configToCheck.userSettings.distanceCorrectionFactor, 'userSettings.distanceCorrectionFactor', 0, 50, false, true, 5) - checkFloatValue(configToCheck.userSettings.weight, 'userSettings.weight', 25, 500, false, true, 80) - checkRangeValue(configToCheck.userSettings.sex, 'userSettings.sex', ['male', 'female'], true, 'male') - checkBinaryValue(configToCheck.userSettings.highlyTrained, 'userSettings.highlyTrained', true, false) + checkFloatValue(configToCheck.userSettings, 'minPower', 1, 500, false, true, 50) + checkFloatValue(configToCheck.userSettings, 'maxPower', 100, 6000, false, true, 500) + checkFloatValue(configToCheck.userSettings, 'distanceCorrectionFactor', 0, 50, false, true, 5) + checkFloatValue(configToCheck.userSettings, 'weight', 25, 500, false, true, 80) + checkRangeValue(configToCheck.userSettings, 'sex', ['male', 'female'], true, 'male') + checkBinaryValue(configToCheck.userSettings, 'highlyTrained', true, false) } - checkIntegerValue(configToCheck.rowerSettings.numOfImpulsesPerRevolution, 'rowerSettings.numOfImpulsesPerRevolution', 1, null, false, false, null) - checkIntegerValue(configToCheck.rowerSettings.flankLength, 'rowerSettings.flankLength', 3, null, false, false, null) - checkFloatValue(configToCheck.rowerSettings.sprocketRadius, 'rowerSettings.sprocketRadius', 0, 20, false, true, 3) - checkFloatValue(configToCheck.rowerSettings.minimumTimeBetweenImpulses, 'rowerSettings.minimumTimeBetweenImpulses', 0, 3, false, false, null) - checkFloatValue(configToCheck.rowerSettings.maximumTimeBetweenImpulses, 'rowerSettings.maximumTimeBetweenImpulses', configToCheck.rowerSettings.minimumTimeBetweenImpulses, 3, false, false, null) - checkFloatValue(configToCheck.rowerSettings.smoothing, 'rowerSettings.smoothing', 1, 1000000, false, true, 1) - checkFloatValue(configToCheck.rowerSettings.dragFactor, 'rowerSettings.dragFactor', 1, 1000000, false, false, null) - checkBinaryValue(configToCheck.rowerSettings.autoAdjustDragFactor, 'rowerSettings.autoAdjustDragFactor', true, false) - checkIntegerValue(configToCheck.rowerSettings.dragFactorSmoothing, 'rowerSettings.dragFactorSmoothing', 1, 1000, false, true, 1) + checkIntegerValue(configToCheck.rowerSettings, 'numOfImpulsesPerRevolution', 1, null, false, false, null) + checkIntegerValue(configToCheck.rowerSettings, 'flankLength', 3, null, false, false, null) + checkFloatValue(configToCheck.rowerSettings, 'sprocketRadius', 0, 20, false, true, 3) + checkFloatValue(configToCheck.rowerSettings, 'minimumTimeBetweenImpulses', 0, 3, false, false, null) + checkFloatValue(configToCheck.rowerSettings, 'maximumTimeBetweenImpulses', configToCheck.rowerSettings.minimumTimeBetweenImpulses, 3, false, false, null) + checkFloatValue(configToCheck.rowerSettings, 'smoothing', 1, 1000000, false, true, 1) + checkFloatValue(configToCheck.rowerSettings, 'dragFactor', 1, 1000000, false, false, null) + checkBinaryValue(configToCheck.rowerSettings, 'autoAdjustDragFactor', true, false) + checkIntegerValue(configToCheck.rowerSettings, 'dragFactorSmoothing', 1, 1000, false, true, 1) if (configToCheck.rowerSettings.autoAdjustDragFactor) { - checkFloatValue(configToCheck.rowerSettings.minimumDragQuality, 'rowerSettings.minimumDragQuality', 0, 1, true, true, 0) + checkFloatValue(configToCheck.rowerSettings, 'minimumDragQuality', 0, 1, true, true, 0) } - checkFloatValue(configToCheck.rowerSettings.flywheelInertia, 'rowerSettings.flywheelInertia', 0, 2, false, false, null) - checkFloatValue(configToCheck.rowerSettings.minumumForceBeforeStroke, 'rowerSettings.minumumForceBeforeStroke', 0, 500, true, true, 0) - checkFloatValue(configToCheck.rowerSettings.minumumRecoverySlope, 'rowerSettings.minumumRecoverySlope', 0, 2, true, true, 0) - checkFloatValue(configToCheck.rowerSettings.minimumStrokeQuality, 'rowerSettings.minimumStrokeQuality', 0, 1, true, true, 0) - checkBinaryValue(configToCheck.rowerSettings.autoAdjustRecoverySlope, 'rowerSettings.autoAdjustRecoverySlope', true, false) + checkFloatValue(configToCheck.rowerSettings, 'flywheelInertia', 0, null, false, false, null) + checkFloatValue(configToCheck.rowerSettings, 'minumumForceBeforeStroke', 0, 500, true, true, 0) + checkFloatValue(configToCheck.rowerSettings, 'minumumRecoverySlope', 0, null, true, true, 0) + checkFloatValue(configToCheck.rowerSettings, 'minimumStrokeQuality', 0, 1, true, true, 0) + checkBinaryValue(configToCheck.rowerSettings, 'autoAdjustRecoverySlope', true, false) if (!configToCheck.rowerSettings.autoAdjustDragFactor && configToCheck.rowerSettings.autoAdjustRecoverySlope) { - log.error('Configuration Error: rowerSettings.autoAdjustRecoverySlope can not be true when configToCheck.rowerSettings.autoAdjustDragFactor is false, ignoring request') + log.error('Configuration Error: rowerSettings.autoAdjustRecoverySlope can not be true when rowerSettings.autoAdjustDragFactor is false, ignoring request') } if (configToCheck.rowerSettings.autoAdjustDragFactor && configToCheck.rowerSettings.autoAdjustRecoverySlope) { - checkFloatValue(configToCheck.rowerSettings.autoAdjustRecoverySlopeMargin, 'autoAdjustRecoverySlopeMargin', 0, 1, false, true, 1) + checkFloatValue(configToCheck.rowerSettings, 'autoAdjustRecoverySlopeMargin', 0, 1, false, true, 1) } - checkFloatValue(configToCheck.rowerSettings.minimumDriveTime, 'rowerSettings.minimumDriveTime', 0, 6, false, true, 0.001) - checkFloatValue(configToCheck.rowerSettings.minimumRecoveryTime, 'rowerSettings.minimumRecoveryTime', 0, 6, false, true, 0.001) - checkFloatValue(configToCheck.rowerSettings.maximumStrokeTimeBeforePause, 'rowerSettings.maximumStrokeTimeBeforePause', 3, 60, false, true, 6) - checkFloatValue(configToCheck.rowerSettings.magicConstant, 'rowerSettings.magicConstant', 0, 28, false, true, 2.8) + checkFloatValue(configToCheck.rowerSettings, 'minimumDriveTime', 0, null, false, true, 0.001) + checkFloatValue(configToCheck.rowerSettings, 'minimumRecoveryTime', 0, null, false, true, 0.001) + checkFloatValue(configToCheck.rowerSettings, 'maximumStrokeTimeBeforePause', 3, 60, false, true, 6) + checkFloatValue(configToCheck.rowerSettings, 'magicConstant', 0, null, false, true, 2.8) } -function checkIntegerValue (parameter, parameterName, minimumValue, maximumvalue, allowZero, allowRepair, defaultValue) { +function checkIntegerValue (parameterSection, parameterName, minimumValue, maximumvalue, allowZero, allowRepair, defaultValue) { + // PLEASE NOTE: the parameterSection, parameterName seperation is needed to force a call by reference, which is needed for the repair action let errors = 0 switch (true) { - case (parameter === undefined): - log.error(`Configuration Error: ${parameterName} isn't defined`) + case (parameterSection[parameterName] === undefined): + log.error(`Configuration Error: ${parameterSection}.${parameterName} isn't defined`) errors++ break - case (!Number.isInteger(parameter)): - log.error(`Configuration Error: ${parameterName} should be an integer value, encountered ${parameter}`) + case (!Number.isInteger(parameterSection[parameterName])): + log.error(`Configuration Error: ${parameterSection}.${parameterName} should be an integer value, encountered ${parameterSection[parameterName]}`) errors++ break - case (minimumValue != null && parameter < minimumValue): - log.error(`Configuration Error: ${parameterName} should be at least ${minimumValue}, encountered ${parameter}`) + case (minimumValue != null && parameterSection[parameterName] < minimumValue): + log.error(`Configuration Error: ${parameterSection}.${parameterName} should be at least ${minimumValue}, encountered ${parameterSection[parameterName]}`) errors++ break - case (maximumvalue != null && parameter > maximumvalue): - log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) + case (maximumvalue != null && parameterSection[parameterName] > maximumvalue): + log.error(`Configuration Error: ${parameterSection}.${parameterName} can't be above ${maximumvalue}, encountered ${parameterSection[parameterName]}`) errors++ break - case (!allowZero && parameter === 0): - log.error(`Configuration Error: ${parameterName} can't be zero`) + case (!allowZero && parameterSection[parameterName] === 0): + log.error(`Configuration Error: ${parameterSection}.${parameterName} can't be zero`) errors++ break default: @@ -105,37 +107,37 @@ function checkIntegerValue (parameter, parameterName, minimumValue, maximumvalue if (errors > 0) { // Errors were made if (allowRepair) { - log.error(` resolved by setting ${parameterName} to ${defaultValue}`) - // ToDo: fix this by making the function call a call by reference!!! - parameter = defaultValue + log.error(` resolved by setting ${parameterSection}.${parameterName} to ${defaultValue}`) + parameterSection[parameterName] = defaultValue } else { - log.error(`As ${parameterName} is a fatal parameter, I'm exiting`) + log.error(` as ${parameterSection}.${parameterName} is a fatal parameter, I'm exiting`) process.exit(9) } } } -function checkFloatValue (parameter, parameterName, minimumValue, maximumvalue, allowZero, allowRepair, defaultValue) { +function checkFloatValue (parameterSection, parameterName, minimumValue, maximumvalue, allowZero, allowRepair, defaultValue) { + // PLEASE NOTE: the parameterSection, parameterName seperation is needed to force a call by reference, which is needed for the repair action let errors = 0 switch (true) { - case (parameter === undefined): - log.error(`Configuration Error: ${parameterName} isn't defined`) + case (parameterSection[parameterName] === undefined): + log.error(`Configuration Error: ${parameterSection}.${parameterName} isn't defined`) errors++ break - case (!typeof (parameter) === 'number'): - log.error(`Configuration Error: ${parameterName} should be a numerical value, encountered ${parameter}`) + case (!typeof (parameterSection[parameterName]) === 'number'): + log.error(`Configuration Error: ${parameterSection}.${parameterName} should be a numerical value, encountered ${parameterSection[parameterName]}`) errors++ break - case (minimumValue != null && parameter < minimumValue): - log.error(`Configuration Error: ${parameterName} should be at least ${minimumValue}, encountered ${parameter}`) + case (minimumValue != null && parameterSection[parameterName] < minimumValue): + log.error(`Configuration Error: ${parameterSection}.${parameterName} should be at least ${minimumValue}, encountered ${parameterSection[parameterName]}`) errors++ break - case (maximumvalue != null && parameter > maximumvalue): - log.error(`Configuration Error: ${parameterName} can't be above ${maximumvalue}, encountered ${parameter}`) + case (maximumvalue != null && parameterSection[parameterName] > maximumvalue): + log.error(`Configuration Error: ${parameterSection}.${parameterName} can't be above ${maximumvalue}, encountered ${parameterSection[parameterName]}`) errors++ break - case (!allowZero && parameter === 0): - log.error(`Configuration Error: ${parameterName} can't be zero`) + case (!allowZero && parameterSection[parameterName] === 0): + log.error(`Configuration Error: ${parameterSection}.${parameterName} can't be zero`) errors++ break default: @@ -144,25 +146,25 @@ function checkFloatValue (parameter, parameterName, minimumValue, maximumvalue, if (errors > 0) { // Errors were made if (allowRepair) { - log.error(` resolved by setting ${parameterName} to ${defaultValue}`) - // ToDo: fix this by making the function call a call by reference!!! - parameter = defaultValue + log.error(` resolved by setting ${parameterSection}.${parameterName} to ${defaultValue}`) + parameterSection[parameterName] = defaultValue } else { - log.error(`As ${parameterName} is a fatal parameter, I'm exiting`) + log.error(` as ${parameterSection}.${parameterName} is a fatal parameter, I'm exiting`) process.exit(9) } } } -function checkBinaryValue (parameter, parameterName, allowRepair, defaultValue) { +function checkBinaryValue (parameterSection, parameterName, allowRepair, defaultValue) { + // PLEASE NOTE: the parameterSection, parameterName seperation is needed to force a call by reference, which is needed for the repair action let errors = 0 switch (true) { - case (parameter === undefined): - log.error(`Configuration Error: ${parameterName} isn't defined`) + case (parameterSection[parameterName] === undefined): + log.error(`Configuration Error: ${parameterSection}.${parameterName} isn't defined`) errors++ break - case (!(parameter === true || parameter === false)): - log.error(`Configuration Error: ${parameterName} should be either false or true, encountered ${parameter}`) + case (!(parameterSection[parameterName] === true || parameterSection[parameterName] === false)): + log.error(`Configuration Error: ${parameterSection}.${parameterName} should be either false or true, encountered ${parameterSection[parameterName]}`) errors++ break default: @@ -171,25 +173,25 @@ function checkBinaryValue (parameter, parameterName, allowRepair, defaultValue) if (errors > 0) { // Errors were made if (allowRepair) { - log.error(` resolved by setting ${parameterName} to ${defaultValue}`) - // ToDo: fix this by making the function call a call by reference!!! - parameter = defaultValue + log.error(` resolved by setting ${parameterSection}.${parameterName} to ${defaultValue}`) + parameterSection[parameterName] = defaultValue } else { - log.error(`As ${parameterName} is a fatal parameter, I'm exiting`) + log.error(` as ${parameterSection}.${parameterName} is a fatal parameter, I'm exiting`) process.exit(9) } } } -function checkRangeValue (parameter, parameterName, range, allowRepair, defaultValue) { +function checkRangeValue (parameterSection, parameterName, range, allowRepair, defaultValue) { + // PLEASE NOTE: the parameterSection, parameterName seperation is needed to force a call by reference, which is needed for the repair action let errors = 0 switch (true) { - case (parameter === undefined): - log.error(`Configuration Error: ${parameterName} isn't defined`) + case (parameterSection[parameterName] === undefined): + log.error(`Configuration Error: ${parameterSection}.${parameterName} isn't defined`) errors++ break - case (!range.includes(parameter)): - log.error(`Configuration Error: ${parameterName} should be come from ${range}, encountered ${parameter}`) + case (!range.includes(parameterSection[parameterName])): + log.error(`Configuration Error: ${parameterSection}.${parameterName} should be come from ${range}, encountered ${parameterSection[parameterName]}`) errors++ break default: @@ -198,11 +200,10 @@ function checkRangeValue (parameter, parameterName, range, allowRepair, defaultV if (errors > 0) { // Errors were made if (allowRepair) { - log.error(` resolved by setting ${parameterName} to ${defaultValue}`) - // ToDo: fix this by making the function call a call by reference!!! - parameter = defaultValue + log.error(` resolved by setting ${parameterSection}.${parameterName} to ${defaultValue}`) + parameterSection[parameterName] = defaultValue } else { - log.error(`As ${parameterName} is a fatal parameter, I'm exiting`) + log.error(` as ${parameterSection}.${parameterName} is a fatal parameter, I'm exiting`) process.exit(9) } } From fcb3944848e2fb6ac15d999a0cc52cd5c1bf00f4 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Thu, 12 Jan 2023 12:28:27 +0100 Subject: [PATCH 014/231] Removed some artificial limits --- app/tools/ConfigManager.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index 3bc3a5b20e..afe90676a7 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -22,7 +22,7 @@ function checkConfig (configToCheck) { checkRangeValue(configToCheck.loglevel, 'RowingEngine', ['trace', 'debug', 'info', 'warn', 'error', 'silent'], true, 'error') checkIntegerValue(configToCheck, 'gpioPin', 1, 27, false, false, null) checkIntegerValue(configToCheck, 'gpioPriority', -7, 0, true, true, 0) - checkIntegerValue(configToCheck, 'gpioMinimumPulseLength', 1, 100000, false, true, 0) + checkIntegerValue(configToCheck, 'gpioMinimumPulseLength', 1, null, false, true, 0) checkIntegerValue(configToCheck, 'gpioPollingInterval', 1, 10, false, true, 10) checkRangeValue(configToCheck, 'gpioPollingInterval', [1, 2, 5, 10], true, 10) checkRangeValue(configToCheck, 'gpioTriggeredFlank', ['Up', 'Down', 'Both'], false, null) @@ -53,10 +53,10 @@ function checkConfig (configToCheck) { checkFloatValue(configToCheck.rowerSettings, 'sprocketRadius', 0, 20, false, true, 3) checkFloatValue(configToCheck.rowerSettings, 'minimumTimeBetweenImpulses', 0, 3, false, false, null) checkFloatValue(configToCheck.rowerSettings, 'maximumTimeBetweenImpulses', configToCheck.rowerSettings.minimumTimeBetweenImpulses, 3, false, false, null) - checkFloatValue(configToCheck.rowerSettings, 'smoothing', 1, 1000000, false, true, 1) - checkFloatValue(configToCheck.rowerSettings, 'dragFactor', 1, 1000000, false, false, null) + checkFloatValue(configToCheck.rowerSettings, 'smoothing', 1, null, false, true, 1) + checkFloatValue(configToCheck.rowerSettings, 'dragFactor', 1, null, false, false, null) checkBinaryValue(configToCheck.rowerSettings, 'autoAdjustDragFactor', true, false) - checkIntegerValue(configToCheck.rowerSettings, 'dragFactorSmoothing', 1, 1000, false, true, 1) + checkIntegerValue(configToCheck.rowerSettings, 'dragFactorSmoothing', 1, null, false, true, 1) if (configToCheck.rowerSettings.autoAdjustDragFactor) { checkFloatValue(configToCheck.rowerSettings, 'minimumDragQuality', 0, 1, true, true, 0) } From 94a2aad8405e67eee9c8d2de1d465c4e09ec5512 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 17:07:48 +0100 Subject: [PATCH 015/231] Added logic for more complex workouts Added logic for more complex workouts: RowingStatistics will handle this completely independently --- app/server.js | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/app/server.js b/app/server.js index 49f87a712a..a63af5c7c4 100644 --- a/app/server.js +++ b/app/server.js @@ -47,11 +47,27 @@ if (config.appPriority) { // Hopefully this will be filled through the WebGUI or through the BLE interface (PM5-BLE can do this...) // When set, ORM will terminate the session after reaching the target. If not set, it will behave as usual (a "Just row" session). // When set, the GUI will behave similar to a PM5 in that it counts down from the target to 0 -const session = { - targetDistance: 0, // Target distance in meters - targetTime: 0 // Target time in seconds +const intervalSettings = [] + +/* an example of the workout setting that RowingStatistics will obey: a 1 minute warmup, a 2K timed piece followed by a 1 minute cooldown +// This should normally come from the PM5 interface or the webinterface +intervalSettings[0] = { + targetDistance: 0, + targetTime: 60 +} + +/* Additional intervals for testing +intervalSettings[1] = { + targetDistance: 2000, + targetTime: 0 } +intervalSettings[2] = { + targetDistance: 0, + targetTime: 60 +} +*/ + log.info(`Session settings: distance limit: ${(session.targetDistance > 0 ? `${session.targetDistance} meters` : 'none')}, time limit: ${(session.targetTime > 0 ? `${session.targetTime} seconds` : 'none')}\n`) const peripheralManager = createPeripheralManager() @@ -120,7 +136,15 @@ function handleRotationImpulse (dataPoint) { rowingStatistics.handleRotationImpulse(dataPoint) } -const rowingStatistics = createRowingStatistics(config, session) +const rowingStatistics = createRowingStatistics(config) +if (intervalSettings.length > 0) { + // There is an interval defined at startup, let's inform RowingStatistics + // ToDo: update these settings when the PM5 or webinterface tells us to + rowingStatistics.setIntervalParameters(intervalSettings) +} else { + log.info('Starting a just row session, no time or distance target set') +} + const workoutRecorder = createWorkoutRecorder() const workoutUploader = createWorkoutUploader(workoutRecorder) @@ -153,11 +177,11 @@ rowingStatistics.on('rowingPaused', (metrics) => { }) rowingStatistics.on('intervalTargetReached', (metrics) => { - // This is called when the RowingStatistics conclude the target is reached - // This isn't the most optimal solution yet, as this interval is the only one set. A logcal extansion would be - // to provide a next intervaltarget. Thus, the use case of a next interval has to be implemented as well - // (i.e. setting a new interval target). For now, this interval is the one and only so we stop. - stopWorkout() + // This is called when the RowingStatistics conclude the intervaltarget is reached + // Update all screens to reflect this change, as targetTime and targetDistance have changed + // ToDo: recording this event in the recordings accordingly should be done as well + webServer.notifyClients('metrics', metrics) + peripheralManager.notifyMetrics('metricsUpdate', metrics) }) rowingStatistics.on('rowingStopped', (metrics) => { From 37edd0290b7bcf99d49e0702bb85c814f24169dd Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 17:29:58 +0100 Subject: [PATCH 016/231] Update to facilitate more complex workouts Update to facilitate more complex workouts. RowingStatistics will now manage the intervals completely independently and alert server.js only when the session has ended. --- app/engine/RowingStatistics.js | 101 +++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index de4d10c939..dc2b0e8cbf 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -13,7 +13,7 @@ import { createCurveAligner } from './utils/CurveAligner.js' import loglevel from 'loglevel' const log = loglevel.getLogger('RowingEngine') -function createRowingStatistics (config, session) { +function createRowingStatistics (config) { const numOfDataPointsForAveraging = config.numOfPhasesForAveragingScreenData const webUpdateInterval = config.webUpdateInterval const peripheralUpdateInterval = config.peripheralUpdateInterval @@ -26,6 +26,12 @@ function createRowingStatistics (config, session) { const cyclePower = createStreamFilter(numOfDataPointsForAveraging, 0) const cycleLinearVelocity = createStreamFilter(numOfDataPointsForAveraging, 0) let sessionStatus = 'WaitingForStart' + let intervalSettings = [] + let currentIntervalNumber = -1 + let intervalTargetDistance = 0 + let intervalTargetTime = 0 + let intervalPrevAccumulatedDistance = 0 + let intervalPrevAccumulatedTime = 0 let heartrateResetTimer let totalLinearDistance = 0.0 let totalMovingTime = 0 @@ -91,7 +97,7 @@ function createRowingStatistics (config, session) { updateContinousMetrics() updateCycleMetrics() handleRecoveryEnd() - emitMetrics('intervalTargetReached') + handleIntervalEnd() break case (sessionStatus === 'Rowing' && lastStrokeState === 'Recovery' && rower.strokeState() === 'Drive'): updateContinousMetrics() @@ -103,7 +109,7 @@ function createRowingStatistics (config, session) { updateContinousMetrics() updateCycleMetrics() handleDriveEnd() - emitMetrics('intervalTargetReached') + handleIntervalEnd() break case (sessionStatus === 'Rowing' && lastStrokeState === 'Drive' && rower.strokeState() === 'Recovery'): updateContinousMetrics() @@ -113,7 +119,7 @@ function createRowingStatistics (config, session) { break case (sessionStatus === 'Rowing' && intervalTargetReached()): updateContinousMetrics() - emitMetrics('intervalTargetReached') + handleIntervalEnd() break case (sessionStatus === 'Rowing'): updateContinousMetrics() @@ -181,6 +187,12 @@ function createRowingStatistics (config, session) { rower.allowMovement() totalMovingTime = 0 totalLinearDistance = 0.0 + intervalSettings = [] + currentIntervalNumber = -1 + intervalTargetDistance = 0 + intervalTargetTime = 0 + intervalPrevAccumulatedDistance = 0 + intervalPrevAccumulatedTime = 0 totalNumberOfStrokes = -1 driveLastStartTime = 0 distanceOverTime.reset() @@ -242,6 +254,63 @@ function createRowingStatistics (config, session) { calories.push(totalMovingTime, totalCalories) } + function setIntervalParameters (intervalParameters) { + intervalSettings = intervalParameters + currentIntervalNumber = -1 + if (intervalSettings.length > 0) { + log.info(`Workout recieved with ${intervalSettings.length} interval(s)`) + activateNextIntervalParameters() + } else { + // intervalParameters were empty, lets log this odd situation + log.error('Recieved workout containing no intervals') + } + } + + function intervalTargetReached () { + // This tests wether the end of the current interval is reached + if ((intervalTargetDistance > 0 && rower.totalLinearDistanceSinceStart() >= intervalTargetDistance) || (intervalTargetTime > 0 && rower.totalMovingTimeSinceStart() >= intervalTargetTime)) { + return true + } else { + return false + } + } + + function handleIntervalEnd () { + // initiated when the state machine has concluded the interval has ended + if (intervalSettings.length > 0 && intervalSettings.length > (currentIntervalNumber + 1)) { + // There is a next interval available + emitMetrics('intervalTargetReached') + activateNextIntervalParameters() + } else { + // There is no additional interval available + stopTraining () + } + } + + function activateNextIntervalParameters () { + if (intervalSettings.length > 0 && intervalSettings.length > (currentIntervalNumber + 1)) { + // This function sets the interval parameters in absolute distances/times + // Thus the interval target always is a projected "finishline" from the current position + intervalPrevAccumulatedTime = rower.totalMovingTimeSinceStart() + intervalPrevAccumulatedDistance = rower.totalLinearDistanceSinceStart() + + currentIntervalNumber++ + if (intervalSettings[currentIntervalNumber].targetDistance > 0) { + // A target distance is set + intervalTargetTime = 0 + intervalTargetDistance = intervalPrevAccumulatedDistance + intervalSettings[currentIntervalNumber].targetDistance + log.info(`Interval settings for interval ${currentIntervalNumber + 1} of ${intervalSettings.length}: Distance target ${intervalSettings[currentIntervalNumber].targetDistance} meters`) + } else { + // A target time is set + intervalTargetTime = intervalPrevAccumulatedTime + intervalSettings[currentIntervalNumber].targetTime + intervalTargetDistance = 0 + log.info(`Interval settings for interval ${currentIntervalNumber + 1} of ${intervalSettings.length}: time target ${secondsToTimeString(intervalSettings[currentIntervalNumber].targetTime)} minutes`) + } + } else { + log.error('Interval error: there is no next interval!') + } + } + // initiated when a new heart rate value is received from heart rate sensor function handleHeartrateMeasurement (value) { // set the heart rate to zero if we did not receive a value for some time @@ -254,14 +323,6 @@ function createRowingStatistics (config, session) { heartrateBatteryLevel = value.batteryLevel } - function intervalTargetReached () { - if ((session.targetDistance > 0 && rower.totalLinearDistanceSinceStart() >= session.targetDistance) || (session.targetTime > 0 && rower.totalMovingTimeSinceStart() >= session.targetTime)) { - return true - } else { - return false - } - } - function measureRecoveryHR () { // This function is called when the rowing session is stopped. postExerciseHR[0] is the last measured excercise HR // Thus postExerciseHR[1] is Recovery HR after 1 min, etc.. @@ -298,11 +359,13 @@ function createRowingStatistics (config, session) { sessionStatus, strokeState: rower.strokeState(), totalMovingTime: totalMovingTime > 0 ? totalMovingTime : 0, - driveLastStartTime: driveLastStartTime > 0 ? driveLastStartTime : 0, - totalMovingTimeFormatted: session.targetTime > 0 ? secondsToTimeString(Math.round(Math.max(session.targetTime - totalMovingTime, 0))) : secondsToTimeString(Math.round(totalMovingTime)), + totalMovingTimeFormatted: intervalTargetTime > 0 ? secondsToTimeString(Math.round(Math.max(intervalTargetTime - totalMovingTime, 0))) : secondsToTimeString(Math.round(totalMovingTime - intervalPrevAccumulatedTime)), totalNumberOfStrokes: totalNumberOfStrokes > 0 ? totalNumberOfStrokes : 0, totalLinearDistance: totalLinearDistance > 0 ? totalLinearDistance : 0, // meters - totalLinearDistanceFormatted: session.targetDistance > 0 ? Math.max(session.targetDistance - totalLinearDistance, 0) : totalLinearDistance, + totalLinearDistanceFormatted: intervalTargetDistance > 0 ? Math.max(intervalTargetDistance - totalLinearDistance, 0) : totalLinearDistance - intervalPrevAccumulatedDistance, + intervalNumber: Math.max(currentIntervalNumber + 1, 0), // Interval number + intervalMovingTime: totalMovingTime - intervalPrevAccumulatedTime, + intervalLinearDistance: totalLinearDistance - intervalPrevAccumulatedDistance, strokeCalories: strokeCalories > 0 ? strokeCalories : 0, // kCal strokeWork: strokeWork > 0 ? strokeWork : 0, // Joules totalCalories: calories.yAtSeriesEnd() > 0 ? calories.yAtSeriesEnd() : 0, // kcal @@ -312,11 +375,12 @@ function createRowingStatistics (config, session) { cycleStrokeRate: cycleDuration.clean() > minimumStrokeTime && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? (60.0 / cycleDuration.clean()) : 0, // strokeRate in SPM cycleDistance: cycleDistance.raw() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cycleDistance.clean() : 0, // meters cycleLinearVelocity: cycleLinearVelocity.clean() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cycleLinearVelocity.clean() : 0, // m/s - cyclePace: cycleLinearVelocity.raw() > 0 ? cyclePace : Infinity, // seconds/500m + cyclePace: cycleLinearVelocity.raw() > 0 ? cyclePace : Infinity, // seconds/50 0m cyclePaceFormatted: cycleLinearVelocity.raw() > 0 ? secondsToTimeString(Math.round(cyclePace)) : Infinity, cyclePower: cyclePower.clean() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cyclePower.clean() : 0, // watts - cycleProjectedEndTime: session.targetDistance > 0 ? distanceOverTime.projectY(session.targetDistance) : session.targetTime, - cycleProjectedEndLinearDistance: session.targetTime > 0 ? distanceOverTime.projectX(session.targetTime) : session.targetDistance, + cycleProjectedEndTime: intervalTargetDistance > 0 ? distanceOverTime.projectY(intervalTargetDistance) : intervalTargetTime, + cycleProjectedEndLinearDistance: intervalTargetTime > 0 ? distanceOverTime.projectX(intervalTargetTime) : intervalTargetDistance, + driveLastStartTime: driveLastStartTime > 0 ? driveLastStartTime : 0, driveDuration: driveDuration.clean() >= config.rowerSettings.minimumDriveTime && totalNumberOfStrokes > 0 && sessionStatus === 'Rowing' ? driveDuration.clean() : NaN, // seconds driveLength: driveLength.clean() > 0 && sessionStatus === 'Rowing' ? driveLength.clean() : NaN, // meters of chain movement driveDistance: driveDistance.clean() >= 0 && sessionStatus === 'Rowing' ? driveDistance.clean() : NaN, // meters @@ -355,6 +419,7 @@ function createRowingStatistics (config, session) { return Object.assign(emitter, { handleHeartrateMeasurement, handleRotationImpulse, + setIntervalParameters, pause: pauseTraining, stop: stopTraining, resume: allowResumeTraining, From 1405b80bc7c008012323c4d3a0bfa8549c7869e7 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 17:31:28 +0100 Subject: [PATCH 017/231] Removal of surplus logging line --- app/server.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/server.js b/app/server.js index a63af5c7c4..ecdd20a75c 100644 --- a/app/server.js +++ b/app/server.js @@ -68,8 +68,6 @@ intervalSettings[2] = { } */ -log.info(`Session settings: distance limit: ${(session.targetDistance > 0 ? `${session.targetDistance} meters` : 'none')}, time limit: ${(session.targetTime > 0 ? `${session.targetTime} seconds` : 'none')}\n`) - const peripheralManager = createPeripheralManager() peripheralManager.on('control', (event) => { From e5696378b002621100625d78df48e729c3dcfa2e Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 17:34:25 +0100 Subject: [PATCH 018/231] Fixed a Lint error --- app/engine/RowingStatistics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index dc2b0e8cbf..616b6cbb8f 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -283,7 +283,7 @@ function createRowingStatistics (config) { activateNextIntervalParameters() } else { // There is no additional interval available - stopTraining () + stopTraining() } } @@ -355,7 +355,7 @@ function createRowingStatistics (config) { function getMetrics () { const cyclePace = cycleLinearVelocity.clean() !== 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? (500.0 / cycleLinearVelocity.clean()) : Infinity return { - sessiontype: session.targetDistance > 0 ? 'Distance' : (session.targetTime > 0 ? 'Time' : 'JustRow'), + sessiontype: intervalTargetDistance > 0 ? 'Distance' : (intervalTargetTime > 0 ? 'Time' : 'JustRow'), sessionStatus, strokeState: rower.strokeState(), totalMovingTime: totalMovingTime > 0 ? totalMovingTime : 0, From 758f2d25ea67a42b55b13e7d3993d1dd9a34e06a Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 17:57:13 +0100 Subject: [PATCH 019/231] Added Intervals.icu compatibility Added Intervals.icu to the list of compatible users of tcx files. --- docs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6563cf8a79..069399301f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,7 +49,7 @@ Open Rowing Monitor also implements different Bluetooth Low Energy (BLE) protoco * **Concept2 PM**: Open Rowing Monitor implements part of the Concept2 PM Bluetooth Smart Communication Interface Definition. This is still work in progress and only implements the most common parts of the spec, so it is not guaranteed to work with all applications that support C2 rowing machines. Our interface currently can only report metrics, but can't recieve commands and session parameters from the app yet. It is known to work with [EXR](https://www.exrgame.com) and all the samples from [The Erg Arcade](https://ergarcade.com), for example you can [row in the clouds](https://ergarcade.github.io/mrdoob-clouds/). -* **FTMS Rower**: This is the FTMS profile for rowing machines and supports all rowing specific metrics (such as stroke rate). So far not many training applications for this profile exist, but the market is evolving. We've successfully tested it with [EXR](https://www.exrgame.com), [MyHomeFit](https://myhomefit.de) and [Kinomap](https://www.kinomap.com). +* **FTMS Rower**: This is the FTMS profile for rowing machines and supports all rowing specific metrics (such as stroke rate). So far not many training applications for this profile exist, but the market is evolving. We've successfully tested it with [EXR](https://www.exrgame.com) (preferred method), [MyHomeFit](https://myhomefit.de) and [Kinomap](https://www.kinomap.com). * **FTMS Indoor Bike**: This FTMS profile is used by Smart Bike Trainers and widely adopted by training applications for bike training. It does not support rowing specific metrics. But it can present metrics such as power and distance to the biking application and use cadence for stroke rate. So why not use your virtual rowing bike to row up a mountain in [Zwift](https://www.zwift.com), [Bkool](https://www.bkool.com), [The Sufferfest](https://thesufferfest.com) or similar :-) @@ -61,7 +61,7 @@ Open Rowing Monitor also implements different Bluetooth Low Energy (BLE) protoco Open Rowing Monitor is based on the idea that metrics should be easily accessible for further analysis. Therefore, Open Rowing Monitor can create the following files: -* **Training Center XML files (TCX)**: These are XML-files that contain the most essential metrics of a rowing session. Most training analysis tools will accept a tcx-file. You can upload these files to training platforms like [Strava](https://www.strava.com), [Garmin Connect](https://connect.garmin.com) or [Trainingpeaks](https://trainingpeaks.com) to track your training sessions; +* **Training Center XML files (TCX)**: These are XML-files that contain the most essential metrics of a rowing session. Most training analysis tools will accept a tcx-file. You can upload these files to training platforms like [Strava](https://www.strava.com), [Garmin Connect](https://connect.garmin.com), [Intervals.icu](https://intervals.icu/), [RowsAndAll](https://rowsandall.com/) or [Trainingpeaks](https://trainingpeaks.com) to track your training sessions; * **RowingData** files, which are comma-seperated files with all metrics Open Rowing Monitor can produce. These can be used with [RowingData](https://pypi.org/project/rowingdata/) to display your results locally, or uploaded to [RowsAndAll](https://rowsandall.com/) for a webbased analysis (including dynamic in-stroke metrics). The csv-files can also be processed manually in Excel, allowing your own custom analysis. Please note that for visualising in-stroke metrics in [RowsAndAll](https://rowsandall.com/) (i.e. force, power and handle speed curves), you need their yearly subscription; From f628a6e7914ac15fa8d32bb261f6f7579ae127cb Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 21:18:43 +0100 Subject: [PATCH 020/231] Updates to fix review comments Updates to fix review comments --- app/tools/ConfigManager.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index afe90676a7..afb2b3f541 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -28,16 +28,16 @@ function checkConfig (configToCheck) { checkRangeValue(configToCheck, 'gpioTriggeredFlank', ['Up', 'Down', 'Both'], false, null) checkIntegerValue(configToCheck, 'appPriority', configToCheck.gpioPriority, 0, true, true, 0) checkIntegerValue(configToCheck, 'webUpdateInterval', 80, 1000, false, true, 1000) - checkBinaryValue(configToCheck, 'heartrateMonitorBLE', true, true) - checkBinaryValue(configToCheck, 'heartrateMonitorANT', true, false) + checkBooleanValue(configToCheck, 'heartrateMonitorBLE', true, true) + checkBooleanValue(configToCheck, 'heartrateMonitorANT', true, false) checkRangeValue(configToCheck, 'bluetoothMode', ['off', 'PM5', 'FTMS', 'FTMSBIKE', 'CPS', 'CSC'], true, 'FTMS') checkIntegerValue(configToCheck, 'peripheralUpdateInterval', 80, 1000, false, true, 1000) checkIntegerValue(configToCheck, 'numOfPhasesForAveragingScreenData', 2, null, false, true, 4) - checkBinaryValue(configToCheck, 'createRowingDataFiles', true, true) - checkBinaryValue(configToCheck, 'createRawDataFiles', true, true) - checkBinaryValue(configToCheck, 'gzipRawDataFiles', true, false) - checkBinaryValue(configToCheck, 'createTcxFiles', true, true) - checkBinaryValue(configToCheck, 'gzipTcxFiles', true, false) + checkBooleanValue(configToCheck, 'createRowingDataFiles', true, true) + checkBooleanValue(configToCheck, 'createRawDataFiles', true, true) + checkBooleanValue(configToCheck, 'gzipRawDataFiles', true, false) + checkBooleanValue(configToCheck, 'createTcxFiles', true, true) + checkBooleanValue(configToCheck, 'gzipTcxFiles', true, false) checkFloatValue(configToCheck.userSettings, 'restingHR', 30, 220, false, true, 40) checkFloatValue(configToCheck.userSettings, 'maxHR', configToCheck.userSettings.restingHR, 220, false, true, 220) if (configToCheck.createTcxFiles) { @@ -46,7 +46,7 @@ function checkConfig (configToCheck) { checkFloatValue(configToCheck.userSettings, 'distanceCorrectionFactor', 0, 50, false, true, 5) checkFloatValue(configToCheck.userSettings, 'weight', 25, 500, false, true, 80) checkRangeValue(configToCheck.userSettings, 'sex', ['male', 'female'], true, 'male') - checkBinaryValue(configToCheck.userSettings, 'highlyTrained', true, false) + checkBooleanValue(configToCheck.userSettings, 'highlyTrained', true, false) } checkIntegerValue(configToCheck.rowerSettings, 'numOfImpulsesPerRevolution', 1, null, false, false, null) checkIntegerValue(configToCheck.rowerSettings, 'flankLength', 3, null, false, false, null) @@ -55,7 +55,7 @@ function checkConfig (configToCheck) { checkFloatValue(configToCheck.rowerSettings, 'maximumTimeBetweenImpulses', configToCheck.rowerSettings.minimumTimeBetweenImpulses, 3, false, false, null) checkFloatValue(configToCheck.rowerSettings, 'smoothing', 1, null, false, true, 1) checkFloatValue(configToCheck.rowerSettings, 'dragFactor', 1, null, false, false, null) - checkBinaryValue(configToCheck.rowerSettings, 'autoAdjustDragFactor', true, false) + checkBooleanValue(configToCheck.rowerSettings, 'autoAdjustDragFactor', true, false) checkIntegerValue(configToCheck.rowerSettings, 'dragFactorSmoothing', 1, null, false, true, 1) if (configToCheck.rowerSettings.autoAdjustDragFactor) { checkFloatValue(configToCheck.rowerSettings, 'minimumDragQuality', 0, 1, true, true, 0) @@ -64,7 +64,7 @@ function checkConfig (configToCheck) { checkFloatValue(configToCheck.rowerSettings, 'minumumForceBeforeStroke', 0, 500, true, true, 0) checkFloatValue(configToCheck.rowerSettings, 'minumumRecoverySlope', 0, null, true, true, 0) checkFloatValue(configToCheck.rowerSettings, 'minimumStrokeQuality', 0, 1, true, true, 0) - checkBinaryValue(configToCheck.rowerSettings, 'autoAdjustRecoverySlope', true, false) + checkBooleanValue(configToCheck.rowerSettings, 'autoAdjustRecoverySlope', true, false) if (!configToCheck.rowerSettings.autoAdjustDragFactor && configToCheck.rowerSettings.autoAdjustRecoverySlope) { log.error('Configuration Error: rowerSettings.autoAdjustRecoverySlope can not be true when rowerSettings.autoAdjustDragFactor is false, ignoring request') } @@ -124,7 +124,7 @@ function checkFloatValue (parameterSection, parameterName, minimumValue, maximum log.error(`Configuration Error: ${parameterSection}.${parameterName} isn't defined`) errors++ break - case (!typeof (parameterSection[parameterName]) === 'number'): + case (!(typeof (parameterSection[parameterName]) === 'number')): log.error(`Configuration Error: ${parameterSection}.${parameterName} should be a numerical value, encountered ${parameterSection[parameterName]}`) errors++ break @@ -155,7 +155,7 @@ function checkFloatValue (parameterSection, parameterName, minimumValue, maximum } } -function checkBinaryValue (parameterSection, parameterName, allowRepair, defaultValue) { +function checkBooleanValue (parameterSection, parameterName, allowRepair, defaultValue) { // PLEASE NOTE: the parameterSection, parameterName seperation is needed to force a call by reference, which is needed for the repair action let errors = 0 switch (true) { From 75e2455b10154dbbd871f8c795db81d4360413a1 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 21:37:25 +0100 Subject: [PATCH 021/231] Added workout management to RowingStatistics Added workout management to the role of RowingStatistics --- docs/Architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index f8dd5c0e7d..0187737d0d 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -95,7 +95,7 @@ stateDiagram-v2 Stopped --> [*] ``` -Please note: the 'Stopped' state isn't directly part of the state machine that is defined in `handleRotationImpulse`, it is a direct consequence of emitting the `intervalTargetReached` message to `Server.js`, where `Server.js` concludes there is no next interval left, and thus `stopTraining()` has to be called (which does set the sessionState to 'Stopped'). This is needed as RowingStatistics shouldn't be aware about the existence of next intervals, as it only deals with the current interval. +Please note: the 'Stopped' state in the `handleRotationImpulse`'s state machine is a direct consequence of either `rower.js` indicating the flywheel is in a freespin or reaching the end of an interval without any subsequent interval being present in the workout. The later is tested in the function ` handleIntervalEnd()`, which determines this. Although this distinction could be handled by the state machine as part of `handleRotationImpulse`, we felt it would make the state machine less readable as it would add another three new states. #### metrics maintained in RowingStatistics.js @@ -107,7 +107,7 @@ In a nutshell: * `RowingStatistics.js` maintains the session state, thus determines whether the rowing machine is 'Rowing', or 'WaitingForDrive', etc., * `RowingStatistics.js` applies a moving median filter across strokes to make metrics less volatile and thus better suited for presentation, * `RowingStatistics.js` calculates derived metrics (like Calories) and trands (like Calories per hour), -* `RowingStatistics.js` gaurds interval and session boundaries, and will chop up the metrics-stream accordingly, where Rower.js will just move on without looking at these artifical boundaries. +* `RowingStatistics.js` maintains the workout intervals, guards interval and session boundaries, and will chop up the metrics-stream accordingly, where `Rower.js` will just move on without looking at these artifical boundaries. In total, this takes full control of the displayed metrics in a specific interval. From d67745c5df8afa20e67674f050425d327f6285c1 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 21:45:25 +0100 Subject: [PATCH 022/231] Fixed Line error Fixed Line error --- docs/Architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 0187737d0d..7858654840 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -95,7 +95,7 @@ stateDiagram-v2 Stopped --> [*] ``` -Please note: the 'Stopped' state in the `handleRotationImpulse`'s state machine is a direct consequence of either `rower.js` indicating the flywheel is in a freespin or reaching the end of an interval without any subsequent interval being present in the workout. The later is tested in the function ` handleIntervalEnd()`, which determines this. Although this distinction could be handled by the state machine as part of `handleRotationImpulse`, we felt it would make the state machine less readable as it would add another three new states. +Please note: the 'Stopped' state in the `handleRotationImpulse`'s state machine is a direct consequence of either `rower.js` indicating the flywheel is in a freespin or reaching the end of an interval without any subsequent interval being present in the workout. The later is tested in the function `handleIntervalEnd()`, which determines this. Although this distinction could be handled by the state machine as part of `handleRotationImpulse`, we felt it would make the state machine less readable as it would add another three new states. #### metrics maintained in RowingStatistics.js From 7251a8144d8e8ed6f7a3d450a8971d90a3d1493b Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 21:52:55 +0100 Subject: [PATCH 023/231] Douncheck with 0.8.4 settings Checked that the sanity check would fix all mew variables, allowing a 0.8.4 config file to be used with V1Beta without any crashes or issues. --- app/tools/ConfigManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index afb2b3f541..225e4b9a2c 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -25,7 +25,7 @@ function checkConfig (configToCheck) { checkIntegerValue(configToCheck, 'gpioMinimumPulseLength', 1, null, false, true, 0) checkIntegerValue(configToCheck, 'gpioPollingInterval', 1, 10, false, true, 10) checkRangeValue(configToCheck, 'gpioPollingInterval', [1, 2, 5, 10], true, 10) - checkRangeValue(configToCheck, 'gpioTriggeredFlank', ['Up', 'Down', 'Both'], false, null) + checkRangeValue(configToCheck, 'gpioTriggeredFlank', ['Up', 'Down', 'Both'], true, 'Up') checkIntegerValue(configToCheck, 'appPriority', configToCheck.gpioPriority, 0, true, true, 0) checkIntegerValue(configToCheck, 'webUpdateInterval', 80, 1000, false, true, 1000) checkBooleanValue(configToCheck, 'heartrateMonitorBLE', true, true) From f1e7d086dfc236c9284c4286877928c455bbca0f Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 22:09:23 +0100 Subject: [PATCH 024/231] Added restart limit to prevent bootloop Added a restart limit to the service definition to prevent an infinite bootloop --- install/webbrowserkiosk.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install/webbrowserkiosk.service b/install/webbrowserkiosk.service index de6f2a4e48..519ac55531 100644 --- a/install/webbrowserkiosk.service +++ b/install/webbrowserkiosk.service @@ -1,6 +1,8 @@ [Unit] Description=X11 Web Browser Kiosk After=multi-user.target +StartLimitIntervalSec=60 +StartLimitBurst=5 [Service] Type=simple From 675c85cb594b10857336e19f6966776ed2f40f7c Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 15 Jan 2023 22:09:27 +0100 Subject: [PATCH 025/231] Added restart limit to prevent bootloop Added a restart limit to the service definition to prevent an infinite bootloop --- install/openrowingmonitor.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install/openrowingmonitor.service b/install/openrowingmonitor.service index 71ece090c4..3e42478381 100644 --- a/install/openrowingmonitor.service +++ b/install/openrowingmonitor.service @@ -1,6 +1,8 @@ [Unit] Description=Open Rowing Monitor After=multi-user.target +StartLimitIntervalSec=60 +StartLimitBurst=5 [Service] Type=simple From 36953315bde771bd7c1398fe1e39ac08435807c5 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:18:39 +0100 Subject: [PATCH 026/231] Move functionality to the Finite State Machine On hindsight, the approach of letting the FSM handle all state transitions explicitly is much clearer --- docs/Architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Architecture.md b/docs/Architecture.md index 7858654840..64fc2ed2a8 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -95,7 +95,7 @@ stateDiagram-v2 Stopped --> [*] ``` -Please note: the 'Stopped' state in the `handleRotationImpulse`'s state machine is a direct consequence of either `rower.js` indicating the flywheel is in a freespin or reaching the end of an interval without any subsequent interval being present in the workout. The later is tested in the function `handleIntervalEnd()`, which determines this. Although this distinction could be handled by the state machine as part of `handleRotationImpulse`, we felt it would make the state machine less readable as it would add another three new states. +Please note: `handleRotationImpulse` implements all these state transitions, where the state transitions for the end of an interval and the end of a session are handled individually as the metrics updates differ slightly. #### metrics maintained in RowingStatistics.js From 6eed136caf59119cbbaf3ac6a03d9f81a5067047 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:18:46 +0100 Subject: [PATCH 027/231] Move functionality to the Finite State Machine On hindsight, the approach of letting the FSM handle all state transitions explicitly is much clearer --- app/engine/RowingStatistics.js | 46 +++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index 616b6cbb8f..3ae9d34346 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -93,11 +93,18 @@ function createRowingStatistics (config) { sessionStatus = 'Paused' pauseTraining() break - case (sessionStatus === 'Rowing' && lastStrokeState === 'Recovery' && rower.strokeState() === 'Drive' && intervalTargetReached()): + case (sessionStatus === 'Rowing' && lastStrokeState === 'Recovery' && rower.strokeState() === 'Drive' && isIntervalTargetReached() && isNextIntervalAvailable()): updateContinousMetrics() updateCycleMetrics() handleRecoveryEnd() - handleIntervalEnd() + activateNextIntervalParameters() + emitMetrics('intervalTargetReached') + break + case (sessionStatus === 'Rowing' && lastStrokeState === 'Recovery' && rower.strokeState() === 'Drive' && isIntervalTargetReached()): + updateContinousMetrics() + updateCycleMetrics() + handleRecoveryEnd() + stopTraining() break case (sessionStatus === 'Rowing' && lastStrokeState === 'Recovery' && rower.strokeState() === 'Drive'): updateContinousMetrics() @@ -105,11 +112,18 @@ function createRowingStatistics (config) { handleRecoveryEnd() emitMetrics('recoveryFinished') break - case (sessionStatus === 'Rowing' && lastStrokeState === 'Drive' && rower.strokeState() === 'Recovery' && intervalTargetReached()): + case (sessionStatus === 'Rowing' && lastStrokeState === 'Drive' && rower.strokeState() === 'Recovery' && isIntervalTargetReached() && isNextIntervalAvailable()): updateContinousMetrics() updateCycleMetrics() handleDriveEnd() - handleIntervalEnd() + activateNextIntervalParameters() + emitMetrics('intervalTargetReached') + break + case (sessionStatus === 'Rowing' && lastStrokeState === 'Drive' && rower.strokeState() === 'Recovery' && isIntervalTargetReached()): + updateContinousMetrics() + updateCycleMetrics() + handleDriveEnd() + stopTraining() break case (sessionStatus === 'Rowing' && lastStrokeState === 'Drive' && rower.strokeState() === 'Recovery'): updateContinousMetrics() @@ -117,9 +131,14 @@ function createRowingStatistics (config) { handleDriveEnd() emitMetrics('driveFinished') break - case (sessionStatus === 'Rowing' && intervalTargetReached()): + case (sessionStatus === 'Rowing' && isIntervalTargetReached() && isNextIntervalAvailable()): + updateContinousMetrics() + activateNextIntervalParameters() + emitMetrics('intervalTargetReached') + break + case (sessionStatus === 'Rowing' && isIntervalTargetReached()): updateContinousMetrics() - handleIntervalEnd() + stopTraining() break case (sessionStatus === 'Rowing'): updateContinousMetrics() @@ -266,7 +285,7 @@ function createRowingStatistics (config) { } } - function intervalTargetReached () { + function isIntervalTargetReached () { // This tests wether the end of the current interval is reached if ((intervalTargetDistance > 0 && rower.totalLinearDistanceSinceStart() >= intervalTargetDistance) || (intervalTargetTime > 0 && rower.totalMovingTimeSinceStart() >= intervalTargetTime)) { return true @@ -275,15 +294,12 @@ function createRowingStatistics (config) { } } - function handleIntervalEnd () { - // initiated when the state machine has concluded the interval has ended - if (intervalSettings.length > 0 && intervalSettings.length > (currentIntervalNumber + 1)) { - // There is a next interval available - emitMetrics('intervalTargetReached') - activateNextIntervalParameters() + function isNextIntervalAvailable () { + // This function tests whether there is a next interval available + if (currentIntervalNumber > -1 && intervalSettings.length > 0 && intervalSettings.length > (currentIntervalNumber + 1)) { + return true } else { - // There is no additional interval available - stopTraining() + return false } } From 3e2f95420520fd93631a14f8e89aa11bbe70f109 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:18:57 +0100 Subject: [PATCH 028/231] Added paragraph about handle-based sensors Added a paragraph about handle-based sensors. --- docs/rower_settings.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/rower_settings.md b/docs/rower_settings.md index 726313280c..759bac466c 100644 --- a/docs/rower_settings.md +++ b/docs/rower_settings.md @@ -64,6 +64,14 @@ Please note that the process identification numbers will differ. ## Making sure the hardware is connected correctly and works as intended +### Checking the physical properties of the rower + +One thing to check is what the original sensor actually measures. You can physically look in the rower, but most manuals also include an exploded view of all parts in the machine. There you need to look at the placement of the sensor and the magnets. Most air-rowers measure the flywheel speed, but most water-rowers measure the handle speed and direction. Open Rowing Monitor is best suited for handling a spinning flywheel or water impellor, or anything directly attached to that. If your machine measures the impellor or flywheel directly, please note the number of magnets per rotation, as you need that parameter later on. So when you encounter a handle-connected machine and it is possible and within your comfort zone, try to add sensors to the flywheel or impellor. + +If you are uncomfortable modifying you machine, you can still make OpenRowingMonitor work, but with a loss of data quality. Where a flywheel or impellor can give information about the force and speeds created, the alternative can not. So you end up with a fixed distance per stroke, but you can connect to tools like EXR and the like. By setting *autoAdjustDragFactor* to false, *autoAdjustRecoverySlope* to false, *minumumRecoverySlope* to 0, *minimumStrokeQuality* to 0.01 and other parameters like dragFactor to a realistic well-choosen value (to make the metrics look plausible), OpenRowingMonitor will essentially calculate distance based on impulses encountered. Although not ideal for metrics, this can result in a working solution. Please note that the distance per stroke is essentially fixed, so many more advanced metrics are not relevant and stroke detection might be a bit vulnerable. + +### Checking the electrical properties of the rower + Before you physically connect anything to anything else, **check the electric properties of the rower** you are connecting to. Skipping this might destroy your Raspberry Pi as some rowers are known to exceed the Raspberry Pi electrical properties. For example, a Concept 2 RowErg provides 15V signals to the monitor, which will destroy the GPIO-ports. Other rowers provide signals aren't directly detectable by the raspberry Pi. For example, the Concept 2 Model C provides 0.2V pulses, thus staying below the detectable 1.8V treshold that the Raspberry Pi uses. Using a scope or a voltmeter is highly recommended. Please observe that the maximum input a Raspberry Pi GPIO pin can handle is 3.3V and 0.5A, and it will switch at 1.8V (see [this overview of the Raspberry Pi electrical properties](https://raspberrypi.stackexchange.com/questions/3209/what-are-the-min-max-voltage-current-values-the-gpio-pins-can-handle)). In our [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) there are some people who are brilliant with electrical connections, so don't be affraid to ask for help there. When you have a working solution, please report it so that we can include it in the documentation, allowing us to help others. Next, when the electric connection has been made, we need to look if the data is recieved well and has sufficient quality to be used. You can change `config/config.js` by From f7aae7b6e058ddc87814d6b0cb1142ed4d27d966 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:21:03 +0100 Subject: [PATCH 029/231] Added reason to modify hardware setup Added reason to modify hardware setup in documentation --- docs/rower_settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rower_settings.md b/docs/rower_settings.md index 759bac466c..75ad3fe8b0 100644 --- a/docs/rower_settings.md +++ b/docs/rower_settings.md @@ -66,7 +66,7 @@ Please note that the process identification numbers will differ. ### Checking the physical properties of the rower -One thing to check is what the original sensor actually measures. You can physically look in the rower, but most manuals also include an exploded view of all parts in the machine. There you need to look at the placement of the sensor and the magnets. Most air-rowers measure the flywheel speed, but most water-rowers measure the handle speed and direction. Open Rowing Monitor is best suited for handling a spinning flywheel or water impellor, or anything directly attached to that. If your machine measures the impellor or flywheel directly, please note the number of magnets per rotation, as you need that parameter later on. So when you encounter a handle-connected machine and it is possible and within your comfort zone, try to add sensors to the flywheel or impellor. +One thing to check is what the original sensor actually measures. You can physically look in the rower, but most manuals also include an exploded view of all parts in the machine. There you need to look at the placement of the sensor and the magnets. Most air-rowers measure the flywheel speed, but most water-rowers measure the handle speed and direction. Open Rowing Monitor is best suited for handling a spinning flywheel or water impellor, or anything directly attached to that. If your machine measures the impellor or flywheel directly, please note the number of magnets per rotation, as you need that parameter later on. So when you encounter a handle-connected machine and it is possible and within your comfort zone, try to add sensors to the flywheel or impellor as it results in much better metrics. If you are uncomfortable modifying you machine, you can still make OpenRowingMonitor work, but with a loss of data quality. Where a flywheel or impellor can give information about the force and speeds created, the alternative can not. So you end up with a fixed distance per stroke, but you can connect to tools like EXR and the like. By setting *autoAdjustDragFactor* to false, *autoAdjustRecoverySlope* to false, *minumumRecoverySlope* to 0, *minimumStrokeQuality* to 0.01 and other parameters like dragFactor to a realistic well-choosen value (to make the metrics look plausible), OpenRowingMonitor will essentially calculate distance based on impulses encountered. Although not ideal for metrics, this can result in a working solution. Please note that the distance per stroke is essentially fixed, so many more advanced metrics are not relevant and stroke detection might be a bit vulnerable. From b4739536d21f8c8666133313cd8f4fbe9390e853 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:14:47 +0100 Subject: [PATCH 030/231] Added link to software setup Added link to software setup --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 3482627e2c..5ce1116be6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -83,7 +83,7 @@ How to connect this to your rowing machine is specific to your device. You need * [Concept 2 RowErg](hardware_setup_Concept2_RowErg.md) * [Sportstech WRX700](hardware_setup_WRX700.md) -If your machine isn't listed, you can still follow this generic manual. +If your machine isn't listed, you can still follow this generic manual for hardware setup, and [adjust the software settings following the settings adjustment guide](rower_settings.md). ![Connecting the reed sensor](img/raspberrypi_reedsensor_wiring.jpg) *Connecting the reed sensor* From 2e50a48e6170b65497f2512cd5de3b6b7f9e37fa Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 17 Jan 2023 11:48:28 +0100 Subject: [PATCH 031/231] Added a list of known machines --- docs/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/README.md b/docs/README.md index 069399301f..f74c3a591e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,6 +69,23 @@ Open Rowing Monitor is based on the idea that metrics should be easily accessibl Uploading your sessions to Strava is an integrated feature, for all other platforms this is currently a manual step. Uploading to [RowsAndAll](https://rowsandall.com/) can be automated through their e-mail interface, see [this description](https://rowsandall.com/rowers/developers/). The Open rowing Monito installer can also set up a network share that contains all training data so it is easy to grab the files from there and manually upload them to the training platform of your choice. +## Supported machines + +Several machines are known to work with Open Rowing Monitor: + +* Abilica Winrower 2.0: Air rower that is known to work, see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/48); +* [Concept 2 Model D, Model E and RowErg](hardware_setup_Concept2_RowErg.md): fully supported Air rower, dynamic drag calculation and all metrics available including force curves +* Decathlon Rower120: known to work by adding magnets to the flywheel, currently basic metrics only, see [this discussion](https://github.com/laberning/openrowingmonitor/issues/110); +* DKN R-320 Air Rower: fully supported Air rower, static drag calculation, basic metrics only; +* FDF Neon Pro V rower: known to work, see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/87); +* ForceUSA R3: supported Air rower, dynamic drag calculation and all metrics available including force curves; +* NordickTrack RX800: fully supported hybrid Magnetic/Air rower, dynamic drag calculation and all metrics available including force curves; +* Sportplus MR-SP-08: work in progress, see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/95); +* [Sportstech WRX700](hardware_setup_WRX700.md): fully supported water rower, static drag calculation, all metrics available including force curves; +* [OpenErgo machines](https://openergo.webs.com/): several machines have been made to work, see [example 1](https://github.com/laberning/openrowingmonitor/discussions/80), [example 2](https://github.com/laberning/openrowingmonitor/discussions/105) and [example 3](https://github.com/laberning/openrowingmonitor/discussions/115) + +If your machine isn't listed, it just means that you need to [adjust the software settings following the settings adjustment guide](rower_settings.md). Don't worry, in the [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) there always are friendly people to help you set up your machine and the settings. + ## Installation You will need a Raspberry Pi Zero W, Raspberry Pi Zero 2 W, Raspberry Pi 3 or a Raspberry Pi 4 with a fresh installation of Raspberry Pi OS Lite for this (the 64Bit kernel is preferred). Connect to the device with SSH and initiate the following command to install Open Rowing Monitor as an automatically starting system service: From 89e534110ea3892db24480a0fb802b3b8f7aae82 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 17 Jan 2023 23:37:56 +0100 Subject: [PATCH 032/231] Added a link to supported rowers --- docs/README.md | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/docs/README.md b/docs/README.md index f74c3a591e..ec1b9aea35 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ Open Rowing Monitor is a free and open source performance monitor for rowing mac It is a Node.js application that runs on a Raspberry Pi and measures the rotation of the rower's flywheel (or similar) to calculate rowing specific metrics, such as power, split time, speed, stroke rate, distance and calories. It can share these metrics for controling games and record these metrics for further analysis. -It is currently developed and tested with a Sportstech WRX700 water-rower and a Concept2 air-rower. In the past, it was also tested extensively on a NordicTrack RX-800 hybrid air/magnetic rower. But it should run fine with any rowing machine that uses some kind of damping mechanism, as long as you can add something to measure the speed of the flywheel. It has shown to work well with DIY rowing machines like the [Openergo](https://openergo.webs.com), providing the construction is decent. +Open Rowing Monitor should run fine with any rowing machine that uses some kind of damping mechanism, as long as you can add something to measure the speed of the flywheel. It has shown to work well with DIY rowing machines like the [Openergo](https://openergo.webs.com), providing the construction is decent. For a full list of supported rowing machines, you can visit the list of [supported rowers](Supported_Rowers.md). If your machine isn't listed, it just means that you need to [adjust the software settings following the settings adjustment guide](rower_settings.md) yourself. But don't worry, in the [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) there always are friendly people to help you set up your machine and the settings. ## Features @@ -69,23 +69,6 @@ Open Rowing Monitor is based on the idea that metrics should be easily accessibl Uploading your sessions to Strava is an integrated feature, for all other platforms this is currently a manual step. Uploading to [RowsAndAll](https://rowsandall.com/) can be automated through their e-mail interface, see [this description](https://rowsandall.com/rowers/developers/). The Open rowing Monito installer can also set up a network share that contains all training data so it is easy to grab the files from there and manually upload them to the training platform of your choice. -## Supported machines - -Several machines are known to work with Open Rowing Monitor: - -* Abilica Winrower 2.0: Air rower that is known to work, see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/48); -* [Concept 2 Model D, Model E and RowErg](hardware_setup_Concept2_RowErg.md): fully supported Air rower, dynamic drag calculation and all metrics available including force curves -* Decathlon Rower120: known to work by adding magnets to the flywheel, currently basic metrics only, see [this discussion](https://github.com/laberning/openrowingmonitor/issues/110); -* DKN R-320 Air Rower: fully supported Air rower, static drag calculation, basic metrics only; -* FDF Neon Pro V rower: known to work, see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/87); -* ForceUSA R3: supported Air rower, dynamic drag calculation and all metrics available including force curves; -* NordickTrack RX800: fully supported hybrid Magnetic/Air rower, dynamic drag calculation and all metrics available including force curves; -* Sportplus MR-SP-08: work in progress, see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/95); -* [Sportstech WRX700](hardware_setup_WRX700.md): fully supported water rower, static drag calculation, all metrics available including force curves; -* [OpenErgo machines](https://openergo.webs.com/): several machines have been made to work, see [example 1](https://github.com/laberning/openrowingmonitor/discussions/80), [example 2](https://github.com/laberning/openrowingmonitor/discussions/105) and [example 3](https://github.com/laberning/openrowingmonitor/discussions/115) - -If your machine isn't listed, it just means that you need to [adjust the software settings following the settings adjustment guide](rower_settings.md). Don't worry, in the [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) there always are friendly people to help you set up your machine and the settings. - ## Installation You will need a Raspberry Pi Zero W, Raspberry Pi Zero 2 W, Raspberry Pi 3 or a Raspberry Pi 4 with a fresh installation of Raspberry Pi OS Lite for this (the 64Bit kernel is preferred). Connect to the device with SSH and initiate the following command to install Open Rowing Monitor as an automatically starting system service: From 47316d5027b19c800ae1daebb8c43475fa8b180e Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 17 Jan 2023 23:45:07 +0100 Subject: [PATCH 033/231] Creation to support users --- docs/Supported_Rowers.md | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/Supported_Rowers.md diff --git a/docs/Supported_Rowers.md b/docs/Supported_Rowers.md new file mode 100644 index 0000000000..36c079c786 --- /dev/null +++ b/docs/Supported_Rowers.md @@ -0,0 +1,61 @@ +# Known rowers and their support status + +The following rowers are known to work, or are even actively supported: + +| Brand | Type | Rower type | Measurement type | HW Modification needed | Support status | Rower profile | Basic Metrics | Advanced Metrics | Limitations | Remarks | +| ----- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| Abilica | Winrower 2.0 | Air rower | Handle drive wheel | No | Known to work | - | Yes | No | Static distance | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/48) | +| Concept 2 | Model B, C | Air rower | Flywheel | Modification to electrical signal | In development | - | - | - | - | [Concept 2 Model C discussion](https://github.com/laberning/openrowingmonitor/issues/77) | +| | Model D, E | Air rower | Flywheel | Modification to electrical signal | Active support | Concept2_RowErg | Yes | Yes | None | [Concept 2 Model D, Model E and RowErg setup](hardware_setup_Concept2_RowErg.md) | +| | RowErg | Air rower | Flywheel | Modification to electrical signal | Active support | Concept2_RowErg | Yes | Yes | None | [Concept 2 Model D, Model E and RowErg setup](hardware_setup_Concept2_RowErg.md) | +| Decathlon | Rower 120 | Physical friction | Flywheel | Adding sensor and adding magnets to the flywheel | In development | - | - | - | - | see [this discussion](https://github.com/laberning/openrowingmonitor/issues/110) | +| DKN | R-320 | Air Rower | Flywheel | No | Full support | DKN_R320 | Yes | No | Static drag | - | +| FDF | Neon Pro V | Air rower | Flywheel | Sensor replacement | Known to work | - | Yes | - | - | - | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/87) | +| ForceUSA | R3 | Air Rower | Flywheel | No | Supported | ForceUSA_R3 | Yes | Yes | None | - | +| NordicTrack | RX800 | Hybrid Magnetic and Air rower | Flywheel | None | Full support | NordicTrack_RX800 | Yes | Yes | None | Also known under ProForm brand | +| Sportplus | MR-SP-08 | Water rower | Handle drive wheel | In development | - | - | - | - | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/95) | +| Sportstech | WRX700 | Water rower | Impellor | Add one magnet | Active support | Sportstech_WRX700 | Yes | Yes | Static drag | see [Sportstech WRX700 setup](hardware_setup_WRX700.md) +| White label | Air Rower | Air rower | Fywheel | None | Supported | Generic_Air_Rower | Yes | Yes | None | Sold under different brand names | +| Open ergo | - | Air rower | Flywheel | Addition of magnets en sensor | Known to work | - | Yes | Yes | None | Machine specific profile is needed, but is done before, see [example 1](https://github.com/laberning/openrowingmonitor/discussions/80), [example 2](https://github.com/laberning/openrowingmonitor/discussions/105) and [example 3](https://github.com/laberning/openrowingmonitor/discussions/115) | + +If your machine isn't listed, it just means that you need to [adjust the software settings following the settings adjustment guide](rower_settings.md) yourself. But don't worry, in the [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) there always are friendly people to help you set up your machine and the settings. + +## Support status +In the table, the support status means the following: +* **Active support**: These are the testmachines of the developers, these are tested almost on a daily basis. These settings are automatically modified to facilitate updates of the rowing engine; +* **Full support**: We actively maintain a the configuration, including automatically updating these settings to facilitate chages of the rowing engine, and are part of the automated regression test set. So as a user, you can be assured this setting will keep working; +* **Supported**: Users have reported a working configuration, and this configuration is part of `rowerProfiles.js`, but we lack the raw data samples to maintain the rower for future updates. This means that future support isn't guaranteed; +* **Configuration known**: Users have reported a working configuration, but it isn't actively supported by these users and it isn't on our rader to maintain. You need to add the configuration to your `config.js` manually and maintain it yourself when there are updates to the engine; +* **Known to work**: Users have reported that the rower is known to work, but the configuration is not known by us; +* **In development**: Users are known to be working to get the rower connected, but the configuration is not yet known by us. + +Please note: the support status largely depends on the willingness of users to report their settings and provide decent samples of their data. So when you have a machine, please provide this information. + +## Basic Metrics +With basic metrics we mean: +* Distance rowed, +* Training Duration, +* Power, +* Pace, +* Strokes per Minute, +* Drive time, +* Recovery Time, +* Calories used, +* Total number of strokes, +* Heart Rate + +## Extended Metrics +With extended metrics, we mean: +* Drag factor, +* Drive length, +* Average handle force, +* Peak handle force, +* Handle force curve, +* Handle velocity curve, +* Handle power curve. + +## Limitations +With the limitation, we mean: +* **None**: No limitations, drag calculation and distance per stroke are dynamic based on flywheel behaviour and automatically adapt to environmental conditions; +* **Static drag**: the drag calculation is fixed, so changes in air/water properties due to temperature or settings are not automatically adjusted; +* **Static distance**: the distance per impulse is fixed, thus making the measurement of a more forceful stroke impossible. This typically happens when the handle movement is measured, but not its effect on the flywheel. From 0ba86d891cd714fb7204ecb8cbaa3b7205b3fd37 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 17 Jan 2023 23:49:00 +0100 Subject: [PATCH 034/231] Fixed Lint errors Fixed Lint errors --- docs/Supported_Rowers.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/Supported_Rowers.md b/docs/Supported_Rowers.md index 36c079c786..259e52907f 100644 --- a/docs/Supported_Rowers.md +++ b/docs/Supported_Rowers.md @@ -21,7 +21,9 @@ The following rowers are known to work, or are even actively supported: If your machine isn't listed, it just means that you need to [adjust the software settings following the settings adjustment guide](rower_settings.md) yourself. But don't worry, in the [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) there always are friendly people to help you set up your machine and the settings. ## Support status + In the table, the support status means the following: + * **Active support**: These are the testmachines of the developers, these are tested almost on a daily basis. These settings are automatically modified to facilitate updates of the rowing engine; * **Full support**: We actively maintain a the configuration, including automatically updating these settings to facilitate chages of the rowing engine, and are part of the automated regression test set. So as a user, you can be assured this setting will keep working; * **Supported**: Users have reported a working configuration, and this configuration is part of `rowerProfiles.js`, but we lack the raw data samples to maintain the rower for future updates. This means that future support isn't guaranteed; @@ -32,7 +34,9 @@ In the table, the support status means the following: Please note: the support status largely depends on the willingness of users to report their settings and provide decent samples of their data. So when you have a machine, please provide this information. ## Basic Metrics + With basic metrics we mean: + * Distance rowed, * Training Duration, * Power, @@ -45,7 +49,9 @@ With basic metrics we mean: * Heart Rate ## Extended Metrics + With extended metrics, we mean: + * Drag factor, * Drive length, * Average handle force, @@ -55,7 +61,9 @@ With extended metrics, we mean: * Handle power curve. ## Limitations + With the limitation, we mean: + * **None**: No limitations, drag calculation and distance per stroke are dynamic based on flywheel behaviour and automatically adapt to environmental conditions; * **Static drag**: the drag calculation is fixed, so changes in air/water properties due to temperature or settings are not automatically adjusted; * **Static distance**: the distance per impulse is fixed, thus making the measurement of a more forceful stroke impossible. This typically happens when the handle movement is measured, but not its effect on the flywheel. From c82a97796c39edc888cf1861808eea1189d0247f Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 17 Jan 2023 23:53:41 +0100 Subject: [PATCH 035/231] Fixed a table error --- docs/Supported_Rowers.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Supported_Rowers.md b/docs/Supported_Rowers.md index 259e52907f..d993a92134 100644 --- a/docs/Supported_Rowers.md +++ b/docs/Supported_Rowers.md @@ -1,5 +1,7 @@ # Known rowers and their support status +Open Rowing Monitor works with a very wide range of rowing machines. It is currently developed and tested with a Sportstech WRX700 water-rower and a Concept2 air-rower. In the past, it was also tested extensively on a NordicTrack RX-800 hybrid air/magnetic rower. But it should run fine with any rowing machine that uses some kind of damping mechanism, as long as you can add something to measure the speed of the flywheel. It has shown to work well with DIY rowing machines like the [Openergo](https://openergo.webs.com/), providing the construction is decent. + The following rowers are known to work, or are even actively supported: | Brand | Type | Rower type | Measurement type | HW Modification needed | Support status | Rower profile | Basic Metrics | Advanced Metrics | Limitations | Remarks | @@ -13,7 +15,7 @@ The following rowers are known to work, or are even actively supported: | FDF | Neon Pro V | Air rower | Flywheel | Sensor replacement | Known to work | - | Yes | - | - | - | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/87) | | ForceUSA | R3 | Air Rower | Flywheel | No | Supported | ForceUSA_R3 | Yes | Yes | None | - | | NordicTrack | RX800 | Hybrid Magnetic and Air rower | Flywheel | None | Full support | NordicTrack_RX800 | Yes | Yes | None | Also known under ProForm brand | -| Sportplus | MR-SP-08 | Water rower | Handle drive wheel | In development | - | - | - | - | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/95) | +| Sportplus | MR-SP-08 | Water rower | Handle drive wheel | - | In development | - | - | - | - | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/95) | | Sportstech | WRX700 | Water rower | Impellor | Add one magnet | Active support | Sportstech_WRX700 | Yes | Yes | Static drag | see [Sportstech WRX700 setup](hardware_setup_WRX700.md) | White label | Air Rower | Air rower | Fywheel | None | Supported | Generic_Air_Rower | Yes | Yes | None | Sold under different brand names | | Open ergo | - | Air rower | Flywheel | Addition of magnets en sensor | Known to work | - | Yes | Yes | None | Machine specific profile is needed, but is done before, see [example 1](https://github.com/laberning/openrowingmonitor/discussions/80), [example 2](https://github.com/laberning/openrowingmonitor/discussions/105) and [example 3](https://github.com/laberning/openrowingmonitor/discussions/115) | From 704c2faaae526fc915c99ebe6a71eefc5ef32746 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 18 Jan 2023 09:29:28 +0100 Subject: [PATCH 036/231] Removed the Sportplus as an option --- docs/Supported_Rowers.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Supported_Rowers.md b/docs/Supported_Rowers.md index d993a92134..17effd2f57 100644 --- a/docs/Supported_Rowers.md +++ b/docs/Supported_Rowers.md @@ -15,7 +15,6 @@ The following rowers are known to work, or are even actively supported: | FDF | Neon Pro V | Air rower | Flywheel | Sensor replacement | Known to work | - | Yes | - | - | - | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/87) | | ForceUSA | R3 | Air Rower | Flywheel | No | Supported | ForceUSA_R3 | Yes | Yes | None | - | | NordicTrack | RX800 | Hybrid Magnetic and Air rower | Flywheel | None | Full support | NordicTrack_RX800 | Yes | Yes | None | Also known under ProForm brand | -| Sportplus | MR-SP-08 | Water rower | Handle drive wheel | - | In development | - | - | - | - | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/95) | | Sportstech | WRX700 | Water rower | Impellor | Add one magnet | Active support | Sportstech_WRX700 | Yes | Yes | Static drag | see [Sportstech WRX700 setup](hardware_setup_WRX700.md) | White label | Air Rower | Air rower | Fywheel | None | Supported | Generic_Air_Rower | Yes | Yes | None | Sold under different brand names | | Open ergo | - | Air rower | Flywheel | Addition of magnets en sensor | Known to work | - | Yes | Yes | None | Machine specific profile is needed, but is done before, see [example 1](https://github.com/laberning/openrowingmonitor/discussions/80), [example 2](https://github.com/laberning/openrowingmonitor/discussions/105) and [example 3](https://github.com/laberning/openrowingmonitor/discussions/115) | From 6e5089d4a3e5827cc24335c9b89c314caf4a608c Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 18 Jan 2023 09:30:10 +0100 Subject: [PATCH 037/231] Update Supported_Rowers.md --- docs/Supported_Rowers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Supported_Rowers.md b/docs/Supported_Rowers.md index 17effd2f57..ab4cd81822 100644 --- a/docs/Supported_Rowers.md +++ b/docs/Supported_Rowers.md @@ -5,7 +5,7 @@ Open Rowing Monitor works with a very wide range of rowing machines. It is curre The following rowers are known to work, or are even actively supported: | Brand | Type | Rower type | Measurement type | HW Modification needed | Support status | Rower profile | Basic Metrics | Advanced Metrics | Limitations | Remarks | -| ----- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| ----- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---------------- | | Abilica | Winrower 2.0 | Air rower | Handle drive wheel | No | Known to work | - | Yes | No | Static distance | see [this discussion](https://github.com/laberning/openrowingmonitor/discussions/48) | | Concept 2 | Model B, C | Air rower | Flywheel | Modification to electrical signal | In development | - | - | - | - | [Concept 2 Model C discussion](https://github.com/laberning/openrowingmonitor/issues/77) | | | Model D, E | Air rower | Flywheel | Modification to electrical signal | Active support | Concept2_RowErg | Yes | Yes | None | [Concept 2 Model D, Model E and RowErg setup](hardware_setup_Concept2_RowErg.md) | From 60350f3dc74e71f9e2f7f3063ec95eca8ed8d920 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:11:19 +0100 Subject: [PATCH 038/231] Added recommendation of OS Lite 64Bit --- docs/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 5ce1116be6..dd03e7200f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -19,11 +19,11 @@ This guide roughly explains how to set up the rowing software and hardware. ### Initialization of the Raspberry Pi -* Install **Raspberry Pi OS Lite** on the SD Card i.e. with the [Raspberry Pi Imager](https://www.raspberrypi.org/software) +* Install **Raspberry Pi OS Lite** on the SD Card i.e. with the [Raspberry Pi Imager](https://www.raspberrypi.org/software). Here, Raspberry Pi OS Lite 64 Bit is recommended as it is better suited for real-time environments; * Configure the network connection and enable SSH, if you use the Raspberry Pi Imager, you can automatically do this while writing the SD Card, just press `Ctrl-Shift-X`(see [here](https://www.raspberrypi.org/blog/raspberry-pi-imager-update-to-v1-6/) for a description), otherwise follow the instructions below * Connect the device to your network ([headless](https://www.raspberrypi.org/documentation/configuration/wireless/headless.md) or via [command line](https://www.raspberrypi.org/documentation/configuration/wireless/wireless-cli.md)) * Enable [SSH](https://www.raspberrypi.org/documentation/remote-access/ssh/README.md) -* Tune the OS if needed [by following this guide](Improving_Raspberry_Performance.md) +* Tune the OS if needed [by following our performance improvement guide](Improving_Raspberry_Performance.md) ### Installation of the Open Rowing Monitor From f737fdb04d7f5bdc7ec280b407b056e77c10557d Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Sun, 22 Jan 2023 14:38:44 +0100 Subject: [PATCH 039/231] Fixed a bad example config The example configuration pointed to a renamed rower.... --- install/config.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/install/config.js b/install/config.js index 35482b2524..ff7f170f25 100644 --- a/install/config.js +++ b/install/config.js @@ -20,19 +20,23 @@ export default { default: 'debug' }, - // example: set a rower profile: - rowerSettings: rowerProfiles.DKNR320 - - // example: set custom rower settings: - rowerSettings: { - numOfImpulsesPerRevolution: 1, - dragFactor: 0.03, - flywheelInertia: 0.3 - } - - // example: set a rower profile, but overwrite some settings: - rowerSettings: Object.assign(rowerProfiles.DKNR320, { - autoAdjustDragFactor: true - }) - */ + // The rower specific settings. Either choose a profile from config/rowerProfiles.js or + // define the settings individually. If you find good settings for a new rowing device + // please send them to us (together with a raw recording of 10 strokes) so we can add + // the device to the profiles. + + // EXAMPLE ROWER CONFIG : using a DKN R-320 Air Rower as is + // rowerSettings: rowerProfiles.DKN_R320 + + // EXAMPLE ROWER CONFIG: Just set custom rower settings to make it work + // rowerSettings: { + // numOfImpulsesPerRevolution: 1, + // dragFactor: 0.03, + // flywheelInertia: 0.3 + // } + + // EXAMPLE ROWER CONFIG: set a rower profile, but overwrite some settings: + // rowerSettings: Object.assign(rowerProfiles.DKN_R320, { + // autoAdjustDragFactor: true + // }) } From ca3dc3aa2ab1a92f47c751586516656fd06354ab Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:29:33 +0100 Subject: [PATCH 040/231] Added depiction of maximumTimeBetweenImpulses Added depiction of maximumTimeBetweenImpulses to illustrate pause behaviour --- docs/img/maximumTimeBetweenImpulses.jpg | Bin 0 -> 415061 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/img/maximumTimeBetweenImpulses.jpg diff --git a/docs/img/maximumTimeBetweenImpulses.jpg b/docs/img/maximumTimeBetweenImpulses.jpg new file mode 100644 index 0000000000000000000000000000000000000000..42b6dd155ce977a395123ec8294c9607f3fbf88e GIT binary patch literal 415061 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 XxOh@n;ipeV#m#+xF1j+*|Gx Date: Fri, 27 Jan 2023 16:00:30 +0100 Subject: [PATCH 041/231] Added an example of a bouncing sensor Added an example of a bouncing sensor --- docs/img/CurrentDt_With_Lots_Of_Bounce.jpg | Bin 0 -> 360828 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/img/CurrentDt_With_Lots_Of_Bounce.jpg diff --git a/docs/img/CurrentDt_With_Lots_Of_Bounce.jpg b/docs/img/CurrentDt_With_Lots_Of_Bounce.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b92517428da176a208e4698b25dbe65f5377413 GIT binary patch literal 360828 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 z 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 literal 0 HcmV?d00001 From 198181dfeb40f372441d3a28940ca8d210b3978d Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Fri, 27 Jan 2023 16:06:43 +0100 Subject: [PATCH 042/231] Updated to help people further Added new insights based on https://github.com/laberning/openrowingmonitor/discussions/115 and https://github.com/JaapvanEkris/openrowingmonitor/discussions/11 --- docs/rower_settings.md | 54 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/docs/rower_settings.md b/docs/rower_settings.md index 75ad3fe8b0..83f59a383b 100644 --- a/docs/rower_settings.md +++ b/docs/rower_settings.md @@ -1,7 +1,7 @@ # 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. In this manual, we will guide you through the settings needed to get your machine working. This is a work in progress, and please get in touch through the [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) when you run into problems. +This guide helps you to adjust the rowing monitor specifically for a new type of rower or even for your specific use, when the supported rowers don't suffice (you can [find a list of supported rowers here](Supported_Rowers.md)). In this manual, we will guide you through the settings needed to get your machine working. This is a work in progress, and please get in touch through the [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) when you run into problems. In this manual, we cover the following topics: @@ -64,6 +64,10 @@ Please note that the process identification numbers will differ. ## Making sure the hardware is connected correctly and works as intended +Because any system follows the mantra "Garbage in is garbage out", we first make sure that the signals Open Rowing Monitor recieves are decent. First we check the physical properties, then the electrical properties and last we check the quality of the incoming signal. + +**Please check and fix any mechanical/electrical/quality issues before proceeding, as the subsequent steps depend on a signal with decent quality!!** + ### Checking the physical properties of the rower One thing to check is what the original sensor actually measures. You can physically look in the rower, but most manuals also include an exploded view of all parts in the machine. There you need to look at the placement of the sensor and the magnets. Most air-rowers measure the flywheel speed, but most water-rowers measure the handle speed and direction. Open Rowing Monitor is best suited for handling a spinning flywheel or water impellor, or anything directly attached to that. If your machine measures the impellor or flywheel directly, please note the number of magnets per rotation, as you need that parameter later on. So when you encounter a handle-connected machine and it is possible and within your comfort zone, try to add sensors to the flywheel or impellor as it results in much better metrics. @@ -74,6 +78,8 @@ If you are uncomfortable modifying you machine, you can still make OpenRowingMon Before you physically connect anything to anything else, **check the electric properties of the rower** you are connecting to. Skipping this might destroy your Raspberry Pi as some rowers are known to exceed the Raspberry Pi electrical properties. For example, a Concept 2 RowErg provides 15V signals to the monitor, which will destroy the GPIO-ports. Other rowers provide signals aren't directly detectable by the raspberry Pi. For example, the Concept 2 Model C provides 0.2V pulses, thus staying below the detectable 1.8V treshold that the Raspberry Pi uses. Using a scope or a voltmeter is highly recommended. Please observe that the maximum input a Raspberry Pi GPIO pin can handle is 3.3V and 0.5A, and it will switch at 1.8V (see [this overview of the Raspberry Pi electrical properties](https://raspberrypi.stackexchange.com/questions/3209/what-are-the-min-max-voltage-current-values-the-gpio-pins-can-handle)). In our [GitHub Discussions](https://github.com/laberning/openrowingmonitor/discussions) there are some people who are brilliant with electrical connections, so don't be affraid to ask for help there. When you have a working solution, please report it so that we can include it in the documentation, allowing us to help others. +### Checking signal and measurement quality + Next, when the electric connection has been made, we need to look if the data is recieved well and has sufficient quality to be used. You can change `config/config.js` by ```zsh @@ -92,21 +98,37 @@ You can use the following commands on the command line to restart after a config sudo systemctl restart openrowingmonitor ``` -After rowing a bit, there should be a csv file created with raw data. Please read this data in Excel (it is in US format, so you might need to adapt it to your local settings), to check if it is sufficiently clean. After loading it into Excel, you can visualise it, and probably see something similar to the following: +After rowing a bit, there should be a csv file created with raw data. If no strokes or pauses are detected, you can force the writing of these files by pushing the reset button on the GUI. Please read this data in Excel (it is in US format, so you might need to adapt it to your local settings), to check if it is sufficiently clean. After loading it into Excel, you can visualise it, and probably see something similar to the following: -When the line goes up, the time between impulses from the flywheel goes up, and thus the flywheel is decellerating. When the line goes down, the time between impulses decreases, and thus the flywheel is accelerating. In the first decellerating flank, we see some noise, which Open Rowing Monitor an deal with perfectly. However, looking at the bottom of the first acceleration flank, we see a series of heavy downward spikes. This could be start-up noise, but it also could be systematic across the rowing session. This is problematic as it throws off both stroke detection and many metrics. Typically, it signals an issue in the mechanical construction of the sensor: the fram and sensor vibrate at high speeds, resulting in much noise. +When the line goes up, the time between impulses from the flywheel goes up, and thus the flywheel is decellerating. When the line goes down, the time between impulses decreases, and thus the flywheel is accelerating. In the first decellerating flank, we see some noise, which Open Rowing Monitor an deal with perfectly. However, looking at the bottom of the first acceleration flank, we see a series of heavy downward spikes. This could be start-up noise, but it also could be systematic across the rowing session. This is problematic as it throws off both stroke detection and many metrics. Typically, it signals an issue in the mechanical construction of the sensor: the frame and sensor vibrate at high speeds, resulting in much noise. Fixing this type of errors is key. We adress two familiar measurement quality issues: -A specific issue to watch out for are systemic errors in the magnet placement. For exmple, these 18 pulses from a Concept2 RowErg show a systematic error, that follows a 6 impulse cycle. As the RowErg has 6 magnets, it is very likely that it is caused by magnets not being perfectly aligned (for example due to production tollerances): +* Switch bounce: where a single magnet triggers multiple signals +* Magnet placement errors: where the timing of magnets is off - +#### Fixing switch bounce + +A specific issue to be aware of is *switch bounce*, which typically is seen as a valid signal followed by a very short spike. When looking at a set of plotted signals in Excel, it manafests itself as the following: -In some cases, changing the magnet placing or orientation can fix this completely (see for example [this discussion](https://github.com/laberning/openrowingmonitor/discussions/87)), which yields very good results and near-perfect data. Sometimes, you can't fix this. Open Rowing Monitor can handle this kind of systematic error, as long as the *FlankLength* (described later) is set to at least two full rotations (in this case, 12 magnets). + -Another specific issue to be aware of is *debounce*, which typically is seen as a valid signal followed by a very short spike. This suggests that the sensor picks up the magnet twice. The preferred solution is to fix the physical underlying cause, this is a better alignment of the magnet or replacing the sensor for a more advanced model that only picks up specific signals. However, this might not be practical: some flywheels are extremely well-balanced, and moving magnets might destroy that balance. To prevent that type of error, the **gpioMinimumPulseLength** setting allows you to require a minimal signal length, removing these ghost readings. This is a bit of a try and error process: you'll need to row and increase the value **gpioMinimumPulseLength** further when you see ghost readings, and repeat this process until the noise is acceptable. +As this example scatterplot curve shows, you can vaguely recognize the typical rowing curve in the measurements between 0.02 and 0.08 seconds. However, you also see a lot of very small spikes where the measurements are below 0.01 seconds. Actually there are so many spikes that it masks the real signal completely for Open Rowing Monitor. It contains sections where the time between pulses is 0,00009 seconds, which would mean that the flywheel would be spinning at 120.000 RPM, which physically is impossible for a simple bicycle wheel. This type of scater plot and the underlying data clearly suggests that the sensor picks up the magnet twice or more. This is a measurement quality issue that must be adressed. -Please fix any mechanical issues before proceeding. +The preferred solution is to fix the physical underlying cause, this is a better alignment of the magnet or replacing the sensor for a more advanced model that only picks up specific signals. Using smaller but more powerful magnets also tends to help. However, this might not be practical: some flywheels are extremely well-balanced, and moving or replacing magnets might destroy that balance. To fix that type of error, there are two options: + +* Changing the **gpioMinimumPulseLength** setting allows you to require a minimal signal length, most likely removing these ghost readings. This is a bit of a try and error process: you'll need to row and increase the value **gpioMinimumPulseLength** further with steps of 50 us when you still see ghost readings, and repeat this process until the noise is acceptable. +* Another aveue to persue is to change the detected flank from the default 'Up' to 'Down' in the **gpioTriggeredFlank**, as sometimes the downward flank might be less affected by this issue. + +#### Fixing magnet placement errors + +Another specific issue to watch out for are systemic errors in the magnet placement. For exmple, these 18 pulses from a Concept2 RowErg show a nice clean signal, but also a systematic error, that follows a 6 impulse cycle. As the RowErg has 6 magnets, it is very likely that it is caused by magnets not being perfectly aligned (for example due to production tollerances): + + + +In some cases, changing the magnet placing or orientation can fix this completely (see for example [this discussion](https://github.com/laberning/openrowingmonitor/discussions/87)), which yields very good results and near-perfect data. Sometimes, you can't fix this or you are unwilling to physically modify the machine. Open Rowing Monitor can handle this kind of systematic error, as long as the *FlankLength* (described later) is set to at least two full rotations (in this case, 12 magnets). + +**Please fix any mechanical/electrical/quality issues before proceeding, as the subsequent steps depend on a signal with decent quality!!** ## Setting up a more detailed logging for a better insight into Open Rowing Monitor @@ -155,13 +177,25 @@ When you look at the raw dump of *CurrentDT*, it should provide a nice curve. Wh Another option is to change the *gpioPollingInterval*, which determines how accurate the measurements are. Please note that increasing this will increase the CPU load, so setting it to 1us might come at a price. Setting this from the default value of 5us to 1us might increase precission, but it could disrupt the entire process as the CPU might get overloeded. So experimenting with this value is key. -**gpioTriggeredFlank** and **gpioMinimumPulseLength** are typically used to prevent bounces in the signal: magnets passing could trigger a reed switch twice. The logs provide help here, as the logs indicate abnormal short and long times between impulses (via the minimumTimeBetweenImpulses and maximumTimeBetweenImpulses settings). Please note that during a first stroke, the **CurrentDt** values obviously are longer. +**gpioTriggeredFlank** and **gpioMinimumPulseLength** are typically used to prevent bounces in the signal: magnets passing could trigger a reed switch twice (as described above). The logs provide help here, as the logs indicate abnormal short and long times between impulses (via the minimumTimeBetweenImpulses and maximumTimeBetweenImpulses settings). Please note that during a first stroke, the **CurrentDt** values obviously are longer. ### Setting minimumTimeBetweenImpulses and maximumTimeBetweenImpulses **minimumTimeBetweenImpulses** and **maximumTimeBetweenImpulses** provide a bandwith where values are deemed credible during an active session. The goal here is to help you detect and log any extremely obvious errors. So take a look at the raw datafiles for several damper settings (if available on your rower) and make sure that normal rowing isn't hindered by these settings (i.e. all normal values should fall within *minimumTimeBetweenImpulses* and *maximumTimeBetweenImpulses*). Here, you should rather allow too much noise, than hurt real valid signal, as Open Rowing Monitor can handle a lot of noise by itself. -When using the raw datafiles, realise that the goal is to distinguish good normal strokes from noise. So at startup it is quite accepted that the flywheel starts too slow to produce valid data during the biggest part of the first drive phase. Also at the end of a session the flywheel should spin down out of valid ranges again. Please note, *maximumTimeBetweenImpulses* is also used to detect wether the flywheel is spinning down due to lack of user input. When a *flankLength* of measurements contains sufficient values above *maximumTimeBetweenImpulses*, the flywheel is still decelerating and the *maximumStrokeTimeBeforePause* is structurally exceeded, the rower will pause. So setting the value for *maximumTimeBetweenImpulses* too high might block this behaviour. +A good quality curve of the time between impulses (as captured in the raw datafiles) looks like this: + + + +Here, aside from the startup and spindown, the blue line shows that the impulses typically vary between 0,035 and 0,120 seconds. The red line depicts the *maximumTimeBetweenImpulses*, which is set to 0.120 seconds. When using the raw datafiles, realise that the goal is to distinguish good normal strokes from noise. So at startup it is quite accepted that the flywheel starts too slow to produce valid data during the biggest part of the first drive phase. Also at the end of a session the flywheel should spin down out of valid ranges again. So *maximumTimeBetweenImpulses* could be set lower, sometimes even hitting the "peaks" of the curves, without causing issues in normal use of Open Rowing Monitor (it will add warnings in the logs). Similarily, *minimumTimeBetweenImpulses* could be slightly increased to include some valleys, without causing much issues. + +An important note is that *maximumTimeBetweenImpulses* is also used to detect wether the flywheel is spinning down due to lack of user input. Open Rowing Monitor pauses/stops the row when: + +* the start of the last drive is at least *maximumStrokeTimeBeforePause* ago; +* the entire flank (i.e. the last *flankLength* of measurements) contains only values above *maximumTimeBetweenImpulses*; +* the flywheel is decelerating throughout the flank. + +So setting the value for *maximumTimeBetweenImpulses* too high might block this behaviour as there aren't enough measurements to fill the flank. Although most air-based rowers have a spin down time of around 2 minutes, water rowers typically stop quite fast (think seconds). Therefore, especially the stop behaviour of water rowers requires a bit more attention. Again looking at the behaviour of the curve and the raw data might help here: looking how many residual samples follow after *maximumTimeBetweenImpulses* is exceeded (there should be more than *flankLength*) and how much time it spans since the last drive (exceeding *maximumStrokeTimeBeforePause*) is critical here. ### Review smoothing From 0c90c183825249481b2f4530d4ddd0ddad5d7835 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Fri, 27 Jan 2023 16:11:05 +0100 Subject: [PATCH 043/231] Small wording changes --- docs/rower_settings.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rower_settings.md b/docs/rower_settings.md index 83f59a383b..8a7552971f 100644 --- a/docs/rower_settings.md +++ b/docs/rower_settings.md @@ -113,7 +113,7 @@ A specific issue to be aware of is *switch bounce*, which typically is seen as a -As this example scatterplot curve shows, you can vaguely recognize the typical rowing curve in the measurements between 0.02 and 0.08 seconds. However, you also see a lot of very small spikes where the measurements are below 0.01 seconds. Actually there are so many spikes that it masks the real signal completely for Open Rowing Monitor. It contains sections where the time between pulses is 0,00009 seconds, which would mean that the flywheel would be spinning at 120.000 RPM, which physically is impossible for a simple bicycle wheel. This type of scater plot and the underlying data clearly suggests that the sensor picks up the magnet twice or more. This is a measurement quality issue that must be adressed. +As this example scatterplot curve shows, you can vaguely recognize the typical rowing curve in the measurements between 0.02 and 0.08 seconds. However, you also see a lot of very small spikes where the measurements are below 0.01 seconds. Actually there are so many spikes that it masks the real signal completely for Open Rowing Monitor. It contains sections where the time between pulses is 0.0001 seconds, which would mean that the flywheel would be spinning at 120.000 RPM, which physically is impossible for a simple bicycle wheel used in this example. This type of scater plot and the underlying data clearly suggests that the sensor picks up the magnet twice or more. This is a measurement quality issue that must be adressed. The preferred solution is to fix the physical underlying cause, this is a better alignment of the magnet or replacing the sensor for a more advanced model that only picks up specific signals. Using smaller but more powerful magnets also tends to help. However, this might not be practical: some flywheels are extremely well-balanced, and moving or replacing magnets might destroy that balance. To fix that type of error, there are two options: @@ -187,7 +187,7 @@ A good quality curve of the time between impulses (as captured in the raw datafi -Here, aside from the startup and spindown, the blue line shows that the impulses typically vary between 0,035 and 0,120 seconds. The red line depicts the *maximumTimeBetweenImpulses*, which is set to 0.120 seconds. When using the raw datafiles, realise that the goal is to distinguish good normal strokes from noise. So at startup it is quite accepted that the flywheel starts too slow to produce valid data during the biggest part of the first drive phase. Also at the end of a session the flywheel should spin down out of valid ranges again. So *maximumTimeBetweenImpulses* could be set lower, sometimes even hitting the "peaks" of the curves, without causing issues in normal use of Open Rowing Monitor (it will add warnings in the logs). Similarily, *minimumTimeBetweenImpulses* could be slightly increased to include some valleys, without causing much issues. +Here, aside from the startup and spindown, the blue line shows that the impulses typically vary between 0.035 and 0.120 seconds. The red line depicts the *maximumTimeBetweenImpulses*, which is set to 0.120 seconds. When using the raw datafiles, realise that the goal is to distinguish good normal strokes from noise. So at startup it is quite accepted that the flywheel starts too slow to produce valid data during the biggest part of the first drive phase. Also at the end of a session the flywheel should spin down out of valid ranges again. So *maximumTimeBetweenImpulses* could be set lower, sometimes even hitting the "peaks" of the curves, without causing issues in normal use of Open Rowing Monitor (it will add warnings in the logs). Similarily, *minimumTimeBetweenImpulses* could be slightly increased to include some valleys, without causing much issues. An important note is that *maximumTimeBetweenImpulses* is also used to detect wether the flywheel is spinning down due to lack of user input. Open Rowing Monitor pauses/stops the row when: @@ -199,7 +199,7 @@ So setting the value for *maximumTimeBetweenImpulses* too high might block this ### Review smoothing -**smoothing** is the ultimate fallback mechanism for rowers with very noisy data. For all known rowers currently maintained by Open Rowing Monitor, **NONE** needed this, so only start working with this when the raw files show you have a very noisy signal, physical measures don't work and you can't get your stroke detection to work with other means (please note that we design the mechanisms here to be robust, so they can take a hit). +**smoothing** is the ultimate fallback mechanism for rowers with very noisy data. Please refrain from using it, unless as a last resort (typically increasing *flankLength* is more effective and leads to better results). For all known rowers currently maintained by Open Rowing Monitor, **NONE** needed this, so only start working with this when the raw files show you have a very noisy signal, physical measures don't work and you can't get your stroke detection to work with other means (please note that we design the mechanisms here to be robust, so they can take a hit). This is a running median filter, effectively killing any extreme values. By default, it is set to 1 (off). A value of 3 will allow it to completely ignore any single extreme values, which should do the trick for most rowers. From 3edfe22434cd8e144e502310b989d27be27f8744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Mon, 12 Dec 2022 14:45:07 +0100 Subject: [PATCH 044/231] Refactor and restructure peripheral related code - Create a new main folder to include ANT and BLE - Move peripheralManager with other files with shared information - Refactor static notify characteristics - Refactor code to be more uniform across the different peripherals --- app/ble/common/StaticReadCharacteristic.js | 22 ----------- .../ftms/IndoorBikeFeatureCharacteristic.js | 36 ------------------ app/ble/ftms/RowerFeatureCharacteristic.js | 37 ------------------- app/ble/pm5/DeviceInformationService.js | 32 ---------------- app/peripherals/PeripheralConstants.js | 16 ++++++++ app/{ble => peripherals}/PeripheralManager.js | 8 ++-- app/{ => peripherals}/ant/AntManager.js | 0 app/{ => peripherals}/ble/BufferBuilder.js | 0 .../ble/BufferBuilder.test.js | 0 app/{ => peripherals}/ble/CentralManager.js | 0 app/{ => peripherals}/ble/CentralService.js | 2 +- app/{ => peripherals}/ble/CpsPeripheral.js | 2 +- app/{ => peripherals}/ble/CscPeripheral.js | 2 +- app/{ => peripherals}/ble/FtmsPeripheral.js | 2 +- app/{ => peripherals}/ble/Pm5Peripheral.js | 4 +- .../ble/common/AdvertisingDataBuilder.js | 0 .../ble/common/AdvertisingDataBuilder.test.js | 0 app/peripherals/ble/common/CommonOpCodes.js | 10 +++++ .../ble/common/DeviceInformationService.js | 9 +++-- .../ble/common/SensorLocation.js | 0 .../ble/common/StaticNotifyCharacteristic.js} | 29 ++++++++------- .../ble/common/StaticReadCharacteristic.js | 36 ++++++++++++++++++ .../ble/cps/CpsControlPointCharacteristic.js | 0 .../ble/cps/CpsMeasurementCharacteristic.js | 0 .../ble/cps/CyclingPowerMeterService.js | 0 .../ble/csc/CscControlPointCharacteristic.js | 0 .../ble/csc/CscMeasurementCharacteristic.js | 0 .../ble/csc/CyclingSpeedCadenceService.js | 0 ...itnessMachineControlPointCharacteristic.js | 28 +++++--------- .../ble/ftms/FitnessMachineService.js | 32 ++++++++++++++-- .../FitnessMachineStatusCharacteristic.js | 0 .../ble/ftms/IndoorBikeDataCharacteristic.js | 23 ++++++++++-- .../ble/ftms/RowerDataCharacteristic.js | 21 ++++++++++- .../ble/pm5/DeviceInformationService.js | 32 ++++++++++++++++ app/{ => peripherals}/ble/pm5/GapService.js | 14 +++---- app/{ => peripherals}/ble/pm5/Pm5Constants.js | 14 +++---- .../ble/pm5/Pm5ControlService.js | 0 .../ble/pm5/Pm5RowingService.js | 16 ++++---- .../pm5/characteristic/AdditionalStatus.js | 0 .../pm5/characteristic/AdditionalStatus2.js | 0 .../characteristic/AdditionalStrokeData.js | 0 .../ble/pm5/characteristic/ControlReceive.js | 0 .../ble/pm5/characteristic/ControlTransmit.js | 0 .../ble/pm5/characteristic/GeneralStatus.js | 0 .../MultiplexedCharacteristic.js | 0 .../ble/pm5/characteristic/StrokeData.js | 0 app/server.js | 6 +-- 47 files changed, 225 insertions(+), 208 deletions(-) delete mode 100644 app/ble/common/StaticReadCharacteristic.js delete mode 100644 app/ble/ftms/IndoorBikeFeatureCharacteristic.js delete mode 100644 app/ble/ftms/RowerFeatureCharacteristic.js delete mode 100644 app/ble/pm5/DeviceInformationService.js create mode 100644 app/peripherals/PeripheralConstants.js rename app/{ble => peripherals}/PeripheralManager.js (91%) rename app/{ => peripherals}/ant/AntManager.js (100%) rename app/{ => peripherals}/ble/BufferBuilder.js (100%) rename app/{ => peripherals}/ble/BufferBuilder.test.js (100%) rename app/{ => peripherals}/ble/CentralManager.js (100%) rename app/{ => peripherals}/ble/CentralService.js (91%) rename app/{ => peripherals}/ble/CpsPeripheral.js (98%) rename app/{ => peripherals}/ble/CscPeripheral.js (98%) rename app/{ => peripherals}/ble/FtmsPeripheral.js (98%) rename app/{ => peripherals}/ble/Pm5Peripheral.js (96%) rename app/{ => peripherals}/ble/common/AdvertisingDataBuilder.js (100%) rename app/{ => peripherals}/ble/common/AdvertisingDataBuilder.test.js (100%) create mode 100644 app/peripherals/ble/common/CommonOpCodes.js rename app/{ => peripherals}/ble/common/DeviceInformationService.js (66%) rename app/{ => peripherals}/ble/common/SensorLocation.js (100%) rename app/{ble/pm5/characteristic/ValueReadCharacteristic.js => peripherals/ble/common/StaticNotifyCharacteristic.js} (57%) create mode 100644 app/peripherals/ble/common/StaticReadCharacteristic.js rename app/{ => peripherals}/ble/cps/CpsControlPointCharacteristic.js (100%) rename app/{ => peripherals}/ble/cps/CpsMeasurementCharacteristic.js (100%) rename app/{ => peripherals}/ble/cps/CyclingPowerMeterService.js (100%) rename app/{ => peripherals}/ble/csc/CscControlPointCharacteristic.js (100%) rename app/{ => peripherals}/ble/csc/CscMeasurementCharacteristic.js (100%) rename app/{ => peripherals}/ble/csc/CyclingSpeedCadenceService.js (100%) rename app/{ => peripherals}/ble/ftms/FitnessMachineControlPointCharacteristic.js (84%) rename app/{ => peripherals}/ble/ftms/FitnessMachineService.js (59%) rename app/{ => peripherals}/ble/ftms/FitnessMachineStatusCharacteristic.js (100%) rename app/{ => peripherals}/ble/ftms/IndoorBikeDataCharacteristic.js (82%) rename app/{ => peripherals}/ble/ftms/RowerDataCharacteristic.js (82%) create mode 100644 app/peripherals/ble/pm5/DeviceInformationService.js rename app/{ => peripherals}/ble/pm5/GapService.js (52%) rename app/{ => peripherals}/ble/pm5/Pm5Constants.js (73%) rename app/{ => peripherals}/ble/pm5/Pm5ControlService.js (100%) rename app/{ => peripherals}/ble/pm5/Pm5RowingService.js (80%) rename app/{ => peripherals}/ble/pm5/characteristic/AdditionalStatus.js (100%) rename app/{ => peripherals}/ble/pm5/characteristic/AdditionalStatus2.js (100%) rename app/{ => peripherals}/ble/pm5/characteristic/AdditionalStrokeData.js (100%) rename app/{ => peripherals}/ble/pm5/characteristic/ControlReceive.js (100%) rename app/{ => peripherals}/ble/pm5/characteristic/ControlTransmit.js (100%) rename app/{ => peripherals}/ble/pm5/characteristic/GeneralStatus.js (100%) rename app/{ => peripherals}/ble/pm5/characteristic/MultiplexedCharacteristic.js (100%) rename app/{ => peripherals}/ble/pm5/characteristic/StrokeData.js (100%) diff --git a/app/ble/common/StaticReadCharacteristic.js b/app/ble/common/StaticReadCharacteristic.js deleted file mode 100644 index ef2248fef6..0000000000 --- a/app/ble/common/StaticReadCharacteristic.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict' - -import bleno from '@abandonware/bleno' - -export default class StaticReadCharacteristic extends bleno.Characteristic { - constructor (uuid, description, value) { - super({ - uuid, - properties: ['read'], - value: Buffer.isBuffer(value) ? value : Buffer.from(value), - descriptors: [ - new bleno.Descriptor({ - uuid: '2901', - value: description - }) - ] - }) - this.uuid = uuid - this.description = description - this.value = Buffer.isBuffer(value) ? value : Buffer.from(value) - } -} diff --git a/app/ble/ftms/IndoorBikeFeatureCharacteristic.js b/app/ble/ftms/IndoorBikeFeatureCharacteristic.js deleted file mode 100644 index 4c01098157..0000000000 --- a/app/ble/ftms/IndoorBikeFeatureCharacteristic.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict' -/* - Open Rowing Monitor, https://github.com/laberning/openrowingmonitor - - This implements the Indoor Bike Feature Characteristic as defined by the specification. - Used to inform the Central about the features that the Open Rowing Monitor supports. - Make sure that The Fitness Machine Features and Target Setting Features that are announced here - are supported in IndoorBikeDataCharacteristic and FitnessMachineControlPointCharacteristic. -*/ -import bleno from '@abandonware/bleno' -import log from 'loglevel' - -export default class IndoorBikeDataCharacteristic extends bleno.Characteristic { - constructor (uuid, description, value) { - super({ - // Fitness Machine Feature - uuid: '2ACC', - properties: ['read'], - value: null - }) - } - - onReadRequest (offset, callback) { - // see https://www.bluetooth.com/specifications/specs/fitness-machine-service-1-0 for details - // Fitness Machine Features for the IndoorBikeDataCharacteristic - // Cadence Supported (1), Total Distance Supported (2), Expended Energy Supported (9), - // Heart Rate Measurement Supported (10), Elapsed Time Supported (12), Power Measurement Supported (14) - // 00000110 01010110 - // Target Setting Features for the IndoorBikeDataCharacteristic - // none - // 0000000 0000000 - const features = [0x06, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - log.debug('Features of Indoor Bike requested') - callback(this.RESULT_SUCCESS, features.slice(offset, features.length)) - } -} diff --git a/app/ble/ftms/RowerFeatureCharacteristic.js b/app/ble/ftms/RowerFeatureCharacteristic.js deleted file mode 100644 index 04e929e85b..0000000000 --- a/app/ble/ftms/RowerFeatureCharacteristic.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict' -/* - Open Rowing Monitor, https://github.com/laberning/openrowingmonitor - - This implements the Rower Feature Characteristic as defined by the specification. - Used to inform the Central about the features that the Open Rowing Monitor supports. - Make sure that The Fitness Machine Features and Target Setting Features that are announced here - are supported in RowerDataCharacteristic and FitnessMachineControlPointCharacteristic. -*/ -import bleno from '@abandonware/bleno' -import log from 'loglevel' - -export default class RowerFeatureCharacteristic extends bleno.Characteristic { - constructor () { - super({ - // Fitness Machine Feature - uuid: '2ACC', - properties: ['read'], - value: null - }) - } - - onReadRequest (offset, callback) { - // see https://www.bluetooth.com/specifications/specs/fitness-machine-service-1-0 for details - // Fitness Machine Features for the RowerDataCharacteristic - // Total Distance Supported (2), Pace Supported (5), Expended Energy Supported (9), - // Heart Rate Measurement Supported (10), Elapsed Time Supported (bit 12), - // Power Measurement Supported (14) - // 00100100 01010110 - // Target Setting Features for the RowerDataCharacteristic - // none - // 0000000 0000000 - const features = [0x24, 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] - log.debug('Features of Rower requested') - callback(this.RESULT_SUCCESS, features.slice(offset, features.length)) - }; -} diff --git a/app/ble/pm5/DeviceInformationService.js b/app/ble/pm5/DeviceInformationService.js deleted file mode 100644 index 9741d54269..0000000000 --- a/app/ble/pm5/DeviceInformationService.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict' -/* - Open Rowing Monitor, https://github.com/laberning/openrowingmonitor - - Provides the required Device Information of the PM5 -*/ -import bleno from '@abandonware/bleno' -import { constants, getFullUUID } from './Pm5Constants.js' -import ValueReadCharacteristic from './characteristic/ValueReadCharacteristic.js' - -export default class DeviceInformationService extends bleno.PrimaryService { - constructor () { - super({ - // InformationenService uuid as defined by the PM5 specification - uuid: getFullUUID('0010'), - characteristics: [ - // C2 module number string - new ValueReadCharacteristic(getFullUUID('0011'), constants.model, 'model'), - // C2 serial number string - new ValueReadCharacteristic(getFullUUID('0012'), constants.serial, 'serial'), - // C2 hardware revision string - new ValueReadCharacteristic(getFullUUID('0013'), constants.hardwareRevision, 'hardwareRevision'), - // C2 firmware revision string - new ValueReadCharacteristic(getFullUUID('0014'), constants.firmwareRevision, 'firmwareRevision'), - // C2 manufacturer name string - new ValueReadCharacteristic(getFullUUID('0015'), constants.manufacturer, 'manufacturer'), - // Erg Machine Type - new ValueReadCharacteristic(getFullUUID('0016'), constants.ergMachineType, 'ergMachineType') - ] - }) - } -} diff --git a/app/peripherals/PeripheralConstants.js b/app/peripherals/PeripheralConstants.js new file mode 100644 index 0000000000..3e1e1369b0 --- /dev/null +++ b/app/peripherals/PeripheralConstants.js @@ -0,0 +1,16 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Some PM5 specific constants +*/ +export const PeripheralConstants = { + serial: '123456789', + model: 'PM5', + name: 'PM5 123456789 Row', + hardwareRevision: '907', + // See https://www.concept2.com/service/monitors/pm5/firmware for available versions + // please note: hardware versions exclude a software version, and thus might confuse the client + firmwareRevision: '210', + manufacturer: 'Concept2' +} diff --git a/app/ble/PeripheralManager.js b/app/peripherals/PeripheralManager.js similarity index 91% rename from app/ble/PeripheralManager.js rename to app/peripherals/PeripheralManager.js index c75861fa24..02e54ee410 100644 --- a/app/ble/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -6,12 +6,12 @@ switching between them */ import config from '../tools/ConfigManager.js' -import { createFtmsPeripheral } from './FtmsPeripheral.js' -import { createPm5Peripheral } from './Pm5Peripheral.js' +import { createFtmsPeripheral } from './ble/FtmsPeripheral.js' +import { createPm5Peripheral } from './ble/Pm5Peripheral.js' import log from 'loglevel' import EventEmitter from 'node:events' -import { createCpsPeripheral } from './CpsPeripheral.js' -import { createCscPeripheral } from './CscPeripheral.js' +import { createCpsPeripheral } from './ble/CpsPeripheral.js' +import { createCscPeripheral } from './ble/CscPeripheral.js' const modes = ['FTMS', 'FTMSBIKE', 'PM5', 'CSC', 'CPS'] function createPeripheralManager () { diff --git a/app/ant/AntManager.js b/app/peripherals/ant/AntManager.js similarity index 100% rename from app/ant/AntManager.js rename to app/peripherals/ant/AntManager.js diff --git a/app/ble/BufferBuilder.js b/app/peripherals/ble/BufferBuilder.js similarity index 100% rename from app/ble/BufferBuilder.js rename to app/peripherals/ble/BufferBuilder.js diff --git a/app/ble/BufferBuilder.test.js b/app/peripherals/ble/BufferBuilder.test.js similarity index 100% rename from app/ble/BufferBuilder.test.js rename to app/peripherals/ble/BufferBuilder.test.js diff --git a/app/ble/CentralManager.js b/app/peripherals/ble/CentralManager.js similarity index 100% rename from app/ble/CentralManager.js rename to app/peripherals/ble/CentralManager.js diff --git a/app/ble/CentralService.js b/app/peripherals/ble/CentralService.js similarity index 91% rename from app/ble/CentralService.js rename to app/peripherals/ble/CentralService.js index f8b28a51ea..0183d5db07 100644 --- a/app/ble/CentralService.js +++ b/app/peripherals/ble/CentralService.js @@ -7,7 +7,7 @@ */ import { createCentralManager } from './CentralManager.js' import process from 'process' -import config from '../tools/ConfigManager.js' +import config from '../../tools/ConfigManager.js' import log from 'loglevel' log.setLevel(config.loglevel.default) diff --git a/app/ble/CpsPeripheral.js b/app/peripherals/ble/CpsPeripheral.js similarity index 98% rename from app/ble/CpsPeripheral.js rename to app/peripherals/ble/CpsPeripheral.js index 5d24e47784..89cce8774a 100644 --- a/app/ble/CpsPeripheral.js +++ b/app/peripherals/ble/CpsPeripheral.js @@ -6,7 +6,7 @@ a Cycling Power Profile */ import bleno from '@abandonware/bleno' -import config from '../tools/ConfigManager.js' +import config from '../../tools/ConfigManager.js' import log from 'loglevel' import CyclingPowerService from './cps/CyclingPowerMeterService.js' import DeviceInformationService from './common/DeviceInformationService.js' diff --git a/app/ble/CscPeripheral.js b/app/peripherals/ble/CscPeripheral.js similarity index 98% rename from app/ble/CscPeripheral.js rename to app/peripherals/ble/CscPeripheral.js index 3c8e99cc79..6915d13dbe 100644 --- a/app/ble/CscPeripheral.js +++ b/app/peripherals/ble/CscPeripheral.js @@ -6,7 +6,7 @@ a Cycling Speed and Cadence Profile */ import bleno from '@abandonware/bleno' -import config from '../tools/ConfigManager.js' +import config from '../../tools/ConfigManager.js' import log from 'loglevel' import DeviceInformationService from './common/DeviceInformationService.js' import CyclingSpeedCadenceService from './csc/CyclingSpeedCadenceService.js' diff --git a/app/ble/FtmsPeripheral.js b/app/peripherals/ble/FtmsPeripheral.js similarity index 98% rename from app/ble/FtmsPeripheral.js rename to app/peripherals/ble/FtmsPeripheral.js index 7a54392f9f..c0845c6864 100644 --- a/app/ble/FtmsPeripheral.js +++ b/app/peripherals/ble/FtmsPeripheral.js @@ -13,7 +13,7 @@ */ import bleno from '@abandonware/bleno' import FitnessMachineService from './ftms/FitnessMachineService.js' -import config from '../tools/ConfigManager.js' +import config from '../../tools/ConfigManager.js' import log from 'loglevel' import DeviceInformationService from './common/DeviceInformationService.js' import AdvertisingDataBuilder from './common/AdvertisingDataBuilder.js' diff --git a/app/ble/Pm5Peripheral.js b/app/peripherals/ble/Pm5Peripheral.js similarity index 96% rename from app/ble/Pm5Peripheral.js rename to app/peripherals/ble/Pm5Peripheral.js index 4e905198a6..badfe8e357 100644 --- a/app/ble/Pm5Peripheral.js +++ b/app/peripherals/ble/Pm5Peripheral.js @@ -8,7 +8,7 @@ see: https://www.concept2.co.uk/files/pdf/us/monitors/PM5_BluetoothSmartInterfaceDefinition.pdf */ import bleno from '@abandonware/bleno' -import { constants } from './pm5/Pm5Constants.js' +import { pm5Constants } from './pm5/Pm5Constants.js' import DeviceInformationService from './pm5/DeviceInformationService.js' import GapService from './pm5/GapService.js' import log from 'loglevel' @@ -16,7 +16,7 @@ import Pm5ControlService from './pm5/Pm5ControlService.js' import Pm5RowingService from './pm5/Pm5RowingService.js' function createPm5Peripheral (controlCallback, options) { - const peripheralName = constants.name + const peripheralName = pm5Constants.name const deviceInformationService = new DeviceInformationService() const gapService = new GapService() const controlService = new Pm5ControlService() diff --git a/app/ble/common/AdvertisingDataBuilder.js b/app/peripherals/ble/common/AdvertisingDataBuilder.js similarity index 100% rename from app/ble/common/AdvertisingDataBuilder.js rename to app/peripherals/ble/common/AdvertisingDataBuilder.js diff --git a/app/ble/common/AdvertisingDataBuilder.test.js b/app/peripherals/ble/common/AdvertisingDataBuilder.test.js similarity index 100% rename from app/ble/common/AdvertisingDataBuilder.test.js rename to app/peripherals/ble/common/AdvertisingDataBuilder.test.js diff --git a/app/peripherals/ble/common/CommonOpCodes.js b/app/peripherals/ble/common/CommonOpCodes.js new file mode 100644 index 0000000000..2c8d16e15d --- /dev/null +++ b/app/peripherals/ble/common/CommonOpCodes.js @@ -0,0 +1,10 @@ +'use-strict' + +export const ResultOpCode = { + reserved: 0x00, + success: 0x01, + opCodeNotSupported: 0x02, + invalidParameter: 0x03, + operationFailed: 0x04, + controlNotPermitted: 0x05 +} diff --git a/app/ble/common/DeviceInformationService.js b/app/peripherals/ble/common/DeviceInformationService.js similarity index 66% rename from app/ble/common/DeviceInformationService.js rename to app/peripherals/ble/common/DeviceInformationService.js index 100f5c4766..c0b2daa3bf 100644 --- a/app/ble/common/DeviceInformationService.js +++ b/app/peripherals/ble/common/DeviceInformationService.js @@ -5,6 +5,7 @@ todo: Could provide some info on the device here, maybe OS, Node version etc... */ import bleno from '@abandonware/bleno' +import { PeripheralConstants } from '../../PeripheralConstants.js' import StaticReadCharacteristic from './StaticReadCharacteristic.js' export default class DeviceInformationService extends bleno.PrimaryService { @@ -13,10 +14,10 @@ export default class DeviceInformationService extends bleno.PrimaryService { // uuid of 'Device Information Service' uuid: '180a', characteristics: [ - new StaticReadCharacteristic('2A24', 'Model Number', 'ORM2'), - new StaticReadCharacteristic('2A25', 'Serial Number', '1234'), - new StaticReadCharacteristic('2A28', 'Software Revision', '2'), - new StaticReadCharacteristic('2A29', 'Manufacturer Name', 'OpenRowingMonitor') + new StaticReadCharacteristic('2A24', 'Model Number', PeripheralConstants.model), + new StaticReadCharacteristic('2A25', 'Serial Number', PeripheralConstants.serial), + new StaticReadCharacteristic('2A28', 'Software Revision', PeripheralConstants.firmwareRevision), + new StaticReadCharacteristic('2A29', 'Manufacturer Name', PeripheralConstants.manufacturer) ] }) } diff --git a/app/ble/common/SensorLocation.js b/app/peripherals/ble/common/SensorLocation.js similarity index 100% rename from app/ble/common/SensorLocation.js rename to app/peripherals/ble/common/SensorLocation.js diff --git a/app/ble/pm5/characteristic/ValueReadCharacteristic.js b/app/peripherals/ble/common/StaticNotifyCharacteristic.js similarity index 57% rename from app/ble/pm5/characteristic/ValueReadCharacteristic.js rename to app/peripherals/ble/common/StaticNotifyCharacteristic.js index 7797cd109f..37b6181874 100644 --- a/app/ble/pm5/characteristic/ValueReadCharacteristic.js +++ b/app/peripherals/ble/common/StaticNotifyCharacteristic.js @@ -1,39 +1,40 @@ 'use strict' -/* - Open Rowing Monitor, https://github.com/laberning/openrowingmonitor - A simple Characteristic that gives read and notify access to a static value - Currently also used as placeholder on a lot of characteristics that are not yet implemented properly -*/ import bleno from '@abandonware/bleno' import log from 'loglevel' -export default class ValueReadCharacteristic extends bleno.Characteristic { - constructor (uuid, value, description) { +export default class StaticNotifyCharacteristic extends bleno.Characteristic { + constructor (uuid, description, value, addRead = false) { super({ uuid, - properties: ['read', 'notify'], - value: null + properties: addRead ? ['read', 'notify'] : ['notify'], + value: null, + descriptors: [ + new bleno.Descriptor({ + uuid: '2901', + value: description + }) + ] }) - this.uuid = uuid - this._value = Buffer.isBuffer(value) ? value : Buffer.from(value) + this._uuid = uuid this._description = description + this._value = Buffer.isBuffer(value) ? value : Buffer.from(value) this._updateValueCallback = null } onReadRequest (offset, callback) { - log.debug(`ValueReadRequest: ${this._description ? this._description : this.uuid}`) + log.debug(`ValueReadRequest: ${this._description ? this._description : this._uuid}`) callback(this.RESULT_SUCCESS, this._value.slice(offset, this._value.length)) } onSubscribe (maxValueSize, updateValueCallback) { - log.debug(`characteristic ${this._description ? this._description : this.uuid} - central subscribed with maxSize: ${maxValueSize}`) + log.debug(`characteristic ${this._description ? this._description : this._uuid} - central subscribed with maxSize: ${maxValueSize}`) this._updateValueCallback = updateValueCallback return this.RESULT_SUCCESS } onUnsubscribe () { - log.debug(`characteristic ${this._description ? this._description : this.uuid} - central unsubscribed`) + log.debug(`characteristic ${this._description ? this._description : this._uuid} - central unsubscribed`) this._updateValueCallback = null return this.RESULT_UNLIKELY_ERROR } diff --git a/app/peripherals/ble/common/StaticReadCharacteristic.js b/app/peripherals/ble/common/StaticReadCharacteristic.js new file mode 100644 index 0000000000..71bfbb8248 --- /dev/null +++ b/app/peripherals/ble/common/StaticReadCharacteristic.js @@ -0,0 +1,36 @@ +'use strict' + +import bleno from '@abandonware/bleno' +import log from 'loglevel' + +export default class StaticReadCharacteristic extends bleno.Characteristic { + constructor (uuid, description, value, addNotify = false) { + super({ + uuid, + properties: addNotify ? ['read', 'notify'] : ['read'], + value: Buffer.isBuffer(value) ? value : Buffer.from(value), + descriptors: [ + new bleno.Descriptor({ + uuid: '2901', + value: description + }) + ] + }) + this._uuid = uuid + this._description = description + this._value = Buffer.isBuffer(value) ? value : Buffer.from(value) + this._updateValueCallback = null + } + + onSubscribe (maxValueSize, updateValueCallback) { + log.debug(`characteristic ${this._description ? this._description : this._uuid} - central subscribed with maxSize: ${maxValueSize}`) + this._updateValueCallback = updateValueCallback + return this.RESULT_SUCCESS + } + + onUnsubscribe () { + log.debug(`characteristic ${this._description ? this._description : this._uuid} - central unsubscribed`) + this._updateValueCallback = null + return this.RESULT_UNLIKELY_ERROR + } +} diff --git a/app/ble/cps/CpsControlPointCharacteristic.js b/app/peripherals/ble/cps/CpsControlPointCharacteristic.js similarity index 100% rename from app/ble/cps/CpsControlPointCharacteristic.js rename to app/peripherals/ble/cps/CpsControlPointCharacteristic.js diff --git a/app/ble/cps/CpsMeasurementCharacteristic.js b/app/peripherals/ble/cps/CpsMeasurementCharacteristic.js similarity index 100% rename from app/ble/cps/CpsMeasurementCharacteristic.js rename to app/peripherals/ble/cps/CpsMeasurementCharacteristic.js diff --git a/app/ble/cps/CyclingPowerMeterService.js b/app/peripherals/ble/cps/CyclingPowerMeterService.js similarity index 100% rename from app/ble/cps/CyclingPowerMeterService.js rename to app/peripherals/ble/cps/CyclingPowerMeterService.js diff --git a/app/ble/csc/CscControlPointCharacteristic.js b/app/peripherals/ble/csc/CscControlPointCharacteristic.js similarity index 100% rename from app/ble/csc/CscControlPointCharacteristic.js rename to app/peripherals/ble/csc/CscControlPointCharacteristic.js diff --git a/app/ble/csc/CscMeasurementCharacteristic.js b/app/peripherals/ble/csc/CscMeasurementCharacteristic.js similarity index 100% rename from app/ble/csc/CscMeasurementCharacteristic.js rename to app/peripherals/ble/csc/CscMeasurementCharacteristic.js diff --git a/app/ble/csc/CyclingSpeedCadenceService.js b/app/peripherals/ble/csc/CyclingSpeedCadenceService.js similarity index 100% rename from app/ble/csc/CyclingSpeedCadenceService.js rename to app/peripherals/ble/csc/CyclingSpeedCadenceService.js diff --git a/app/ble/ftms/FitnessMachineControlPointCharacteristic.js b/app/peripherals/ble/ftms/FitnessMachineControlPointCharacteristic.js similarity index 84% rename from app/ble/ftms/FitnessMachineControlPointCharacteristic.js rename to app/peripherals/ble/ftms/FitnessMachineControlPointCharacteristic.js index 7d96096f12..9d52b3c207 100644 --- a/app/ble/ftms/FitnessMachineControlPointCharacteristic.js +++ b/app/peripherals/ble/ftms/FitnessMachineControlPointCharacteristic.js @@ -10,6 +10,7 @@ */ import bleno from '@abandonware/bleno' import log from 'loglevel' +import { ResultOpCode } from '../common/CommonOpCodes.js' // see https://www.bluetooth.com/specifications/specs/fitness-machine-service-1-0 for details const ControlPointOpCode = { @@ -37,15 +38,6 @@ const ControlPointOpCode = { responseCode: 0x80 } -const ResultCode = { - reserved: 0x00, - success: 0x01, - opCodeNotSupported: 0x02, - invalidParameter: 0x03, - operationFailed: 0x04, - controlNotPermitted: 0x05 -} - export default class FitnessMachineControlPointCharacteristic extends bleno.Characteristic { constructor (controlPointCallback) { super({ @@ -70,12 +62,12 @@ export default class FitnessMachineControlPointCharacteristic extends bleno.Char if (this.controlPointCallback({ name: 'requestControl' })) { log.debug('requestControl sucessful') this.controlled = true - callback(this.buildResponse(opCode, ResultCode.success)) + callback(this.buildResponse(opCode, ResultOpCode.success)) } else { - callback(this.buildResponse(opCode, ResultCode.operationFailed)) + callback(this.buildResponse(opCode, ResultOpCode.operationFailed)) } } else { - callback(this.buildResponse(opCode, ResultCode.controlNotPermitted)) + callback(this.buildResponse(opCode, ResultOpCode.controlNotPermitted)) } break @@ -109,30 +101,30 @@ export default class FitnessMachineControlPointCharacteristic extends bleno.Char const crr = data.readUInt8(5) * 0.0001 const cw = data.readUInt8(6) * 0.01 if (this.controlPointCallback({ name: 'setIndoorBikeSimulationParameters', value: { windspeed, grade, crr, cw } })) { - callback(this.buildResponse(opCode, ResultCode.success)) + callback(this.buildResponse(opCode, ResultOpCode.success)) } else { - callback(this.buildResponse(opCode, ResultCode.operationFailed)) + callback(this.buildResponse(opCode, ResultOpCode.operationFailed)) } break } default: log.info(`opCode ${opCode} is not supported`) - callback(this.buildResponse(opCode, ResultCode.opCodeNotSupported)) + callback(this.buildResponse(opCode, ResultOpCode.opCodeNotSupported)) } } handleSimpleCommand (opCode, opName, callback) { if (this.controlled) { if (this.controlPointCallback({ name: opName })) { - const response = this.buildResponse(opCode, ResultCode.success) + const response = this.buildResponse(opCode, ResultOpCode.success) callback(response) } else { - callback(this.buildResponse(opCode, ResultCode.operationFailed)) + callback(this.buildResponse(opCode, ResultOpCode.operationFailed)) } } else { log.info(`initating command '${opName}' requires 'requestControl'`) - callback(this.buildResponse(opCode, ResultCode.controlNotPermitted)) + callback(this.buildResponse(opCode, ResultOpCode.controlNotPermitted)) } } diff --git a/app/ble/ftms/FitnessMachineService.js b/app/peripherals/ble/ftms/FitnessMachineService.js similarity index 59% rename from app/ble/ftms/FitnessMachineService.js rename to app/peripherals/ble/ftms/FitnessMachineService.js index d4703742b4..6a411fe355 100644 --- a/app/ble/ftms/FitnessMachineService.js +++ b/app/peripherals/ble/ftms/FitnessMachineService.js @@ -18,23 +18,25 @@ import bleno from '@abandonware/bleno' import RowerDataCharacteristic from './RowerDataCharacteristic.js' -import RowerFeatureCharacteristic from './RowerFeatureCharacteristic.js' import IndoorBikeDataCharacteristic from './IndoorBikeDataCharacteristic.js' -import IndoorBikeFeatureCharacteristic from './IndoorBikeFeatureCharacteristic.js' import FitnessMachineControlPointCharacteristic from './FitnessMachineControlPointCharacteristic.js' import FitnessMachineStatusCharacteristic from './FitnessMachineStatusCharacteristic.js' +import StaticReadCharacteristic from '../common/StaticReadCharacteristic.js' +import BufferBuilder from '../BufferBuilder.js' export default class FitnessMachineService extends bleno.PrimaryService { constructor (options, controlPointCallback) { const simulateIndoorBike = options?.simulateIndoorBike === true const dataCharacteristic = simulateIndoorBike ? new IndoorBikeDataCharacteristic() : new RowerDataCharacteristic() - const featureCharacteristic = simulateIndoorBike ? new IndoorBikeFeatureCharacteristic() : new RowerFeatureCharacteristic() const statusCharacteristic = new FitnessMachineStatusCharacteristic() + const ftmsFeaturesBuffer = new BufferBuilder() + ftmsFeaturesBuffer.writeUInt16LE(featuresFlag) + super({ // Fitness Machine uuid: '1826', characteristics: [ - featureCharacteristic, + new StaticReadCharacteristic('2ACC', 'FTMS Feature', ftmsFeaturesBuffer.getBuffer()), dataCharacteristic, new FitnessMachineControlPointCharacteristic(controlPointCallback), statusCharacteristic @@ -52,3 +54,25 @@ export default class FitnessMachineService extends bleno.PrimaryService { this.statusCharacteristic.notify(event) } } + +export const FtmsBikeFeaturesFlags = { + averageSpeedSupported: (0x01 << 0), + cadenceSupported: (0x01 << 1), + totalDistanceSupported: (0x01 << 2), + inclinationSupported: (0x01 << 3), + elevationGainSupported: (0x01 << 4), + paceSupported: (0x01 << 5), + stepCountSupported: (0x01 << 6), + resistanceLevelSupported: (0x01 << 7), + strideCountSupported: (0x01 << 8), + expendedEnergySupported: (0x01 << 9), + heartRateMeasurementSupported: (0x01 << 10), + metabolicEquivalentSupported: (0x01 << 11), + elapsedTimeSupported: (0x01 << 12), + remainingTimeSupported: (0x01 << 13), + powerMeasurementSupported: (0x01 << 14), + forceOnBeltAndPowerOutputSupported: (0x01 << 15), + userDataRetentionSupported: (0x01 << 16) +} + +export const featuresFlag = FtmsBikeFeaturesFlags.cadenceSupported | FtmsBikeFeaturesFlags.totalDistanceSupported | FtmsBikeFeaturesFlags.paceSupported | FtmsBikeFeaturesFlags.expendedEnergySupported | FtmsBikeFeaturesFlags.heartRateMeasurementSupported | FtmsBikeFeaturesFlags.elapsedTimeSupported | FtmsBikeFeaturesFlags.powerMeasurementSupported diff --git a/app/ble/ftms/FitnessMachineStatusCharacteristic.js b/app/peripherals/ble/ftms/FitnessMachineStatusCharacteristic.js similarity index 100% rename from app/ble/ftms/FitnessMachineStatusCharacteristic.js rename to app/peripherals/ble/ftms/FitnessMachineStatusCharacteristic.js diff --git a/app/ble/ftms/IndoorBikeDataCharacteristic.js b/app/peripherals/ble/ftms/IndoorBikeDataCharacteristic.js similarity index 82% rename from app/ble/ftms/IndoorBikeDataCharacteristic.js rename to app/peripherals/ble/ftms/IndoorBikeDataCharacteristic.js index 9a1b71c017..f33e05e729 100644 --- a/app/ble/ftms/IndoorBikeDataCharacteristic.js +++ b/app/peripherals/ble/ftms/IndoorBikeDataCharacteristic.js @@ -60,10 +60,9 @@ export default class IndoorBikeDataCharacteristic extends bleno.Characteristic { // Field flags as defined in the Bluetooth Documentation // Instantaneous speed (default), Instantaneous Cadence (2), Total Distance (4), // Instantaneous Power (6), Total / Expended Energy (8), Heart Rate (9), Elapsed Time (11) - // 01010100 - bufferBuilder.writeUInt8(0x54) // 00001011 - bufferBuilder.writeUInt8(0x0B) + // 01010100 + bufferBuilder.writeUInt16LE(measurementFlag) // see https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-3/ // for some of the data types @@ -98,3 +97,21 @@ export default class IndoorBikeDataCharacteristic extends bleno.Characteristic { return this.RESULT_SUCCESS } } + +export const RowingMeasurementFlags = { + moreDataPresent: (0x01 << 0), + averageSpeedPresent: (0x01 << 1), + instantaneousCadencePresent: (0x01 << 2), + averageCadencePresent: (0x01 << 3), + totalDistancePresent: (0x01 << 4), + resistanceLevelPresent: (0x01 << 5), + instantaneousPowerPresent: (0x01 << 6), + averagePowerPresent: (0x01 << 7), + expendedEnergyPresent: (0x01 << 8), + heartRatePresent: (0x01 << 9), + metabolicEquivalentPresent: (0x01 << 10), + elapsedTimePresent: (0x01 << 11), + remainingTimePresent: (0x01 << 12) +} + +export const measurementFlag = RowingMeasurementFlags.instantaneousCadencePresent | RowingMeasurementFlags.totalDistancePresent | RowingMeasurementFlags.instantaneousPowerPresent | RowingMeasurementFlags.expendedEnergyPresent | RowingMeasurementFlags.heartRatePresent | RowingMeasurementFlags.elapsedTimePresent diff --git a/app/ble/ftms/RowerDataCharacteristic.js b/app/peripherals/ble/ftms/RowerDataCharacteristic.js similarity index 82% rename from app/ble/ftms/RowerDataCharacteristic.js rename to app/peripherals/ble/ftms/RowerDataCharacteristic.js index a3a376266e..81df3f6a1d 100644 --- a/app/ble/ftms/RowerDataCharacteristic.js +++ b/app/peripherals/ble/ftms/RowerDataCharacteristic.js @@ -55,9 +55,8 @@ export default class RowerDataCharacteristic extends bleno.Characteristic { // todo: might add: Average Stroke Rate (1), Average Pace (4), Average Power (6) // Remaining Time (12) // 00101100 - bufferBuilder.writeUInt8(0x2c) + bufferBuilder.writeUInt16LE(measurementFlag) // 00001011 - bufferBuilder.writeUInt8(0x0B) // see https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-3/ // for some of the data types @@ -98,3 +97,21 @@ export default class RowerDataCharacteristic extends bleno.Characteristic { return this.RESULT_SUCCESS } } + +export const RowingMeasurementFlags = { + moreDataPresent: (0x01 << 0), + averageStrokeRatePresent: (0x01 << 1), + totalDistancePresent: (0x01 << 2), + instantaneousPacePresent: (0x01 << 3), + averagePacePresent: (0x01 << 4), + instantaneousPowerPresent: (0x01 << 5), + averagePowerPresent: (0x01 << 6), + resistanceLevelPresent: (0x01 << 7), + expendedEnergyPresent: (0x01 << 8), + heartRatePresent: (0x01 << 9), + metabolicEquivalentPresent: (0x01 << 10), + elapsedTimePresent: (0x01 << 11), + remainingTimePresent: (0x01 << 12) +} + +export const measurementFlag = RowingMeasurementFlags.totalDistancePresent | RowingMeasurementFlags.instantaneousPacePresent | RowingMeasurementFlags.instantaneousPowerPresent | RowingMeasurementFlags.expendedEnergyPresent | RowingMeasurementFlags.heartRatePresent | RowingMeasurementFlags.elapsedTimePresent diff --git a/app/peripherals/ble/pm5/DeviceInformationService.js b/app/peripherals/ble/pm5/DeviceInformationService.js new file mode 100644 index 0000000000..42a25efaba --- /dev/null +++ b/app/peripherals/ble/pm5/DeviceInformationService.js @@ -0,0 +1,32 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Provides the required Device Information of the PM5 +*/ +import bleno from '@abandonware/bleno' +import StaticNotifyCharacteristic from '../common/StaticNotifyCharacteristic.js' +import { getFullUUID, pm5Constants } from './Pm5Constants.js' + +export default class DeviceInformationService extends bleno.PrimaryService { + constructor () { + super({ + // InformationenService uuid as defined by the PM5 specification + uuid: getFullUUID('0010'), + characteristics: [ + // C2 module number string + new StaticNotifyCharacteristic(getFullUUID('0011'), 'model', 'PM5', true), + // C2 serial number string + new StaticNotifyCharacteristic(getFullUUID('0012'), 'serial', pm5Constants.serial, true), + // C2 hardware revision string + new StaticNotifyCharacteristic(getFullUUID('0013'), 'hardwareRevision', pm5Constants.hardwareRevision, true), + // C2 firmware revision string + new StaticNotifyCharacteristic(getFullUUID('0014'), 'firmwareRevision', pm5Constants.firmwareRevision, true), + // C2 manufacturer name string + new StaticNotifyCharacteristic(getFullUUID('0015'), 'manufacturer', pm5Constants.manufacturer, true), + // Erg Machine Type + new StaticNotifyCharacteristic(getFullUUID('0016'), 'ergMachineType', pm5Constants.ergMachineType, true) + ] + }) + } +} diff --git a/app/ble/pm5/GapService.js b/app/peripherals/ble/pm5/GapService.js similarity index 52% rename from app/ble/pm5/GapService.js rename to app/peripherals/ble/pm5/GapService.js index f90c42c8b0..168cdd5116 100644 --- a/app/ble/pm5/GapService.js +++ b/app/peripherals/ble/pm5/GapService.js @@ -6,8 +6,8 @@ todo: not sure if this is correct, the normal GAP service has 0x1800 */ import bleno from '@abandonware/bleno' -import { constants, getFullUUID } from './Pm5Constants.js' -import ValueReadCharacteristic from './characteristic/ValueReadCharacteristic.js' +import StaticNotifyCharacteristic from '../common/StaticNotifyCharacteristic.js' +import { getFullUUID, pm5Constants } from './Pm5Constants.js' export default class GapService extends bleno.PrimaryService { constructor () { @@ -16,15 +16,15 @@ export default class GapService extends bleno.PrimaryService { uuid: getFullUUID('0000'), characteristics: [ // GAP device name - new ValueReadCharacteristic('2A00', constants.name), + new StaticNotifyCharacteristic('2A00', undefined, pm5Constants.name, true), // GAP appearance - new ValueReadCharacteristic('2A01', [0x00, 0x00]), + new StaticNotifyCharacteristic('2A01', undefined, [0x00, 0x00], true), // GAP peripheral privacy - new ValueReadCharacteristic('2A02', [0x00]), + new StaticNotifyCharacteristic('2A02', undefined, [0x00], true), // GAP reconnect address - new ValueReadCharacteristic('2A03', '00:00:00:00:00:00'), + new StaticNotifyCharacteristic('2A03', undefined, '00:00:00:00:00:00', true), // Peripheral preferred connection parameters - new ValueReadCharacteristic('2A04', [0x18, 0x00, 0x18, 0x00, 0x00, 0x00, 0xE8, 0x03]) + new StaticNotifyCharacteristic('2A04', undefined, [0x18, 0x00, 0x18, 0x00, 0x00, 0x00, 0xE8, 0x03], true) ] }) } diff --git a/app/ble/pm5/Pm5Constants.js b/app/peripherals/ble/pm5/Pm5Constants.js similarity index 73% rename from app/ble/pm5/Pm5Constants.js rename to app/peripherals/ble/pm5/Pm5Constants.js index e4c352d4f5..c71762126f 100644 --- a/app/ble/pm5/Pm5Constants.js +++ b/app/peripherals/ble/pm5/Pm5Constants.js @@ -1,18 +1,16 @@ 'use strict' + +import { PeripheralConstants } from '../../PeripheralConstants.js' + /* Open Rowing Monitor, https://github.com/laberning/openrowingmonitor Some PM5 specific constants */ -const constants = { - serial: '123456789', - model: 'PM5', - name: 'PM5 123456789 Row', - hardwareRevision: '907', +const pm5Constants = { + ...PeripheralConstants, // See https://www.concept2.com/service/monitors/pm5/firmware for available versions // please note: hardware versions exclude a software version, and thus might confuse the client - firmwareRevision: '210', - manufacturer: 'Concept2', ergMachineType: [0x05] } @@ -23,5 +21,5 @@ function getFullUUID (uuid) { export { getFullUUID, - constants + pm5Constants } diff --git a/app/ble/pm5/Pm5ControlService.js b/app/peripherals/ble/pm5/Pm5ControlService.js similarity index 100% rename from app/ble/pm5/Pm5ControlService.js rename to app/peripherals/ble/pm5/Pm5ControlService.js diff --git a/app/ble/pm5/Pm5RowingService.js b/app/peripherals/ble/pm5/Pm5RowingService.js similarity index 80% rename from app/ble/pm5/Pm5RowingService.js rename to app/peripherals/ble/pm5/Pm5RowingService.js index 8e00cf5f1d..e0fc3842f7 100644 --- a/app/ble/pm5/Pm5RowingService.js +++ b/app/peripherals/ble/pm5/Pm5RowingService.js @@ -20,13 +20,13 @@ */ import bleno from '@abandonware/bleno' import { getFullUUID } from './Pm5Constants.js' -import ValueReadCharacteristic from './characteristic/ValueReadCharacteristic.js' import MultiplexedCharacteristic from './characteristic/MultiplexedCharacteristic.js' import GeneralStatus from './characteristic/GeneralStatus.js' import AdditionalStatus from './characteristic/AdditionalStatus.js' import AdditionalStatus2 from './characteristic/AdditionalStatus2.js' import AdditionalStrokeData from './characteristic/AdditionalStrokeData.js' import StrokeData from './characteristic/StrokeData.js' +import StaticNotifyCharacteristic from '../common/StaticNotifyCharacteristic.js' export default class PM5RowingService extends bleno.PrimaryService { constructor () { @@ -46,23 +46,23 @@ export default class PM5RowingService extends bleno.PrimaryService { // C2 rowing additional status 2 additionalStatus2, // C2 rowing general status and additional status samplerate - new ValueReadCharacteristic(getFullUUID('0034'), 'samplerate', 'samplerate'), + new StaticNotifyCharacteristic(getFullUUID('0034'), 'samplerate', 'samplerate', true), // C2 rowing stroke data strokeData, // C2 rowing additional stroke data additionalStrokeData, // C2 rowing split/interval data - new ValueReadCharacteristic(getFullUUID('0037'), 'split data', 'split data'), + new StaticNotifyCharacteristic(getFullUUID('0037'), 'split data', 'split data', true), // C2 rowing additional split/interval data - new ValueReadCharacteristic(getFullUUID('0038'), 'additional split data', 'additional split data'), + new StaticNotifyCharacteristic(getFullUUID('0038'), 'additional split data', 'additional split data', true), // C2 rowing end of workout summary data - new ValueReadCharacteristic(getFullUUID('0039'), 'workout summary', 'workout summary'), + new StaticNotifyCharacteristic(getFullUUID('0039'), 'workout summary', 'workout summary', true), // C2 rowing end of workout additional summary data - new ValueReadCharacteristic(getFullUUID('003A'), 'additional workout summary', 'additional workout summary'), + new StaticNotifyCharacteristic(getFullUUID('003A'), 'additional workout summary', 'additional workout summary', true), // C2 rowing heart rate belt information - new ValueReadCharacteristic(getFullUUID('003B'), 'heart rate belt information', 'heart rate belt information'), + new StaticNotifyCharacteristic(getFullUUID('003B'), 'heart rate belt information', 'heart rate belt information', true), // C2 force curve data - new ValueReadCharacteristic(getFullUUID('003D'), 'force curve data', 'force curve data'), + new StaticNotifyCharacteristic(getFullUUID('003D'), 'force curve data', 'force curve data', true), // C2 multiplexed information multiplexedCharacteristic ] diff --git a/app/ble/pm5/characteristic/AdditionalStatus.js b/app/peripherals/ble/pm5/characteristic/AdditionalStatus.js similarity index 100% rename from app/ble/pm5/characteristic/AdditionalStatus.js rename to app/peripherals/ble/pm5/characteristic/AdditionalStatus.js diff --git a/app/ble/pm5/characteristic/AdditionalStatus2.js b/app/peripherals/ble/pm5/characteristic/AdditionalStatus2.js similarity index 100% rename from app/ble/pm5/characteristic/AdditionalStatus2.js rename to app/peripherals/ble/pm5/characteristic/AdditionalStatus2.js diff --git a/app/ble/pm5/characteristic/AdditionalStrokeData.js b/app/peripherals/ble/pm5/characteristic/AdditionalStrokeData.js similarity index 100% rename from app/ble/pm5/characteristic/AdditionalStrokeData.js rename to app/peripherals/ble/pm5/characteristic/AdditionalStrokeData.js diff --git a/app/ble/pm5/characteristic/ControlReceive.js b/app/peripherals/ble/pm5/characteristic/ControlReceive.js similarity index 100% rename from app/ble/pm5/characteristic/ControlReceive.js rename to app/peripherals/ble/pm5/characteristic/ControlReceive.js diff --git a/app/ble/pm5/characteristic/ControlTransmit.js b/app/peripherals/ble/pm5/characteristic/ControlTransmit.js similarity index 100% rename from app/ble/pm5/characteristic/ControlTransmit.js rename to app/peripherals/ble/pm5/characteristic/ControlTransmit.js diff --git a/app/ble/pm5/characteristic/GeneralStatus.js b/app/peripherals/ble/pm5/characteristic/GeneralStatus.js similarity index 100% rename from app/ble/pm5/characteristic/GeneralStatus.js rename to app/peripherals/ble/pm5/characteristic/GeneralStatus.js diff --git a/app/ble/pm5/characteristic/MultiplexedCharacteristic.js b/app/peripherals/ble/pm5/characteristic/MultiplexedCharacteristic.js similarity index 100% rename from app/ble/pm5/characteristic/MultiplexedCharacteristic.js rename to app/peripherals/ble/pm5/characteristic/MultiplexedCharacteristic.js diff --git a/app/ble/pm5/characteristic/StrokeData.js b/app/peripherals/ble/pm5/characteristic/StrokeData.js similarity index 100% rename from app/ble/pm5/characteristic/StrokeData.js rename to app/peripherals/ble/pm5/characteristic/StrokeData.js diff --git a/app/server.js b/app/server.js index ecdd20a75c..965f62e937 100644 --- a/app/server.js +++ b/app/server.js @@ -13,12 +13,12 @@ import log from 'loglevel' import config from './tools/ConfigManager.js' import { createRowingStatistics } from './engine/RowingStatistics.js' import { createWebServer } from './WebServer.js' -import { createPeripheralManager } from './ble/PeripheralManager.js' -import { createAntManager } from './ant/AntManager.js' +import { createPeripheralManager } from './peripherals/PeripheralManager.js' // eslint-disable-next-line no-unused-vars import { replayRowingSession } from './tools/RowingRecorder.js' import { createWorkoutRecorder } from './engine/WorkoutRecorder.js' import { createWorkoutUploader } from './engine/WorkoutUploader.js' +import { createAntManager } from './peripherals/ant/AntManager.js' const exec = promisify(child_process.exec) // set the log levels @@ -193,7 +193,7 @@ rowingStatistics.on('rowingStopped', (metrics) => { }) if (config.heartrateMonitorBLE) { - const bleCentralService = child_process.fork('./app/ble/CentralService.js') + const bleCentralService = child_process.fork('./app/peripherals/ble/CentralService.js') bleCentralService.on('message', (heartrateMeasurement) => { rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement) }) From 7ef338d85627d1f95ab33f2f52e5841403f5c966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Mon, 12 Dec 2022 23:20:39 +0100 Subject: [PATCH 045/231] Refactor heart rate peripherals Refactor ANT manager to be a central class managing the ANT stick state. This state could be used for future implementation of other ANT profiles and the peripheralManager should be responsible for creating once needed --- app/engine/RowingStatistics.js | 2 +- app/peripherals/PeripheralManager.js | 24 ++++++++ app/peripherals/ant/AntManager.js | 57 +++++-------------- app/peripherals/ant/HrmPeripheral.js | 51 +++++++++++++++++ .../{CentralService.js => HrmPeripheral.js} | 8 +-- .../HeartRateManager.js} | 32 +++++------ app/server.js | 17 +++--- 7 files changed, 118 insertions(+), 73 deletions(-) create mode 100644 app/peripherals/ant/HrmPeripheral.js rename app/peripherals/ble/{CentralService.js => HrmPeripheral.js} (60%) rename app/peripherals/ble/{CentralManager.js => hrm/HeartRateManager.js} (83%) diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index 616b6cbb8f..f6e2ff8f7d 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -417,7 +417,7 @@ function createRowingStatistics (config) { } return Object.assign(emitter, { - handleHeartrateMeasurement, + handleHeartRateMeasurement: handleHeartrateMeasurement, handleRotationImpulse, setIntervalParameters, pause: pauseTraining, diff --git a/app/peripherals/PeripheralManager.js b/app/peripherals/PeripheralManager.js index 02e54ee410..f518bc7a90 100644 --- a/app/peripherals/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -12,6 +12,9 @@ import log from 'loglevel' import EventEmitter from 'node:events' import { createCpsPeripheral } from './ble/CpsPeripheral.js' import { createCscPeripheral } from './ble/CscPeripheral.js' +import child_process from 'child_process' +import AntManager from './ant/AntManager.js' +import { createAntHrmPeripheral } from './ant/HrmPeripheral.js' const modes = ['FTMS', 'FTMSBIKE', 'PM5', 'CSC', 'CPS'] function createPeripheralManager () { @@ -94,11 +97,32 @@ function createPeripheralManager () { }) } + function startBleHeartRateService () { + const hrmPeripheral = child_process.fork('./app/peripherals/ble/HrmPeripheral.js') + hrmPeripheral.on('message', (heartRateMeasurement) => { + emitter.emit('heartRateBleMeasurement', heartRateMeasurement) + }) + } + + function startAntHeartRateService () { + if (!this._antManager) { + this._antManager = new AntManager() + } + + const antHrm = createAntHrmPeripheral(this._antManager) + + antHrm.on('heartRateMeasurement', (heartRateMeasurement) => { + emitter.emit('heartRateAntMeasurement', heartRateMeasurement) + }) + } + function controlCallback (event) { emitter.emit('control', event) } return Object.assign(emitter, { + startAntHeartRateService, + startBleHeartRateService, getPeripheral, getPeripheralMode, switchPeripheralMode, diff --git a/app/peripherals/ant/AntManager.js b/app/peripherals/ant/AntManager.js index 8a6bcec4d5..3961477d73 100644 --- a/app/peripherals/ant/AntManager.js +++ b/app/peripherals/ant/AntManager.js @@ -9,55 +9,26 @@ - Garmin USB or USB2 ANT+ or an off-brand clone of it (ID 0x1008) - Garmin mini ANT+ (ID 0x1009) */ -import log from 'loglevel' import Ant from 'ant-plus' -import EventEmitter from 'node:events' -function createAntManager () { - const emitter = new EventEmitter() - const antStick = new Ant.GarminStick2() - const antStick3 = new Ant.GarminStick3() +export default class AntManager { + constructor () { // it seems that we have to use two separate heart rate sensors to support both old and new // ant sticks, since the library requires them to be bound before open is called - const heartrateSensor = new Ant.HeartRateSensor(antStick) - const heartrateSensor3 = new Ant.HeartRateSensor(antStick3) - - heartrateSensor.on('hbData', (data) => { - emitter.emit('heartrateMeasurement', { heartrate: data.ComputedHeartRate, batteryLevel: data.BatteryLevel }) - }) - - heartrateSensor3.on('hbData', (data) => { - emitter.emit('heartrateMeasurement', { heartrate: data.ComputedHeartRate, batteryLevel: data.BatteryLevel }) - }) - - antStick.on('startup', () => { - log.info('classic ANT+ stick found') - heartrateSensor.attach(0, 0) - }) - - antStick3.on('startup', () => { - log.info('mini ANT+ stick found') - heartrateSensor3.attach(0, 0) - }) - - antStick.on('shutdown', () => { - log.info('classic ANT+ stick lost') - }) - - antStick3.on('shutdown', () => { - log.info('mini ANT+ stick lost') - }) - - if (!antStick.open()) { - log.debug('classic ANT+ stick NOT found') + this._stick = new Ant.GarminStick3() // 0fcf:1009 + if (!this._stick.is_present()) { + this._stick = new Ant.GarminStick2() // 0fcf:1008 + } } - if (!antStick3.open()) { - log.debug('mini ANT+ stick NOT found') + openAntStick () { + if (!this._stick.open()) { + return false + } + return this._stick } - return Object.assign(emitter, { - }) + getAntStick () { + return this._stick + } } - -export { createAntManager } diff --git a/app/peripherals/ant/HrmPeripheral.js b/app/peripherals/ant/HrmPeripheral.js new file mode 100644 index 0000000000..4f6883d67a --- /dev/null +++ b/app/peripherals/ant/HrmPeripheral.js @@ -0,0 +1,51 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Creates a Bluetooth Low Energy (BLE) Peripheral with all the Services that are required for + a Cycling Speed and Cadence Profile +*/ +import EventEmitter from 'node:events' +import Ant from 'ant-plus' +import log from 'loglevel' + +function createAntHrmPeripheral (antManager) { + const emitter = new EventEmitter() + const antStick = antManager.getAntStick() + + const heartRateSensor = new Ant.HeartRateSensor(antStick) + + heartRateSensor.on('hbData', (data) => { + emitter.emit('heartRateMeasurement', { heartrate: data.ComputedHeartRate, batteryLevel: data.BatteryLevel }) + }) + + antStick.on('startup', () => { + log.info('ANT+ stick found') + heartRateSensor.attach(0, 0) + }) + + antStick.on('shutdown', () => { + log.info('classic ANT+ stick lost') + }) + + if (!antManager.openAntStick()) { + throw new Error('Error opening Ant Stick') + } + + function destroy () { + return new Promise((resolve) => { + heartRateSensor.detach() + heartRateSensor.on('detached', () => { + antStick.removeAllListeners() + heartRateSensor.removeAllListeners() + resolve() + }) + }) + } + + return Object.assign(emitter, { + destroy + }) +} + +export { createAntHrmPeripheral } diff --git a/app/peripherals/ble/CentralService.js b/app/peripherals/ble/HrmPeripheral.js similarity index 60% rename from app/peripherals/ble/CentralService.js rename to app/peripherals/ble/HrmPeripheral.js index 0183d5db07..1916c700d6 100644 --- a/app/peripherals/ble/CentralService.js +++ b/app/peripherals/ble/HrmPeripheral.js @@ -5,14 +5,14 @@ Starts the central manager in a forked thread since noble does not like to run in the same thread as bleno */ -import { createCentralManager } from './CentralManager.js' import process from 'process' import config from '../../tools/ConfigManager.js' import log from 'loglevel' +import { createHeartRateManager } from './hrm/HeartRateManager.js' log.setLevel(config.loglevel.default) -const centralManager = createCentralManager() +const heartRateManager = createHeartRateManager() -centralManager.on('heartrateMeasurement', (heartrateMeasurement) => { - process.send(heartrateMeasurement) +heartRateManager.on('heartRateMeasurement', (heartRateMeasurement) => { + process.send(heartRateMeasurement) }) diff --git a/app/peripherals/ble/CentralManager.js b/app/peripherals/ble/hrm/HeartRateManager.js similarity index 83% rename from app/peripherals/ble/CentralManager.js rename to app/peripherals/ble/hrm/HeartRateManager.js index c21c340447..ae92e208b4 100644 --- a/app/peripherals/ble/CentralManager.js +++ b/app/peripherals/ble/hrm/HeartRateManager.js @@ -49,7 +49,7 @@ NobleBindings.prototype.onDisconnComplete = function (handle, reason) { const noble = new Noble(new NobleBindings()) // END of noble patch -function createCentralManager () { +function createHeartRateManager () { const emitter = new EventEmitter() let batteryLevel @@ -64,10 +64,10 @@ function createCentralManager () { noble.on('discover', (peripheral) => { noble.stopScanning() - connectHeartratePeripheral(peripheral) + connectHeartRatePeripheral(peripheral) }) - function connectHeartratePeripheral (peripheral) { + function connectHeartRatePeripheral (peripheral) { // connect to the heart rate sensor peripheral.connect((error) => { if (error) { @@ -75,7 +75,7 @@ function createCentralManager () { return } log.info(`heart rate peripheral connected, name: '${peripheral.advertisement?.localName}', id: ${peripheral.id}`) - subscribeToHeartrateMeasurement(peripheral) + subscribeToHeartRateMeasurement(peripheral) }) peripheral.once('disconnect', () => { @@ -87,33 +87,33 @@ function createCentralManager () { } // see https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/ - function subscribeToHeartrateMeasurement (peripheral) { - const heartrateMeasurementUUID = '2a37' + function subscribeToHeartRateMeasurement (peripheral) { + const heartRateMeasurementUUID = '2a37' const batteryLevelUUID = '2a19' - peripheral.discoverSomeServicesAndCharacteristics([], [heartrateMeasurementUUID, batteryLevelUUID], + peripheral.discoverSomeServicesAndCharacteristics([], [heartRateMeasurementUUID, batteryLevelUUID], (error, services, characteristics) => { if (error) { log.error(error) return } - const heartrateMeasurementCharacteristic = characteristics.find( - characteristic => characteristic.uuid === heartrateMeasurementUUID + const heartRateMeasurementCharacteristic = characteristics.find( + characteristic => characteristic.uuid === heartRateMeasurementUUID ) const batteryLevelCharacteristic = characteristics.find( characteristic => characteristic.uuid === batteryLevelUUID ) - if (heartrateMeasurementCharacteristic !== undefined) { - heartrateMeasurementCharacteristic.notify(true, (error) => { + if (heartRateMeasurementCharacteristic !== undefined) { + heartRateMeasurementCharacteristic.notify(true, (error) => { if (error) { log.error(error) return } - heartrateMeasurementCharacteristic.on('data', (data, isNotification) => { + heartRateMeasurementCharacteristic.on('data', (data, isNotification) => { const buffer = Buffer.from(data) const flags = buffer.readUInt8(0) // bits of the feature flag: @@ -121,7 +121,7 @@ function createCentralManager () { // 1 + 2: Sensor Contact Status // 3: Energy Expended Status // 4: RR-Interval - const heartrateUint16LE = flags & 0b1 + const heartRateUint16LE = flags & 0b1 // from the specs: // While most human applications require support for only 255 bpm or less, special @@ -129,8 +129,8 @@ function createCentralManager () { // If the Heart Rate Measurement Value is less than or equal to 255 bpm a UINT8 format // should be used for power savings. // If the Heart Rate Measurement Value exceeds 255 bpm a UINT16 format shall be used. - const heartrate = heartrateUint16LE ? buffer.readUInt16LE(1) : buffer.readUInt8(1) - emitter.emit('heartrateMeasurement', { heartrate, batteryLevel }) + const heartrate = heartRateUint16LE ? buffer.readUInt16LE(1) : buffer.readUInt8(1) + emitter.emit('heartRateMeasurement', { heartrate, batteryLevel }) }) }) } @@ -155,4 +155,4 @@ function createCentralManager () { }) } -export { createCentralManager } +export { createHeartRateManager } diff --git a/app/server.js b/app/server.js index 965f62e937..32e449c4e9 100644 --- a/app/server.js +++ b/app/server.js @@ -18,7 +18,6 @@ import { createPeripheralManager } from './peripherals/PeripheralManager.js' import { replayRowingSession } from './tools/RowingRecorder.js' import { createWorkoutRecorder } from './engine/WorkoutRecorder.js' import { createWorkoutUploader } from './engine/WorkoutUploader.js' -import { createAntManager } from './peripherals/ant/AntManager.js' const exec = promisify(child_process.exec) // set the log levels @@ -192,17 +191,17 @@ rowingStatistics.on('rowingStopped', (metrics) => { workoutRecorder.writeRecordings() }) -if (config.heartrateMonitorBLE) { - const bleCentralService = child_process.fork('./app/peripherals/ble/CentralService.js') - bleCentralService.on('message', (heartrateMeasurement) => { - rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement) +if (config.heartRateMonitorBLE) { + peripheralManager.startBleHeartRateService() + peripheralManager.on('heartRateBleMeasurement', (heartRateMeasurement) => { + rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement) }) } -if (config.heartrateMonitorANT) { - const antManager = createAntManager() - antManager.on('heartrateMeasurement', (heartrateMeasurement) => { - rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement) +if (config.heartRateMonitorANT) { + peripheralManager.startAntHeartRateService() + peripheralManager.on('heartRateAntMeasurement', (heartRateMeasurement) => { + rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement) }) } From a3cd6e6f39aaea122800e89036ac88e1ed4c0249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 17 Dec 2022 21:35:22 +0100 Subject: [PATCH 046/231] Add OFF mode to BLE peripheral and refactoring Add OFF mode to be able to turn BLE advertisement off. Rename switchPeripheralMode function to include BLE in order to be more descriptive. --- app/client/components/DashboardActions.js | 8 +-- app/client/lib/app.js | 4 +- app/peripherals/PeripheralManager.js | 71 ++++++++++++----------- app/server.js | 8 +-- config/default.config.js | 1 + 5 files changed, 49 insertions(+), 43 deletions(-) diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index 109cdfa4f4..cde18ec60d 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -65,7 +65,7 @@ export class DashboardActions extends AppElement { return html` ${this.renderOptionalButtons()} - +

${this.dialog ? this.dialog : ''} ` @@ -116,7 +116,7 @@ export class DashboardActions extends AppElement { case 'FTMS': return 'FTMS Rower' default: - return '' + return 'Off' } } @@ -135,8 +135,8 @@ export class DashboardActions extends AppElement { this.sendEvent('triggerAction', { command: 'reset' }) } - switchPeripheralMode () { - this.sendEvent('triggerAction', { command: 'switchPeripheralMode' }) + switchBlePeripheralMode () { + this.sendEvent('triggerAction', { command: 'switchBlePeripheralMode' }) } uploadTraining () { diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 86da9f67b7..5961202a8b 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -130,8 +130,8 @@ export function createApp (app) { function handleAction (action) { switch (action.command) { - case 'switchPeripheralMode': { - if (socket)socket.send(JSON.stringify({ command: 'switchPeripheralMode' })) + case 'switchBlePeripheralMode': { + if (socket)socket.send(JSON.stringify({ command: 'switchBlePeripheralMode' })) break } case 'reset': { diff --git a/app/peripherals/PeripheralManager.js b/app/peripherals/PeripheralManager.js index f518bc7a90..efa8ed203d 100644 --- a/app/peripherals/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -16,83 +16,88 @@ import child_process from 'child_process' import AntManager from './ant/AntManager.js' import { createAntHrmPeripheral } from './ant/HrmPeripheral.js' -const modes = ['FTMS', 'FTMSBIKE', 'PM5', 'CSC', 'CPS'] +const bleModes = ['FTMS', 'FTMSBIKE', 'PM5', 'CSC', 'CPS', 'OFF'] function createPeripheralManager () { const emitter = new EventEmitter() - let peripheral - let mode + let blePeripheral + let bleMode - createPeripheral(config.bluetoothMode) + createBlePeripheral(config.bluetoothMode) - function getPeripheral () { - return peripheral + function getBlePeripheral () { + return blePeripheral } - function getPeripheralMode () { - return mode + function getBlePeripheralMode () { + return bleMode } - function switchPeripheralMode (newMode) { + function switchBlePeripheralMode (newMode) { // if now mode was passed, select the next one from the list if (newMode === undefined) { - newMode = modes[(modes.indexOf(mode) + 1) % modes.length] + newMode = bleModes[(bleModes.indexOf(bleMode) + 1) % bleModes.length] } - createPeripheral(newMode) + createBlePeripheral(newMode) } function notifyMetrics (type, metrics) { - peripheral.notifyData(type, metrics) + blePeripheral.notifyData(type, metrics) } function notifyStatus (status) { - peripheral.notifyStatus(status) + blePeripheral.notifyStatus(status) } - async function createPeripheral (newMode) { - if (peripheral) { - await peripheral.destroy() + async function createBlePeripheral (newMode) { + if (blePeripheral) { + await blePeripheral.destroy() } switch (newMode) { case 'PM5': log.info('bluetooth profile: Concept2 PM5') - peripheral = createPm5Peripheral(controlCallback) - mode = 'PM5' + blePeripheral = createPm5Peripheral(controlCallback) + bleMode = 'PM5' break case 'FTMSBIKE': log.info('bluetooth profile: FTMS Indoor Bike') - peripheral = createFtmsPeripheral(controlCallback, { + blePeripheral = createFtmsPeripheral(controlCallback, { simulateIndoorBike: true }) - mode = 'FTMSBIKE' + bleMode = 'FTMSBIKE' break + case 'CSC': log.info('bluetooth profile: Cycling Speed and Cadence') - peripheral = createCscPeripheral() - mode = 'CSC' + blePeripheral = createCscPeripheral() + bleMode = 'CSC' break + case 'CPS': log.info('bluetooth profile: Cycling Power Meter') - peripheral = createCpsPeripheral() - mode = 'CPS' + blePeripheral = createCpsPeripheral() + bleMode = 'CPS' break case 'FTMS': - default: log.info('bluetooth profile: FTMS Rower') - peripheral = createFtmsPeripheral(controlCallback, { + blePeripheral = createFtmsPeripheral(controlCallback, { simulateIndoorBike: false }) - mode = 'FTMS' + bleMode = 'FTMS' break + + default: + log.info('bluetooth profile: Off') + bleMode = 'OFF' } - peripheral.triggerAdvertising() + if (bleMode.toLocaleLowerCase() !== 'OFF'.toLocaleLowerCase()) { blePeripheral.triggerAdvertising() } emitter.emit('control', { req: { - name: 'peripheralMode', - peripheralMode: mode + name: 'blePeripheralMode', + peripheralMode: bleMode } }) } @@ -123,9 +128,9 @@ function createPeripheralManager () { return Object.assign(emitter, { startAntHeartRateService, startBleHeartRateService, - getPeripheral, - getPeripheralMode, - switchPeripheralMode, + getBlePeripheral, + getBlePeripheralMode, + switchBlePeripheralMode, notifyMetrics, notifyStatus }) diff --git a/app/server.js b/app/server.js index 32e449c4e9..d287559a74 100644 --- a/app/server.js +++ b/app/server.js @@ -98,7 +98,7 @@ peripheralManager.on('control', (event) => { peripheralManager.notifyStatus({ name: 'startedOrResumedByUser' }) event.res = true break - case 'peripheralMode': + case 'blePeripheralMode': webServer.notifyClients('config', getConfig()) event.res = true break @@ -216,8 +216,8 @@ workoutUploader.on('resetWorkout', () => { const webServer = createWebServer() webServer.on('messageReceived', async (message, client) => { switch (message.command) { - case 'switchPeripheralMode': - peripheralManager.switchPeripheralMode() + case 'switchBlePeripheralMode': + peripheralManager.switchBlePeripheralMode() break case 'reset': resetWorkout() @@ -243,7 +243,7 @@ webServer.on('clientConnected', (client) => { // todo: extract this into some kind of state manager function getConfig () { return { - peripheralMode: peripheralManager.getPeripheralMode(), + peripheralMode: peripheralManager.getBlePeripheralMode(), stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret, shutdownEnabled: !!config.shutdownCommand } diff --git a/config/default.config.js b/config/default.config.js index fa16cfa77b..e44371de35 100644 --- a/config/default.config.js +++ b/config/default.config.js @@ -78,6 +78,7 @@ export default { // - FTMSBIKE: The FTMS profile is used by Smart Bike Trainers (please note: the speed and power are still aimed for rowing, NOT for a bike!) // - CPS: The BLE Cycling Power Profile simulates a bike for more modern Garmin watches // - CSC: The BLE Cycling Speed and Cadence Profile simulates a bike for older Garmin watches + // - OFF: Turns Bluetooth advertisement off bluetoothMode: 'FTMS', // Turn this on if you want support for Bluetooth Low Energy heart rate monitors From 17d6a74332175265fd4d90b7788b58797d9def1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sun, 18 Dec 2022 15:19:47 +0100 Subject: [PATCH 047/231] Enable rotation of the heart rate monitor modes Add button to the GUI so the user can switch between heart rate monitor modes (BLE, ANT, OFF). Update peripheralManager to handle switching and implement necessary changes to the structure of the peripheralManager. --- app/client/components/DashboardActions.js | 14 +++-- app/client/lib/app.js | 4 ++ app/client/store/appState.js | 6 +- app/peripherals/PeripheralManager.js | 76 ++++++++++++++++++----- app/peripherals/ant/AntManager.js | 32 ++++++++-- app/peripherals/ant/HrmPeripheral.js | 27 ++++---- app/peripherals/ble/HrmPeripheral.js | 33 +++++++--- app/peripherals/ble/hrm/HrmService.js | 17 +++++ app/server.js | 28 ++++----- config/default.config.js | 14 ++--- 10 files changed, 178 insertions(+), 73 deletions(-) create mode 100644 app/peripherals/ble/hrm/HrmService.js diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index cde18ec60d..d6564dff1b 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -7,7 +7,7 @@ import { AppElement, html, css } from './AppElement.js' import { customElement, state } from 'lit/decorators.js' -import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload } from '../lib/icons.js' +import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload, icon_heartbeat } from '../lib/icons.js' import './AppDialog.js' @customElement('dashboard-actions') @@ -66,7 +66,9 @@ export class DashboardActions extends AppElement { ${this.renderOptionalButtons()} -
${this.peripheralMode()}
+
${this.blePeripheralMode()}
+ +
${this.appState?.config?.hrmPeripheralMode}
${this.dialog ? this.dialog : ''} ` } @@ -101,8 +103,8 @@ export class DashboardActions extends AppElement { return buttons } - peripheralMode () { - const value = this.appState?.config?.peripheralMode + blePeripheralMode () { + const value = this.appState?.config?.blePeripheralMode switch (value) { case 'PM5': @@ -139,6 +141,10 @@ export class DashboardActions extends AppElement { this.sendEvent('triggerAction', { command: 'switchBlePeripheralMode' }) } + switchHrmPeripheralMode () { + this.sendEvent('triggerAction', { command: 'switchHrmMode' }) + } + uploadTraining () { this.dialog = html` diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 5961202a8b..e68071572a 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -134,6 +134,10 @@ export function createApp (app) { if (socket)socket.send(JSON.stringify({ command: 'switchBlePeripheralMode' })) break } + case 'switchHrmMode': { + if (socket)socket.send(JSON.stringify({ command: 'switchHrmMode' })) + break + } case 'reset': { resetFields() if (socket)socket.send(JSON.stringify({ command: 'reset' })) diff --git a/app/client/store/appState.js b/app/client/store/appState.js index 12666d7de6..ef27e1204d 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -11,8 +11,10 @@ export const APP_STATE = { // contains all the rowing metrics that are delivered from the backend metrics: {}, config: { - // currently can be FTMS, FTMSBIKE, PM5, CSC, CPS - peripheralMode: '', + // currently can be FTMS, FTMSBIKE, PM5, CSC, CPS, OFF + blePeripheralMode: '', + // currently can be ANT, BLE, OFF + hrmPeripheralMode: '', // true if upload to strava is enabled stravaUploadEnabled: false, // true if remote device shutdown is enabled diff --git a/app/peripherals/PeripheralManager.js b/app/peripherals/PeripheralManager.js index efa8ed203d..803985438f 100644 --- a/app/peripherals/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -12,17 +12,23 @@ import log from 'loglevel' import EventEmitter from 'node:events' import { createCpsPeripheral } from './ble/CpsPeripheral.js' import { createCscPeripheral } from './ble/CscPeripheral.js' -import child_process from 'child_process' import AntManager from './ant/AntManager.js' import { createAntHrmPeripheral } from './ant/HrmPeripheral.js' +import { createBleHrmPeripheral } from './ble/HrmPeripheral.js' const bleModes = ['FTMS', 'FTMSBIKE', 'PM5', 'CSC', 'CPS', 'OFF'] +const hrmModes = ['ANT', 'BLE', 'OFF'] function createPeripheralManager () { const emitter = new EventEmitter() + let _antManager let blePeripheral let bleMode + let hrmPeripheral + let hrmMode + createBlePeripheral(config.bluetoothMode) + createHrmPeripheral(config.heartRateMode) function getBlePeripheral () { return blePeripheral @@ -32,6 +38,14 @@ function createPeripheralManager () { return bleMode } + function getHrmPeripheral () { + return hrmPeripheral + } + + function getHrmPeripheralMode () { + return hrmMode + } + function switchBlePeripheralMode (newMode) { // if now mode was passed, select the next one from the list if (newMode === undefined) { @@ -102,22 +116,55 @@ function createPeripheralManager () { }) } - function startBleHeartRateService () { - const hrmPeripheral = child_process.fork('./app/peripherals/ble/HrmPeripheral.js') - hrmPeripheral.on('message', (heartRateMeasurement) => { - emitter.emit('heartRateBleMeasurement', heartRateMeasurement) - }) + function switchHrmMode (newMode) { + if (newMode === undefined) { + newMode = hrmModes[(hrmModes.indexOf(hrmMode) + 1) % hrmModes.length] + } + createHrmPeripheral(newMode) } - function startAntHeartRateService () { - if (!this._antManager) { - this._antManager = new AntManager() + async function createHrmPeripheral (newMode) { + if (hrmPeripheral) { + await hrmPeripheral.destroy() + hrmPeripheral.removeAllListeners() + + if (_antManager && newMode !== 'ANT') { await _antManager.closeAntStick() } } - const antHrm = createAntHrmPeripheral(this._antManager) + switch (newMode) { + case 'ANT': + log.info('heart rate profile: ANT') + if (!_antManager) { + _antManager = new AntManager() + } + + hrmPeripheral = createAntHrmPeripheral(_antManager) + hrmMode = 'ANT' + await hrmPeripheral.attach() + break - antHrm.on('heartRateMeasurement', (heartRateMeasurement) => { - emitter.emit('heartRateAntMeasurement', heartRateMeasurement) + case 'BLE': + log.info('heart rate profile: BLE') + hrmPeripheral = createBleHrmPeripheral() + hrmMode = 'BLE' + break + + default: + log.info('heart rate profile: Off') + hrmMode = 'OFF' + } + + if (hrmMode.toLocaleLowerCase() !== 'OFF'.toLocaleLowerCase()) { + hrmPeripheral.on('heartRateMeasurement', (heartRateMeasurement) => { + emitter.emit('heartRateMeasurement', heartRateMeasurement) + }) + } + + emitter.emit('control', { + req: { + name: 'hrmPeripheralMode', + peripheralMode: hrmMode + } }) } @@ -126,10 +173,11 @@ function createPeripheralManager () { } return Object.assign(emitter, { - startAntHeartRateService, - startBleHeartRateService, getBlePeripheral, getBlePeripheralMode, + getHrmPeripheral, + getHrmPeripheralMode, + switchHrmMode, switchBlePeripheralMode, notifyMetrics, notifyStatus diff --git a/app/peripherals/ant/AntManager.js b/app/peripherals/ant/AntManager.js index 3961477d73..d268c54afe 100644 --- a/app/peripherals/ant/AntManager.js +++ b/app/peripherals/ant/AntManager.js @@ -9,9 +9,12 @@ - Garmin USB or USB2 ANT+ or an off-brand clone of it (ID 0x1008) - Garmin mini ANT+ (ID 0x1009) */ +import log from 'loglevel' import Ant from 'ant-plus' export default class AntManager { + _isStickOpen = false + constructor () { // it seems that we have to use two separate heart rate sensors to support both old and new // ant sticks, since the library requires them to be bound before open is called @@ -22,10 +25,31 @@ export default class AntManager { } openAntStick () { - if (!this._stick.open()) { - return false - } - return this._stick + return new Promise((resolve, reject) => { + if (!this._stick.open()) { + reject(new Error('Error opening Ant Stick')) + } + this._stick.once('startup', () => { + log.info('ANT+ stick found') + this._isStickOpen = true + resolve(this._stick) + }) + }) + } + + closeAntStick () { + return new Promise(resolve => { + this._stick.once('shutdown', () => { + log.info('ANT+ stick is closed') + this._isStickOpen = false + resolve() + }) + this._stick.close() + }) + } + + isStickOpen () { + return this._isStickOpen } getAntStick () { diff --git a/app/peripherals/ant/HrmPeripheral.js b/app/peripherals/ant/HrmPeripheral.js index 4f6883d67a..e85ea664cf 100644 --- a/app/peripherals/ant/HrmPeripheral.js +++ b/app/peripherals/ant/HrmPeripheral.js @@ -7,7 +7,6 @@ */ import EventEmitter from 'node:events' import Ant from 'ant-plus' -import log from 'loglevel' function createAntHrmPeripheral (antManager) { const emitter = new EventEmitter() @@ -19,32 +18,28 @@ function createAntHrmPeripheral (antManager) { emitter.emit('heartRateMeasurement', { heartrate: data.ComputedHeartRate, batteryLevel: data.BatteryLevel }) }) - antStick.on('startup', () => { - log.info('ANT+ stick found') - heartRateSensor.attach(0, 0) - }) - - antStick.on('shutdown', () => { - log.info('classic ANT+ stick lost') - }) - - if (!antManager.openAntStick()) { - throw new Error('Error opening Ant Stick') + function attach () { + return new Promise(resolve => { + heartRateSensor.once('attached', () => { + resolve() + }) + heartRateSensor.attach(0, 0) + }) } function destroy () { return new Promise((resolve) => { - heartRateSensor.detach() - heartRateSensor.on('detached', () => { - antStick.removeAllListeners() + heartRateSensor.once('detached', () => { heartRateSensor.removeAllListeners() resolve() }) + heartRateSensor.detach() }) } return Object.assign(emitter, { - destroy + destroy, + attach }) } diff --git a/app/peripherals/ble/HrmPeripheral.js b/app/peripherals/ble/HrmPeripheral.js index 1916c700d6..cd3de2767b 100644 --- a/app/peripherals/ble/HrmPeripheral.js +++ b/app/peripherals/ble/HrmPeripheral.js @@ -5,14 +5,29 @@ Starts the central manager in a forked thread since noble does not like to run in the same thread as bleno */ -import process from 'process' -import config from '../../tools/ConfigManager.js' -import log from 'loglevel' -import { createHeartRateManager } from './hrm/HeartRateManager.js' +import EventEmitter from 'node:events' +import child_process from 'child_process' -log.setLevel(config.loglevel.default) -const heartRateManager = createHeartRateManager() +function createBleHrmPeripheral () { + const emitter = new EventEmitter() -heartRateManager.on('heartRateMeasurement', (heartRateMeasurement) => { - process.send(heartRateMeasurement) -}) + const bleHrmProcess = child_process.fork('./app/peripherals/ble/hrm/HrmService.js') + + bleHrmProcess.on('message', (heartRateMeasurement) => { + emitter.emit('heartRateMeasurement', heartRateMeasurement) + }) + + function destroy () { + return new Promise(resolve => { + bleHrmProcess.kill() + bleHrmProcess.removeAllListeners() + resolve() + }) + } + + return Object.assign(emitter, { + destroy + }) +} + +export { createBleHrmPeripheral } diff --git a/app/peripherals/ble/hrm/HrmService.js b/app/peripherals/ble/hrm/HrmService.js new file mode 100644 index 0000000000..518af51d39 --- /dev/null +++ b/app/peripherals/ble/hrm/HrmService.js @@ -0,0 +1,17 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Starts the central manager in a forked thread since noble does not like + to run in the same thread as bleno +*/ +import process from 'process' +import log from 'loglevel' +import config from '../../../tools/ConfigManager.js' +import { createHeartRateManager } from './HeartRateManager.js' + +log.setLevel(config.loglevel.default) +const heartRateManager = createHeartRateManager() +heartRateManager.on('heartRateMeasurement', (heartRateMeasurement) => { + process.send(heartRateMeasurement) +}) diff --git a/app/server.js b/app/server.js index d287559a74..7ecef8652e 100644 --- a/app/server.js +++ b/app/server.js @@ -102,11 +102,19 @@ peripheralManager.on('control', (event) => { webServer.notifyClients('config', getConfig()) event.res = true break + case 'hrmPeripheralMode': + webServer.notifyClients('config', getConfig()) + event.res = true + break default: log.info('unhandled Command', event.req) } }) +peripheralManager.on('heartRateMeasurement', (heartRateMeasurement) => { + rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement) +}) + function pauseWorkout () { rowingStatistics.pause() } @@ -191,20 +199,6 @@ rowingStatistics.on('rowingStopped', (metrics) => { workoutRecorder.writeRecordings() }) -if (config.heartRateMonitorBLE) { - peripheralManager.startBleHeartRateService() - peripheralManager.on('heartRateBleMeasurement', (heartRateMeasurement) => { - rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement) - }) -} - -if (config.heartRateMonitorANT) { - peripheralManager.startAntHeartRateService() - peripheralManager.on('heartRateAntMeasurement', (heartRateMeasurement) => { - rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement) - }) -} - workoutUploader.on('authorizeStrava', (data, client) => { webServer.notifyClient(client, 'authorizeStrava', data) }) @@ -219,6 +213,9 @@ webServer.on('messageReceived', async (message, client) => { case 'switchBlePeripheralMode': peripheralManager.switchBlePeripheralMode() break + case 'switchHrmMode': + peripheralManager.switchHrmMode() + break case 'reset': resetWorkout() break @@ -243,7 +240,8 @@ webServer.on('clientConnected', (client) => { // todo: extract this into some kind of state manager function getConfig () { return { - peripheralMode: peripheralManager.getBlePeripheralMode(), + blePeripheralMode: peripheralManager.getBlePeripheralMode(), + hrmPeripheralMode: peripheralManager.getHrmPeripheralMode(), stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret, shutdownEnabled: !!config.shutdownCommand } diff --git a/config/default.config.js b/config/default.config.js index e44371de35..94efe8616f 100644 --- a/config/default.config.js +++ b/config/default.config.js @@ -81,15 +81,11 @@ export default { // - OFF: Turns Bluetooth advertisement off bluetoothMode: 'FTMS', - // Turn this on if you want support for Bluetooth Low Energy heart rate monitors - // Will currenty connect to the first device found - heartrateMonitorBLE: true, - - // Turn this on if you want support for ANT+ heart rate monitors - // You will need an ANT+ USB stick for this to work, the following models might work: - // - Garmin USB or USB2 ANT+ or an off-brand clone of it (ID 0x1008) - // - Garmin mini ANT+ (ID 0x1009) - heartrateMonitorANT: false, + // Selects the heart rate monitor mode. Supported modes: + // - BLE: Use Bluetooth Low Energy to connect Heart Rate Monitor (Will currently connect to the first device found) + // - ANT: Use Ant+ to connect Heart Rate Monitor + // - OFF: turns of Heart Rate Monitor discovery + heartRateMode: 'BLE', // Defines the name that is used to announce the FTMS Rower via Bluetooth Low Energy (BLE) // Some rowing training applications expect that the rowing device is announced with a certain name From 3bb0229d3a629bff96e36eff378d6a0b28db5674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sun, 18 Dec 2022 23:50:06 +0100 Subject: [PATCH 048/231] Move to new ant-plus library Move to a new, still maintained, ant-plus library that solves the connection dropout issue experienced with the original when using HRM as well as that provides a cleaner interface to manage the ANT device. --- app/peripherals/ant/AntManager.js | 43 +++++++++------------------- app/peripherals/ant/HrmPeripheral.js | 38 ++++++++++++------------ package.json | 2 +- 3 files changed, 33 insertions(+), 50 deletions(-) diff --git a/app/peripherals/ant/AntManager.js b/app/peripherals/ant/AntManager.js index d268c54afe..c4a538414c 100644 --- a/app/peripherals/ant/AntManager.js +++ b/app/peripherals/ant/AntManager.js @@ -10,42 +10,27 @@ - Garmin mini ANT+ (ID 0x1009) */ import log from 'loglevel' -import Ant from 'ant-plus' +import { AntDevice } from 'incyclist-ant-plus/lib/bindings/index.js' export default class AntManager { _isStickOpen = false + _stick = new AntDevice({ startupTimeout: 2000 }) - constructor () { - // it seems that we have to use two separate heart rate sensors to support both old and new - // ant sticks, since the library requires them to be bound before open is called - this._stick = new Ant.GarminStick3() // 0fcf:1009 - if (!this._stick.is_present()) { - this._stick = new Ant.GarminStick2() // 0fcf:1008 - } - } + async openAntStick () { + if (this._isStickOpen) return + if (!(await this._stick.open())) { throw (new Error('Error opening Ant Stick')) } - openAntStick () { - return new Promise((resolve, reject) => { - if (!this._stick.open()) { - reject(new Error('Error opening Ant Stick')) - } - this._stick.once('startup', () => { - log.info('ANT+ stick found') - this._isStickOpen = true - resolve(this._stick) - }) - }) + log.info('ANT+ stick found') + this._isStickOpen = true } - closeAntStick () { - return new Promise(resolve => { - this._stick.once('shutdown', () => { - log.info('ANT+ stick is closed') - this._isStickOpen = false - resolve() - }) - this._stick.close() - }) + async closeAntStick () { + if (!this._isStickOpen) return + + if (!(await this._stick.close())) { throw (new Error('Error closing Ant Stick')) } + + log.info('ANT+ stick is closed') + this._isStickOpen = false } isStickOpen () { diff --git a/app/peripherals/ant/HrmPeripheral.js b/app/peripherals/ant/HrmPeripheral.js index e85ea664cf..99fe289d7e 100644 --- a/app/peripherals/ant/HrmPeripheral.js +++ b/app/peripherals/ant/HrmPeripheral.js @@ -6,35 +6,33 @@ a Cycling Speed and Cadence Profile */ import EventEmitter from 'node:events' -import Ant from 'ant-plus' +import log from 'loglevel' +import { HeartRateSensor } from 'incyclist-ant-plus' function createAntHrmPeripheral (antManager) { const emitter = new EventEmitter() const antStick = antManager.getAntStick() + const heartRateSensor = new HeartRateSensor(0) - const heartRateSensor = new Ant.HeartRateSensor(antStick) + async function attach () { + if (!antManager.isStickOpen()) { await antManager.openAntStick() } + this.channel = await antStick.getChannel() - heartRateSensor.on('hbData', (data) => { - emitter.emit('heartRateMeasurement', { heartrate: data.ComputedHeartRate, batteryLevel: data.BatteryLevel }) - }) - - function attach () { - return new Promise(resolve => { - heartRateSensor.once('attached', () => { - resolve() - }) - heartRateSensor.attach(0, 0) + this.channel.on('data', (profile, deviceID, data) => { + emitter.emit('heartRateMeasurement', { heartrate: data.ComputedHeartRate, batteryLevel: data.BatteryLevel }) }) + + if (!(await this.channel.startSensor(heartRateSensor))) { + log.error('Could not start ANT+ heart rate sensor') + } } - function destroy () { - return new Promise((resolve) => { - heartRateSensor.once('detached', () => { - heartRateSensor.removeAllListeners() - resolve() - }) - heartRateSensor.detach() - }) + async function destroy () { + if (!this.channel) { + log.debug('Ant Sensor does not seem to be running') + return + } + await this.channel.stopSensor(heartRateSensor) } return Object.assign(emitter, { diff --git a/package.json b/package.json index e59db4080f..d1fbe6e2c9 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,9 @@ "dependencies": { "@abandonware/bleno": "0.5.1-4", "@abandonware/noble": "1.9.2-15", - "ant-plus": "0.1.24", "finalhandler": "1.1.2", "form-data": "4.0.0", + "incyclist-ant-plus": "^0.1.15", "lit": "2.1.3", "loglevel": "1.8.0", "nosleep.js": "0.12.0", From e938f5d8a00f068234119c5011db871c37b47ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Tue, 20 Dec 2022 09:46:28 +0100 Subject: [PATCH 049/231] Fix app crash when bleMode is set to OFF initially Add guard clauses to notifyMetric call if bleMode is off. Set blePeripheral and hrmPeripheral to undefined to avoid repeated call to destroy() (i.e. calling destroy() on an already destroyed peripheral) --- app/peripherals/PeripheralManager.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/peripherals/PeripheralManager.js b/app/peripherals/PeripheralManager.js index 803985438f..9dcb4e2b0c 100644 --- a/app/peripherals/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -55,16 +55,17 @@ function createPeripheralManager () { } function notifyMetrics (type, metrics) { - blePeripheral.notifyData(type, metrics) + if (bleMode !== 'OFF') { blePeripheral.notifyData(type, metrics) } } function notifyStatus (status) { - blePeripheral.notifyStatus(status) + if (bleMode !== 'OFF') { blePeripheral.notifyStatus(status) } } async function createBlePeripheral (newMode) { if (blePeripheral) { await blePeripheral.destroy() + blePeripheral = undefined } switch (newMode) { @@ -127,7 +128,7 @@ function createPeripheralManager () { if (hrmPeripheral) { await hrmPeripheral.destroy() hrmPeripheral.removeAllListeners() - + hrmPeripheral = undefined if (_antManager && newMode !== 'ANT') { await _antManager.closeAntStick() } } From 4f62b2322e3f65109333dbebddee9297770f38d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Wed, 21 Dec 2022 13:01:58 +0100 Subject: [PATCH 050/231] Add ANT+ profile manager - Implement an ANT+ profile manager similar to BLE - Implement ANT+ Fitness Equipment profile to be able to broadcast data - Add button to the UI to scroll through the ANT+ profiles --- app/client/components/DashboardActions.js | 8 +- app/client/lib/app.js | 4 + app/client/lib/icons.js | 2 + app/client/store/appState.js | 2 + app/peripherals/PeripheralManager.js | 61 +++++- app/peripherals/ant/AntManager.js | 2 +- app/peripherals/ant/FEPeripheral.js | 218 ++++++++++++++++++++++ app/server.js | 8 + config/default.config.js | 7 +- 9 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 app/peripherals/ant/FEPeripheral.js diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index d6564dff1b..6cf0c91c10 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -7,7 +7,7 @@ import { AppElement, html, css } from './AppElement.js' import { customElement, state } from 'lit/decorators.js' -import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload, icon_heartbeat } from '../lib/icons.js' +import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload, icon_heartbeat, icon_antplus } from '../lib/icons.js' import './AppDialog.js' @customElement('dashboard-actions') @@ -69,6 +69,8 @@ export class DashboardActions extends AppElement {
${this.blePeripheralMode()}
${this.appState?.config?.hrmPeripheralMode}
+ +
${this.appState?.config?.antPeripheralMode}
${this.dialog ? this.dialog : ''} ` } @@ -141,6 +143,10 @@ export class DashboardActions extends AppElement { this.sendEvent('triggerAction', { command: 'switchBlePeripheralMode' }) } + switchAntPeripheralMode () { + this.sendEvent('triggerAction', { command: 'switchAntPeripheralMode' }) + } + switchHrmPeripheralMode () { this.sendEvent('triggerAction', { command: 'switchHrmMode' }) } diff --git a/app/client/lib/app.js b/app/client/lib/app.js index e68071572a..c4104893c9 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -134,6 +134,10 @@ export function createApp (app) { if (socket)socket.send(JSON.stringify({ command: 'switchBlePeripheralMode' })) break } + case 'switchAntPeripheralMode': { + if (socket)socket.send(JSON.stringify({ command: 'switchAntPeripheralMode' })) + break + } case 'switchHrmMode': { if (socket)socket.send(JSON.stringify({ command: 'switchHrmMode' })) break diff --git a/app/client/lib/icons.js b/app/client/lib/icons.js index 23b9a75668..e23328061b 100644 --- a/app/client/lib/icons.js +++ b/app/client/lib/icons.js @@ -25,3 +25,5 @@ export const icon_expand = svg`` export const icon_bluetooth = svg`` export const icon_upload = svg`` + +export const icon_antplus = svg`` diff --git a/app/client/store/appState.js b/app/client/store/appState.js index ef27e1204d..77c78e2def 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -15,6 +15,8 @@ export const APP_STATE = { blePeripheralMode: '', // currently can be ANT, BLE, OFF hrmPeripheralMode: '', + // currently can be FE, OFF + antPeripheralMode: '', // true if upload to strava is enabled stravaUploadEnabled: false, // true if remote device shutdown is enabled diff --git a/app/peripherals/PeripheralManager.js b/app/peripherals/PeripheralManager.js index 9dcb4e2b0c..afba110164 100644 --- a/app/peripherals/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -15,8 +15,10 @@ import { createCscPeripheral } from './ble/CscPeripheral.js' import AntManager from './ant/AntManager.js' import { createAntHrmPeripheral } from './ant/HrmPeripheral.js' import { createBleHrmPeripheral } from './ble/HrmPeripheral.js' +import { createFEPeripheral } from './ant/FEPeripheral.js' const bleModes = ['FTMS', 'FTMSBIKE', 'PM5', 'CSC', 'CPS', 'OFF'] +const antModes = ['FE', 'OFF'] const hrmModes = ['ANT', 'BLE', 'OFF'] function createPeripheralManager () { const emitter = new EventEmitter() @@ -24,11 +26,15 @@ function createPeripheralManager () { let blePeripheral let bleMode + let antPeripheral + let antMode + let hrmPeripheral let hrmMode createBlePeripheral(config.bluetoothMode) createHrmPeripheral(config.heartRateMode) + createAntPeripheral(config.antplusMode) function getBlePeripheral () { return blePeripheral @@ -38,6 +44,14 @@ function createPeripheralManager () { return bleMode } + function getAntPeripheral () { + return antPeripheral + } + + function getAntPeripheralMode () { + return antMode + } + function getHrmPeripheral () { return hrmPeripheral } @@ -56,10 +70,12 @@ function createPeripheralManager () { function notifyMetrics (type, metrics) { if (bleMode !== 'OFF') { blePeripheral.notifyData(type, metrics) } + if (antMode !== 'OFF') { antPeripheral.notifyData(type, metrics) } } function notifyStatus (status) { if (bleMode !== 'OFF') { blePeripheral.notifyStatus(status) } + if (antMode !== 'OFF') { antPeripheral.notifyStatus(status) } } async function createBlePeripheral (newMode) { @@ -117,6 +133,46 @@ function createPeripheralManager () { }) } + function switchAntPeripheralMode (newMode) { + if (newMode === undefined) { + newMode = antModes[(antModes.indexOf(antMode) + 1) % antModes.length] + } + createAntPeripheral(newMode) + } + + async function createAntPeripheral (newMode) { + if (antPeripheral) { + await antPeripheral.destroy() + antPeripheral = undefined + + if (_antManager && hrmMode !== 'ANT' && newMode === 'OFF') { await _antManager.closeAntStick() } + } + + switch (newMode) { + case 'FE': + log.info('ant plus profile: FE') + if (!_antManager) { + _antManager = new AntManager() + } + + antPeripheral = createFEPeripheral(_antManager) + antMode = 'FE' + await antPeripheral.attach() + break + + default: + log.info('ant plus profile: Off') + antMode = 'OFF' + } + + emitter.emit('control', { + req: { + name: 'antPeripheralMode', + peripheralMode: antMode + } + }) + } + function switchHrmMode (newMode) { if (newMode === undefined) { newMode = hrmModes[(hrmModes.indexOf(hrmMode) + 1) % hrmModes.length] @@ -129,7 +185,7 @@ function createPeripheralManager () { await hrmPeripheral.destroy() hrmPeripheral.removeAllListeners() hrmPeripheral = undefined - if (_antManager && newMode !== 'ANT') { await _antManager.closeAntStick() } + if (_antManager && newMode !== 'ANT' && antMode === 'OFF') { await _antManager.closeAntStick() } } switch (newMode) { @@ -176,10 +232,13 @@ function createPeripheralManager () { return Object.assign(emitter, { getBlePeripheral, getBlePeripheralMode, + getAntPeripheral, + getAntPeripheralMode, getHrmPeripheral, getHrmPeripheralMode, switchHrmMode, switchBlePeripheralMode, + switchAntPeripheralMode, notifyMetrics, notifyStatus }) diff --git a/app/peripherals/ant/AntManager.js b/app/peripherals/ant/AntManager.js index c4a538414c..e6759d4b3b 100644 --- a/app/peripherals/ant/AntManager.js +++ b/app/peripherals/ant/AntManager.js @@ -10,7 +10,7 @@ - Garmin mini ANT+ (ID 0x1009) */ import log from 'loglevel' -import { AntDevice } from 'incyclist-ant-plus/lib/bindings/index.js' +import { AntDevice } from 'incyclist-ant-plus/lib/ant-device.js' export default class AntManager { _isStickOpen = false diff --git a/app/peripherals/ant/FEPeripheral.js b/app/peripherals/ant/FEPeripheral.js new file mode 100644 index 0000000000..86e89a9319 --- /dev/null +++ b/app/peripherals/ant/FEPeripheral.js @@ -0,0 +1,218 @@ +'use strict' + +import log from 'loglevel' +import { Messages } from 'incyclist-ant-plus' +import { PeripheralConstants } from '../PeripheralConstants.js' + +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Creates a Bluetooth Low Energy (BLE) Peripheral with all the Services that are required for + a Cycling Speed and Cadence Profile +*/ + +function createFEPeripheral (antManager) { + const antStick = antManager.getAntStick() + const deviceType = 0x11 // Ant FE-C device + const deviceNumber = 1 + const deviceId = parseInt(PeripheralConstants.serial, 10) & 0xFFFF + const channel = 1 + const broadcastPeriod = 8192 // 8192/32768 ~4hz + const broadcastInterval = broadcastPeriod / 32768 * 1000 // millisecond + const rfChannel = 57 // 2457 MHz + let dataPageCount = 0 + let commonPageCount = 0 + let timer + + let sessionData = { + accumulatedStrokes: 0, + accumulatedDistance: 0, + accumulatedTime: 0, + accumulatedPower: 0, + cycleLinearVelocity: 0, + strokeRate: 0, + instantaneousPower: 0, + distancePerStroke: 0, + fitnessEquipmentState: fitnessEquipmentStates.inUse, + sessionStatus: 'WaitingForStart' + } + + async function attach () { + if (!antManager.isStickOpen()) { await antManager.openAntStick() } + + const messages = [ + Messages.assignChannel(channel, 'transmit'), + Messages.setDevice(channel, deviceId, deviceType, deviceNumber), + Messages.setFrequency(channel, rfChannel), + Messages.setPeriod(channel, broadcastPeriod), + Messages.openChannel(channel) + ] + + log.info(`ANT+ FE server start [deviceId=${deviceId} channel=${channel}]`) + for (const message of messages) { + antStick.write(message) + } + + timer = setInterval(onBroadcastInterval, broadcastInterval) + } + + function destroy () { + return new Promise((resolve) => { + clearInterval(timer) + log.info(`ANT+ FE server stopped [deviceId=${deviceId} channel=${channel}]`) + + const messages = [ + Messages.closeChannel(channel), + Messages.unassignChannel(channel) + ] + for (const message of messages) { + antStick.write(message) + } + resolve() + }) + } + + function onBroadcastInterval () { + dataPageCount++ + let data + + switch (true) { + case dataPageCount === 65 || dataPageCount === 66: + if (commonPageCount % 2 === 0) { // 0x50 - Common Page for Manufacturers Identification (approx twice a minute) + data = [ + channel, + 0x50, // Page 80 + 0xFF, // Reserved + 0xFF, // Reserved + parseInt(PeripheralConstants.hardwareRevision, 10) & 0xFF, // Hardware Revision + ...Messages.intToLEHexArray(40, 2), // Manufacturer ID (value 255 = Development ID, value 40 = concept2) + 0x0001 // Model Number + ] + } + if (commonPageCount % 2 === 1) { // 0x51 - Common Page for Product Information (approx twice a minute) + data = [ + channel, + 0x51, // Page 81 + 0xFF, // Reserved + parseInt(PeripheralConstants.firmwareRevision.slice(-2), 10), // SW Revision (Supplemental) + parseInt(PeripheralConstants.firmwareRevision[0], 10), // SW Version + ...Messages.intToLEHexArray(parseInt(PeripheralConstants.serial, 10), 4) // Serial Number (None) + ] + } + + if (dataPageCount === 66) { + commonPageCount++ + dataPageCount = 0 + } + break + case dataPageCount % 8 === 4: // 0x11 - General Settings Page (once a second) + case dataPageCount % 8 === 7: + data = [ + channel, + 0x11, // Page 17 + 0xFF, // Reserved + 0xFF, // Reserved + ...Messages.intToLEHexArray(sessionData.distancePerStroke, 1), // Stroke Length in 0.01 m + 0x7FFF, // Incline (Not Used) + 0x00, // Resistance (DF may be reported if conversion to the % is worked out (value in % with a resolution of 0.5%). + ...Messages.intToLEHexArray(feCapabilitiesBitField, 1) + ] + if (sessionData.sessionStatus === 'Rowing') { + log.debug(`Page 17 Data Sent. Event=${dataPageCount}. Stroke Length=${sessionData.distancePerStroke}.`) + log.debug(`Hex Stroke Length=0x${sessionData.distancePerStroke.toString(16)}.`) + } + break + case dataPageCount % 8 === 3: // 0x16 - Specific Rower Data (once a second) + case dataPageCount % 8 === 0: + data = [ + channel, + 0x16, // Page 22 + 0xFF, // Reserved + 0xFF, // Reserved + ...Messages.intToLEHexArray(sessionData.accumulatedStrokes, 1), // Stroke Count + ...Messages.intToLEHexArray(sessionData.strokeRate, 1), // Cadence / Stroke Rate + ...Messages.intToLEHexArray(sessionData.instantaneousPower, 2), // Instant Power (2 bytes) + ...Messages.intToLEHexArray((sessionData.fitnessEquipmentState + rowingCapabilitiesBitField), 1) + ] + if (sessionData.sessionStatus === 'Rowing') { + log.debug(`Page 22 Data Sent. Event=${dataPageCount}. Strokes=${sessionData.accumulatedStrokes}. Stroke Rate=${sessionData.strokeRate}. Power=${sessionData.instantaneousPower}`) + log.debug(`Hex Strokes=0x${sessionData.accumulatedStrokes.toString(16)}. Hex Stroke Rate=0x${sessionData.strokeRate.toString(16)}. Hex Power=0x${Messages.intToLEHexArray(sessionData.instantaneousPower, 2)}.`) + } + break + case dataPageCount % 4 === 2: // 0x10 - General FE Data (twice a second) + default: + data = [ + channel, + 0x10, // Page 16 + 0x16, // Rowing Machine (22) + ...Messages.intToLEHexArray(sessionData.accumulatedTime, 1), // elapsed time + ...Messages.intToLEHexArray(sessionData.accumulatedDistance, 1), // distance travelled + ...Messages.intToLEHexArray(sessionData.cycleLinearVelocity, 2), // speed in 0.001 m/s + 0xFF, // heart rate not being sent + ...Messages.intToLEHexArray((sessionData.fitnessEquipmentState + feCapabilitiesBitField), 1) + ] + if (sessionData.sessionStatus === 'Rowing') { + log.debug(`Page 16 Data Sent. Event=${dataPageCount}. Time=${sessionData.accumulatedTime}. Distance=${sessionData.accumulatedDistance}. Speed=${sessionData.cycleLinearVelocity}.`) + log.debug(`Hex Time=0x${sessionData.accumulatedTime.toString(16)}. Hex Distance=0x${sessionData.accumulatedDistance.toString(16)}. Hex Speed=0x${Messages.intToLEHexArray(sessionData.cycleLinearVelocity, 2)}.`) + } + break + } + + const message = Messages.broadcastData(data) + antStick.write(message) + } + + function notifyData (type, data) { + if (type === 'strokeFinished' || type === 'metricsUpdate') { + sessionData = { + ...sessionData, + accumulatedDistance: data.totalLinearDistance & 0xFF, + accumulatedStrokes: data.totalNumberOfStrokes & 0xFF, + accumulatedTime: Math.trunc(data.totalMovingTime * 4) & 0xFF, + cycleLinearVelocity: Math.round(data.cycleLinearVelocity * 1000), + strokeRate: Math.round(data.cycleStrokeRate) & 0xFF, + instantaneousPower: Math.round(data.cyclePower) & 0xFFFF, + distancePerStroke: Math.round(data.cycleDistance * 100), + sessionStatus: data.sessionStatus + } + } + } + + // FE does not have status characteristic + function notifyStatus (status) { + } + + return { + notifyData, + notifyStatus, + attach, + destroy + } +} + +const fitnessEquipmentStates = { + asleep: (1 << 0x04), + ready: (2 << 0x04), + inUse: (3 << 0x04), + finished: (4 << 0x04), + lapToggleBit: (8 << 0x04) +} + +const fitnessEquipmentCapabilities = { + hrDataSourceHandContactSensors: (0x03 << 0), + hrDataSourceEmSensors: (0x02 << 0), + hrDataSourceAntSensors: (0x01 << 0), + hrDataSourceInvalid: (0x00 << 0), + distanceTraveledEnabled: (0x01 << 2), + virtualSpeed: (0x01 << 3), + realSpeed: (0x00 << 3) +} + +const rowingMachineCapabilities = { + accumulatedStrokesEnabled: (0x01 << 0) +} + +const feCapabilitiesBitField = fitnessEquipmentCapabilities.hrDataSourceInvalid | fitnessEquipmentCapabilities.distanceTraveledEnabled | fitnessEquipmentCapabilities.realSpeed +const rowingCapabilitiesBitField = rowingMachineCapabilities.accumulatedStrokesEnabled + +export { createFEPeripheral } diff --git a/app/server.js b/app/server.js index 7ecef8652e..b64bb2644c 100644 --- a/app/server.js +++ b/app/server.js @@ -102,6 +102,10 @@ peripheralManager.on('control', (event) => { webServer.notifyClients('config', getConfig()) event.res = true break + case 'antPeripheralMode': + webServer.notifyClients('config', getConfig()) + event.res = true + break case 'hrmPeripheralMode': webServer.notifyClients('config', getConfig()) event.res = true @@ -213,6 +217,9 @@ webServer.on('messageReceived', async (message, client) => { case 'switchBlePeripheralMode': peripheralManager.switchBlePeripheralMode() break + case 'switchAntPeripheralMode': + peripheralManager.switchAntPeripheralMode() + break case 'switchHrmMode': peripheralManager.switchHrmMode() break @@ -241,6 +248,7 @@ webServer.on('clientConnected', (client) => { function getConfig () { return { blePeripheralMode: peripheralManager.getBlePeripheralMode(), + antPeripheralMode: peripheralManager.getAntPeripheralMode(), hrmPeripheralMode: peripheralManager.getHrmPeripheralMode(), stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret, shutdownEnabled: !!config.shutdownCommand diff --git a/config/default.config.js b/config/default.config.js index 94efe8616f..099bc400de 100644 --- a/config/default.config.js +++ b/config/default.config.js @@ -81,11 +81,16 @@ export default { // - OFF: Turns Bluetooth advertisement off bluetoothMode: 'FTMS', + // Selects the AN+ that is broadcasted to external peripherals and apps. Supported modes: + // - FE: ANT+ Fitness Equipment + // - OFF: Turns Bluetooth advertisement off + antplusMode: 'OFF', + // Selects the heart rate monitor mode. Supported modes: // - BLE: Use Bluetooth Low Energy to connect Heart Rate Monitor (Will currently connect to the first device found) // - ANT: Use Ant+ to connect Heart Rate Monitor // - OFF: turns of Heart Rate Monitor discovery - heartRateMode: 'BLE', + heartRateMode: 'OFF', // Defines the name that is used to announce the FTMS Rower via Bluetooth Low Energy (BLE) // Some rowing training applications expect that the rowing device is announced with a certain name From cd1ff3d00143534633bc3b0f1fcb07419c81fec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Wed, 21 Dec 2022 22:21:00 +0100 Subject: [PATCH 051/231] Fix crash when peripheral mode changed too quickly Add boolean guard flag to fix potential crash when user clicks the peripheral change buttons without waiting for the first request to complete in the GUI (i.e. clicks too quickly). --- app/peripherals/PeripheralManager.js | 55 ++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/app/peripherals/PeripheralManager.js b/app/peripherals/PeripheralManager.js index afba110164..1563c6c438 100644 --- a/app/peripherals/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -32,6 +32,8 @@ function createPeripheralManager () { let hrmPeripheral let hrmMode + let isPeripheralChangeInProgress = false + createBlePeripheral(config.bluetoothMode) createHrmPeripheral(config.heartRateMode) createAntPeripheral(config.antplusMode) @@ -61,11 +63,14 @@ function createPeripheralManager () { } function switchBlePeripheralMode (newMode) { + if (isPeripheralChangeInProgress) return + isPeripheralChangeInProgress = true // if now mode was passed, select the next one from the list if (newMode === undefined) { newMode = bleModes[(bleModes.indexOf(bleMode) + 1) % bleModes.length] } createBlePeripheral(newMode) + isPeripheralChangeInProgress = false } function notifyMetrics (type, metrics) { @@ -80,7 +85,7 @@ function createPeripheralManager () { async function createBlePeripheral (newMode) { if (blePeripheral) { - await blePeripheral.destroy() + await blePeripheral?.destroy() blePeripheral = undefined } @@ -134,18 +139,26 @@ function createPeripheralManager () { } function switchAntPeripheralMode (newMode) { + if (isPeripheralChangeInProgress) return + isPeripheralChangeInProgress = true if (newMode === undefined) { newMode = antModes[(antModes.indexOf(antMode) + 1) % antModes.length] } createAntPeripheral(newMode) + isPeripheralChangeInProgress = false } async function createAntPeripheral (newMode) { if (antPeripheral) { - await antPeripheral.destroy() + await antPeripheral?.destroy() antPeripheral = undefined - if (_antManager && hrmMode !== 'ANT' && newMode === 'OFF') { await _antManager.closeAntStick() } + try { + if (_antManager && hrmMode !== 'ANT' && newMode === 'OFF') { await _antManager.closeAntStick() } + } catch (error) { + log.error(error) + return + } } switch (newMode) { @@ -155,9 +168,14 @@ function createPeripheralManager () { _antManager = new AntManager() } - antPeripheral = createFEPeripheral(_antManager) - antMode = 'FE' - await antPeripheral.attach() + try { + antPeripheral = createFEPeripheral(_antManager) + antMode = 'FE' + await antPeripheral.attach() + } catch (error) { + log.error(error) + return + } break default: @@ -174,18 +192,26 @@ function createPeripheralManager () { } function switchHrmMode (newMode) { + if (isPeripheralChangeInProgress) return + isPeripheralChangeInProgress = true if (newMode === undefined) { newMode = hrmModes[(hrmModes.indexOf(hrmMode) + 1) % hrmModes.length] } createHrmPeripheral(newMode) + isPeripheralChangeInProgress = false } async function createHrmPeripheral (newMode) { if (hrmPeripheral) { - await hrmPeripheral.destroy() - hrmPeripheral.removeAllListeners() + await hrmPeripheral?.destroy() + hrmPeripheral?.removeAllListeners() hrmPeripheral = undefined - if (_antManager && newMode !== 'ANT' && antMode === 'OFF') { await _antManager.closeAntStick() } + try { + if (_antManager && newMode !== 'ANT' && antMode === 'OFF') { await _antManager.closeAntStick() } + } catch (error) { + log.error(error) + return + } } switch (newMode) { @@ -195,9 +221,14 @@ function createPeripheralManager () { _antManager = new AntManager() } - hrmPeripheral = createAntHrmPeripheral(_antManager) - hrmMode = 'ANT' - await hrmPeripheral.attach() + try { + hrmPeripheral = createAntHrmPeripheral(_antManager) + hrmMode = 'ANT' + await hrmPeripheral.attach() + } catch (error) { + log.error(error) + return + } break case 'BLE': From a0686150a2b04f4dba5358e683c00e1cf8ef5add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Thu, 22 Dec 2022 14:20:20 +0100 Subject: [PATCH 052/231] Update button layout styling for the GUI Restyle the button layout to be responsive and fix potential overflows --- app/client/components/DashboardActions.js | 52 +++++++++++++++++------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index 6cf0c91c10..84db932e01 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -14,28 +14,44 @@ import './AppDialog.js' export class DashboardActions extends AppElement { static styles = css` button { + position: relative; outline:none; background-color: var(--theme-button-color); border: 0; border-radius: var(--theme-border-radius); color: var(--theme-font-color); - margin: 0.2em 0; + margin: 0.2em 4px; font-size: 60%; text-decoration: none; display: inline-flex; - width: 3.5em; - height: 2.5em; + width: 3.2em; + min-width: 3.2em; + height: 2.2em; justify-content: center; align-items: center; } + button:hover { filter: brightness(150%); } + button > div.text { + position: absolute; + left: 2px; + bottom: 2px; + font-size: 40%; + } + #fullscreen-icon { display: inline-flex; } + .top-button-group { + display: flex; + flex-wrap: wrap; + justify-content: center; + } + #windowed-icon { display: none; } @@ -45,7 +61,7 @@ export class DashboardActions extends AppElement { } .peripheral-mode { - font-size: 80%; + font-size: 50%; } @media (display-mode: fullscreen) { @@ -63,14 +79,22 @@ export class DashboardActions extends AppElement { render () { return html` - - ${this.renderOptionalButtons()} - -
${this.blePeripheralMode()}
- -
${this.appState?.config?.hrmPeripheralMode}
- -
${this.appState?.config?.antPeripheralMode}
+
+ + ${this.renderOptionalButtons()} + + +
+
+ +
${this.blePeripheralMode()}
+
${this.dialog ? this.dialog : ''} ` } @@ -114,9 +138,9 @@ export class DashboardActions extends AppElement { case 'FTMSBIKE': return 'FTMS Bike' case 'CSC': - return 'BLE Bike Speed + Cadence' + return 'Bike Speed + Cadence' case 'CPS': - return 'BLE Bike Power' + return 'Bike Power' case 'FTMS': return 'FTMS Rower' default: From 405bcab55a45f476efd628a5d979db75317c788e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sun, 1 Jan 2023 23:23:26 +0100 Subject: [PATCH 053/231] Code cleanup Change logging level for the broadcast data of the ANT+ FE peripheral. Fix callback signature mismatch in bleno `stopAdvertising()` method --- app/peripherals/ant/FEPeripheral.js | 12 ++++++------ app/peripherals/ble/CpsPeripheral.js | 2 +- app/peripherals/ble/CscPeripheral.js | 2 +- app/peripherals/ble/FtmsPeripheral.js | 2 +- app/peripherals/ble/Pm5Peripheral.js | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/peripherals/ant/FEPeripheral.js b/app/peripherals/ant/FEPeripheral.js index 86e89a9319..0a3b99cc38 100644 --- a/app/peripherals/ant/FEPeripheral.js +++ b/app/peripherals/ant/FEPeripheral.js @@ -118,8 +118,8 @@ function createFEPeripheral (antManager) { ...Messages.intToLEHexArray(feCapabilitiesBitField, 1) ] if (sessionData.sessionStatus === 'Rowing') { - log.debug(`Page 17 Data Sent. Event=${dataPageCount}. Stroke Length=${sessionData.distancePerStroke}.`) - log.debug(`Hex Stroke Length=0x${sessionData.distancePerStroke.toString(16)}.`) + log.trace(`Page 17 Data Sent. Event=${dataPageCount}. Stroke Length=${sessionData.distancePerStroke}.`) + log.trace(`Hex Stroke Length=0x${sessionData.distancePerStroke.toString(16)}.`) } break case dataPageCount % 8 === 3: // 0x16 - Specific Rower Data (once a second) @@ -135,8 +135,8 @@ function createFEPeripheral (antManager) { ...Messages.intToLEHexArray((sessionData.fitnessEquipmentState + rowingCapabilitiesBitField), 1) ] if (sessionData.sessionStatus === 'Rowing') { - log.debug(`Page 22 Data Sent. Event=${dataPageCount}. Strokes=${sessionData.accumulatedStrokes}. Stroke Rate=${sessionData.strokeRate}. Power=${sessionData.instantaneousPower}`) - log.debug(`Hex Strokes=0x${sessionData.accumulatedStrokes.toString(16)}. Hex Stroke Rate=0x${sessionData.strokeRate.toString(16)}. Hex Power=0x${Messages.intToLEHexArray(sessionData.instantaneousPower, 2)}.`) + log.trace(`Page 22 Data Sent. Event=${dataPageCount}. Strokes=${sessionData.accumulatedStrokes}. Stroke Rate=${sessionData.strokeRate}. Power=${sessionData.instantaneousPower}`) + log.trace(`Hex Strokes=0x${sessionData.accumulatedStrokes.toString(16)}. Hex Stroke Rate=0x${sessionData.strokeRate.toString(16)}. Hex Power=0x${Messages.intToLEHexArray(sessionData.instantaneousPower, 2)}.`) } break case dataPageCount % 4 === 2: // 0x10 - General FE Data (twice a second) @@ -152,8 +152,8 @@ function createFEPeripheral (antManager) { ...Messages.intToLEHexArray((sessionData.fitnessEquipmentState + feCapabilitiesBitField), 1) ] if (sessionData.sessionStatus === 'Rowing') { - log.debug(`Page 16 Data Sent. Event=${dataPageCount}. Time=${sessionData.accumulatedTime}. Distance=${sessionData.accumulatedDistance}. Speed=${sessionData.cycleLinearVelocity}.`) - log.debug(`Hex Time=0x${sessionData.accumulatedTime.toString(16)}. Hex Distance=0x${sessionData.accumulatedDistance.toString(16)}. Hex Speed=0x${Messages.intToLEHexArray(sessionData.cycleLinearVelocity, 2)}.`) + log.trace(`Page 16 Data Sent. Event=${dataPageCount}. Time=${sessionData.accumulatedTime}. Distance=${sessionData.accumulatedDistance}. Speed=${sessionData.cycleLinearVelocity}.`) + log.trace(`Hex Time=0x${sessionData.accumulatedTime.toString(16)}. Hex Distance=0x${sessionData.accumulatedDistance.toString(16)}. Hex Speed=0x${Messages.intToLEHexArray(sessionData.cycleLinearVelocity, 2)}.`) } break } diff --git a/app/peripherals/ble/CpsPeripheral.js b/app/peripherals/ble/CpsPeripheral.js index 89cce8774a..48dc712781 100644 --- a/app/peripherals/ble/CpsPeripheral.js +++ b/app/peripherals/ble/CpsPeripheral.js @@ -65,7 +65,7 @@ function createCpsPeripheral () { return new Promise((resolve) => { bleno.disconnect() bleno.removeAllListeners() - bleno.stopAdvertising(resolve) + bleno.stopAdvertising(() => resolve()) }) } diff --git a/app/peripherals/ble/CscPeripheral.js b/app/peripherals/ble/CscPeripheral.js index 6915d13dbe..cc457fe93b 100644 --- a/app/peripherals/ble/CscPeripheral.js +++ b/app/peripherals/ble/CscPeripheral.js @@ -65,7 +65,7 @@ function createCscPeripheral () { return new Promise((resolve) => { bleno.disconnect() bleno.removeAllListeners() - bleno.stopAdvertising(resolve) + bleno.stopAdvertising(() => resolve()) }) } diff --git a/app/peripherals/ble/FtmsPeripheral.js b/app/peripherals/ble/FtmsPeripheral.js index c0845c6864..53b363ec52 100644 --- a/app/peripherals/ble/FtmsPeripheral.js +++ b/app/peripherals/ble/FtmsPeripheral.js @@ -78,7 +78,7 @@ function createFtmsPeripheral (controlCallback, options) { return new Promise((resolve) => { bleno.disconnect() bleno.removeAllListeners() - bleno.stopAdvertising(resolve) + bleno.stopAdvertising(() => resolve()) }) } diff --git a/app/peripherals/ble/Pm5Peripheral.js b/app/peripherals/ble/Pm5Peripheral.js index badfe8e357..d9050647dc 100644 --- a/app/peripherals/ble/Pm5Peripheral.js +++ b/app/peripherals/ble/Pm5Peripheral.js @@ -68,7 +68,7 @@ function createPm5Peripheral (controlCallback, options) { return new Promise((resolve) => { bleno.disconnect() bleno.removeAllListeners() - bleno.stopAdvertising(resolve) + bleno.stopAdvertising(() => resolve()) }) } From 35303547e2f633ff78e88b2bf625b1cc2e53fe57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Tue, 3 Jan 2023 10:39:29 +0100 Subject: [PATCH 054/231] Fix error on simultaneous ANT peripheral startup Move the peripheral startup sequence to an async function to ensure proper startup order and to avoid multiple simultaneous calls to the ANT stick causing app crash. --- app/peripherals/PeripheralManager.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/peripherals/PeripheralManager.js b/app/peripherals/PeripheralManager.js index 1563c6c438..349863a410 100644 --- a/app/peripherals/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -34,9 +34,13 @@ function createPeripheralManager () { let isPeripheralChangeInProgress = false - createBlePeripheral(config.bluetoothMode) - createHrmPeripheral(config.heartRateMode) - createAntPeripheral(config.antplusMode) + setupPeripherals() + + async function setupPeripherals () { + await createBlePeripheral(config.bluetoothMode) + await createHrmPeripheral(config.heartRateMode) + await createAntPeripheral(config.antplusMode) + } function getBlePeripheral () { return blePeripheral @@ -74,13 +78,13 @@ function createPeripheralManager () { } function notifyMetrics (type, metrics) { - if (bleMode !== 'OFF') { blePeripheral.notifyData(type, metrics) } - if (antMode !== 'OFF') { antPeripheral.notifyData(type, metrics) } + if (bleMode !== 'OFF') { blePeripheral?.notifyData(type, metrics) } + if (antMode !== 'OFF') { antPeripheral?.notifyData(type, metrics) } } function notifyStatus (status) { - if (bleMode !== 'OFF') { blePeripheral.notifyStatus(status) } - if (antMode !== 'OFF') { antPeripheral.notifyStatus(status) } + if (bleMode !== 'OFF') { blePeripheral?.notifyStatus(status) } + if (antMode !== 'OFF') { antPeripheral?.notifyStatus(status) } } async function createBlePeripheral (newMode) { From 3f129ceec2c1709bad9d1be2bfb2d85d650f22cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Tue, 3 Jan 2023 10:50:27 +0100 Subject: [PATCH 055/231] Add peripheral shutdown for app termination In order to avoid errors with peripheral processes on the next startup (e.g. ANT Stick getting stuck) graceful explicit shutdown of these is necessary on app termination. --- app/peripherals/PeripheralManager.js | 14 ++++++++++++++ app/server.js | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/app/peripherals/PeripheralManager.js b/app/peripherals/PeripheralManager.js index 349863a410..df793d7e4f 100644 --- a/app/peripherals/PeripheralManager.js +++ b/app/peripherals/PeripheralManager.js @@ -264,7 +264,21 @@ function createPeripheralManager () { emitter.emit('control', event) } + async function shutdownAllPeripherals () { + log.debug('shutting down all peripherals') + + try { + await blePeripheral?.destroy() + await antPeripheral?.destroy() + await hrmPeripheral?.destroy() + await _antManager?.closeAntStick() + } catch (error) { + log.error('peripheral shutdown was unsuccessful, restart of Pi may required', error) + } + } + return Object.assign(emitter, { + shutdownAllPeripherals, getBlePeripheral, getBlePeripheralMode, getAntPeripheral, diff --git a/app/server.js b/app/server.js index b64bb2644c..b6a28fce5a 100644 --- a/app/server.js +++ b/app/server.js @@ -140,6 +140,22 @@ function resetWorkout () { const gpioTimerService = child_process.fork('./app/gpio/GpioTimerService.js') gpioTimerService.on('message', handleRotationImpulse) +process.once('SIGINT', async (signal) => { + log.debug(`${signal} signal was received, shutting down gracefully`) + await peripheralManager.shutdownAllPeripherals() + process.exit(0) +}) +process.once('SIGTERM', async (signal) => { + log.debug(`${signal} signal was received, shutting down gracefully`) + await peripheralManager.shutdownAllPeripherals() + process.exit(0) +}) +process.once('uncaughtException', async (error) => { + log.error('Uncaught Exception:', error) + await peripheralManager.shutdownAllPeripherals() + process.exit(1) +}) + function handleRotationImpulse (dataPoint) { workoutRecorder.recordRotationImpulse(dataPoint) rowingStatistics.handleRotationImpulse(dataPoint) From aa3b39f86ab00c0f745db43189aa78499c0b9d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sun, 12 Feb 2023 22:55:26 +0100 Subject: [PATCH 056/231] Change PM5 serial to comply with C2 standards The PM5 serial number should start with "43" (in line with consistent HW and SW revision) --- app/peripherals/PeripheralConstants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/peripherals/PeripheralConstants.js b/app/peripherals/PeripheralConstants.js index 3e1e1369b0..89a82ffab8 100644 --- a/app/peripherals/PeripheralConstants.js +++ b/app/peripherals/PeripheralConstants.js @@ -5,7 +5,7 @@ Some PM5 specific constants */ export const PeripheralConstants = { - serial: '123456789', + serial: '431234567', model: 'PM5', name: 'PM5 123456789 Row', hardwareRevision: '907', From f28580fef36e031b603ae785846aed068db91fed Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:45:16 +0100 Subject: [PATCH 057/231] Improvement of the stroke detection Improved the explanation of the stroke detection algorithm based on https://github.com/laberning/openrowingmonitor/issues/124 --- docs/rower_settings.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/rower_settings.md b/docs/rower_settings.md index 8a7552971f..a978ce0479 100644 --- a/docs/rower_settings.md +++ b/docs/rower_settings.md @@ -221,13 +221,21 @@ Their accuracy isn't super-critical. In later sections, we will describe how to These settings are the core of the stroke detection and are the ones that require the most effort to get right. The most cricial settings are the *flankLength* and *minimumStrokeQuality*, where other metrics are much less critical. -**minimumStrokeQuality** is a setting that defines the minimal goodness of fit of the beforementioned recovery slope with the datapoints. When the slope doesn't fit the data well, this will block moving to the next phase. A value of 0.1 is extrmely relaxed, where 0.95 would be extremely tight. This is set to 0.34 for most rowers, which is a working setting for all maintained rowers to date. The accuracy of this setting isn't super critical for stroke detection to work: for example, on a Concept2 values between 0.28 to 0.42 are known to give reliable stroke detection. Setting this too relaxed will result in earlier phase changes, settng this too strict will delay phase detection. This setting is primarily used to optimise the stroke detection for advanced metrics (like drive time, drive length, force curves), so unless it gets in the way, there is no immediate need to change it. +In a nutshell, a rowingstroke contains a drive phase and a recovery phase, and Open Rowing Monitor needs to recognise both reliably to work well. Please note, that for an actual transition to another phase respectively **minimumDriveTime** or **minimumRecoveryTime** have to be exceeded as well. -The **flankLength** setting determines 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. Generally, a *flankLength* of 3 to 4 typically works. The technical minimum is 3, the maximum is limited by CPU-time. 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. If any, increasing the *flanklength* has the side-effect that some calculations are performed with more rigour, making them more precise as they get more data. Please note that the rower itself might limit the *flankLength*: some rowers only have 4 or 5 datapoints in a drive phase, naturally limiting the number of datapoints that can be used for stroke phase detection. +To detect strokes, Open Rowing Monitor uses the following criteria before attempting a stroke phase transition: -Please note that a longer *flankLength* also requires more CPU time, where the calculation grows exponentially as *flankLength* becomes longer. On a Raspberry Pi 4B, a *flankLength* of 12 has been succesfully used without issue. What the practical limit on a Rapberry Pi Zero 2 W is, is still a matter of investigation. +* a drive is detected when the handleforce is above **minumumForceBeforeStroke** AND the slope of a series of *flankLength* times between impulses is below the **minumumRecoverySlope** (i.e. accelerating) +* a recovery is detected when the handleforce is below **minumumForceBeforeStroke** OR the slope of a series of *flankLength* times between impulses is above the **minumumRecoverySlope** (i.e. decelerating) where the goodness of fit of that slope exceeds the **minimumStrokeQuality** -To make life a bit easier, it is possible to replay a recorded raw rowing session. To do this, uncomment and modify the following lines in `server.js`: +**minimumStrokeQuality** is a setting that defines the minimal goodness of fit of the beforementioned minumumRecoverySlope with the datapoints. When the slope doesn't fit the data well, this will block moving to the next phase. A value of 0.1 is extrmely relaxed, where 0.95 would be extremely tight. This is set to 0.34 for most rowers, which is a working setting for all maintained rowers to date. The accuracy of this setting isn't super critical for stroke detection to work: for example, on a Concept2 values between 0.28 to 0.42 are known to give reliable stroke detection. Setting this too relaxed will result in earlier phase changes, settng this too strict will delay phase detection. This setting is primarily used to optimise the stroke detection for advanced metrics (like drive time, drive length, force curves), so unless it gets in the way, there is no immediate need to change it. Please note that setting this value to 1, it will effectively disable half of the criteria for detecting a recovery, effectively making it completely handle-force based. + +The **flankLength** and **minumumRecoverySlope** settings 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 (with slope **minumumRecoverySlope**), and the **flankLength** determines how many consecutive flanks have to be seen before the stroke detection considers a stroke to begin or end. Setting these paramters requires some trial and error: + +* **minumumRecoverySlope** can be set to 0, where Open Rowing Monitor will use a quite robust selection on an accelerating or decelerating flywheel. This is recomended as a starting point for getting stroke detection to work. It can be further optimised later (see the later section on advanced stroke detection); +* Generally, a *flankLength* of 3 to 4 typically works. The technical minimum is 3, the maximum is limited by CPU-time. 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. If any, increasing the *flanklength* has the side-effect that some calculations are performed with more rigour, making them more precise as they get more data. Please note that the rower itself might limit the *flankLength*: some rowers only have 4 or 5 datapoints in a drive phase, naturally limiting the number of datapoints that can be used for stroke phase detection. Increasing this number too far (beyond a significant part of the stroke) will remove the fluctuations in the flywheel speed needed for stroke detections, so there is a practical upper limit to what the value of *flankLength* can be for a specific rower. Please note that a longer *flankLength* also requires more CPU time, where the calculation grows exponentially as *flankLength* becomes longer. On a Raspberry Pi 4B, a *flankLength* of 15 has been succesfully used without issue. What the practical limit on a Rapberry Pi Zero 2 W is, is still a matter of investigation. + +To make life a bit easier, it is possible to replay a raw recording of a previous rowing session. To do this, uncomment and modify the following lines in `server.js`: ```js replayRowingSession(handleRotationImpulse, { From 3de19c08c7980e5a2cdd7eabbf68a5de20393b45 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:48:54 +0100 Subject: [PATCH 058/231] Fixed a Lint error --- docs/rower_settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rower_settings.md b/docs/rower_settings.md index a978ce0479..dc6d4744e2 100644 --- a/docs/rower_settings.md +++ b/docs/rower_settings.md @@ -226,7 +226,7 @@ In a nutshell, a rowingstroke contains a drive phase and a recovery phase, and O To detect strokes, Open Rowing Monitor uses the following criteria before attempting a stroke phase transition: * a drive is detected when the handleforce is above **minumumForceBeforeStroke** AND the slope of a series of *flankLength* times between impulses is below the **minumumRecoverySlope** (i.e. accelerating) -* a recovery is detected when the handleforce is below **minumumForceBeforeStroke** OR the slope of a series of *flankLength* times between impulses is above the **minumumRecoverySlope** (i.e. decelerating) where the goodness of fit of that slope exceeds the **minimumStrokeQuality** +* a recovery is detected when the handleforce is below **minumumForceBeforeStroke** OR the slope of a series of *flankLength* times between impulses is above the **minumumRecoverySlope** (i.e. decelerating) where the goodness of fit of that slope exceeds the **minimumStrokeQuality** **minimumStrokeQuality** is a setting that defines the minimal goodness of fit of the beforementioned minumumRecoverySlope with the datapoints. When the slope doesn't fit the data well, this will block moving to the next phase. A value of 0.1 is extrmely relaxed, where 0.95 would be extremely tight. This is set to 0.34 for most rowers, which is a working setting for all maintained rowers to date. The accuracy of this setting isn't super critical for stroke detection to work: for example, on a Concept2 values between 0.28 to 0.42 are known to give reliable stroke detection. Setting this too relaxed will result in earlier phase changes, settng this too strict will delay phase detection. This setting is primarily used to optimise the stroke detection for advanced metrics (like drive time, drive length, force curves), so unless it gets in the way, there is no immediate need to change it. Please note that setting this value to 1, it will effectively disable half of the criteria for detecting a recovery, effectively making it completely handle-force based. From 674df2f6fc0d90f8b3b2d6809d39c2562eecb707 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:19:33 +0100 Subject: [PATCH 059/231] Added link to rower settings manual --- config/rowerProfiles.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/rowerProfiles.js b/config/rowerProfiles.js index 74a33f64c4..7515039a37 100644 --- a/config/rowerProfiles.js +++ b/config/rowerProfiles.js @@ -4,9 +4,10 @@ This file contains the rower specific settings for different models of ergometers. - These have been generated by the community. If your rower is not listed here and you did find - good settings for your rowing device please send them to us (together with a raw recording of - 10 strokes) so we can add the device here. + These have been generated by the community. If your rower is not listed here, please follow + https://github.com/laberning/openrowingmonitor/blob/main/docs/rower_settings.md to find the right settings + After you found good settings for your rowing device please send them to us (together with a raw recording + of at least 10 strokes) so we can add the device here and start to maintain it. */ export default { From bef044110da4b38745131592ad5d7aaa13a04650 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:23:44 +0100 Subject: [PATCH 060/231] Added link to documentation --- install/config.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/install/config.js b/install/config.js index ff7f170f25..0f434d6da4 100644 --- a/install/config.js +++ b/install/config.js @@ -20,10 +20,11 @@ export default { default: 'debug' }, - // The rower specific settings. Either choose a profile from config/rowerProfiles.js or - // define the settings individually. If you find good settings for a new rowing device - // please send them to us (together with a raw recording of 10 strokes) so we can add - // the device to the profiles. + // The rower specific settings. Either choose a profile from config/rowerProfiles.js (see + // https://github.com/laberning/openrowingmonitor/blob/main/docs/Supported_Rowers.md) or define + // the settings manually (see https://github.com/laberning/openrowingmonitor/blob/main/docs/rower_settings.md + // on how to do this). If you find good settings for a new rowing device please send them to us (together + // with a raw recording of at least 10 strokes) so we can add the device to the profiles. // EXAMPLE ROWER CONFIG : using a DKN R-320 Air Rower as is // rowerSettings: rowerProfiles.DKN_R320 From 1cadf0008ece76d5f61cccdaadf7dc5e7fc124e5 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:11:04 +0100 Subject: [PATCH 061/231] Added a description of the new ANT+ functionality Added a description of the new ANT+ functionality --- docs/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index ec1b9aea35..55c8177c0e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,7 +45,7 @@ If you connect a physical screen directly to the Raspberry Pi, then this interfa ### Bluetooth Low Energy (BLE) -Open Rowing Monitor also implements different Bluetooth Low Energy (BLE) protocols so you can use your rowing machine with different fitness applications. Some apps use the Fitness Machine Service (FTMS), which is a standardized GATT protocol for different types of fitness machines. Other apps prefer to see a Concept 2 PM5. To help you connect to your app and game of choice, Open Rowing Monitor currently supports the following Bluetooth protocols: +Open Rowing Monitor can recieve recieve heartrate data via BLE. Asides this functionality, Open Rowing Monitor also implements different Bluetooth Low Energy (BLE) protocols so you can use your rowing machine to share rowing metrics with different fitness applications. Some apps use the Fitness Machine Service (FTMS), which is a standardized GATT protocol for different types of fitness machines. Other apps prefer to see a Concept 2 PM5. To help you connect to your app and game of choice, Open Rowing Monitor currently supports the following Bluetooth protocols: * **Concept2 PM**: Open Rowing Monitor implements part of the Concept2 PM Bluetooth Smart Communication Interface Definition. This is still work in progress and only implements the most common parts of the spec, so it is not guaranteed to work with all applications that support C2 rowing machines. Our interface currently can only report metrics, but can't recieve commands and session parameters from the app yet. It is known to work with [EXR](https://www.exrgame.com) and all the samples from [The Erg Arcade](https://ergarcade.com), for example you can [row in the clouds](https://ergarcade.github.io/mrdoob-clouds/). @@ -57,6 +57,10 @@ Open Rowing Monitor also implements different Bluetooth Low Energy (BLE) protoco * **BLE Cycling Speed and Cadence Profile**: used for older Garmin Forerunner and Garmin Venu watches and similar types, again simulating a bike activity. Please note to set the wheel circumference to 10mm to make this work well. +### ANT+ + +You can add a ANT+ USB-stick to your Raspberry Pi, which allows to to recieve data from your ANT+ heartrate monitor. On top of recieving the heartrate data, Open Rowing Monitor can also broadcast rowing metrics via ANT+, which can be recieved by the more expensive series of Garmin smartwatches, which then can calculate metrics like training load etc.. + ### Export of Training Sessions Open Rowing Monitor is based on the idea that metrics should be easily accessible for further analysis. Therefore, Open Rowing Monitor can create the following files: From 7b4a716eb36b7356f6276a95023429e39f6e5126 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:19:24 +0100 Subject: [PATCH 062/231] Added incyclist-ant-plus as dependency Added incyclist-ant-plus as dependency --- package-lock.json | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/package-lock.json b/package-lock.json index 995beada5d..ec4af7ad66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "ant-plus": "0.1.24", "finalhandler": "1.1.2", "form-data": "4.0.0", + "incyclist-ant-plus": "^0.1.16", "lit": "2.1.3", "loglevel": "1.8.0", "nosleep.js": "0.12.0", @@ -5969,6 +5970,41 @@ "node": ">=0.8.19" } }, + "node_modules/incyclist-ant-plus": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/incyclist-ant-plus/-/incyclist-ant-plus-0.1.16.tgz", + "integrity": "sha512-gNbdZR1IhucfCX8zjZnshQ0eJHqO1nZNTzf6/3fZdI8h3MgJMp7K0HxpKK9Tpw2ibPj2ORy/ujqLr5/EqCYtlw==", + "dependencies": { + "queue-fifo": "^0.2.6" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "usb": "^2.7.0" + } + }, + "node_modules/incyclist-ant-plus/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "optional": true + }, + "node_modules/incyclist-ant-plus/node_modules/usb": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.8.0.tgz", + "integrity": "sha512-umwfJjG3ADI+0xO+7pkrblX2+2BYDgzJTgWrSoxisXncsA2zW30VX2yly5W2U/gqldx6x2sn9b1Uk2gZht6JBQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">=10.20.0 <11.x || >=12.17.0 <13.0 || >=14.0.0" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -15682,6 +15718,34 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "incyclist-ant-plus": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/incyclist-ant-plus/-/incyclist-ant-plus-0.1.16.tgz", + "integrity": "sha512-gNbdZR1IhucfCX8zjZnshQ0eJHqO1nZNTzf6/3fZdI8h3MgJMp7K0HxpKK9Tpw2ibPj2ORy/ujqLr5/EqCYtlw==", + "requires": { + "queue-fifo": "^0.2.6", + "usb": "^2.7.0" + }, + "dependencies": { + "node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "optional": true + }, + "usb": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.8.0.tgz", + "integrity": "sha512-umwfJjG3ADI+0xO+7pkrblX2+2BYDgzJTgWrSoxisXncsA2zW30VX2yly5W2U/gqldx6x2sn9b1Uk2gZht6JBQ==", + "optional": true, + "requires": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.5.0" + } + } + } + }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", From 1ae7128dc74f85e4a2754662af45d91b8682314c Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:37:44 +0100 Subject: [PATCH 063/231] Added missing dependencies Added missing dependencies: queue-fifo@0.2.6, dbly-linked-list@0.3.4, lodash.isequal@4.5.0, @types/w3c-web-usb@1.0.6. --- package-lock.json | 66 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec4af7ad66..c6eb8f1b51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2556,6 +2556,12 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.6.tgz", + "integrity": "sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==", + "optional": true + }, "node_modules/@web/parse5-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-1.3.0.tgz", @@ -3845,6 +3851,14 @@ "node": ">=0.10" } }, + "node_modules/dbly-linked-list": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/dbly-linked-list/-/dbly-linked-list-0.3.4.tgz", + "integrity": "sha512-327vOlwspi9i1T3Kc9yZhRUR8qDdgMQ4HmXsFDDCQ/HTc3sNe7gnF5b0UrsnaOJ0rvmG7yBZpK0NoOux9rKYKw==", + "dependencies": { + "lodash.isequal": "^4.5.0" + } + }, "node_modules/debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -6797,6 +6811,11 @@ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7373,9 +7392,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -8938,6 +8957,14 @@ "node": ">=0.6" } }, + "node_modules/queue-fifo": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/queue-fifo/-/queue-fifo-0.2.6.tgz", + "integrity": "sha512-rwlnZHAaTmWEGKC7ziasK8u4QnZW/uN6kSiG+tHNf/1GA+R32FArZi18s3SYUpKcA0Y6jJoUDn5GT3Anoc2mWw==", + "dependencies": { + "dbly-linked-list": "0.3.4" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -13124,6 +13151,12 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" }, + "@types/w3c-web-usb": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.6.tgz", + "integrity": "sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==", + "optional": true + }, "@web/parse5-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-1.3.0.tgz", @@ -14117,6 +14150,14 @@ "assert-plus": "^1.0.0" } }, + "dbly-linked-list": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/dbly-linked-list/-/dbly-linked-list-0.3.4.tgz", + "integrity": "sha512-327vOlwspi9i1T3Kc9yZhRUR8qDdgMQ4HmXsFDDCQ/HTc3sNe7gnF5b0UrsnaOJ0rvmG7yBZpK0NoOux9rKYKw==", + "requires": { + "lodash.isequal": "^4.5.0" + } + }, "debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -16357,6 +16398,11 @@ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -16812,9 +16858,9 @@ } }, "node-gyp-build": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==" + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==" }, "node-pre-gyp": { "version": "0.17.0", @@ -17988,6 +18034,14 @@ "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true }, + "queue-fifo": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/queue-fifo/-/queue-fifo-0.2.6.tgz", + "integrity": "sha512-rwlnZHAaTmWEGKC7ziasK8u4QnZW/uN6kSiG+tHNf/1GA+R32FArZi18s3SYUpKcA0Y6jJoUDn5GT3Anoc2mWw==", + "requires": { + "dbly-linked-list": "0.3.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", From c029a42d374bb76b481839cac3bcee8f5ed40aac Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:53:54 +0100 Subject: [PATCH 064/231] Update to accommodate the new peripheral settings Update to respect new peripheral settings introduced with the new peripheralmanager --- app/tools/ConfigManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/tools/ConfigManager.js b/app/tools/ConfigManager.js index 225e4b9a2c..1a3a0aba23 100644 --- a/app/tools/ConfigManager.js +++ b/app/tools/ConfigManager.js @@ -28,10 +28,10 @@ function checkConfig (configToCheck) { checkRangeValue(configToCheck, 'gpioTriggeredFlank', ['Up', 'Down', 'Both'], true, 'Up') checkIntegerValue(configToCheck, 'appPriority', configToCheck.gpioPriority, 0, true, true, 0) checkIntegerValue(configToCheck, 'webUpdateInterval', 80, 1000, false, true, 1000) - checkBooleanValue(configToCheck, 'heartrateMonitorBLE', true, true) - checkBooleanValue(configToCheck, 'heartrateMonitorANT', true, false) - checkRangeValue(configToCheck, 'bluetoothMode', ['off', 'PM5', 'FTMS', 'FTMSBIKE', 'CPS', 'CSC'], true, 'FTMS') checkIntegerValue(configToCheck, 'peripheralUpdateInterval', 80, 1000, false, true, 1000) + checkRangeValue(configToCheck, 'bluetoothMode', ['OFF', 'PM5', 'FTMS', 'FTMSBIKE', 'CPS', 'CSC'], true, 'OFF') + checkRangeValue(configToCheck, 'antplusMode', ['OFF', 'FE'], true, 'OFF') + checkRangeValue(configToCheck, 'heartRateMode', ['OFF', 'ANT', 'BLE'], true, 'OFF') checkIntegerValue(configToCheck, 'numOfPhasesForAveragingScreenData', 2, null, false, true, 4) checkBooleanValue(configToCheck, 'createRowingDataFiles', true, true) checkBooleanValue(configToCheck, 'createRawDataFiles', true, true) From 765d975b04a9dc37096ccbf920cd8f2e51eee3d9 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 21 Feb 2023 12:42:31 +0100 Subject: [PATCH 065/231] Better description of stroke detection --- docs/physics_openrowingmonitor.md | 60 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/docs/physics_openrowingmonitor.md b/docs/physics_openrowingmonitor.md index ffae93fd1a..8a2aee521b 100644 --- a/docs/physics_openrowingmonitor.md +++ b/docs/physics_openrowingmonitor.md @@ -179,9 +179,29 @@ As ${Δω \over Δt}$ = α and D = k \* ω2 As α and ω have been derived in a robust manner, and there are no alternative more robust approaches to determining instant τ that allows for handle force curves, we consider this the best attainable result. Testing shows that the results are quite useable. -### Detecting force on the flywheel +## Detecting the stroke phase + +One of the key elements of rowing is detecting the stroke phases and thus calculate the associated metrics for that phase. Assuming that `engine/Flywheel.js` has determined whether there is a force present on the flywheel, `engine/Rower.js` can now transform this information into the phase of the rowing stroke. On an indoor rower, the rowing cycle will always start with a drive, followed by a recovery. This results in the follwing phases: + +* The **Drive phase**, where the rower pulls on the handle, some force on the flywheel is excerted and the flywheel is accelerating or at least not decelerating in accordance with the drag; -One of the key elements of rowing is detecting the stroke phases and thus calculate the associated metrics. From the perspective of Open Rowing Monitor, there only is a stream of *CurrentDt*'s, which should form the basis of this detection: +* The **Recovery Phase**, where the rower returns to his starting position and the flywheel decelerates as the drag on the flywheel is slowing it down; + +As the rowing cycle always follows this fixed schema, Open Rowing Monitor models it as a finite state machine (implemented in `handleRotationImpulse` in `engine/Rower.js`). + +```mermaid +stateDiagram-v2 + direction LR + Drive --> Recovery: Flywheel
isn't powered + Drive --> Drive: Flywheel
is powered + Recovery --> Drive: Flywheel
is powered + Recovery --> Recovery: Flywheel
isn't powered +``` + + +*Finite state machine of rowing cycle* + +From the perspective of Open Rowing Monitor, there only is a stream of *CurrentDt*'s, which should form the basis of this detection: The following picture shows the time between impulses through time: ![Measurements of flywheel](img/physics/flywheelmeasurement.png) @@ -189,17 +209,17 @@ The following picture shows the time between impulses through time: Open Rowing Monitor combines two types of force detection, which work independently: *basic force detection* and *advanced stroke detection*. Both can detect a stroke accuratly, and the combination has proven its use. -In `engine/Flywheel.js`, two functions provide force detection: +In `engine/Flywheel.js`, two functions provide force detection, which use the following criteria before attempting a stroke phase transition: -* `isUnpowered()`: which indicates that the simple or the advanced force detection indicate that a force is absent; +* `isPowered()`: which indicates a force is present, suggesting a drive phase. This is true when the slope of a series of *flankLength* times between impulses is below the **minumumRecoverySlope** (i.e. accelerating) AND the handleforce is above **minumumForceBeforeStroke** (i.e. the torque τ is above a certain threshold); -* `isPowered()`: which indicates that both the simple or the advanced force detection indicate that a force is present. +* `isUnpowered()`: which indicates that there is no force present, suggesting a recovery phase. This is true when the slope of a series of *flankLength* times between impulses is above the **minumumRecoverySlope** (i.e. decelerating) where the goodness of fit of that slope exceeds the **minimumStrokeQuality** OR the handleforce is below **minumumForceBeforeStroke** (i.e. the torque τ is below a certain threshold) -The choice for the logical relations between the two types of force detection is based on testing: where a sudden presence of force on a flywheel (i.e. the start of a drive) is quite easily and consistently detected, its abscence has proven to be more difficult. In practice, the beginning of a drive is easily recognised as strong leg muscles excert much force onto the flywheel in a very short period of time. The end of the drive is more difficult to assess, as the dragforce of the flywheel increases with its speed, and the weaker arm muscles have taken over, making the transition to the recovery much harder to detect. In theory, in the end of the drive phase the drag force might be bigger than the force from the arms, resulting in an overall negative torque. +The choice for the logical relations between the two types of force detection is based on testing: where a sudden presence of force on a flywheel (i.e. the start of a drive) is quite easily and consistently detected, its abscence has proven to be more difficult. In practice, the beginning of a drive is easily recognised as strong leg muscles excert much force onto the flywheel in a very short period of time, leading to an easily recognisable (large) torque τ and a sudden decrease in currentDt's. The end of the drive is more difficult to assess, as the dragforce of the flywheel increases with its speed, and the weaker arm muscles have taken over, making the transition to the recovery much harder to detect. In theory, in the end of the drive phase the drag force might be bigger than the force from the arms, resulting in an overall negative torque. -In the remainder of this paragraph, we describe the underlying physics of these force detection methods. +In the remainder of this paragraph, we describe the underlying physics of both these force detection methods. -#### Basic force detection +### Basic force detection through currentDt slope One of the key indicator is the acceleration/decelleration of the flywheel. Looking at a simple visualisation of the rowing stroke, we try to achieve the following: @@ -218,7 +238,7 @@ A more nuanced, but more vulnerable, approach is to compare the slope of this fu In Open Rowing Monitor, the settings allow for using the more robust ascending/descending approach (by setting *minumumRecoverySlope* to 0), for a more accurate approach (by setting *minumumRecoverySlope* to a static value) or even a dynamic approach (by setting *autoAdjustRecoverySlope* to true) -#### Advanced force detection +### Advanced force detection through torque τ The more advanced, but more vulnerable approach depends on the calculated torque. When looking at *CurrentDt* and Torque over time, we get the following picture: @@ -366,28 +386,6 @@ From theory [[13]](#13)), we know that the handle Power is > $$ P_{Handle} = τ * ω $$ -## Detecting the stroke phase - -Knowing that `engine/Flywheel.js` has determined whether there is a force on the flywheel, `engine/Rower.js` can now transform this into the phase of the rowing stroke. On an indoor rower, the rowing cycle will always start with a stroke, followed by a recovery. This results in the follwing phases: - -* The **Drive phase**, where the rower pulls on the handle, some force on the flywheel is excerted and the flywheel is accelerating or at least not decelerating in accordance with the drag; - -* The **Recovery Phase**, where the rower returns to his starting position and the flywheel decelerates as the drag on the flywheel is slowing it down; - -As the rowing cycle always follows this fixed schema, Open Rowing Monitor models it as a finite state machine (implemented in `handleRotationImpulse` in `engine/Rower.js`). - -```mermaid -stateDiagram-v2 - direction LR - Drive --> Recovery: Flywheel
isn't powered - Drive --> Drive: Flywheel
is powered - Recovery --> Drive: Flywheel
is powered - Recovery --> Recovery: Flywheel
isn't powered -``` - - -*Finite state machine of rowing cycle* - ## A mathematical perspective on key metrics ### Noise Filtering algorithms applied From f31372b27c3d98190eaf3290660fef810f4c66b5 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 21 Feb 2023 12:45:03 +0100 Subject: [PATCH 066/231] Better wording --- docs/physics_openrowingmonitor.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/physics_openrowingmonitor.md b/docs/physics_openrowingmonitor.md index 8a2aee521b..cc9457e66b 100644 --- a/docs/physics_openrowingmonitor.md +++ b/docs/physics_openrowingmonitor.md @@ -205,15 +205,15 @@ From the perspective of Open Rowing Monitor, there only is a stream of *CurrentD The following picture shows the time between impulses through time: ![Measurements of flywheel](img/physics/flywheelmeasurement.png) -*Measurements of flywheel* +*example currentDt Measurements of flywheel* Open Rowing Monitor combines two types of force detection, which work independently: *basic force detection* and *advanced stroke detection*. Both can detect a stroke accuratly, and the combination has proven its use. In `engine/Flywheel.js`, two functions provide force detection, which use the following criteria before attempting a stroke phase transition: -* `isPowered()`: which indicates a force is present, suggesting a drive phase. This is true when the slope of a series of *flankLength* times between impulses is below the **minumumRecoverySlope** (i.e. accelerating) AND the handleforce is above **minumumForceBeforeStroke** (i.e. the torque τ is above a certain threshold); +* `isPowered()`: which indicates a force is present, suggesting a drive phase. This is true when the slope of a series of *flankLength* times between impulses is below the **minumumRecoverySlope** (i.e. accelerating, as is the case in the measurements in above figure before the dotted line) AND the handleforce is above **minumumForceBeforeStroke** (i.e. the torque τ is above a certain threshold); -* `isUnpowered()`: which indicates that there is no force present, suggesting a recovery phase. This is true when the slope of a series of *flankLength* times between impulses is above the **minumumRecoverySlope** (i.e. decelerating) where the goodness of fit of that slope exceeds the **minimumStrokeQuality** OR the handleforce is below **minumumForceBeforeStroke** (i.e. the torque τ is below a certain threshold) +* `isUnpowered()`: which indicates that there is no force present, suggesting a recovery phase. This is true when the slope of a series of *flankLength* times between impulses is above the **minumumRecoverySlope** (i.e. decelerating, as is the case in the measurements in above figure after the dotted line) where the goodness of fit of that slope exceeds the **minimumStrokeQuality** OR the handleforce is below **minumumForceBeforeStroke** (i.e. the torque τ is below a certain threshold) The choice for the logical relations between the two types of force detection is based on testing: where a sudden presence of force on a flywheel (i.e. the start of a drive) is quite easily and consistently detected, its abscence has proven to be more difficult. In practice, the beginning of a drive is easily recognised as strong leg muscles excert much force onto the flywheel in a very short period of time, leading to an easily recognisable (large) torque τ and a sudden decrease in currentDt's. The end of the drive is more difficult to assess, as the dragforce of the flywheel increases with its speed, and the weaker arm muscles have taken over, making the transition to the recovery much harder to detect. In theory, in the end of the drive phase the drag force might be bigger than the force from the arms, resulting in an overall negative torque. From 025c41f5d62aa4c877b2f5c89981e3bf321e66cb Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 28 Feb 2023 20:50:04 +0100 Subject: [PATCH 067/231] Update attribution.md --- docs/attribution.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/attribution.md b/docs/attribution.md index f5a8ec1333..e3925344e1 100644 --- a/docs/attribution.md +++ b/docs/attribution.md @@ -6,7 +6,7 @@ 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. -* 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. +* 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, which is at the base of our physics engine. * Bluetooth is quite a complex beast, luckily the Bluetooth SIG releases all the [Bluetooth Specifications](https://www.bluetooth.com/specifications/specs). @@ -14,6 +14,6 @@ Open Rowing Monitor uses some great work by others. Thank you for all the great * The frontend uses some icons from [Font Awesome](https://fontawesome.com/), licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). -* Thank you to [Jaap van Ekris](https://github.com/JaapvanEkris) for his contributions to this project. +* Thank you to [Jaap van Ekris](https://github.com/JaapvanEkris) for his many contributions to this project, especially the physics engine and the file exports. -* Thanks to [Abasz](https://github.com/Abasz) for his great contributions to the GPIO and BLE implementation +* Thanks to [Abasz](https://github.com/Abasz) for his great contributions to the GPIO, BLE and Ant+ implementations, as well as the many constructive feedback that helped improve many areas of OpenRowingMonitor. From 65b273bbea16b3548189a06f45b0737b470bb7ea Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 1 Mar 2023 09:01:34 +0100 Subject: [PATCH 068/231] Fixed Lint errors --- docs/attribution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/attribution.md b/docs/attribution.md index e3925344e1..257e59e557 100644 --- a/docs/attribution.md +++ b/docs/attribution.md @@ -6,7 +6,7 @@ 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. -* 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, which is at the base of our physics engine. +* 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, which is at the base of our physics engine. * Bluetooth is quite a complex beast, luckily the Bluetooth SIG releases all the [Bluetooth Specifications](https://www.bluetooth.com/specifications/specs). From 97c1da96d69582a2d4d3e86e95a58894e381d8c8 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Thu, 2 Mar 2023 11:25:20 +0100 Subject: [PATCH 069/231] Dealt with an edge case Dealt with an edge case where there are no strokes detected, but a number of impulses is recorded (Typically during first calibration). --- app/engine/WorkoutRecorder.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/engine/WorkoutRecorder.js b/app/engine/WorkoutRecorder.js index 05e59849d5..693d4553dd 100644 --- a/app/engine/WorkoutRecorder.js +++ b/app/engine/WorkoutRecorder.js @@ -311,8 +311,12 @@ function createWorkoutRecorder () { function minimumRecordingTimeHasPassed () { const minimumRecordingTimeInSeconds = 10 const rotationImpulseTimeTotal = rotationImpulses.reduce((acc, impulse) => acc + impulse, 0) - const strokeTimeTotal = strokes[strokes.length - 1].totalMovingTime - return (Math.max(rotationImpulseTimeTotal, strokeTimeTotal) > minimumRecordingTimeInSeconds) + if (strokes.length > 0) { + const strokeTimeTotal = strokes[strokes.length - 1].totalMovingTime + return (Math.max(rotationImpulseTimeTotal, strokeTimeTotal) > minimumRecordingTimeInSeconds) + } else { + return (rotationImpulseTimeTotal > minimumRecordingTimeInSeconds) + } } return { From bd97723f8b969082e79c8fd6ad303ed9f69b280c Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Thu, 9 Mar 2023 10:55:45 +0100 Subject: [PATCH 070/231] Cut/copy/paste error in log message --- app/engine/Flywheel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/engine/Flywheel.js b/app/engine/Flywheel.js index ee2e4d8448..d45ad7779a 100644 --- a/app/engine/Flywheel.js +++ b/app/engine/Flywheel.js @@ -71,7 +71,7 @@ function createFlywheel (rowerSettings) { if (dataPoint < rowerSettings.minimumTimeBetweenImpulses && maintainMetrics) { // This shouldn't happen, but let's log it to clarify there is some issue going on here - log.debug(`*** WARNING: currentDt of ${dataPoint} sec is above minimumTimeBetweenImpulses (${rowerSettings.minimumTimeBetweenImpulses} sec)`) + log.debug(`*** WARNING: currentDt of ${dataPoint} sec is below minimumTimeBetweenImpulses (${rowerSettings.minimumTimeBetweenImpulses} sec)`) } currentDt.push(dataPoint) From 6aa5c696c6bffc2b58760bcbd64316694d026469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Wed, 22 Mar 2023 12:54:33 +0100 Subject: [PATCH 071/231] Add force curve component Add a component that shows the force curve of the last drive in a chart. --- app/client/components/DashboardForceCurve.js | 105 ++++++++++++++++++ app/client/components/PerformanceDashboard.js | 4 +- app/client/lib/app.js | 8 +- package.json | 3 +- 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 app/client/components/DashboardForceCurve.js diff --git a/app/client/components/DashboardForceCurve.js b/app/client/components/DashboardForceCurve.js new file mode 100644 index 0000000000..d73f0b55ee --- /dev/null +++ b/app/client/components/DashboardForceCurve.js @@ -0,0 +1,105 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Component that renders a metric of the dashboard +*/ + +import { AppElement, html, css } from './AppElement.js' +import { customElement, property } from 'lit/decorators.js' +import Chart from 'chart.js/auto' + +@customElement('dashboard-force-curve') +export class DashboardForceCurve extends AppElement { + static styles = css` + canvas { + margin-top: 24px; + } + ` + @property({ type: Object }) + value = [] + + chart + + firstUpdated () { + const ctx = this.renderRoot.querySelector('#chart').getContext('2d') + this.chart = new Chart( + ctx, + { + type: 'line', + data: { + datasets: [ + { + fill: true, + data: this.value?.map((data, index) => ({ y: data, x: index })), + pointRadius: 1, + borderColor: 'rgb(255,255,255)', + backgroundColor: 'rgb(220,220,220)' + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + title: { + display: true, + text: 'Force Curve', + color: 'rgb(255,255,255)', + font: { + size: 32 + }, + padding: { + } + }, + labels: { + boxWidth: 0, + font: { + size: 0 + } + } + } + }, + scales: { + x: { + type: 'linear', + display: false + }, + y: { + ticks: { + color: 'rgb(255,255,255)' + } + } + }, + animations: { + tension: { + duration: 200, + easing: 'easeInQuad' + }, + y: { + duration: 200, + easing: 'easeInQuad' + }, + x: { + duration: 200, + easing: 'easeInQuad' + } + } + } + } + ) + } + + render () { + if (this.chart?.data) { + this.chart.data.datasets[0].data = this.value?.map((data, index) => ({ y: data, x: index })) + this.forceCurve = this.value + this.chart.update() + } + + return html` + + ` + } +} diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index a37fcb41fc..7243452322 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -8,6 +8,7 @@ import { AppElement, html, css } from './AppElement.js' import { APP_STATE } from '../store/appState.js' import { customElement, property } from 'lit/decorators.js' +import './DashboardForceCurve.js' import './DashboardMetric.js' import './DashboardActions.js' import './BatteryIcon.js' @@ -32,7 +33,7 @@ export class PerformanceDashboard extends AppElement { } } - dashboard-metric, dashboard-actions { + dashboard-metric, dashboard-actions,dashboard-force-curve { background: var(--theme-widget-color); text-align: center; position: relative; @@ -69,6 +70,7 @@ export class PerformanceDashboard extends AppElement { } ` : html``} + diff --git a/app/client/lib/app.js b/app/client/lib/app.js index c4104893c9..fff8a1b96f 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -9,7 +9,7 @@ import NoSleep from 'nosleep.js' import { filterObjectByKeys } from './helper.js' const rowingMetricsFields = ['totalNumberOfStrokes', 'totalLinearDistanceFormatted', 'totalCalories', 'cyclePower', 'heartrate', - 'heartrateBatteryLevel', 'cyclePaceFormatted', 'cycleStrokeRate', 'totalMovingTimeFormatted'] + 'heartrateBatteryLevel', 'cyclePaceFormatted', 'cycleStrokeRate', 'totalMovingTimeFormatted', 'driveHandleForceCurve'] export function createApp (app) { const urlParameters = new URLSearchParams(window.location.search) @@ -79,11 +79,11 @@ export function createApp (app) { // if we are in reset state only update heart rate and peripheral mode if (data.totalNumberOfStrokes < 1) { if (data.totalLinearDistanceFormatted > 0) { - activeFields = ['totalLinearDistanceFormatted', 'heartrate', 'heartrateBatteryLevel'] + activeFields = ['totalLinearDistanceFormatted', 'heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve'] } else if (data.totalMovingTimeFormatted !== '00:00') { - activeFields = ['totalMovingTimeFormatted', 'heartrate', 'heartrateBatteryLevel'] + activeFields = ['totalMovingTimeFormatted', 'heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve'] } else { - activeFields = ['heartrate', 'heartrateBatteryLevel'] + activeFields = ['heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve'] } } diff --git a/package.json b/package.json index d1fbe6e2c9..60e95ad56d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "pigpio": "3.3.1", "serve-static": "1.14.2", "ws": "8.5.0", - "xml2js": "0.4.23" + "xml2js": "0.4.23", + "chart.js": "^4.2.1" }, "//fix1Comment": "version 0.5.3-8 currently does not work with bleno", "optionalDependencies": { From 00d9c824d0bc05b7df506710b2f51c5a487fcb74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Wed, 22 Mar 2023 13:34:27 +0100 Subject: [PATCH 072/231] Create settings dialog and improve AppDialog Update AppDialog to be able to handle invalid forms, and implement the settings dialog that communicates via appState --- app/client/components/AppDialog.js | 22 +- app/client/components/PerformanceDashboard.js | 36 ++- app/client/components/SettingsDialog.js | 206 ++++++++++++++++++ app/client/lib/icons.js | 1 + 4 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 app/client/components/SettingsDialog.js diff --git a/app/client/components/AppDialog.js b/app/client/components/AppDialog.js index eab5d2efae..e37a27901c 100644 --- a/app/client/components/AppDialog.js +++ b/app/client/components/AppDialog.js @@ -46,10 +46,15 @@ export class AppDialog extends AppElement { justify-content: center; align-items: center; } - button:hover { + button:hover:not(.disabled) { filter: brightness(150%); } + button.disabled { + filter: brightness(50%); + pointer: none + } + fieldset { border: 0; margin: unset; @@ -67,6 +72,8 @@ export class AppDialog extends AppElement { padding: 0; } ` + @property({ type: Boolean }) + isValid = true @property({ type: Boolean, reflect: true }) dialogOpen @@ -74,13 +81,13 @@ export class AppDialog extends AppElement { render () { return html` -
+
- - + +
@@ -95,6 +102,13 @@ export class AppDialog extends AppElement { } } + confirm () { + if (this.isValid) { + this.close({ target: { returnValue: 'confirm' } }) + this.dialogOpen = false + } + } + firstUpdated () { this.dialog.value.showModal() } diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 7243452322..4e5c91324a 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -7,12 +7,13 @@ import { AppElement, html, css } from './AppElement.js' import { APP_STATE } from '../store/appState.js' -import { customElement, property } from 'lit/decorators.js' +import { customElement, property, state } from 'lit/decorators.js' import './DashboardForceCurve.js' import './DashboardMetric.js' import './DashboardActions.js' import './BatteryIcon.js' -import { icon_route, icon_stopwatch, icon_bolt, icon_paddle, icon_heartbeat, icon_fire, icon_clock } from '../lib/icons.js' +import './SettingsDialog' +import { icon_route, icon_stopwatch, icon_bolt, icon_paddle, icon_heartbeat, icon_fire, icon_clock, icon_settings } from '../lib/icons.js' @customElement('performance-dashboard') export class PerformanceDashboard extends AppElement { @@ -44,7 +45,26 @@ export class PerformanceDashboard extends AppElement { dashboard-actions { padding: 0.5em 0 0 0; } + + .settings { + padding: 0.1em 0; + position: absolute; + bottom: 0; + right: 0; + z-index: 20; + } + + .settings .icon { + cursor: pointer; + height: 1em; + } + + .settings:hover .icon { + filter: brightness(150%); + } ` + @state({ type: Object }) + dialog @property({ type: Object }) metrics @@ -55,6 +75,10 @@ export class PerformanceDashboard extends AppElement { render () { const metrics = this.calculateFormattedMetrics(this.appState.metrics) return html` +
+ ${icon_settings} + ${this.dialog ? this.dialog : ''} +
@@ -77,6 +101,14 @@ export class PerformanceDashboard extends AppElement { ` } + openSettings () { + this.dialog = html`` + + function dialogClosed (event) { + this.dialog = undefined + } + } + // todo: so far this is just a port of the formatter from the initial proof of concept client // we could split this up to make it more readable and testable calculateFormattedMetrics (metrics) { diff --git a/app/client/components/SettingsDialog.js b/app/client/components/SettingsDialog.js new file mode 100644 index 0000000000..9e15267b6e --- /dev/null +++ b/app/client/components/SettingsDialog.js @@ -0,0 +1,206 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Component that renders the action buttons of the dashboard +*/ + +import { AppElement, html, css } from './AppElement.js' +import { customElement, property, queryAll } from 'lit/decorators.js' +import { icon_settings } from '../lib/icons.js' +import './AppDialog.js' + +@customElement('settings-dialog') +export class DashboardActions extends AppElement { + static styles = css` + .metric-selector-feedback{ + font-size: 0.4em; + padding-top: 8px; + } + + .metric-selector-feedback>div { + display: grid; + grid-template-columns: repeat(4,1fr); + grid-template-rows: repeat(2, max-content); + gap: 8px; + } + + .settings-dialog>div.metric-selector{ + display: grid; + grid-template-columns: repeat(4,max-content); + grid-template-rows: repeat(3, max-content); + gap: 8px; + + } + + .settings-dialog>div>label{ + font-size: 0.6em; + } + + input[type="checkbox"]{ + cursor: pointer; + align-self: center; + width: 1.5em; + height: 1.5em; + } + + .icon { + height: 1.6em; + } + + legend{ + text-align: center; + } + + table { + min-height: 70px; + margin-top: 8px; + width: 100%; + } + + table, th, td { + font-size: 0.8em; + border: 1px solid white; + border-collapse: collapse; + } + + tr { + height: 50%; + } + + th, td { + padding: 8px; + text-align: center; + background-color: var(--theme-widget-color); + } + ` + + @property({ type: Object }) + config + + @queryAll('input') + inputs + + static get properties () { + return { + selectedMetrics: { type: Array }, + sumSelectedSlots: { type: Number }, + isValid: { type: Boolean } + } + } + + constructor () { + super() + this.selectedMetrics = [] + this.sumSelectedSlots = 0 + this.isValid = false + } + + @property({ type: Object }) + icon + + render () { + return html` + + ${icon_settings}
Settings
+ +

Select metrics to be shown:

+
+ + + + + + + + + + + + + + + + + + + + +
+
Slots remaining: ${8 - this.sumSelectedSlots} + + ${this.renderSelectedMetrics()} +
+
+
+ ` + } + + firstUpdated () { + this.selectedMetrics = this.config.dashboardMetrics + this.sumSelectedSlots = this.selectedMetrics.length + if (this.sumSelectedSlots === 8) { + this.isValid = true + } else { + this.isValid = false + } + [...this.inputs].forEach(input => { + input.checked = this.selectedMetrics.find(metric => metric === input.name) !== undefined + }) + } + + renderSelectedMetrics () { + const selectedMetrics = [html`${[0, 1, 2, 3].map(index => html`${this.selectedMetrics[index]}`)}`] + selectedMetrics.push(html`${[4, 5, 6, 7].map(index => html`${this.selectedMetrics[index]}`)}`) + + return selectedMetrics + } + + toggleCheck (e) { + if ((e.target.checked && this.selectedMetrics.length < 4 && e.target.size > 1 && this.selectedMetrics.length + e.target.size > 4) || (e.target.checked && this.sumSelectedSlots + 1 > 8)) { + this.isValid = this.isFormValid() + e.target.checked = false + return + } + + if (e.target.checked) { + for (let index = 0; index < e.target.size; index++) { + this.selectedMetrics = [...this.selectedMetrics, e.target.name] + } + } else { + for (let index = 0; index < e.target.size; index++) { + this.selectedMetrics.splice(this.selectedMetrics.findIndex(metric => metric === e.target.name), 1) + } + } + + this.sumSelectedSlots = this.selectedMetrics.length + if (this.isFormValid()) { + this.isValid = true + } else { + this.isValid = false + } + } + + isFormValid () { + return this.sumSelectedSlots === 8 && this.selectedMetrics[3] !== this.selectedMetrics[4] + } + + close (event) { + this.dispatchEvent(new CustomEvent('close')) + if (event.detail === 'confirm') { + this.dispatchEvent(new CustomEvent('changeGuiSetting', { + detail: { + ...this.appState, + config: { + ...this.appState.config, + guiConfigs: { + dashboardMetrics: this.selectedMetrics + } + } + }, + bubbles: true, + composed: true + })) + } + } +} diff --git a/app/client/lib/icons.js b/app/client/lib/icons.js index e23328061b..78d6e13543 100644 --- a/app/client/lib/icons.js +++ b/app/client/lib/icons.js @@ -27,3 +27,4 @@ export const icon_bluetooth = svg`` export const icon_antplus = svg`` +export const icon_settings = svg`` From cac178f06d1975ea460b5acbf7ef7b43191fe09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Wed, 22 Mar 2023 13:44:34 +0100 Subject: [PATCH 073/231] Add settable metric tiles and settings persistence Make the metric tiles settable via the settings dialog and implement persistence of these settings to the browser localStorage (partially fix #131) --- app/client/components/PerformanceDashboard.js | 62 ++++++++++++------- app/client/index.js | 16 ++++- app/client/lib/app.js | 4 +- app/client/store/appState.js | 5 +- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 4e5c91324a..91492b6287 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -34,7 +34,7 @@ export class PerformanceDashboard extends AppElement { } } - dashboard-metric, dashboard-actions,dashboard-force-curve { + dashboard-metric, dashboard-actions, dashboard-force-curve { background: var(--theme-widget-color); text-align: center; position: relative; @@ -63,6 +63,32 @@ export class PerformanceDashboard extends AppElement { filter: brightness(150%); } ` + dashboardMetricComponents = (formattedMetrics, appState) => ({ + distance: html``, + + pace: html``, + + power: html``, + + stkRate: html``, + + heartRate: html` + ${formattedMetrics?.heartrateBatteryLevel?.value + ? html`` + : ''} + `, + + totalStk: html``, + + calories: html``, + + timer: html``, + + forceCurve: html``, + + actions: html`` + }) + @state({ type: Object }) dialog @@ -73,31 +99,19 @@ export class PerformanceDashboard extends AppElement { appState = APP_STATE render () { - const metrics = this.calculateFormattedMetrics(this.appState.metrics) + const metricConfig = [...new Set(this.appState.config.guiConfigs.dashboardMetrics)].reduce((prev, metricName) => { + prev.push(this.dashboardMetricComponents(this.metrics, this.appState)[metricName]) + return prev + }, []) + + this.metrics = this.calculateFormattedMetrics(this.appState.metrics) return html`
${icon_settings} ${this.dialog ? this.dialog : ''}
- - - - - ${metrics?.heartrate?.value - ? html` - - ${metrics?.heartrateBatteryLevel?.value - ? html` - - ` - : '' - } - ` - : html``} - - - - + + ${metricConfig} ` } @@ -124,10 +138,10 @@ export class PerformanceDashboard extends AppElement { const formattedMetrics = {} for (const [key, value] of Object.entries(metrics)) { const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value - if (valueFormatted.value !== undefined && valueFormatted.unit !== undefined) { + if (valueFormatted?.value !== undefined && valueFormatted?.unit !== undefined) { formattedMetrics[key] = { - value: valueFormatted.value, - unit: valueFormatted.unit + value: valueFormatted?.value, + unit: valueFormatted?.unit } } else { formattedMetrics[key] = { diff --git a/app/client/index.js b/app/client/index.js index b26dfcd4e6..d81f2cd3a4 100644 --- a/app/client/index.js +++ b/app/client/index.js @@ -28,6 +28,11 @@ export class App extends LitElement { // todo: we also want a mechanism here to get notified of state changes }) + const config = this.appState.config.guiConfigs + Object.keys(config).forEach(key => { + config[key] = JSON.parse(localStorage.getItem(key)) ?? config[key] + }) + // this is how we implement changes to the global state: // once any child component sends this CustomEvent we update the global state according // to the changes that were passed to us @@ -39,13 +44,22 @@ export class App extends LitElement { this.addEventListener('triggerAction', (event) => { this.app.handleAction(event.detail) }) + + // notify the app about the triggered action + this.addEventListener('changeGuiSetting', (event) => { + Object.keys(event.detail.config.guiConfigs).forEach(key => { + localStorage.setItem(key, JSON.stringify(event.detail.config.guiConfigs[key])) + }) + + this.updateState(event.detail) + }) } // the global state is updated by replacing the appState with a copy of the new state // todo: maybe it is more convenient to just pass the state elements that should be changed? // i.e. do something like this.appState = { ..this.appState, ...newState } updateState = (newState) => { - this.appState = { ...newState } + this.appState = { ...this.appState, ...newState } } // return a deep copy of the state to other components to minimize risk of side effects diff --git a/app/client/lib/app.js b/app/client/lib/app.js index fff8a1b96f..740499806c 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -38,7 +38,7 @@ export function createApp (app) { let initialWebsocketOpenend = true function initWebsocket () { // use the native websocket implementation of browser to communicate with backend - socket = new WebSocket(`ws://${location.host}/websocket`) + socket = new WebSocket('ws://localhost:100/websocket') socket.addEventListener('open', (event) => { console.log('websocket opened') @@ -71,7 +71,7 @@ export function createApp (app) { const data = message.data switch (message.type) { case 'config': { - app.updateState({ ...app.getState(), config: data }) + app.updateState({ ...app.getState(), config: { ...app.getState().config, ...data } }) break } case 'metrics': { diff --git a/app/client/store/appState.js b/app/client/store/appState.js index 77c78e2def..454a0cd174 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -20,6 +20,9 @@ export const APP_STATE = { // true if upload to strava is enabled stravaUploadEnabled: false, // true if remote device shutdown is enabled - shutdownEnabled: false + shutdownEnabled: false, + guiConfigs: { + dashboardMetrics: ['distance', 'timer', 'pace', 'power', 'stkRate', 'totalStk', 'calories', 'actions'] + } } } From 28d223c1094a5ef33dd48156ed5578db3aa71104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Wed, 22 Mar 2023 15:02:16 +0100 Subject: [PATCH 074/231] Add the ability to hide icons Implement GUI setting to hide icons in exchange for increased font size (implements: #131) --- app/client/components/DashboardMetric.js | 4 +-- app/client/components/PerformanceDashboard.js | 16 +++++----- app/client/components/SettingsDialog.js | 30 +++++++++++++++++-- app/client/store/appState.js | 3 +- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/app/client/components/DashboardMetric.js b/app/client/components/DashboardMetric.js index 185c89f470..215a89a72b 100644 --- a/app/client/components/DashboardMetric.js +++ b/app/client/components/DashboardMetric.js @@ -35,7 +35,7 @@ export class DashboardMetric extends AppElement { ` @property({ type: Object }) - icon + icon = '' @property({ type: String }) unit = '' @@ -47,7 +47,7 @@ export class DashboardMetric extends AppElement { return html`
${this.icon}
- ${this.value !== undefined ? this.value : '--'} + ${this.value !== undefined ? this.value : '--'} ${this.unit}
diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 91492b6287..22c01b5889 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -64,25 +64,25 @@ export class PerformanceDashboard extends AppElement { } ` dashboardMetricComponents = (formattedMetrics, appState) => ({ - distance: html``, + distance: html``, - pace: html``, + pace: html``, - power: html``, + power: html``, - stkRate: html``, + stkRate: html``, - heartRate: html` + heartRate: html` ${formattedMetrics?.heartrateBatteryLevel?.value ? html`` : ''} `, - totalStk: html``, + totalStk: html``, - calories: html``, + calories: html``, - timer: html``, + timer: html``, forceCurve: html``, diff --git a/app/client/components/SettingsDialog.js b/app/client/components/SettingsDialog.js index 9e15267b6e..9bec86ba31 100644 --- a/app/client/components/SettingsDialog.js +++ b/app/client/components/SettingsDialog.js @@ -6,7 +6,7 @@ */ import { AppElement, html, css } from './AppElement.js' -import { customElement, property, queryAll } from 'lit/decorators.js' +import { customElement, property, query, queryAll } from 'lit/decorators.js' import { icon_settings } from '../lib/icons.js' import './AppDialog.js' @@ -73,14 +73,26 @@ export class DashboardActions extends AppElement { text-align: center; background-color: var(--theme-widget-color); } + + .show-icons-selector { + display: flex; + gap: 8px; + } + + app-dialog > *:last-child { + margin-bottom: -24px; + } ` @property({ type: Object }) config - @queryAll('input') + @queryAll('.metric-selector>input') inputs + @query('input[name="showIcons"]') + showIconInput + static get properties () { return { selectedMetrics: { type: Array }, @@ -93,6 +105,7 @@ export class DashboardActions extends AppElement { super() this.selectedMetrics = [] this.sumSelectedSlots = 0 + this.showIcons = true this.isValid = false } @@ -132,6 +145,10 @@ export class DashboardActions extends AppElement { ${this.renderSelectedMetrics()} +

+ + +

` } @@ -139,6 +156,7 @@ export class DashboardActions extends AppElement { firstUpdated () { this.selectedMetrics = this.config.dashboardMetrics this.sumSelectedSlots = this.selectedMetrics.length + this.showIcons = this.config.showIcons if (this.sumSelectedSlots === 8) { this.isValid = true } else { @@ -147,6 +165,7 @@ export class DashboardActions extends AppElement { [...this.inputs].forEach(input => { input.checked = this.selectedMetrics.find(metric => metric === input.name) !== undefined }) + this.showIconInput.checked = this.showIcons } renderSelectedMetrics () { @@ -181,6 +200,10 @@ export class DashboardActions extends AppElement { } } + toggleIcons (e) { + this.showIcons = e.target.checked + } + isFormValid () { return this.sumSelectedSlots === 8 && this.selectedMetrics[3] !== this.selectedMetrics[4] } @@ -194,7 +217,8 @@ export class DashboardActions extends AppElement { config: { ...this.appState.config, guiConfigs: { - dashboardMetrics: this.selectedMetrics + dashboardMetrics: this.selectedMetrics, + showIcons: this.showIcons } } }, diff --git a/app/client/store/appState.js b/app/client/store/appState.js index 454a0cd174..ee1af51447 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -22,7 +22,8 @@ export const APP_STATE = { // true if remote device shutdown is enabled shutdownEnabled: false, guiConfigs: { - dashboardMetrics: ['distance', 'timer', 'pace', 'power', 'stkRate', 'totalStk', 'calories', 'actions'] + dashboardMetrics: ['distance', 'timer', 'pace', 'power', 'stkRate', 'totalStk', 'calories', 'actions'], + showIcons: true } } } From 457c96fe3c77aac648962a60a602dabf3e6e8b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Wed, 22 Mar 2023 20:21:28 +0100 Subject: [PATCH 075/231] Fix webSocket address bug --- app/client/lib/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 740499806c..7ce477615c 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -38,7 +38,7 @@ export function createApp (app) { let initialWebsocketOpenend = true function initWebsocket () { // use the native websocket implementation of browser to communicate with backend - socket = new WebSocket('ws://localhost:100/websocket') + socket = new WebSocket(`ws://${location.host}/websocket`) socket.addEventListener('open', (event) => { console.log('websocket opened') From 6834e3a5588effd344be254a278be8b8870ef902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Thu, 23 Mar 2023 16:08:56 +0100 Subject: [PATCH 076/231] Code refactoring and improve code consistency --- app/client/components/DashboardActions.js | 14 +-- app/client/components/DashboardForceCurve.js | 14 +-- app/client/components/DashboardMetric.js | 2 +- app/client/components/PerformanceDashboard.js | 21 ++-- app/client/components/SettingsDialog.js | 100 ++++++++---------- app/client/index.js | 14 +-- 6 files changed, 73 insertions(+), 92 deletions(-) diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index 84db932e01..e416917a11 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -74,8 +74,8 @@ export class DashboardActions extends AppElement { } ` - @state({ type: Object }) - dialog + @state() + _dialog render () { return html` @@ -95,7 +95,7 @@ export class DashboardActions extends AppElement {
${this.blePeripheralMode()}
- ${this.dialog ? this.dialog : ''} + ${this._dialog ? this._dialog : ''} ` } @@ -176,14 +176,14 @@ export class DashboardActions extends AppElement { } uploadTraining () { - this.dialog = html` + this._dialog = html` ${icon_upload}
Upload to Strava?

Do you want to finish your workout and upload it to Strava?

` function dialogClosed (event) { - this.dialog = undefined + this._dialog = undefined if (event.detail === 'confirm') { this.sendEvent('triggerAction', { command: 'uploadTraining' }) } @@ -191,14 +191,14 @@ export class DashboardActions extends AppElement { } shutdown () { - this.dialog = html` + this._dialog = html` ${icon_poweroff}
Shutdown Open Rowing Monitor?

Do you want to shutdown the device?

` function dialogClosed (event) { - this.dialog = undefined + this._dialog = undefined if (event.detail === 'confirm') { this.sendEvent('triggerAction', { command: 'shutdown' }) } diff --git a/app/client/components/DashboardForceCurve.js b/app/client/components/DashboardForceCurve.js index d73f0b55ee..5222a2e1c3 100644 --- a/app/client/components/DashboardForceCurve.js +++ b/app/client/components/DashboardForceCurve.js @@ -6,7 +6,7 @@ */ import { AppElement, html, css } from './AppElement.js' -import { customElement, property } from 'lit/decorators.js' +import { customElement, property, state } from 'lit/decorators.js' import Chart from 'chart.js/auto' @customElement('dashboard-force-curve') @@ -16,14 +16,16 @@ export class DashboardForceCurve extends AppElement { margin-top: 24px; } ` + @property({ type: Object }) value = [] - chart + @state() + _chart firstUpdated () { const ctx = this.renderRoot.querySelector('#chart').getContext('2d') - this.chart = new Chart( + this._chart = new Chart( ctx, { type: 'line', @@ -92,10 +94,10 @@ export class DashboardForceCurve extends AppElement { } render () { - if (this.chart?.data) { - this.chart.data.datasets[0].data = this.value?.map((data, index) => ({ y: data, x: index })) + if (this._chart?.data) { + this._chart.data.datasets[0].data = this.value?.map((data, index) => ({ y: data, x: index })) this.forceCurve = this.value - this.chart.update() + this._chart.update() } return html` diff --git a/app/client/components/DashboardMetric.js b/app/client/components/DashboardMetric.js index 215a89a72b..18d7a9f83c 100644 --- a/app/client/components/DashboardMetric.js +++ b/app/client/components/DashboardMetric.js @@ -41,7 +41,7 @@ export class DashboardMetric extends AppElement { unit = '' @property({ type: String }) - value = '' + value render () { return html` diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 22c01b5889..bca882145e 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -6,8 +6,7 @@ */ import { AppElement, html, css } from './AppElement.js' -import { APP_STATE } from '../store/appState.js' -import { customElement, property, state } from 'lit/decorators.js' +import { customElement, state } from 'lit/decorators.js' import './DashboardForceCurve.js' import './DashboardMetric.js' import './DashboardActions.js' @@ -63,6 +62,9 @@ export class PerformanceDashboard extends AppElement { filter: brightness(150%); } ` + @state() + _dialog + dashboardMetricComponents = (formattedMetrics, appState) => ({ distance: html``, @@ -89,15 +91,6 @@ export class PerformanceDashboard extends AppElement { actions: html`` }) - @state({ type: Object }) - dialog - - @property({ type: Object }) - metrics - - @property({ type: Object }) - appState = APP_STATE - render () { const metricConfig = [...new Set(this.appState.config.guiConfigs.dashboardMetrics)].reduce((prev, metricName) => { prev.push(this.dashboardMetricComponents(this.metrics, this.appState)[metricName]) @@ -108,7 +101,7 @@ export class PerformanceDashboard extends AppElement { return html`
${icon_settings} - ${this.dialog ? this.dialog : ''} + ${this._dialog ? this._dialog : ''}
${metricConfig} @@ -116,10 +109,10 @@ export class PerformanceDashboard extends AppElement { } openSettings () { - this.dialog = html`` + this._dialog = html`` function dialogClosed (event) { - this.dialog = undefined + this._dialog = undefined } } diff --git a/app/client/components/SettingsDialog.js b/app/client/components/SettingsDialog.js index 9bec86ba31..d76c1cf5e0 100644 --- a/app/client/components/SettingsDialog.js +++ b/app/client/components/SettingsDialog.js @@ -6,7 +6,7 @@ */ import { AppElement, html, css } from './AppElement.js' -import { customElement, property, query, queryAll } from 'lit/decorators.js' +import { customElement, property, query, queryAll, state } from 'lit/decorators.js' import { icon_settings } from '../lib/icons.js' import './AppDialog.js' @@ -85,36 +85,29 @@ export class DashboardActions extends AppElement { ` @property({ type: Object }) - config + config = {} @queryAll('.metric-selector>input') - inputs + _inputs @query('input[name="showIcons"]') - showIconInput + _showIconInput - static get properties () { - return { - selectedMetrics: { type: Array }, - sumSelectedSlots: { type: Number }, - isValid: { type: Boolean } - } - } + @state() + _selectedMetrics = [] - constructor () { - super() - this.selectedMetrics = [] - this.sumSelectedSlots = 0 - this.showIcons = true - this.isValid = false - } + @state() + _sumSelectedSlots = 0 - @property({ type: Object }) - icon + @state() + _isValid = false + + @state() + _showIcons = true render () { return html` - + ${icon_settings}
Settings

Select metrics to be shown:

@@ -140,7 +133,7 @@ export class DashboardActions extends AppElement { -
Slots remaining: ${8 - this.sumSelectedSlots} +
Slots remaining: ${8 - this._sumSelectedSlots} ${this.renderSelectedMetrics()}
@@ -154,77 +147,74 @@ export class DashboardActions extends AppElement { } firstUpdated () { - this.selectedMetrics = this.config.dashboardMetrics - this.sumSelectedSlots = this.selectedMetrics.length - this.showIcons = this.config.showIcons - if (this.sumSelectedSlots === 8) { - this.isValid = true + this._selectedMetrics = [...this.config.dashboardMetrics] + this._sumSelectedSlots = this._selectedMetrics.length + this._showIcons = this.config.showIcons + if (this._sumSelectedSlots === 8) { + this._isValid = true } else { - this.isValid = false + this._isValid = false } - [...this.inputs].forEach(input => { - input.checked = this.selectedMetrics.find(metric => metric === input.name) !== undefined + [...this._inputs].forEach(input => { + input.checked = this._selectedMetrics.find(metric => metric === input.name) !== undefined }) - this.showIconInput.checked = this.showIcons + this._showIconInput.checked = this._showIcons } renderSelectedMetrics () { - const selectedMetrics = [html`${[0, 1, 2, 3].map(index => html`${this.selectedMetrics[index]}`)}`] - selectedMetrics.push(html`${[4, 5, 6, 7].map(index => html`${this.selectedMetrics[index]}`)}`) + const selectedMetrics = [html`${[0, 1, 2, 3].map(index => html`${this._selectedMetrics[index]}`)}`] + selectedMetrics.push(html`${[4, 5, 6, 7].map(index => html`${this._selectedMetrics[index]}`)}`) return selectedMetrics } toggleCheck (e) { - if ((e.target.checked && this.selectedMetrics.length < 4 && e.target.size > 1 && this.selectedMetrics.length + e.target.size > 4) || (e.target.checked && this.sumSelectedSlots + 1 > 8)) { - this.isValid = this.isFormValid() + if ((e.target.checked && this._selectedMetrics.length < 4 && e.target.size > 1 && this._selectedMetrics.length + e.target.size > 4) || (e.target.checked && this._sumSelectedSlots + 1 > 8)) { + this._isValid = this.isFormValid() e.target.checked = false return } if (e.target.checked) { for (let index = 0; index < e.target.size; index++) { - this.selectedMetrics = [...this.selectedMetrics, e.target.name] + this._selectedMetrics = [...this._selectedMetrics, e.target.name] } } else { for (let index = 0; index < e.target.size; index++) { - this.selectedMetrics.splice(this.selectedMetrics.findIndex(metric => metric === e.target.name), 1) + this._selectedMetrics.splice(this._selectedMetrics.findIndex(metric => metric === e.target.name), 1) + this._selectedMetrics = [...this._selectedMetrics] } } - this.sumSelectedSlots = this.selectedMetrics.length + this._sumSelectedSlots = this._selectedMetrics.length if (this.isFormValid()) { - this.isValid = true + this._isValid = true } else { - this.isValid = false + this._isValid = false } } toggleIcons (e) { - this.showIcons = e.target.checked + this._showIcons = e.target.checked } isFormValid () { - return this.sumSelectedSlots === 8 && this.selectedMetrics[3] !== this.selectedMetrics[4] + return this._sumSelectedSlots === 8 && this._selectedMetrics[3] !== this._selectedMetrics[4] } close (event) { this.dispatchEvent(new CustomEvent('close')) if (event.detail === 'confirm') { - this.dispatchEvent(new CustomEvent('changeGuiSetting', { - detail: { - ...this.appState, - config: { - ...this.appState.config, - guiConfigs: { - dashboardMetrics: this.selectedMetrics, - showIcons: this.showIcons - } + this.sendEvent('changeGuiSetting', { + ...this.appState, + config: { + ...this.appState.config, + guiConfigs: { + dashboardMetrics: this._selectedMetrics, + showIcons: this._showIcons } - }, - bubbles: true, - composed: true - })) + } + }) } } } diff --git a/app/client/index.js b/app/client/index.js index d81f2cd3a4..a4ce57e6a7 100644 --- a/app/client/index.js +++ b/app/client/index.js @@ -14,10 +14,7 @@ import './components/PerformanceDashboard.js' @customElement('web-app') export class App extends LitElement { @state() - appState = APP_STATE - - @state() - metrics + _appState = APP_STATE constructor () { super() @@ -28,7 +25,7 @@ export class App extends LitElement { // todo: we also want a mechanism here to get notified of state changes }) - const config = this.appState.config.guiConfigs + const config = this._appState.config.guiConfigs Object.keys(config).forEach(key => { config[key] = JSON.parse(localStorage.getItem(key)) ?? config[key] }) @@ -59,14 +56,14 @@ export class App extends LitElement { // todo: maybe it is more convenient to just pass the state elements that should be changed? // i.e. do something like this.appState = { ..this.appState, ...newState } updateState = (newState) => { - this.appState = { ...this.appState, ...newState } + this._appState = { ...this._appState, ...newState } } // return a deep copy of the state to other components to minimize risk of side effects getState = () => { // could use structuredClone once the browser support is wider // https://developer.mozilla.org/en-US/docs/Web/API/structuredClone - return JSON.parse(JSON.stringify(this.appState)) + return JSON.parse(JSON.stringify(this._appState)) } // once we have multiple views, then we would rather reference some kind of router here @@ -74,8 +71,7 @@ export class App extends LitElement { render () { return html` ` } From fe5a7e7ed13890ab3d48bb37b0ea692574220ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Thu, 23 Mar 2023 18:14:44 +0100 Subject: [PATCH 077/231] Remove dependency on server formatted values Remove dependency on metric formatting logic at the server. Implement the formatting of the raw data on the client side. Make adding new metric tiles more modular and extensible via simplified creation API --- app/client/components/PerformanceDashboard.js | 70 ++++--------------- app/client/components/SettingsDialog.js | 31 +++----- app/client/lib/app.js | 21 +----- app/client/lib/helper.js | 56 +++++++++++++++ app/client/store/dashboardMetrics.js | 35 ++++++++++ 5 files changed, 114 insertions(+), 99 deletions(-) create mode 100644 app/client/store/dashboardMetrics.js diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index bca882145e..48c1620d1e 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -7,12 +7,9 @@ import { AppElement, html, css } from './AppElement.js' import { customElement, state } from 'lit/decorators.js' -import './DashboardForceCurve.js' -import './DashboardMetric.js' -import './DashboardActions.js' -import './BatteryIcon.js' import './SettingsDialog' -import { icon_route, icon_stopwatch, icon_bolt, icon_paddle, icon_heartbeat, icon_fire, icon_clock, icon_settings } from '../lib/icons.js' +import { icon_settings } from '../lib/icons.js' +import { DASHBOARD_METRICS } from '../store/dashboardMetrics.js' @customElement('performance-dashboard') export class PerformanceDashboard extends AppElement { @@ -65,39 +62,25 @@ export class PerformanceDashboard extends AppElement { @state() _dialog - dashboardMetricComponents = (formattedMetrics, appState) => ({ - distance: html``, + dashboardMetricComponentsFactory = (appState) => { + const metrics = appState.metrics + const configs = appState.config.guiConfigs - pace: html``, + const dashboardMetricComponents = Object.keys(DASHBOARD_METRICS).reduce((dashboardMetrics, key) => { + dashboardMetrics[key] = DASHBOARD_METRICS[key].template(metrics, configs.showIcons) - power: html``, + return dashboardMetrics + }, {}) - stkRate: html``, - - heartRate: html` - ${formattedMetrics?.heartrateBatteryLevel?.value - ? html`` - : ''} - `, - - totalStk: html``, - - calories: html``, - - timer: html``, - - forceCurve: html``, - - actions: html`` - }) + return dashboardMetricComponents + } render () { const metricConfig = [...new Set(this.appState.config.guiConfigs.dashboardMetrics)].reduce((prev, metricName) => { - prev.push(this.dashboardMetricComponents(this.metrics, this.appState)[metricName]) + prev.push(this.dashboardMetricComponentsFactory(this.appState)[metricName]) return prev }, []) - this.metrics = this.calculateFormattedMetrics(this.appState.metrics) return html`
${icon_settings} @@ -115,33 +98,4 @@ export class PerformanceDashboard extends AppElement { this._dialog = undefined } } - - // todo: so far this is just a port of the formatter from the initial proof of concept client - // we could split this up to make it more readable and testable - calculateFormattedMetrics (metrics) { - const fieldFormatter = { - totalLinearDistanceFormatted: (value) => value >= 10000 - ? { value: (value / 1000).toFixed(2), unit: 'km' } - : { value: Math.round(value), unit: 'm' }, - totalCalories: (value) => Math.round(value), - cyclePower: (value) => Math.round(value), - cycleStrokeRate: (value) => Math.round(value) - } - - const formattedMetrics = {} - for (const [key, value] of Object.entries(metrics)) { - const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value - if (valueFormatted?.value !== undefined && valueFormatted?.unit !== undefined) { - formattedMetrics[key] = { - value: valueFormatted?.value, - unit: valueFormatted?.unit - } - } else { - formattedMetrics[key] = { - value: valueFormatted - } - } - } - return formattedMetrics - } } diff --git a/app/client/components/SettingsDialog.js b/app/client/components/SettingsDialog.js index d76c1cf5e0..e6b1f7b631 100644 --- a/app/client/components/SettingsDialog.js +++ b/app/client/components/SettingsDialog.js @@ -9,6 +9,7 @@ import { AppElement, html, css } from './AppElement.js' import { customElement, property, query, queryAll, state } from 'lit/decorators.js' import { icon_settings } from '../lib/icons.js' import './AppDialog.js' +import { DASHBOARD_METRICS } from '../store/dashboardMetrics.js' @customElement('settings-dialog') export class DashboardActions extends AppElement { @@ -28,9 +29,7 @@ export class DashboardActions extends AppElement { .settings-dialog>div.metric-selector{ display: grid; grid-template-columns: repeat(4,max-content); - grid-template-rows: repeat(3, max-content); gap: 8px; - } .settings-dialog>div>label{ @@ -112,26 +111,7 @@ export class DashboardActions extends AppElement {

Select metrics to be shown:

- - - - - - - - - - - - - - - - - - - - + ${this.renderAvailableMetricList()}
Slots remaining: ${8 - this._sumSelectedSlots} @@ -161,6 +141,13 @@ export class DashboardActions extends AppElement { this._showIconInput.checked = this._showIcons } + renderAvailableMetricList () { + return Object.keys(DASHBOARD_METRICS).map(key => html` + + + `) + } + renderSelectedMetrics () { const selectedMetrics = [html`${[0, 1, 2, 3].map(index => html``)}`] selectedMetrics.push(html`${[4, 5, 6, 7].map(index => html``)}`) diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 7ce477615c..214c4d54a6 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -8,9 +8,6 @@ import NoSleep from 'nosleep.js' import { filterObjectByKeys } from './helper.js' -const rowingMetricsFields = ['totalNumberOfStrokes', 'totalLinearDistanceFormatted', 'totalCalories', 'cyclePower', 'heartrate', - 'heartrateBatteryLevel', 'cyclePaceFormatted', 'cycleStrokeRate', 'totalMovingTimeFormatted', 'driveHandleForceCurve'] - export function createApp (app) { const urlParameters = new URLSearchParams(window.location.search) const mode = urlParameters.get('mode') @@ -75,20 +72,7 @@ export function createApp (app) { break } case 'metrics': { - let activeFields = rowingMetricsFields - // if we are in reset state only update heart rate and peripheral mode - if (data.totalNumberOfStrokes < 1) { - if (data.totalLinearDistanceFormatted > 0) { - activeFields = ['totalLinearDistanceFormatted', 'heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve'] - } else if (data.totalMovingTimeFormatted !== '00:00') { - activeFields = ['totalMovingTimeFormatted', 'heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve'] - } else { - activeFields = ['heartrate', 'heartrateBatteryLevel', 'driveHandleForceCurve'] - } - } - - const filteredData = filterObjectByKeys(data, activeFields) - app.updateState({ ...app.getState(), metrics: filteredData }) + app.updateState({ ...app.getState(), metrics: data }) break } case 'authorizeStrava': { @@ -124,8 +108,7 @@ export function createApp (app) { function resetFields () { const appState = app.getState() // drop all metrics except heartrate - appState.metrics = filterObjectByKeys(appState.metrics, ['heartrate', 'heartrateBatteryLevel']) - app.updateState(appState) + app.updateState({ ...appState, metrics: { ...filterObjectByKeys(appState.metrics, ['heartrate', 'heartrateBatteryLevel']) } }) } function handleAction (action) { diff --git a/app/client/lib/helper.js b/app/client/lib/helper.js index 16bae1394f..1bca643560 100644 --- a/app/client/lib/helper.js +++ b/app/client/lib/helper.js @@ -1,4 +1,8 @@ 'use strict' + +import { html } from 'lit' +import '../components/DashboardMetric.js' + /* Open Rowing Monitor, https://github.com/laberning/openrowingmonitor @@ -14,3 +18,55 @@ export function filterObjectByKeys (object, keys) { return obj }, {}) } + +/** + * Pipe for converting seconds to pace format 00:00 + * + * @param seconds The actual time in seconds. +*/ +export function secondsToPace (seconds) { + const hours = Math.floor((seconds % 86400) / 3600) + const mins = Math.floor(((seconds % 86400) % 3600) / 60) + + if (seconds === undefined || seconds === null || seconds === Infinity || isNaN(seconds)) return '--' + if (hours > 0) { + return `${hours}:${mins.toString().padStart(2, '0')}:${(Math.round(seconds) % 60) + .toString() + .padStart(2, '0')}` + } else { + return `${mins}:${(Math.round(seconds) % 60).toString().padStart(2, '0')}` + } +} + +/** + * Pipe for formatting distance in meters with units + * + * @param value The distance in meters. + * @param showInMiles Boolean whether to use imperial metric (default: false). +*/ +export function formatDistance (value, showInMiles = false) { + if (showInMiles === false) { + return value >= 10000 + ? { distance: formatNumber((value / 1000), 2), unit: 'km' } + : { distance: formatNumber(value), unit: 'm' } + } + + return { distance: formatNumber((value / 1609.344), 2), unit: 'mi' } +} + +/** + * Pipe for formatting numbers to specific decimal + * + * @param value The number. + * @param decimalPlaces The number of decimal places to round to (default: 0). +*/ +export function formatNumber (value, decimalPlaces = 0) { + const decimal = decimalPlaces > 0 ? decimalPlaces * 10 : 1 + if (value === undefined || value === null || value === Infinity || isNaN(value) || value === 0) { return '--' } + + return Math.round(value * decimal) / decimal +} + +export function simpleMetricFactory (value, unit, icon) { + return html`` +} diff --git a/app/client/store/dashboardMetrics.js b/app/client/store/dashboardMetrics.js new file mode 100644 index 0000000000..cd6ee12589 --- /dev/null +++ b/app/client/store/dashboardMetrics.js @@ -0,0 +1,35 @@ +import { html } from 'lit' +import { simpleMetricFactory, formatDistance, formatNumber, secondsToPace } from '../lib/helper' +import { icon_bolt, icon_clock, icon_fire, icon_heartbeat, icon_paddle, icon_route, icon_stopwatch } from '../lib/icons' +import '../components/DashboardForceCurve.js' +import '../components/DashboardActions.js' +import '../components/BatteryIcon.js' + +export const DASHBOARD_METRICS = { + distance: { + displayName: 'Distance', + size: 1, + template: (metrics, showIcon) => { + const linearDistance = formatDistance(metrics?.totalLinearDistance) + + return simpleMetricFactory(linearDistance.distance, linearDistance.unit, showIcon ? icon_route : '') + } + }, + pace: { displayName: 'Pace/500', size: 1, template: (metrics, showIcon) => simpleMetricFactory(secondsToPace(500 / metrics?.cycleLinearVelocity), '/500m', showIcon ? icon_stopwatch : '') }, + power: { displayName: 'Power', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.cyclePower), 'watt', showIcon ? icon_bolt : '') }, + stkRate: { displayName: 'Stroke rate', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.cycleStrokeRate), '/min', showIcon ? icon_paddle : '') }, + heartRate: { + displayName: 'Heart rate', + size: 1, + template: (metrics, showIcon) => html` + ${metrics?.heartrateBatteryLevel + ? html`` + : ''} +` + }, + totalStk: { displayName: 'Total strokes', size: 1, template: (metrics, showIcon) => simpleMetricFactory(metrics?.totalNumberOfStrokes, 'stk', showIcon ? icon_paddle : '') }, + calories: { displayName: 'Calories', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.totalCalories), 'kcal', showIcon ? icon_fire : '') }, + timer: { displayName: 'Timer', size: 1, template: (metrics, showIcon) => simpleMetricFactory(secondsToPace(metrics?.totalMovingTime), '', showIcon ? icon_clock : '') }, + forceCurve: { displayName: 'Force curve', size: 2, template: (metrics) => html`` }, + actions: { displayName: 'Actions', size: 1, template: () => html`` } +} From a4ef6d86b072b691885daeca88ee7080edd542c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Thu, 23 Mar 2023 23:02:48 +0100 Subject: [PATCH 078/231] Show peak value in the force curve Add the capability of showing the peak value for the handle force curve --- app/client/components/DashboardForceCurve.js | 24 +++++++++++++++++--- package.json | 5 ++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/client/components/DashboardForceCurve.js b/app/client/components/DashboardForceCurve.js index 5222a2e1c3..74a8b7aced 100644 --- a/app/client/components/DashboardForceCurve.js +++ b/app/client/components/DashboardForceCurve.js @@ -7,7 +7,8 @@ import { AppElement, html, css } from './AppElement.js' import { customElement, property, state } from 'lit/decorators.js' -import Chart from 'chart.js/auto' +import ChartDataLabels from 'chartjs-plugin-datalabels' +import { Chart, Filler, Legend, LinearScale, LineController, LineElement, PointElement } from 'chart.js/auto' @customElement('dashboard-force-curve') export class DashboardForceCurve extends AppElement { @@ -17,6 +18,11 @@ export class DashboardForceCurve extends AppElement { } ` + constructor () { + super() + Chart.register(ChartDataLabels, Legend, Filler, LinearScale, LineController, PointElement, LineElement) + } + @property({ type: Object }) value = [] @@ -33,7 +39,7 @@ export class DashboardForceCurve extends AppElement { datasets: [ { fill: true, - data: this.value?.map((data, index) => ({ y: data, x: index })), + data: this.value?.map((data, index) => ({ y: parseInt(data, 10), x: index })), pointRadius: 1, borderColor: 'rgb(255,255,255)', backgroundColor: 'rgb(220,220,220)' @@ -44,6 +50,18 @@ export class DashboardForceCurve extends AppElement { responsive: true, maintainAspectRatio: false, plugins: { + datalabels: { + anchor: 'center', + align: 'top', + formatter: (value) => `Peak: ${Math.round(value.y)}`, + display: (ctx) => Math.max( + ...ctx.dataset.data.map((point) => point.y) + ) === ctx.dataset.data[ctx.dataIndex].y, + font: { + size: 16 + }, + color: 'rgb(255,255,255)' + }, legend: { title: { display: true, @@ -95,7 +113,7 @@ export class DashboardForceCurve extends AppElement { render () { if (this._chart?.data) { - this._chart.data.datasets[0].data = this.value?.map((data, index) => ({ y: data, x: index })) + this._chart.data.datasets[0].data = this.value?.map((data, index) => ({ y: parseInt(data, 10), x: index })) this.forceCurve = this.value this._chart.update() } diff --git a/package.json b/package.json index 60e95ad56d..518b9646b6 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "dependencies": { "@abandonware/bleno": "0.5.1-4", "@abandonware/noble": "1.9.2-15", + "chart.js": "^4.2.1", + "chartjs-plugin-datalabels": "^2.2.0", "finalhandler": "1.1.2", "form-data": "4.0.0", "incyclist-ant-plus": "^0.1.15", @@ -42,8 +44,7 @@ "pigpio": "3.3.1", "serve-static": "1.14.2", "ws": "8.5.0", - "xml2js": "0.4.23", - "chart.js": "^4.2.1" + "xml2js": "0.4.23" }, "//fix1Comment": "version 0.5.3-8 currently does not work with bleno", "optionalDependencies": { From 89432fef996db8bda80f3d6ecdd040de236b1afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Fri, 24 Mar 2023 21:35:21 +0100 Subject: [PATCH 079/231] Expose app configs to the metric tile factory Make all settings available to the metric creator factory in order for subcomponents (e.g DashboardAction) to use it without depending directly on the global appState. --- app/client/components/DashboardActions.js | 21 ++++++++++++------- app/client/components/PerformanceDashboard.js | 4 ++-- app/client/store/dashboardMetrics.js | 20 +++++++++--------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index e416917a11..e8aa034e2a 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -6,7 +6,7 @@ */ import { AppElement, html, css } from './AppElement.js' -import { customElement, state } from 'lit/decorators.js' +import { customElement, property, state } from 'lit/decorators.js' import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload, icon_heartbeat, icon_antplus } from '../lib/icons.js' import './AppDialog.js' @@ -74,6 +74,12 @@ export class DashboardActions extends AppElement { } ` + @property({ type: Object }) + config = {} + + @property({ type: Object }) + appMode = 'BROWSER' + @state() _dialog @@ -84,11 +90,11 @@ export class DashboardActions extends AppElement { ${this.renderOptionalButtons()}
@@ -104,7 +110,7 @@ export class DashboardActions extends AppElement { // changing to fullscreen mode only makes sence when the app is openend in a regular // webbrowser (kiosk and standalone mode are always in fullscreen view) and if the // browser supports this feature - if (this.appState?.appMode === 'BROWSER' && document.documentElement.requestFullscreen) { + if (this.appMode === 'BROWSER' && document.documentElement.requestFullscreen) { buttons.push(html` `) } - if (this.appState?.config?.stravaUploadEnabled) { + if (this.config?.stravaUploadEnabled) { buttons.push(html` `) @@ -130,8 +136,7 @@ export class DashboardActions extends AppElement { } blePeripheralMode () { - const value = this.appState?.config?.blePeripheralMode - + const value = this.config?.blePeripheralMode switch (value) { case 'PM5': return 'C2 PM5' diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 48c1620d1e..5b6f2442ff 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -64,10 +64,10 @@ export class PerformanceDashboard extends AppElement { dashboardMetricComponentsFactory = (appState) => { const metrics = appState.metrics - const configs = appState.config.guiConfigs + const configs = appState.config const dashboardMetricComponents = Object.keys(DASHBOARD_METRICS).reduce((dashboardMetrics, key) => { - dashboardMetrics[key] = DASHBOARD_METRICS[key].template(metrics, configs.showIcons) + dashboardMetrics[key] = DASHBOARD_METRICS[key].template(metrics, configs) return dashboardMetrics }, {}) diff --git a/app/client/store/dashboardMetrics.js b/app/client/store/dashboardMetrics.js index cd6ee12589..3d3975db6d 100644 --- a/app/client/store/dashboardMetrics.js +++ b/app/client/store/dashboardMetrics.js @@ -9,27 +9,27 @@ export const DASHBOARD_METRICS = { distance: { displayName: 'Distance', size: 1, - template: (metrics, showIcon) => { + template: (metrics, config) => { const linearDistance = formatDistance(metrics?.totalLinearDistance) - return simpleMetricFactory(linearDistance.distance, linearDistance.unit, showIcon ? icon_route : '') + return simpleMetricFactory(linearDistance.distance, linearDistance.unit, config.guiConfigs.showIcons ? icon_route : '') } }, - pace: { displayName: 'Pace/500', size: 1, template: (metrics, showIcon) => simpleMetricFactory(secondsToPace(500 / metrics?.cycleLinearVelocity), '/500m', showIcon ? icon_stopwatch : '') }, - power: { displayName: 'Power', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.cyclePower), 'watt', showIcon ? icon_bolt : '') }, - stkRate: { displayName: 'Stroke rate', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.cycleStrokeRate), '/min', showIcon ? icon_paddle : '') }, + pace: { displayName: 'Pace/500', size: 1, template: (metrics, config) => simpleMetricFactory(secondsToPace(500 / metrics?.cycleLinearVelocity), '/500m', config.guiConfigs.showIcons ? icon_stopwatch : '') }, + power: { displayName: 'Power', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.cyclePower), 'watt', config.guiConfigs.showIcons ? icon_bolt : '') }, + stkRate: { displayName: 'Stroke rate', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.cycleStrokeRate), '/min', config.guiConfigs.showIcons ? icon_paddle : '') }, heartRate: { displayName: 'Heart rate', size: 1, - template: (metrics, showIcon) => html` + template: (metrics, config) => html` ${metrics?.heartrateBatteryLevel ? html`` : ''} ` }, - totalStk: { displayName: 'Total strokes', size: 1, template: (metrics, showIcon) => simpleMetricFactory(metrics?.totalNumberOfStrokes, 'stk', showIcon ? icon_paddle : '') }, - calories: { displayName: 'Calories', size: 1, template: (metrics, showIcon) => simpleMetricFactory(formatNumber(metrics?.totalCalories), 'kcal', showIcon ? icon_fire : '') }, - timer: { displayName: 'Timer', size: 1, template: (metrics, showIcon) => simpleMetricFactory(secondsToPace(metrics?.totalMovingTime), '', showIcon ? icon_clock : '') }, + totalStk: { displayName: 'Total strokes', size: 1, template: (metrics, config) => simpleMetricFactory(metrics?.totalNumberOfStrokes, 'stk', config.guiConfigs.showIcons ? icon_paddle : '') }, + calories: { displayName: 'Calories', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.totalCalories), 'kcal', config.guiConfigs.showIcons ? icon_fire : '') }, + timer: { displayName: 'Timer', size: 1, template: (metrics, config) => simpleMetricFactory(secondsToPace(metrics?.totalMovingTime), '', config.guiConfigs.showIcons ? icon_clock : '') }, forceCurve: { displayName: 'Force curve', size: 2, template: (metrics) => html`` }, - actions: { displayName: 'Actions', size: 1, template: () => html`` } + actions: { displayName: 'Actions', size: 1, template: (appState, config) => html`` } } From 5b44b88c398d7ea745a0b4de2b65120d74f38f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Fri, 24 Mar 2023 21:42:07 +0100 Subject: [PATCH 080/231] Change `changeGuiSetting` event API Restrict the event payload to data this component is responsible for and handle immutable object construction at the main event handler side. --- app/client/components/SettingsDialog.js | 10 ++-------- app/client/index.js | 7 +++---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/app/client/components/SettingsDialog.js b/app/client/components/SettingsDialog.js index e6b1f7b631..d3440cea02 100644 --- a/app/client/components/SettingsDialog.js +++ b/app/client/components/SettingsDialog.js @@ -193,14 +193,8 @@ export class DashboardActions extends AppElement { this.dispatchEvent(new CustomEvent('close')) if (event.detail === 'confirm') { this.sendEvent('changeGuiSetting', { - ...this.appState, - config: { - ...this.appState.config, - guiConfigs: { - dashboardMetrics: this._selectedMetrics, - showIcons: this._showIcons - } - } + dashboardMetrics: this._selectedMetrics, + showIcons: this._showIcons }) } } diff --git a/app/client/index.js b/app/client/index.js index a4ce57e6a7..b88be5bc6d 100644 --- a/app/client/index.js +++ b/app/client/index.js @@ -44,11 +44,10 @@ export class App extends LitElement { // notify the app about the triggered action this.addEventListener('changeGuiSetting', (event) => { - Object.keys(event.detail.config.guiConfigs).forEach(key => { - localStorage.setItem(key, JSON.stringify(event.detail.config.guiConfigs[key])) + Object.keys(event.detail).forEach(key => { + localStorage.setItem(key, JSON.stringify(event.detail[key])) }) - - this.updateState(event.detail) + this.updateState({ config: { ...this._appState.config, guiConfigs: { ...event.detail } } }) }) } From 11db116f5e397479ba03d05a21dcd9fd52159e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Fri, 24 Mar 2023 21:55:55 +0100 Subject: [PATCH 081/231] Remove global appState property Remove appState property from the base class to avoid property drilling. Expose full appState to those components only that really need it in order to decouple dependency on global objects to prevent potential future issues when refactoring and making changes. --- app/client/components/AppElement.js | 13 ------------- app/client/components/PerformanceDashboard.js | 7 +++++-- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/app/client/components/AppElement.js b/app/client/components/AppElement.js index efe6c39454..59b00cabed 100644 --- a/app/client/components/AppElement.js +++ b/app/client/components/AppElement.js @@ -6,22 +6,9 @@ */ import { LitElement } from 'lit' -import { property } from 'lit/decorators.js' -import { APP_STATE } from '../store/appState.js' export * from 'lit' export class AppElement extends LitElement { - // this is how we implement a global state: a global state object is passed via properties - // to child components - @property({ type: Object }) - appState = APP_STATE - - // ..and state changes are send back to the root component of the app by dispatching - // a CustomEvent - updateState () { - this.sendEvent('appStateChanged', this.appState) - } - // a helper to dispatch events to the parent components sendEvent (eventType, eventData) { this.dispatchEvent( diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 5b6f2442ff..0e12ad44f8 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -6,7 +6,7 @@ */ import { AppElement, html, css } from './AppElement.js' -import { customElement, state } from 'lit/decorators.js' +import { customElement, property, state } from 'lit/decorators.js' import './SettingsDialog' import { icon_settings } from '../lib/icons.js' import { DASHBOARD_METRICS } from '../store/dashboardMetrics.js' @@ -20,7 +20,7 @@ export class PerformanceDashboard extends AppElement { padding: 1vw; grid-gap: 1vw; grid-template-columns: repeat(4, minmax(0, 1fr)); - grid-template-rows: repeat(2, minmax(0, 1fr)); + grid-template-rows: repeat(3, minmax(0, 1fr)); } @media (orientation: portrait) { @@ -59,6 +59,9 @@ export class PerformanceDashboard extends AppElement { filter: brightness(150%); } ` + @property() + appState = {} + @state() _dialog From 594f8099c3a67462d33c7e59ea8922252b5be83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 14:22:37 +0100 Subject: [PATCH 082/231] Change settings styling and fix a formatter bug --- app/client/components/SettingsDialog.js | 33 ++++++++++++++----------- app/client/lib/helper.js | 2 +- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/app/client/components/SettingsDialog.js b/app/client/components/SettingsDialog.js index d3440cea02..7ada79635a 100644 --- a/app/client/components/SettingsDialog.js +++ b/app/client/components/SettingsDialog.js @@ -15,25 +15,19 @@ import { DASHBOARD_METRICS } from '../store/dashboardMetrics.js' export class DashboardActions extends AppElement { static styles = css` .metric-selector-feedback{ - font-size: 0.4em; + font-size: 0.5em; padding-top: 8px; } - .metric-selector-feedback>div { - display: grid; - grid-template-columns: repeat(4,1fr); - grid-template-rows: repeat(2, max-content); - gap: 8px; - } - .settings-dialog>div.metric-selector{ display: grid; - grid-template-columns: repeat(4,max-content); + grid-template-columns: repeat(3,max-content); gap: 8px; } .settings-dialog>div>label{ font-size: 0.6em; + width: fit-content; } input[type="checkbox"]{ @@ -43,6 +37,12 @@ export class DashboardActions extends AppElement { height: 1.5em; } + label>span { + cursor: pointer; + -webkit-user-select: none; + user-select: none; + } + .icon { height: 1.6em; } @@ -58,7 +58,7 @@ export class DashboardActions extends AppElement { } table, th, td { - font-size: 0.8em; + font-size: 0.9em; border: 1px solid white; border-collapse: collapse; } @@ -86,7 +86,7 @@ export class DashboardActions extends AppElement { @property({ type: Object }) config = {} - @queryAll('.metric-selector>input') + @queryAll('.metric-selector input') _inputs @query('input[name="showIcons"]') @@ -119,8 +119,10 @@ export class DashboardActions extends AppElement {
${this._selectedMetrics[index]}
${this._selectedMetrics[index]}

- - +

` @@ -143,8 +145,9 @@ export class DashboardActions extends AppElement { renderAvailableMetricList () { return Object.keys(DASHBOARD_METRICS).map(key => html` - - + `) } diff --git a/app/client/lib/helper.js b/app/client/lib/helper.js index 1bca643560..e4fab7ce02 100644 --- a/app/client/lib/helper.js +++ b/app/client/lib/helper.js @@ -61,7 +61,7 @@ export function formatDistance (value, showInMiles = false) { * @param decimalPlaces The number of decimal places to round to (default: 0). */ export function formatNumber (value, decimalPlaces = 0) { - const decimal = decimalPlaces > 0 ? decimalPlaces * 10 : 1 + const decimal = Math.pow(10, decimalPlaces) if (value === undefined || value === null || value === Infinity || isNaN(value) || value === 0) { return '--' } return Math.round(value * decimal) / decimal From bf55638e82accb82426ee5b65391507f1c3a7a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 14:27:18 +0100 Subject: [PATCH 083/231] Add new metric tiles - Add drag factor (int) - Add distance per stroke (meters with 1 decimal place) - Add drive length (meters with 2 decimal places) - Add drive duration (seconds with 2 decimal places) - Add recovery duration (seconds with 2 decimal places) --- app/client/lib/icons.js | 1 + app/client/store/dashboardMetrics.js | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/client/lib/icons.js b/app/client/lib/icons.js index 78d6e13543..4adea7a67b 100644 --- a/app/client/lib/icons.js +++ b/app/client/lib/icons.js @@ -28,3 +28,4 @@ export const icon_upload = svg`` export const icon_settings = svg`` +export const rower_icon = svg`` diff --git a/app/client/store/dashboardMetrics.js b/app/client/store/dashboardMetrics.js index 3d3975db6d..300db7b6ab 100644 --- a/app/client/store/dashboardMetrics.js +++ b/app/client/store/dashboardMetrics.js @@ -1,6 +1,6 @@ import { html } from 'lit' import { simpleMetricFactory, formatDistance, formatNumber, secondsToPace } from '../lib/helper' -import { icon_bolt, icon_clock, icon_fire, icon_heartbeat, icon_paddle, icon_route, icon_stopwatch } from '../lib/icons' +import { icon_bolt, icon_clock, icon_fire, icon_heartbeat, icon_paddle, icon_route, icon_stopwatch, rower_icon } from '../lib/icons' import '../components/DashboardForceCurve.js' import '../components/DashboardActions.js' import '../components/BatteryIcon.js' @@ -15,21 +15,39 @@ export const DASHBOARD_METRICS = { return simpleMetricFactory(linearDistance.distance, linearDistance.unit, config.guiConfigs.showIcons ? icon_route : '') } }, + pace: { displayName: 'Pace/500', size: 1, template: (metrics, config) => simpleMetricFactory(secondsToPace(500 / metrics?.cycleLinearVelocity), '/500m', config.guiConfigs.showIcons ? icon_stopwatch : '') }, + power: { displayName: 'Power', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.cyclePower), 'watt', config.guiConfigs.showIcons ? icon_bolt : '') }, + stkRate: { displayName: 'Stroke rate', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.cycleStrokeRate), '/min', config.guiConfigs.showIcons ? icon_paddle : '') }, heartRate: { displayName: 'Heart rate', size: 1, template: (metrics, config) => html` - ${metrics?.heartrateBatteryLevel - ? html`` - : ''} -` + ${metrics?.heartrateBatteryLevel + ? html`` + : ''} + ` }, + totalStk: { displayName: 'Total strokes', size: 1, template: (metrics, config) => simpleMetricFactory(metrics?.totalNumberOfStrokes, 'stk', config.guiConfigs.showIcons ? icon_paddle : '') }, + calories: { displayName: 'Calories', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.totalCalories), 'kcal', config.guiConfigs.showIcons ? icon_fire : '') }, + timer: { displayName: 'Timer', size: 1, template: (metrics, config) => simpleMetricFactory(secondsToPace(metrics?.totalMovingTime), '', config.guiConfigs.showIcons ? icon_clock : '') }, + + distancePerStk: { displayName: 'Dist per Stroke', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.cycleDistance, 1), 'm', config.guiConfigs.showIcons ? rower_icon : '') }, + + dragFactor: { displayName: 'Drag factor', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.dragFactor), '', config.guiConfigs.showIcons ? 'Drag' : '') }, + + driveLength: { displayName: 'Drive length', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.driveLength, 2), 'm', config.guiConfigs.showIcons ? 'Drive' : '') }, + + driveDuration: { displayName: 'Drive duration', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.driveDuration, 2), 'sec', config.guiConfigs.showIcons ? 'Drive' : '') }, + + recoveryDuration: { displayName: 'Recovery duration', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.recoveryDuration, 2), 'sec', config.guiConfigs.showIcons ? 'Recovery' : '') }, + forceCurve: { displayName: 'Force curve', size: 2, template: (metrics) => html`` }, + actions: { displayName: 'Actions', size: 1, template: (appState, config) => html`` } } From 74a73c571a64b672359a4ff2eb7584fcc65cf946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 17:15:54 +0100 Subject: [PATCH 084/231] Add experimental 12 cell grid mode Implement 12 cell grid mode to show allow showing more metrics. Experimental means that this layout will not render properly on every screen size (though should work on bigger screens well). That would require more media queries and css logic to tweak the styling This may be implemented at a later stage. --- app/client/components/PerformanceDashboard.js | 6 +- app/client/components/SettingsDialog.js | 63 +++++++++++++++++-- app/client/store/appState.js | 3 +- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 0e12ad44f8..4ab15dbb33 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -20,7 +20,6 @@ export class PerformanceDashboard extends AppElement { padding: 1vw; grid-gap: 1vw; grid-template-columns: repeat(4, minmax(0, 1fr)); - grid-template-rows: repeat(3, minmax(0, 1fr)); } @media (orientation: portrait) { @@ -85,6 +84,11 @@ export class PerformanceDashboard extends AppElement { }, []) return html` +
${icon_settings} ${this._dialog ? this._dialog : ''} diff --git a/app/client/components/SettingsDialog.js b/app/client/components/SettingsDialog.js index 7ada79635a..8bd7fd01b9 100644 --- a/app/client/components/SettingsDialog.js +++ b/app/client/components/SettingsDialog.js @@ -25,6 +25,21 @@ export class DashboardActions extends AppElement { gap: 8px; } + .experimental-settings { + display: flex; + flex-direction: column; + } + + .experimental-settings label { + width: fit-content; + margin-top: 8px; + font-size: 0.7em; + } + + .experimental-settings label>input { + font-size: 0.7em; + } + .settings-dialog>div>label{ font-size: 0.6em; width: fit-content; @@ -92,6 +107,9 @@ export class DashboardActions extends AppElement { @query('input[name="showIcons"]') _showIconInput + @query('input[name="maxNumberOfTiles"]') + _maxNumberOfTilesInput + @state() _selectedMetrics = [] @@ -104,6 +122,9 @@ export class DashboardActions extends AppElement { @state() _showIcons = true + @state() + _maxNumberOfTiles = 8 + render () { return html` @@ -113,7 +134,7 @@ export class DashboardActions extends AppElement {
${this.renderAvailableMetricList()}
-
Slots remaining: ${8 - this._sumSelectedSlots} +
Slots remaining: ${this._maxNumberOfTiles - this._sumSelectedSlots} ${this.renderSelectedMetrics()}
@@ -124,6 +145,13 @@ export class DashboardActions extends AppElement {

+

+ Experimental settings: + +

` } @@ -132,7 +160,8 @@ export class DashboardActions extends AppElement { this._selectedMetrics = [...this.config.dashboardMetrics] this._sumSelectedSlots = this._selectedMetrics.length this._showIcons = this.config.showIcons - if (this._sumSelectedSlots === 8) { + this._maxNumberOfTiles = this.config.maxNumberOfTiles + if (this._sumSelectedSlots === this._maxNumberOfTiles) { this._isValid = true } else { this._isValid = false @@ -141,6 +170,7 @@ export class DashboardActions extends AppElement { input.checked = this._selectedMetrics.find(metric => metric === input.name) !== undefined }) this._showIconInput.checked = this._showIcons + this._maxNumberOfTilesInput.checked = this._maxNumberOfTiles === 12 } renderAvailableMetricList () { @@ -153,13 +183,28 @@ export class DashboardActions extends AppElement { renderSelectedMetrics () { const selectedMetrics = [html`${[0, 1, 2, 3].map(index => html`${this._selectedMetrics[index]}`)}`] - selectedMetrics.push(html`${[4, 5, 6, 7].map(index => html`${this._selectedMetrics[index]}`)}`) + selectedMetrics.push(html`${[4, 5, 6, 7].map(index => html`${this._selectedMetrics[index]}`)}`) + if (this._maxNumberOfTiles === 12) { + selectedMetrics.push(html`${[8, 9, 10, 11].map(index => html`${this._selectedMetrics[index]}`)}`) + } return selectedMetrics } toggleCheck (e) { - if ((e.target.checked && this._selectedMetrics.length < 4 && e.target.size > 1 && this._selectedMetrics.length + e.target.size > 4) || (e.target.checked && this._sumSelectedSlots + 1 > 8)) { + if (e.target.checked && + ((this._selectedMetrics.length % 4 === 3 && e.target.size > 1) || + (this._sumSelectedSlots + e.target.size > this._maxNumberOfTiles))) { this._isValid = this.isFormValid() e.target.checked = false return @@ -188,8 +233,13 @@ export class DashboardActions extends AppElement { this._showIcons = e.target.checked } + toggleMaxTiles (e) { + this._maxNumberOfTiles = e.target.checked ? 12 : 8 + this._isValid = this.isFormValid() + } + isFormValid () { - return this._sumSelectedSlots === 8 && this._selectedMetrics[3] !== this._selectedMetrics[4] + return this._sumSelectedSlots === this._maxNumberOfTiles && this._selectedMetrics[3] !== this._selectedMetrics[4] && this._selectedMetrics[7] !== this._selectedMetrics?.[8] } close (event) { @@ -197,7 +247,8 @@ export class DashboardActions extends AppElement { if (event.detail === 'confirm') { this.sendEvent('changeGuiSetting', { dashboardMetrics: this._selectedMetrics, - showIcons: this._showIcons + showIcons: this._showIcons, + maxNumberOfTiles: this._maxNumberOfTiles }) } } diff --git a/app/client/store/appState.js b/app/client/store/appState.js index ee1af51447..7865fff50e 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -23,7 +23,8 @@ export const APP_STATE = { shutdownEnabled: false, guiConfigs: { dashboardMetrics: ['distance', 'timer', 'pace', 'power', 'stkRate', 'totalStk', 'calories', 'actions'], - showIcons: true + showIcons: true, + maxNumberOfTiles: 8 } } } From c5bec08ea9468a0a7573faa3263798bd849d3dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 19:33:23 +0100 Subject: [PATCH 085/231] Fix styling when text is used for metric tile --- app/client/components/DashboardMetric.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/components/DashboardMetric.js b/app/client/components/DashboardMetric.js index 18d7a9f83c..76f0d5ca2f 100644 --- a/app/client/components/DashboardMetric.js +++ b/app/client/components/DashboardMetric.js @@ -45,7 +45,7 @@ export class DashboardMetric extends AppElement { render () { return html` -
${this.icon}
+
${this.icon}
${this.value !== undefined ? this.value : '--'} ${this.unit} From ee667d727a119f2f6ce0f659e5bce7d26217b157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 19:43:26 +0100 Subject: [PATCH 086/231] Prevent BLE HR monitor process crash (fixes: #135) In case of an uncaught exception in the BLE HR child process the process would crash and not restart. Add code to catch these uncaught exceptions to prevent such crash and enable code to continue. --- app/peripherals/ble/hrm/HrmService.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/peripherals/ble/hrm/HrmService.js b/app/peripherals/ble/hrm/HrmService.js index 518af51d39..00a6551160 100644 --- a/app/peripherals/ble/hrm/HrmService.js +++ b/app/peripherals/ble/hrm/HrmService.js @@ -11,7 +11,15 @@ import config from '../../../tools/ConfigManager.js' import { createHeartRateManager } from './HeartRateManager.js' log.setLevel(config.loglevel.default) -const heartRateManager = createHeartRateManager() -heartRateManager.on('heartRateMeasurement', (heartRateMeasurement) => { - process.send(heartRateMeasurement) -}) +start() + +function start () { + const heartRateManager = createHeartRateManager() + heartRateManager.on('heartRateMeasurement', (heartRateMeasurement) => { + process.send(heartRateMeasurement) + }) + + process.on('uncaughtException', (err) => { + log.error('An error occurred in BLE Heart Rate service if you experience issues with the bluetooth connection to your heart rate sensor please restart app: ', err) + }) +} From e7c40873cdb949b4d66907114b46f5e238ed118c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 20:09:24 +0100 Subject: [PATCH 087/231] Fix HR battery level not showing (fixes: #132) Fix the HR battery level not showing on the HR metric tile (ANT and BLE) For ANT handle case when BatteryLevel is not present only BatteryStatus. BLE devices do not necessarily send battery level with all HR broadcast so read the battery level on connect. --- app/client/lib/app.js | 2 +- app/client/store/dashboardMetrics.js | 4 +-- app/engine/RowingStatistics.js | 20 +++++++-------- app/peripherals/ant/HrmPeripheral.js | 27 ++++++++++++++++++++- app/peripherals/ble/hrm/HeartRateManager.js | 12 ++++++++- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 214c4d54a6..004acaa280 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -108,7 +108,7 @@ export function createApp (app) { function resetFields () { const appState = app.getState() // drop all metrics except heartrate - app.updateState({ ...appState, metrics: { ...filterObjectByKeys(appState.metrics, ['heartrate', 'heartrateBatteryLevel']) } }) + app.updateState({ ...appState, metrics: { ...filterObjectByKeys(appState.metrics, ['heartrate', 'heartRateBatteryLevel']) } }) } function handleAction (action) { diff --git a/app/client/store/dashboardMetrics.js b/app/client/store/dashboardMetrics.js index 300db7b6ab..092f3ea4c4 100644 --- a/app/client/store/dashboardMetrics.js +++ b/app/client/store/dashboardMetrics.js @@ -25,8 +25,8 @@ export const DASHBOARD_METRICS = { displayName: 'Heart rate', size: 1, template: (metrics, config) => html` - ${metrics?.heartrateBatteryLevel - ? html`` + ${metrics?.heartRateBatteryLevel > 0 + ? html`` : ''} ` }, diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index b456c79478..c7dc1ae116 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -32,7 +32,7 @@ function createRowingStatistics (config) { let intervalTargetTime = 0 let intervalPrevAccumulatedDistance = 0 let intervalPrevAccumulatedTime = 0 - let heartrateResetTimer + let heartRateResetTimer let totalLinearDistance = 0.0 let totalMovingTime = 0 let totalNumberOfStrokes = 0 @@ -52,7 +52,7 @@ function createRowingStatistics (config) { const driveHandlePowerCurve = createCurveAligner(50) let dragFactor = config.rowerSettings.dragFactor let heartrate = 0 - let heartrateBatteryLevel = 0 + let heartRateBatteryLevel = 0 const postExerciseHR = [] let instantPower = 0.0 let lastStrokeState = 'WaitingForDrive' @@ -328,15 +328,15 @@ function createRowingStatistics (config) { } // initiated when a new heart rate value is received from heart rate sensor - function handleHeartrateMeasurement (value) { + function handleHeartRateMeasurement (value) { // set the heart rate to zero if we did not receive a value for some time - if (heartrateResetTimer)clearInterval(heartrateResetTimer) - heartrateResetTimer = setTimeout(() => { + if (heartRateResetTimer)clearInterval(heartRateResetTimer) + heartRateResetTimer = setTimeout(() => { heartrate = 0 - heartrateBatteryLevel = 0 + heartRateBatteryLevel = 0 }, 6000) heartrate = value.heartrate - heartrateBatteryLevel = value.batteryLevel + heartRateBatteryLevel = value.batteryLevel } function measureRecoveryHR () { @@ -408,8 +408,8 @@ function createRowingStatistics (config) { recoveryDuration: recoveryDuration.clean() >= config.rowerSettings.minimumRecoveryTime && totalNumberOfStrokes > 0 && sessionStatus === 'Rowing' ? recoveryDuration.clean() : NaN, // seconds dragFactor: dragFactor > 0 ? dragFactor : config.rowerSettings.dragFactor, // Dragfactor instantPower: instantPower > 0 && rower.strokeState() === 'Drive' ? instantPower : 0, - heartrate: heartrate > 30 ? heartrate : undefined, - heartrateBatteryLevel: heartrateBatteryLevel > 0 ? heartrateBatteryLevel : undefined // BE AWARE, changing undefined to NaN kills the GUI!!! + heartrate: heartrate > 30 ? heartrate : 0, + heartRateBatteryLevel } } @@ -433,7 +433,7 @@ function createRowingStatistics (config) { } return Object.assign(emitter, { - handleHeartRateMeasurement: handleHeartrateMeasurement, + handleHeartRateMeasurement, handleRotationImpulse, setIntervalParameters, pause: pauseTraining, diff --git a/app/peripherals/ant/HrmPeripheral.js b/app/peripherals/ant/HrmPeripheral.js index 99fe289d7e..8e7cff85fe 100644 --- a/app/peripherals/ant/HrmPeripheral.js +++ b/app/peripherals/ant/HrmPeripheral.js @@ -13,13 +13,38 @@ function createAntHrmPeripheral (antManager) { const emitter = new EventEmitter() const antStick = antManager.getAntStick() const heartRateSensor = new HeartRateSensor(0) + let batteryLevel = 0 async function attach () { if (!antManager.isStickOpen()) { await antManager.openAntStick() } this.channel = await antStick.getChannel() this.channel.on('data', (profile, deviceID, data) => { - emitter.emit('heartRateMeasurement', { heartrate: data.ComputedHeartRate, batteryLevel: data.BatteryLevel }) + switch (data.BatteryStatus) { + case 'New': + batteryLevel = 100 + break + case 'Good': + batteryLevel = 80 + break + case 'Ok': + batteryLevel = 60 + break + case 'Low': + batteryLevel = 40 + break + case 'Critical': + batteryLevel = 20 + break + default: + batteryLevel = 0 + } + + if (data.BatteryLevel > 0) { + batteryLevel = data.BatteryLevel + } + + emitter.emit('heartRateMeasurement', { heartrate: data.ComputedHeartRate, batteryLevel }) }) if (!(await this.channel.startSensor(heartRateSensor))) { diff --git a/app/peripherals/ble/hrm/HeartRateManager.js b/app/peripherals/ble/hrm/HeartRateManager.js index ae92e208b4..1b6c91f76e 100644 --- a/app/peripherals/ble/hrm/HeartRateManager.js +++ b/app/peripherals/ble/hrm/HeartRateManager.js @@ -81,7 +81,7 @@ function createHeartRateManager () { peripheral.once('disconnect', () => { // todo: figure out if we have to dispose the peripheral somehow to prevent memory leaks log.info('heart rate peripheral disconnected, searching new one') - batteryLevel = undefined + batteryLevel = 0 noble.startScanning(['180d'], false) }) } @@ -136,6 +136,16 @@ function createHeartRateManager () { } if (batteryLevelCharacteristic !== undefined) { + batteryLevelCharacteristic.read((error, data) => { + if (error) { + log.error(error) + return + } + + const buffer = Buffer.from(data) + batteryLevel = buffer.readUInt8(0) + }) + batteryLevelCharacteristic.notify(true, (error) => { if (error) { log.error(error) From a1068cf77fe3c444480fc37558c40b97d9956277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 20:21:00 +0100 Subject: [PATCH 088/231] Fix type conversion in the curveMetrics Fix the string conversion of the curveMetrics and push the formatting logic to the consumer that require this (eg. WorkoutRecorder). This enables other consumers of curveMetrics to use non-rounded, "raw" data instead of preformatted as well as avoid potential future bugs from the type conversion. --- app/client/components/DashboardForceCurve.js | 4 ++-- app/client/lib/helper.js | 19 +++++++++++++------ app/engine/Rower.js | 6 +++--- app/engine/RowingStatistics.js | 6 +++--- app/engine/WorkoutRecorder.js | 4 ++-- app/engine/utils/curveMetrics.js | 4 ++-- 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/app/client/components/DashboardForceCurve.js b/app/client/components/DashboardForceCurve.js index 74a8b7aced..1d064e1ddc 100644 --- a/app/client/components/DashboardForceCurve.js +++ b/app/client/components/DashboardForceCurve.js @@ -39,7 +39,7 @@ export class DashboardForceCurve extends AppElement { datasets: [ { fill: true, - data: this.value?.map((data, index) => ({ y: parseInt(data, 10), x: index })), + data: this.value?.map((data, index) => ({ y: data, x: index })), pointRadius: 1, borderColor: 'rgb(255,255,255)', backgroundColor: 'rgb(220,220,220)' @@ -113,7 +113,7 @@ export class DashboardForceCurve extends AppElement { render () { if (this._chart?.data) { - this._chart.data.datasets[0].data = this.value?.map((data, index) => ({ y: parseInt(data, 10), x: index })) + this._chart.data.datasets[0].data = this.value?.map((data, index) => ({ y: data, x: index })) this.forceCurve = this.value this._chart.update() } diff --git a/app/client/lib/helper.js b/app/client/lib/helper.js index e4fab7ce02..444de37340 100644 --- a/app/client/lib/helper.js +++ b/app/client/lib/helper.js @@ -22,7 +22,7 @@ export function filterObjectByKeys (object, keys) { /** * Pipe for converting seconds to pace format 00:00 * - * @param seconds The actual time in seconds. + * @param {number} seconds The actual time in seconds. */ export function secondsToPace (seconds) { const hours = Math.floor((seconds % 86400) / 3600) @@ -41,8 +41,8 @@ export function secondsToPace (seconds) { /** * Pipe for formatting distance in meters with units * - * @param value The distance in meters. - * @param showInMiles Boolean whether to use imperial metric (default: false). + * @param {number} value The distance in meters. + * @param {boolean} showInMiles Boolean whether to use imperial metric (default: false). */ export function formatDistance (value, showInMiles = false) { if (showInMiles === false) { @@ -57,8 +57,8 @@ export function formatDistance (value, showInMiles = false) { /** * Pipe for formatting numbers to specific decimal * - * @param value The number. - * @param decimalPlaces The number of decimal places to round to (default: 0). + * @param {number} value The number. + * @param {number} decimalPlaces The number of decimal places to round to (default: 0). */ export function formatNumber (value, decimalPlaces = 0) { const decimal = Math.pow(10, decimalPlaces) @@ -67,6 +67,13 @@ export function formatNumber (value, decimalPlaces = 0) { return Math.round(value * decimal) / decimal } -export function simpleMetricFactory (value, unit, icon) { +/** + * Helper function to create a simple metric tile + * + * @param {string | number} value The metric to show + * @param {string} unit The unit of the metric. + * @param {string | import('lit').TemplateResult<2>} icon The number of decimal places to round to (default: 0). +*/ +export function simpleMetricFactory (value = '--', unit = '', icon = '') { return html`` } diff --git a/app/engine/Rower.js b/app/engine/Rower.js index 82f73e2523..e66b466e26 100644 --- a/app/engine/Rower.js +++ b/app/engine/Rower.js @@ -20,9 +20,9 @@ const log = loglevel.getLogger('RowingEngine') function createRower (rowerSettings) { const flywheel = createFlywheel(rowerSettings) const sprocketRadius = rowerSettings.sprocketRadius / 100 - const driveHandleForce = createCurveMetrics(2) - const driveHandleVelocity = createCurveMetrics(3) - const driveHandlePower = createCurveMetrics(1) + const driveHandleForce = createCurveMetrics() + const driveHandleVelocity = createCurveMetrics() + const driveHandlePower = createCurveMetrics() let _strokeState = 'WaitingForDrive' let _totalNumberOfStrokes = -1.0 let recoveryPhaseStartTime = 0.0 diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index c7dc1ae116..155a7b6575 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -402,9 +402,9 @@ function createRowingStatistics (config) { driveDistance: driveDistance.clean() >= 0 && sessionStatus === 'Rowing' ? driveDistance.clean() : NaN, // meters driveAverageHandleForce: driveAverageHandleForce.clean() > 0 && sessionStatus === 'Rowing' ? driveAverageHandleForce.clean() : NaN, drivePeakHandleForce: drivePeakHandleForce.clean() > 0 && sessionStatus === 'Rowing' ? drivePeakHandleForce.clean() : NaN, - driveHandleForceCurve: drivePeakHandleForce.clean() > 0 && sessionStatus === 'Rowing' ? driveHandleForceCurve.lastCompleteCurve() : [NaN], - driveHandleVelocityCurve: drivePeakHandleForce.clean() > 0 && sessionStatus === 'Rowing' ? driveHandleVelocityCurve.lastCompleteCurve() : [NaN], - driveHandlePowerCurve: drivePeakHandleForce.clean() > 0 && sessionStatus === 'Rowing' ? driveHandlePowerCurve.lastCompleteCurve() : [NaN], + driveHandleForceCurve: drivePeakHandleForce.clean() > 0 && sessionStatus === 'Rowing' ? driveHandleForceCurve.lastCompleteCurve() : [], + driveHandleVelocityCurve: drivePeakHandleForce.clean() > 0 && sessionStatus === 'Rowing' ? driveHandleVelocityCurve.lastCompleteCurve() : [], + driveHandlePowerCurve: drivePeakHandleForce.clean() > 0 && sessionStatus === 'Rowing' ? driveHandlePowerCurve.lastCompleteCurve() : [], recoveryDuration: recoveryDuration.clean() >= config.rowerSettings.minimumRecoveryTime && totalNumberOfStrokes > 0 && sessionStatus === 'Rowing' ? recoveryDuration.clean() : NaN, // seconds dragFactor: dragFactor > 0 ? dragFactor : config.rowerSettings.dragFactor, // Dragfactor instantPower: instantPower > 0 && rower.strokeState() === 'Drive' ? instantPower : 0, diff --git a/app/engine/WorkoutRecorder.js b/app/engine/WorkoutRecorder.js index 693d4553dd..b538f47ae2 100644 --- a/app/engine/WorkoutRecorder.js +++ b/app/engine/WorkoutRecorder.js @@ -83,8 +83,8 @@ function createWorkoutRecorder () { `${currentstroke.cycleStrokeRate.toFixed(1)},${(currentstroke.totalNumberOfStrokes > 0 ? currentstroke.cyclePace.toFixed(2) : NaN)},${(currentstroke.totalNumberOfStrokes > 0 ? currentstroke.cyclePower.toFixed(0) : NaN)},` + `${currentstroke.cycleDistance.toFixed(2)},${(currentstroke.driveDuration * 1000).toFixed(0)},${(currentstroke.totalNumberOfStrokes > 0 ? currentstroke.driveLength.toFixed(2) : NaN)},${(currentstroke.recoveryDuration * 1000).toFixed(0)},` + `${(currentstroke.totalNumberOfStrokes > 0 ? currentstroke.cycleLinearVelocity.toFixed(2) : 0)},${currentstroke.totalLinearDistance.toFixed(1)},${currentstroke.totalCalories.toFixed(1)},${currentstroke.dragFactor.toFixed(1)},` + - `${(currentstroke.totalNumberOfStrokes > 0 ? currentstroke.drivePeakHandleForce.toFixed(1) : NaN)},${(currentstroke.totalNumberOfStrokes > 0 ? currentstroke.driveAverageHandleForce.toFixed(1) : 0)},"${currentstroke.driveHandleForceCurve}",` + - `"${currentstroke.driveHandleVelocityCurve}","${currentstroke.driveHandlePowerCurve}"\n` + `${(currentstroke.totalNumberOfStrokes > 0 ? currentstroke.drivePeakHandleForce.toFixed(1) : NaN)},${(currentstroke.totalNumberOfStrokes > 0 ? currentstroke.driveAverageHandleForce.toFixed(1) : 0)},"${currentstroke.driveHandleForceCurve.map(value => value.toFixed(2))}",` + + `"${currentstroke.driveHandleVelocityCurve.map(value => value.toFixed(3))}","${currentstroke.driveHandlePowerCurve.map(value => value.toFixed(1))}"\n` i++ } diff --git a/app/engine/utils/curveMetrics.js b/app/engine/utils/curveMetrics.js index 4725ae74c7..1236424011 100644 --- a/app/engine/utils/curveMetrics.js +++ b/app/engine/utils/curveMetrics.js @@ -6,7 +6,7 @@ */ import { createSeries } from './Series.js' -function createCurveMetrics (precission = 0) { +function createCurveMetrics () { const _curve = createSeries() let _max = 0 let totalInputXTime = 0 @@ -15,7 +15,7 @@ function createCurveMetrics (precission = 0) { function push (deltaTime, inputValue) { // add the new dataPoint to the array, we have to move datapoints starting at the oldst ones if (inputValue > 0) { - _curve.push(inputValue.toFixed(precission)) + _curve.push(inputValue) _max = Math.max(_max, inputValue) totalInputXTime += deltaTime * inputValue totaltime += deltaTime From 918c9b53fa4a0cf349c523d30aeda31f31099839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 20:28:56 +0100 Subject: [PATCH 089/231] Remove formatting logic from RowingStatistics Remove logic of formatting metrics in RowingStatistics and move these to the consuming end (e.g. client). --- app/engine/RowingStatistics.js | 18 ++---------------- app/server.js | 3 ++- app/tools/Helper.js | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index 155a7b6575..3c475492ed 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -11,6 +11,7 @@ import { createStreamFilter } from './utils/StreamFilter.js' import { createCurveAligner } from './utils/CurveAligner.js' import loglevel from 'loglevel' +import { secondsToTimeString } from '../tools/Helper.js' const log = loglevel.getLogger('RowingEngine') function createRowingStatistics (config) { @@ -375,12 +376,11 @@ function createRowingStatistics (config) { sessionStatus, strokeState: rower.strokeState(), totalMovingTime: totalMovingTime > 0 ? totalMovingTime : 0, - totalMovingTimeFormatted: intervalTargetTime > 0 ? secondsToTimeString(Math.round(Math.max(intervalTargetTime - totalMovingTime, 0))) : secondsToTimeString(Math.round(totalMovingTime - intervalPrevAccumulatedTime)), totalNumberOfStrokes: totalNumberOfStrokes > 0 ? totalNumberOfStrokes : 0, totalLinearDistance: totalLinearDistance > 0 ? totalLinearDistance : 0, // meters - totalLinearDistanceFormatted: intervalTargetDistance > 0 ? Math.max(intervalTargetDistance - totalLinearDistance, 0) : totalLinearDistance - intervalPrevAccumulatedDistance, intervalNumber: Math.max(currentIntervalNumber + 1, 0), // Interval number intervalMovingTime: totalMovingTime - intervalPrevAccumulatedTime, + intervalRemainingTime: intervalTargetTime - totalMovingTime, intervalLinearDistance: totalLinearDistance - intervalPrevAccumulatedDistance, strokeCalories: strokeCalories > 0 ? strokeCalories : 0, // kCal strokeWork: strokeWork > 0 ? strokeWork : 0, // Joules @@ -392,7 +392,6 @@ function createRowingStatistics (config) { cycleDistance: cycleDistance.raw() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cycleDistance.clean() : 0, // meters cycleLinearVelocity: cycleLinearVelocity.clean() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cycleLinearVelocity.clean() : 0, // m/s cyclePace: cycleLinearVelocity.raw() > 0 ? cyclePace : Infinity, // seconds/50 0m - cyclePaceFormatted: cycleLinearVelocity.raw() > 0 ? secondsToTimeString(Math.round(cyclePace)) : Infinity, cyclePower: cyclePower.clean() > 0 && cycleLinearVelocity.raw() > 0 && sessionStatus === 'Rowing' ? cyclePower.clean() : 0, // watts cycleProjectedEndTime: intervalTargetDistance > 0 ? distanceOverTime.projectY(intervalTargetDistance) : intervalTargetTime, cycleProjectedEndLinearDistance: intervalTargetTime > 0 ? distanceOverTime.projectX(intervalTargetTime) : intervalTargetDistance, @@ -413,19 +412,6 @@ function createRowingStatistics (config) { } } - // converts a timeStamp in seconds to a human readable hh:mm:ss format - function secondsToTimeString (secondsTimeStamp) { - if (secondsTimeStamp === Infinity) return '∞' - const hours = Math.floor(secondsTimeStamp / 60 / 60) - const minutes = Math.floor(secondsTimeStamp / 60) - (hours * 60) - const seconds = Math.floor(secondsTimeStamp % 60) - if (hours > 0) { - return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` - } else { - return `${minutes}:${seconds.toString().padStart(2, '0')}` - } - } - function caloriesPerPeriod (periodBegin, periodEnd) { const beginCalories = calories.projectX(periodBegin) const endCalories = calories.projectX(periodEnd) diff --git a/app/server.js b/app/server.js index b6a28fce5a..099658f638 100644 --- a/app/server.js +++ b/app/server.js @@ -18,6 +18,7 @@ import { createPeripheralManager } from './peripherals/PeripheralManager.js' import { replayRowingSession } from './tools/RowingRecorder.js' import { createWorkoutRecorder } from './engine/WorkoutRecorder.js' import { createWorkoutUploader } from './engine/WorkoutUploader.js' +import { secondsToTimeString } from './tools/Helper.js' const exec = promisify(child_process.exec) // set the log levels @@ -290,7 +291,7 @@ async function shutdown () { function logMetrics (metrics) { log.info(`stroke: ${metrics.totalNumberOfStrokes}, dist: ${metrics.totalLinearDistance.toFixed(1)}m, speed: ${metrics.cycleLinearVelocity.toFixed(2)}m/s` + - `, pace: ${metrics.cyclePaceFormatted}/500m, power: ${Math.round(metrics.cyclePower)}W, cal: ${metrics.totalCalories.toFixed(1)}kcal` + + `, pace: ${secondsToTimeString(metrics.cyclePace)}/500m, power: ${Math.round(metrics.cyclePower)}W, cal: ${metrics.totalCalories.toFixed(1)}kcal` + `, SPM: ${metrics.cycleStrokeRate.toFixed(1)}, drive dur: ${metrics.driveDuration.toFixed(2)}s, rec. dur: ${metrics.recoveryDuration.toFixed(2)}s` + `, stroke dur: ${metrics.cycleDuration.toFixed(2)}s`) } diff --git a/app/tools/Helper.js b/app/tools/Helper.js index 63f388e04a..2a809fe709 100644 --- a/app/tools/Helper.js +++ b/app/tools/Helper.js @@ -26,3 +26,29 @@ export function deepMerge (...objects) { return prev }, {}) } + +// converts a timeStamp in seconds to a human readable hh:mm:ss format +export function secondsToTimeString (secondsTimeStamp) { + if (secondsTimeStamp === Infinity) return '∞' + const hours = Math.floor(secondsTimeStamp / 60 / 60) + const minutes = Math.floor(secondsTimeStamp / 60) - (hours * 60) + const seconds = Math.floor(secondsTimeStamp % 60) + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` + } else { + return `${minutes}:${seconds.toString().padStart(2, '0')}` + } +} + +/** + * Pipe for formatting numbers to specific decimal + * + * @param {number} value The number. + * @param {number} decimalPlaces The number of decimal places to round to (default: 0). +*/ +export function formatNumber (value, decimalPlaces = 0) { + const decimal = Math.pow(10, decimalPlaces) + if (value === undefined || value === null || value === Infinity || isNaN(value) || value === 0) { return '--' } + + return Math.round(value * decimal) / decimal +} From b494be4b9dd5b796efdb07cb9009efa3bdd3ae49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Sat, 25 Mar 2023 21:59:21 +0100 Subject: [PATCH 090/231] Fix bug re. ignoring app mode Refactor appMode setting so it becomes internal to the DashboardAction component as it is not used anywhere else. This prevents exposing unnecessary - essentially internal - state to the global appState. --- app/client/components/DashboardActions.js | 21 +++++++++++++++++---- app/client/lib/app.js | 7 +------ app/client/store/appState.js | 2 -- app/client/store/dashboardMetrics.js | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index e8aa034e2a..60823fc08d 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -77,8 +77,8 @@ export class DashboardActions extends AppElement { @property({ type: Object }) config = {} - @property({ type: Object }) - appMode = 'BROWSER' + @state() + _appMode = 'BROWSER' @state() _dialog @@ -105,12 +105,25 @@ export class DashboardActions extends AppElement { ` } + firstUpdated () { + switch (new URLSearchParams(window.location.search).get('mode')) { + case 'standalone': + this._appMode = 'STANDALONE' + break + case 'kiosk': + this._appMode = 'KIOSK' + break + default: + this._appMode = 'BROWSER' + } + } + renderOptionalButtons () { const buttons = [] // changing to fullscreen mode only makes sence when the app is openend in a regular // webbrowser (kiosk and standalone mode are always in fullscreen view) and if the // browser supports this feature - if (this.appMode === 'BROWSER' && document.documentElement.requestFullscreen) { + if (this._appMode === 'BROWSER' && document.documentElement.requestFullscreen) { buttons.push(html` `) diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 004acaa280..b5e4904885 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -9,12 +9,7 @@ import NoSleep from 'nosleep.js' import { filterObjectByKeys } from './helper.js' export function createApp (app) { - const urlParameters = new URLSearchParams(window.location.search) - const mode = urlParameters.get('mode') - const appMode = mode === 'standalone' ? 'STANDALONE' : mode === 'kiosk' ? 'KIOSK' : 'BROWSER' - app.updateState({ ...app.getState(), appMode }) - - const stravaAuthorizationCode = urlParameters.get('code') + const stravaAuthorizationCode = new URLSearchParams(window.location.search).get('code') let socket diff --git a/app/client/store/appState.js b/app/client/store/appState.js index 7865fff50e..2e1e0a1883 100644 --- a/app/client/store/appState.js +++ b/app/client/store/appState.js @@ -6,8 +6,6 @@ */ export const APP_STATE = { - // currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default) - appMode: '', // contains all the rowing metrics that are delivered from the backend metrics: {}, config: { diff --git a/app/client/store/dashboardMetrics.js b/app/client/store/dashboardMetrics.js index 092f3ea4c4..0d2d6b551e 100644 --- a/app/client/store/dashboardMetrics.js +++ b/app/client/store/dashboardMetrics.js @@ -49,5 +49,5 @@ export const DASHBOARD_METRICS = { forceCurve: { displayName: 'Force curve', size: 2, template: (metrics) => html`` }, - actions: { displayName: 'Actions', size: 1, template: (appState, config) => html`` } + actions: { displayName: 'Actions', size: 1, template: (_, config) => html`` } } From 475fe85c00b8c1fcbaddd3d17d3b06d61942778d Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 18 Apr 2023 10:27:02 +0200 Subject: [PATCH 091/231] Clarification of minimumStrokeQuality Clarification of the minimumStrokeQuality parameters role in the Recovery detection --- config/rowerProfiles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/rowerProfiles.js b/config/rowerProfiles.js index 7515039a37..2721504335 100644 --- a/config/rowerProfiles.js +++ b/config/rowerProfiles.js @@ -43,7 +43,7 @@ export default { minumumRecoverySlope: 0, // The minimum quality level of the stroke detection: 1.0 is perfect, 0.1 pretty bad. Normally around 0.33. Setting this too high will stop - // the recovery phase from being detected. + // the recovery phase from being detected through the slope angle (i.e. it will completely rely on the absence of the minumumForceBeforeStroke). minimumStrokeQuality: 0.34, // ORM can automatically calculate the recovery slope and adjust it dynamically. For this to work, autoAdjustDragFactor MUST be set to true From bc9dc83596995828adc3e21f1d200169b002c0f4 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Tue, 18 Apr 2023 10:32:48 +0200 Subject: [PATCH 092/231] Made position of createRawDataFiles clearer Added a better description of the placement of createRawDataFiles based on https://github.com/laberning/openrowingmonitor/discussions/139 --- docs/rower_settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rower_settings.md b/docs/rower_settings.md index dc6d4744e2..5390872e2f 100644 --- a/docs/rower_settings.md +++ b/docs/rower_settings.md @@ -86,7 +86,7 @@ Next, when the electric connection has been made, we need to look if the data is sudo nano /opt/openrowingmonitor/config/config.js ``` -Here, you can change the setting for **createRawDataFiles** by setting: +Here, you can change the setting for **createRawDataFiles** by setting/adding the following BEFORE the rowerSettings element (so outside the rowerSettings scope): ```js createRawDataFiles: true, From dfc2881f3667ff3b32b03814e367adee0beb0eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=A1sz?= <> Date: Tue, 18 Apr 2023 23:19:53 +0200 Subject: [PATCH 093/231] Make changes to GUI to better support intervals Change distance, timer and calories tile to change behaviour when current workout is an interval session (i.e. not JustRow). Expose data necessary for this in RowingStatistics. Remove miles option from formatter. --- app/client/lib/helper.js | 13 ++++--------- app/client/store/dashboardMetrics.js | 23 ++++++++++++++++++++--- app/engine/RowingStatistics.js | 3 ++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/app/client/lib/helper.js b/app/client/lib/helper.js index 444de37340..e6c98a46b9 100644 --- a/app/client/lib/helper.js +++ b/app/client/lib/helper.js @@ -42,16 +42,11 @@ export function secondsToPace (seconds) { * Pipe for formatting distance in meters with units * * @param {number} value The distance in meters. - * @param {boolean} showInMiles Boolean whether to use imperial metric (default: false). */ -export function formatDistance (value, showInMiles = false) { - if (showInMiles === false) { - return value >= 10000 - ? { distance: formatNumber((value / 1000), 2), unit: 'km' } - : { distance: formatNumber(value), unit: 'm' } - } - - return { distance: formatNumber((value / 1609.344), 2), unit: 'mi' } +export function formatDistance (value) { + return value >= 10000 + ? { distance: formatNumber((value / 1000), 2), unit: 'km' } + : { distance: formatNumber(value), unit: 'm' } } /** diff --git a/app/client/store/dashboardMetrics.js b/app/client/store/dashboardMetrics.js index 0d2d6b551e..8848f20286 100644 --- a/app/client/store/dashboardMetrics.js +++ b/app/client/store/dashboardMetrics.js @@ -10,7 +10,8 @@ export const DASHBOARD_METRICS = { displayName: 'Distance', size: 1, template: (metrics, config) => { - const linearDistance = formatDistance(metrics?.totalLinearDistance) + const distance = metrics?.sessiontype === 'Distance' ? Math.max(metrics?.intervalTargetDistance - metrics?.intervalLinearDistance, 0) : metrics?.totalLinearDistance + const linearDistance = formatDistance(distance ?? 0) return simpleMetricFactory(linearDistance.distance, linearDistance.unit, config.guiConfigs.showIcons ? icon_route : '') } @@ -33,9 +34,25 @@ export const DASHBOARD_METRICS = { totalStk: { displayName: 'Total strokes', size: 1, template: (metrics, config) => simpleMetricFactory(metrics?.totalNumberOfStrokes, 'stk', config.guiConfigs.showIcons ? icon_paddle : '') }, - calories: { displayName: 'Calories', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.totalCalories), 'kcal', config.guiConfigs.showIcons ? icon_fire : '') }, + calories: { + displayName: 'Calories', + size: 1, + template: (metrics, config) => { + const calories = metrics?.sessiontype === 'Calories' ? Math.max(metrics?.intervalTargetCalories - metrics?.intervalLinearCalories, 0) : metrics?.totalCalories + + return simpleMetricFactory(formatNumber(calories ?? 0), 'kcal', config.guiConfigs.showIcons ? icon_fire : '') + } + }, - timer: { displayName: 'Timer', size: 1, template: (metrics, config) => simpleMetricFactory(secondsToPace(metrics?.totalMovingTime), '', config.guiConfigs.showIcons ? icon_clock : '') }, + timer: { + displayName: 'Timer', + size: 1, + template: (metrics, config) => { + const time = metrics?.sessiontype === 'Time' ? Math.max(metrics?.intervalTargetTime - metrics?.intervalMovingTime, 0) : metrics?.totalMovingTime + + return simpleMetricFactory(secondsToPace(time ?? 0), '', config.guiConfigs.showIcons ? icon_clock : '') + } + }, distancePerStk: { displayName: 'Dist per Stroke', size: 1, template: (metrics, config) => simpleMetricFactory(formatNumber(metrics?.cycleDistance, 1), 'm', config.guiConfigs.showIcons ? rower_icon : '') }, diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index 3c475492ed..0edc6f9f7e 100644 --- a/app/engine/RowingStatistics.js +++ b/app/engine/RowingStatistics.js @@ -380,8 +380,9 @@ function createRowingStatistics (config) { totalLinearDistance: totalLinearDistance > 0 ? totalLinearDistance : 0, // meters intervalNumber: Math.max(currentIntervalNumber + 1, 0), // Interval number intervalMovingTime: totalMovingTime - intervalPrevAccumulatedTime, - intervalRemainingTime: intervalTargetTime - totalMovingTime, + intervalTargetTime: intervalTargetTime > intervalPrevAccumulatedTime ? intervalTargetTime - intervalPrevAccumulatedTime : 0, intervalLinearDistance: totalLinearDistance - intervalPrevAccumulatedDistance, + intervalTargetDistance: intervalTargetDistance > intervalPrevAccumulatedDistance ? intervalTargetDistance - intervalPrevAccumulatedDistance : 0, strokeCalories: strokeCalories > 0 ? strokeCalories : 0, // kCal strokeWork: strokeWork > 0 ? strokeWork : 0, // Joules totalCalories: calories.yAtSeriesEnd() > 0 ? calories.yAtSeriesEnd() : 0, // kcal From c0770891ce2e740e9153d76c0bcfd6a77c069568 Mon Sep 17 00:00:00 2001 From: Jaap van Ekris <82339657+JaapvanEkris@users.noreply.github.com> Date: Wed, 19 Apr 2023 10:25:36 +0200 Subject: [PATCH 094/231] Metric selection screen added --- docs/img/Metrics_Selection.png | Bin 0 -> 140133 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/img/Metrics_Selection.png diff --git a/docs/img/Metrics_Selection.png b/docs/img/Metrics_Selection.png new file mode 100644 index 0000000000000000000000000000000000000000..77730dec05f479f0645c28006819146a9377709e GIT binary patch literal 140133 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

TD*jEj37T0+^{$`-6UCr>0k=w^ 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