adds logging framework, cleans documentation
This commit is contained in:
parent
1b0cc5d9da
commit
361e1c65f8
15
README.md
15
README.md
|
|
@ -1,14 +1,15 @@
|
|||
<img width="150" height="150" src="doc/img/openrowingmonitor.png">
|
||||
|
||||
# Open Rowing Monitor
|
||||
|
||||
<!-- markdownlint-disable-next-line no-inline-html -->
|
||||
<img width="200" height="200" align="left" src="doc/img/openrowingmonitor.png">
|
||||
|
||||
An open source rowing monitor for rowing exercise machines.
|
||||
|
||||
The Open Rowing Monitor runs on a Raspberry Pi and measures the rotation of the rower's flywheel to calculate rowing specific metrics such as power, split time, speed, stroke rate, distance and calories.
|
||||
Open Rowing Monitor runs on a Raspberry Pi and measures the rotation of the rower's flywheel to calculate rowing specific metrics such as power, split time, speed, stroke rate, distance and calories.
|
||||
|
||||
The web interface can be used to view those metrics on any device that runs a browser (i.e. a smartphone that you attach to your rowing machine while training).
|
||||
A web interface visualizes those metrics on any device that can run a browser (i.e. a smartphone that you attach to your rowing machine while training).
|
||||
|
||||
The Open Rowing Monitor also implements the Bluetooth Low Energy (BLE) protocol for Fitness Machine Service (FTMS). This allows using your rowing machine with Fitness Applications that support FTMS.
|
||||
Open Rowing Monitor also implements the Bluetooth Low Energy (BLE) protocol for Fitness Machine Service (FTMS). This allows using your rowing machine with any Fitness Application that supports FTMS.
|
||||
|
||||
FTMS supports different types of fitness machines. Open Rowing Monitor currently supports the type **FTMS Rower** and simulates the type **FTMS Indoor Bike**.
|
||||
|
||||
|
|
@ -16,9 +17,9 @@ FTMS supports different types of fitness machines. Open Rowing Monitor currently
|
|||
|
||||
**FTMS Indoor Bike** is widely adopted by training applications for bike training. The simulated Indoor Bike offers metrics such as power and distance to the biking application. So why not use your rowing machine to row up a mountain in Zwift, Bkool, Sufferfest or similar :-)
|
||||
|
||||
I basically started this project, because my rowing machine (WRX700) has a very crappy computer and I wanted to build something with more realistic metrics and more features. But there is not much that is specific to that rowing machine. It should run fine with any rowing machine that uses an air or water resistance mechanism.
|
||||
I originally started this project, because my rowing machine (WRX700) has a very simple computer and I wanted to build something with a clean interface that calculates more realistic metrics. But there is not much that is specific to that rowing machine. It should run fine with any rowing machine that uses an air or water resistance mechanism.
|
||||
|
||||
Feel free to contact me if you have any questions to this project. Let me know if you run this with a different rowing machine setup so I can expand the documentation.
|
||||
Feel free to contact me if you have any questions about this project. Let me know if you run Open Rowing Machine with a different setup so I can expand the documentation.
|
||||
|
||||
This project is already in a very usable stage, but some things are still a bit rough on the edges.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
- Zwift: uses startOrResume and setIndoorBikeSimulationParameters
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import log from 'loglevel'
|
||||
|
||||
// see https://www.bluetooth.com/specifications/specs/fitness-machine-service-1-0 for details
|
||||
const ControlPointOpCode = {
|
||||
|
|
@ -67,7 +68,7 @@ export default class FitnessMachineControlPointCharacteristic extends bleno.Char
|
|||
case ControlPointOpCode.requestControl:
|
||||
if (!this.controlled) {
|
||||
if (this.controlPointCallback({ name: 'requestControl' })) {
|
||||
console.log('requestControl sucessful')
|
||||
log.debug('requestControl sucessful')
|
||||
this.controlled = true
|
||||
callback(this.buildResponse(opCode, ResultCode.success))
|
||||
} else {
|
||||
|
|
@ -95,7 +96,7 @@ export default class FitnessMachineControlPointCharacteristic extends bleno.Char
|
|||
} else if (controlParameter === 2) {
|
||||
this.handleSimpleCommand(ControlPointOpCode.stopOrPause, 'pause', callback)
|
||||
} else {
|
||||
console.log(`stopOrPause with invalid controlParameter: ${controlParameter}`)
|
||||
log.error(`stopOrPause with invalid controlParameter: ${controlParameter}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -116,7 +117,7 @@ export default class FitnessMachineControlPointCharacteristic extends bleno.Char
|
|||
}
|
||||
|
||||
default:
|
||||
console.log(`opCode ${opCode} is not supported`)
|
||||
log.info(`opCode ${opCode} is not supported`)
|
||||
callback(this.buildResponse(opCode, ResultCode.opCodeNotSupported))
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +131,7 @@ export default class FitnessMachineControlPointCharacteristic extends bleno.Char
|
|||
callback(this.buildResponse(opCode, ResultCode.operationFailed))
|
||||
}
|
||||
} else {
|
||||
console.log(`initating command '${opName}' requires 'requestControl'`)
|
||||
log.info(`initating command '${opName}' requires 'requestControl'`)
|
||||
callback(this.buildResponse(opCode, ResultCode.controlNotPermitted))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
shall be exposed by the Server. Otherwise, supporting the Fitness Machine Status characteristic is optional.
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import log from 'loglevel'
|
||||
|
||||
// see page 67 https://www.bluetooth.com/specifications/specs/fitness-machine-service-1-0
|
||||
const StatusOpCode = {
|
||||
|
|
@ -49,20 +50,20 @@ export default class FitnessMachineStatusCharacteristic extends bleno.Characteri
|
|||
}
|
||||
|
||||
onSubscribe (maxValueSize, updateValueCallback) {
|
||||
console.log('FitnessMachineStatusCharacteristic - central subscribed')
|
||||
log.debug('FitnessMachineStatusCharacteristic - central subscribed')
|
||||
this._updateValueCallback = updateValueCallback
|
||||
return this.RESULT_SUCCESS
|
||||
}
|
||||
|
||||
onUnsubscribe () {
|
||||
console.log('FitnessMachineStatusCharacteristic - central unsubscribed')
|
||||
log.debug('FitnessMachineStatusCharacteristic - central unsubscribed')
|
||||
this._updateValueCallback = null
|
||||
return this.RESULT_UNLIKELY_ERROR
|
||||
}
|
||||
|
||||
notify (status) {
|
||||
if (!(status && status.name)) {
|
||||
console.log('can not deliver status without name')
|
||||
log.error('can not deliver status without name')
|
||||
return this.RESULT_SUCCESS
|
||||
}
|
||||
if (this._updateValueCallback) {
|
||||
|
|
@ -78,11 +79,11 @@ export default class FitnessMachineStatusCharacteristic extends bleno.Characteri
|
|||
buffer.writeUInt8(StatusOpCode.startedOrResumedByUser, 0)
|
||||
break
|
||||
default:
|
||||
console.log(`status ${status.name} is not supported`)
|
||||
log.error(`status ${status.name} is not supported`)
|
||||
}
|
||||
this._updateValueCallback(buffer)
|
||||
} else {
|
||||
// console.log('can not notify status, no central subscribed')
|
||||
log.debug('can not notify status, no central subscribed')
|
||||
}
|
||||
return this.RESULT_SUCCESS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
while in a connection and the interval is not configurable by the Client
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import log from 'loglevel'
|
||||
|
||||
export default class IndoorBikeDataCharacteristic extends bleno.Characteristic {
|
||||
constructor () {
|
||||
|
|
@ -32,13 +33,13 @@ export default class IndoorBikeDataCharacteristic extends bleno.Characteristic {
|
|||
}
|
||||
|
||||
onSubscribe (maxValueSize, updateValueCallback) {
|
||||
console.log('IndooBikeDataCharacteristic - central subscribed')
|
||||
log.debug('IndooBikeDataCharacteristic - central subscribed')
|
||||
this._updateValueCallback = updateValueCallback
|
||||
return this.RESULT_SUCCESS
|
||||
};
|
||||
|
||||
onUnsubscribe () {
|
||||
console.log('IndooBikeDataCharacteristic - central unsubscribed')
|
||||
log.debug('IndooBikeDataCharacteristic - central unsubscribed')
|
||||
this._updateValueCallback = null
|
||||
return this.RESULT_UNLIKELY_ERROR
|
||||
};
|
||||
|
|
@ -46,7 +47,7 @@ export default class IndoorBikeDataCharacteristic extends bleno.Characteristic {
|
|||
notify (data) {
|
||||
// ignore events without the mandatory fields
|
||||
if (!data.speed) {
|
||||
console.log('can not deliver bike data without mandatory fields')
|
||||
log.error('can not deliver bike data without mandatory fields')
|
||||
return this.RESULT_SUCCESS
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +88,7 @@ export default class IndoorBikeDataCharacteristic extends bleno.Characteristic {
|
|||
}
|
||||
this._updateValueCallback(buffer)
|
||||
} else {
|
||||
// console.log('can not notify indoor bike data, no central subscribed')
|
||||
log.debug('can not notify indoor bike data, no central subscribed')
|
||||
}
|
||||
return this.RESULT_SUCCESS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
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) {
|
||||
|
|
@ -29,7 +30,7 @@ export default class IndoorBikeDataCharacteristic extends bleno.Characteristic {
|
|||
// none
|
||||
// 0000000 0000000
|
||||
const features = [0x04, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
console.log('Features of Indoor Bike requested')
|
||||
log.debug('Features of Indoor Bike requested')
|
||||
callback(this.RESULT_SUCCESS, features.slice(offset, features.length))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
while in a connection and the interval is not configurable by the Client
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import log from 'loglevel'
|
||||
|
||||
export default class RowerDataCharacteristic extends bleno.Characteristic {
|
||||
constructor () {
|
||||
|
|
@ -25,13 +26,13 @@ export default class RowerDataCharacteristic extends bleno.Characteristic {
|
|||
}
|
||||
|
||||
onSubscribe (maxValueSize, updateValueCallback) {
|
||||
console.log('RowerDataCharacteristic - central subscribed')
|
||||
log.debug('RowerDataCharacteristic - central subscribed')
|
||||
this._updateValueCallback = updateValueCallback
|
||||
return this.RESULT_SUCCESS
|
||||
};
|
||||
|
||||
onUnsubscribe () {
|
||||
console.log('RowerDataCharacteristic - central unsubscribed')
|
||||
log.debug('RowerDataCharacteristic - central unsubscribed')
|
||||
this._updateValueCallback = null
|
||||
return this.RESULT_UNLIKELY_ERROR
|
||||
};
|
||||
|
|
@ -85,7 +86,7 @@ export default class RowerDataCharacteristic extends bleno.Characteristic {
|
|||
}
|
||||
this._updateValueCallback(buffer)
|
||||
} else {
|
||||
// console.log('can not notify rower data, no central subscribed')
|
||||
log.debug('can not notify rower data, no central subscribed')
|
||||
}
|
||||
return this.RESULT_SUCCESS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
are supported in RowerDataCharacteristic and FitnessMachineControlPointCharacteristic.
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import log from 'loglevel'
|
||||
|
||||
export default class RowerFeatureCharacteristic extends bleno.Characteristic {
|
||||
constructor () {
|
||||
|
|
@ -29,7 +30,7 @@ export default class RowerFeatureCharacteristic extends bleno.Characteristic {
|
|||
// none
|
||||
// 0000000 0000000
|
||||
const features = [0x24, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||
console.log('Features of Rower requested')
|
||||
log.debug('Features of Rower requested')
|
||||
callback(this.RESULT_SUCCESS, features.slice(offset, features.length))
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import bleno from '@abandonware/bleno'
|
|||
import { EventEmitter } from 'events'
|
||||
import FitnessMachineService from './FitnessMachineService.js'
|
||||
import DeviceInformationService from './DeviceInformationService.js'
|
||||
import log from 'loglevel'
|
||||
|
||||
function createRowingMachinePeripheral (options) {
|
||||
const emitter = new EventEmitter()
|
||||
|
|
@ -29,7 +30,7 @@ function createRowingMachinePeripheral (options) {
|
|||
peripheralName,
|
||||
[fitnessMachineService.uuid, deviceInformationService.uuid],
|
||||
(error) => {
|
||||
if (error) console.log(error)
|
||||
if (error) log.error(error)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
|
@ -42,46 +43,45 @@ function createRowingMachinePeripheral (options) {
|
|||
bleno.setServices(
|
||||
[fitnessMachineService, deviceInformationService],
|
||||
(error) => {
|
||||
if (error) console.log(error)
|
||||
if (error) log.error(error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
bleno.on('accept', (clientAddress) => {
|
||||
console.log(`ble central connected: ${clientAddress}`)
|
||||
log.debug(`ble central connected: ${clientAddress}`)
|
||||
// todo: do we need this?
|
||||
bleno.updateRssi()
|
||||
})
|
||||
|
||||
bleno.on('disconnect', (clientAddress) => {
|
||||
console.log(`ble central disconnected: ${clientAddress}`)
|
||||
log.debug(`ble central disconnected: ${clientAddress}`)
|
||||
})
|
||||
/*
|
||||
|
||||
bleno.on('platform', (event) => {
|
||||
console.log('platform', event)
|
||||
log.debug('platform', event)
|
||||
})
|
||||
bleno.on('addressChange', (event) => {
|
||||
console.log('addressChange', event)
|
||||
log.debug('addressChange', event)
|
||||
})
|
||||
bleno.on('mtuChange', (event) => {
|
||||
console.log('mtuChange', event)
|
||||
log.debug('mtuChange', event)
|
||||
})
|
||||
bleno.on('advertisingStartError', (event) => {
|
||||
console.log('advertisingStartError', event)
|
||||
log.debug('advertisingStartError', event)
|
||||
})
|
||||
bleno.on('advertisingStop', (event) => {
|
||||
console.log('advertisingStop', event)
|
||||
log.debug('advertisingStop', event)
|
||||
})
|
||||
bleno.on('servicesSet', (event) => {
|
||||
console.log('servicesSet', event)
|
||||
log.debug('servicesSet', event)
|
||||
})
|
||||
bleno.on('servicesSetError', (event) => {
|
||||
console.log('servicesSetError', event)
|
||||
log.debug('servicesSetError', event)
|
||||
})
|
||||
bleno.on('rssiUpdate', (event) => {
|
||||
console.log('rssiUpdate', event)
|
||||
log.debug('rssiUpdate', event)
|
||||
})
|
||||
*/
|
||||
|
||||
function controlPointCallback (event) {
|
||||
const obj = {
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@ span.unit {
|
|||
|
||||
button {
|
||||
outline:none;
|
||||
background-color: #0059B3;
|
||||
background-color: #00468c;
|
||||
border: 0;
|
||||
color: white;
|
||||
padding: 1vw 2vw;
|
||||
padding: 1.5vw 2vw;
|
||||
margin: 1vw;
|
||||
font-size: 60%;
|
||||
font-size: 80%;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
Physics of Rowing by Anu Dudhia: http://eodg.atm.ox.ac.uk/user/dudhia/rowing/physics
|
||||
Also Dave Vernooy has some good explanations here: https://dvernooy.github.io/projects/ergware
|
||||
*/
|
||||
import log from 'loglevel'
|
||||
import { createAverager } from './Averager.js'
|
||||
import { createTimer } from './Timer.js'
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ function createRowingEngine () {
|
|||
// todo: we should inform the workoutHandler in this case
|
||||
// (if we want to track the training history)
|
||||
if (currentDt > 3.0) {
|
||||
console.log(`training pause detected: ${currentDt}`)
|
||||
log.debug(`training pause detected: ${currentDt}`)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -136,25 +137,25 @@ function createRowingEngine () {
|
|||
if (!isInDrivePhase && !wasInDrivePhase) { updateRecoveryPhase(currentDt) }
|
||||
|
||||
timer.updateTimers(currentDt)
|
||||
// console.log(`𝑑t: ${currentDt} ω: ${omegaVector[0].toFixed(2)} ωdot: ${omegaDotVector[0].toFixed(2)} ωdotdot: ${omegaDotDot.toFixed(2)} aPos: ${accelerationIsPositive} aChange: ${accelerationIsChanging}`)
|
||||
log.debug(`𝑑t: ${currentDt} ω: ${omegaVector[0].toFixed(2)} ωdot: ${omegaDotVector[0].toFixed(2)} ωdotdot: ${omegaDotDot.toFixed(2)} aPos: ${accelerationIsPositive} aChange: ${accelerationIsChanging}`)
|
||||
}
|
||||
|
||||
function startDrivePhase (currentDt) {
|
||||
// console.log('*** drive phase started')
|
||||
log.debug('*** drive phase started')
|
||||
timer.start('drive')
|
||||
jPower = 0.0
|
||||
kPower = 0.0
|
||||
if (strokeElapsed - driveElapsed !== 0) {
|
||||
kDampEstimatorAverager.pushValue(kDampEstimator / (strokeElapsed - driveElapsed))
|
||||
}
|
||||
// console.log(`estimated kDamp: ${jMoment * (-1 * kDampEstimatorAverager.weightedAverage())}`)
|
||||
// console.log(`estimated omegaDotDivOmegaSquare: ${-1 * kDampEstimatorAverager.weightedAverage()}`)
|
||||
log.debug(`estimated kDamp: ${jMoment * (-1 * kDampEstimatorAverager.weightedAverage())}`)
|
||||
log.debug(`estimated omegaDotDivOmegaSquare: ${-1 * kDampEstimatorAverager.weightedAverage()}`)
|
||||
}
|
||||
|
||||
function updateDrivePhase (currentDt) {
|
||||
jPower = jPower + jMoment * omegaVector[0] * omegaDotVector[0] * currentDt
|
||||
kPower = kPower + kDamp * (omegaVector[0] * omegaVector[0] * omegaVector[0]) * currentDt
|
||||
// console.log(`Jpower: ${jPower}, kPower: ${kPower}`)
|
||||
log.debug(`Jpower: ${jPower}, kPower: ${kPower}`)
|
||||
}
|
||||
|
||||
function startRecoveryPhase () {
|
||||
|
|
@ -162,7 +163,7 @@ function createRowingEngine () {
|
|||
timer.stop('drive')
|
||||
strokeElapsed = timer.getValue('stroke')
|
||||
timer.stop('stroke')
|
||||
// console.log(`driveElapsed: ${driveElapsed}, strokeElapsed: ${strokeElapsed}`)
|
||||
log.debug(`driveElapsed: ${driveElapsed}, strokeElapsed: ${strokeElapsed}`)
|
||||
timer.start('stroke')
|
||||
|
||||
if (strokeElapsed !== 0 && workoutHandler) {
|
||||
|
|
@ -177,7 +178,7 @@ function createRowingEngine () {
|
|||
// stroke finished, reset stroke specific measurements
|
||||
kDampEstimator = 0.0
|
||||
strokeDistance = 0
|
||||
// console.log('*** recovery phase started')
|
||||
log.debug('*** recovery phase started')
|
||||
}
|
||||
|
||||
function updateRecoveryPhase (currentDt) {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,6 @@
|
|||
everything together while figuring out the physics and model of the application.
|
||||
todo: refactor this as we progress
|
||||
*/
|
||||
import { createRowingMachinePeripheral } from './ble/RowingMachinePeripheral.js'
|
||||
import { createRowingEngine } from './engine/RowingEngine.js'
|
||||
import { createRowingStatistics } from './engine/RowingStatistics.js'
|
||||
// import { recordRowingSession } from './tools/RowingRecorder.js'
|
||||
// import readline from 'readline'
|
||||
// import fs from 'fs'
|
||||
import { fork } from 'child_process'
|
||||
|
|
@ -17,7 +13,14 @@ import WebSocket from 'ws'
|
|||
import finalhandler from 'finalhandler'
|
||||
import http from 'http'
|
||||
import serveStatic from 'serve-static'
|
||||
import log from 'loglevel'
|
||||
import { createRowingMachinePeripheral } from './ble/RowingMachinePeripheral.js'
|
||||
import { createRowingEngine } from './engine/RowingEngine.js'
|
||||
import { createRowingStatistics } from './engine/RowingStatistics.js'
|
||||
// import { recordRowingSession } from './tools/RowingRecorder.js'
|
||||
|
||||
// sets the global log level
|
||||
log.setLevel(log.levels.INFO)
|
||||
let websocket
|
||||
// recordRowingSession('recordings/wrx700_2magnets.csv')
|
||||
const peripheral = createRowingMachinePeripheral({
|
||||
|
|
@ -28,31 +31,30 @@ peripheral.on('controlPoint', (event) => {
|
|||
if (event?.req?.name === 'requestControl') {
|
||||
event.res = true
|
||||
} else if (event?.req?.name === 'reset') {
|
||||
console.log('reset requested')
|
||||
log.debug('reset requested')
|
||||
rowingStatistics.reset()
|
||||
peripheral.notifyStatus({ name: 'reset' })
|
||||
event.res = true
|
||||
// todo: we could use these controls once we implement a concept of a rowing session
|
||||
} else if (event?.req?.name === 'stop') {
|
||||
console.log('stop requested')
|
||||
log.debug('stop requested')
|
||||
peripheral.notifyStatus({ name: 'stoppedOrPausedByUser' })
|
||||
event.res = true
|
||||
} else if (event?.req?.name === 'pause') {
|
||||
console.log('pause requested')
|
||||
log.debug('pause requested')
|
||||
peripheral.notifyStatus({ name: 'stoppedOrPausedByUser' })
|
||||
event.res = true
|
||||
} else if (event?.req?.name === 'startOrResume') {
|
||||
console.log('startOrResume requested')
|
||||
log.debug('startOrResume requested')
|
||||
peripheral.notifyStatus({ name: 'startedOrResumedByUser' })
|
||||
event.res = true
|
||||
} else {
|
||||
console.log('unhandled Command', event.req)
|
||||
log.info('unhandled Command', event.req)
|
||||
}
|
||||
})
|
||||
|
||||
const gpioTimerService = fork('./app/gpio/GpioTimerService.js')
|
||||
gpioTimerService.on('message', (dataPoint) => {
|
||||
// console.log(dataPoint)
|
||||
rowingEngine.handleRotationImpulse(dataPoint)
|
||||
})
|
||||
|
||||
|
|
@ -61,7 +63,7 @@ const rowingStatistics = createRowingStatistics()
|
|||
rowingEngine.notify(rowingStatistics)
|
||||
|
||||
rowingStatistics.on('strokeFinished', (data) => {
|
||||
console.log(`stroke: ${data.strokesTotal}, dur: ${data.strokeTime}s, power: ${data.power}w` +
|
||||
log.info(`stroke: ${data.strokesTotal}, dur: ${data.strokeTime}s, power: ${data.power}w` +
|
||||
`, split: ${data.splitFormatted}, ratio: ${data.powerRatio}, dist: ${data.distanceTotal}m` +
|
||||
`, cal: ${data.caloriesTotal}kcal, SPM: ${data.strokesPerMinute}, speed: ${data.speed}km/h`)
|
||||
|
||||
|
|
@ -110,10 +112,10 @@ wss.on('connection', function connection (ws) {
|
|||
rowingStatistics.reset()
|
||||
peripheral.notifyStatus({ name: 'reset' })
|
||||
} else {
|
||||
console.log(`invalid command received: ${data}`)
|
||||
log.info(`invalid command received: ${data}`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
log.error(err)
|
||||
}
|
||||
})
|
||||
/*
|
||||
|
|
@ -155,7 +157,6 @@ function simulateRowing () {
|
|||
strokesPerMinute: 10 + 20 * (Math.random() - 0.5),
|
||||
speed: (15 + 20 * (Math.random() - 0.5)).toFixed(2)
|
||||
}
|
||||
// console.log(metrics)
|
||||
peripheral.notifyData(metrics)
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,14 +7,15 @@
|
|||
import { fork } from 'child_process'
|
||||
|
||||
import fs from 'fs'
|
||||
import log from 'loglevel'
|
||||
|
||||
function recordRowingSession (filename) {
|
||||
// measure the gpio interrupts in another process, since we need
|
||||
// to track time close to realtime
|
||||
const gpioTimerService = fork('./app/tools/GpioTimerService.js')
|
||||
gpioTimerService.on('message', (dataPoint) => {
|
||||
console.log(dataPoint.delta)
|
||||
fs.appendFile(filename, `${dataPoint.delta}\n`, (err) => { if (err) console.log(err) })
|
||||
log.debug(dataPoint.delta)
|
||||
fs.appendFile(filename, `${dataPoint.delta}\n`, (err) => { if (err) log.error(err) })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ 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.
|
||||
|
||||
* The app icon is based on this [image of a rowing machine](https://thenounproject.com/term/rowing-machine/659265) by [Gan Khoon Lay](https://thenounproject.com/leremy/) licensed under [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/).
|
||||
* The app icon is based on this [image of a rowing machine](https://thenounproject.com/term/rowing-machine/659265) by [Gan Khoon Lay](https://thenounproject.com/leremy/) licensed under [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/).
|
||||
|
|
|
|||
|
|
@ -8,16 +8,15 @@ This is the very minimalistic Backlog for further development of this project.
|
|||
* handle training interruptions (set stroke specific metrics to "0" if no impulse detected for x seconds)
|
||||
* check todo markers in code and add them to this backlog
|
||||
* cleanup of the server.js start file
|
||||
* add a logging framework
|
||||
* figure out where to set the Service Advertising Data (FTMS.pdf p 15)
|
||||
* Web UI: Replace Fullscreen Button with Exit Button when started from Homescreen
|
||||
* investigate bug: crash, when one unsubscribe to BLE "Generic Attribute", probably a bleno bug "handleAttribute.emit is not a function"
|
||||
* set up a raspi with the installation instructions to see if they are correct
|
||||
|
||||
## Later
|
||||
|
||||
* add a config file
|
||||
* presets for rowing machine specific config parameters
|
||||
* set a more appropriate ble Appearance (currently 0x2A01 Generic Computer)
|
||||
* validate FTMS with more Training Applications and harden implementation
|
||||
* make Web UI a proper Web Application (tooling and SPA framework)
|
||||
* record the workout and show a visual graph of metrics
|
||||
|
|
@ -25,5 +24,7 @@ This is the very minimalistic Backlog for further development of this project.
|
|||
|
||||
## Ideas
|
||||
|
||||
* add support for BLE Heart Rate Sensor and show pulse
|
||||
* add Video playback in Background of Web UI
|
||||
* implement or integrate some rowing games
|
||||
* add possibility to define Workouts (i.e. training intervals with goals)
|
||||
|
|
|
|||
|
|
@ -1627,6 +1627,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
|
||||
},
|
||||
"loglevel": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
|
||||
"integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"@abandonware/bleno": "^0.5.1-3",
|
||||
"finalhandler": "^1.1.2",
|
||||
"http": "0.0.1-security",
|
||||
"loglevel": "^1.7.1",
|
||||
"onoff": "^6.0.1",
|
||||
"serve-static": "^1.14.1",
|
||||
"ws": "^7.4.3"
|
||||
|
|
|
|||
Loading…
Reference in New Issue