diff --git a/app/engine/RowingStatistics.js b/app/engine/RowingStatistics.js index 616b6cb..f6e2ff8 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 02e54ee..f518bc7 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 8a6bcec..3961477 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 0000000..4f6883d --- /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 0183d5d..1916c70 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 c21c340..ae92e20 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 965f62e..32e449c 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) }) }