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
This commit is contained in:
parent
3edfe22434
commit
7ef338d856
|
|
@ -417,7 +417,7 @@ function createRowingStatistics (config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign(emitter, {
|
return Object.assign(emitter, {
|
||||||
handleHeartrateMeasurement,
|
handleHeartRateMeasurement: handleHeartrateMeasurement,
|
||||||
handleRotationImpulse,
|
handleRotationImpulse,
|
||||||
setIntervalParameters,
|
setIntervalParameters,
|
||||||
pause: pauseTraining,
|
pause: pauseTraining,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ import log from 'loglevel'
|
||||||
import EventEmitter from 'node:events'
|
import EventEmitter from 'node:events'
|
||||||
import { createCpsPeripheral } from './ble/CpsPeripheral.js'
|
import { createCpsPeripheral } from './ble/CpsPeripheral.js'
|
||||||
import { createCscPeripheral } from './ble/CscPeripheral.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']
|
const modes = ['FTMS', 'FTMSBIKE', 'PM5', 'CSC', 'CPS']
|
||||||
function createPeripheralManager () {
|
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) {
|
function controlCallback (event) {
|
||||||
emitter.emit('control', event)
|
emitter.emit('control', event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign(emitter, {
|
return Object.assign(emitter, {
|
||||||
|
startAntHeartRateService,
|
||||||
|
startBleHeartRateService,
|
||||||
getPeripheral,
|
getPeripheral,
|
||||||
getPeripheralMode,
|
getPeripheralMode,
|
||||||
switchPeripheralMode,
|
switchPeripheralMode,
|
||||||
|
|
|
||||||
|
|
@ -9,55 +9,26 @@
|
||||||
- Garmin USB or USB2 ANT+ or an off-brand clone of it (ID 0x1008)
|
- Garmin USB or USB2 ANT+ or an off-brand clone of it (ID 0x1008)
|
||||||
- Garmin mini ANT+ (ID 0x1009)
|
- Garmin mini ANT+ (ID 0x1009)
|
||||||
*/
|
*/
|
||||||
import log from 'loglevel'
|
|
||||||
import Ant from 'ant-plus'
|
import Ant from 'ant-plus'
|
||||||
import EventEmitter from 'node:events'
|
|
||||||
|
|
||||||
function createAntManager () {
|
export default class AntManager {
|
||||||
const emitter = new EventEmitter()
|
constructor () {
|
||||||
const antStick = new Ant.GarminStick2()
|
|
||||||
const antStick3 = new Ant.GarminStick3()
|
|
||||||
// it seems that we have to use two separate heart rate sensors to support both old and new
|
// 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
|
// ant sticks, since the library requires them to be bound before open is called
|
||||||
const heartrateSensor = new Ant.HeartRateSensor(antStick)
|
this._stick = new Ant.GarminStick3() // 0fcf:1009
|
||||||
const heartrateSensor3 = new Ant.HeartRateSensor(antStick3)
|
if (!this._stick.is_present()) {
|
||||||
|
this._stick = new Ant.GarminStick2() // 0fcf:1008
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!antStick3.open()) {
|
openAntStick () {
|
||||||
log.debug('mini ANT+ stick NOT found')
|
if (!this._stick.open()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return this._stick
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign(emitter, {
|
getAntStick () {
|
||||||
})
|
return this._stick
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createAntManager }
|
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -5,14 +5,14 @@
|
||||||
Starts the central manager in a forked thread since noble does not like
|
Starts the central manager in a forked thread since noble does not like
|
||||||
to run in the same thread as bleno
|
to run in the same thread as bleno
|
||||||
*/
|
*/
|
||||||
import { createCentralManager } from './CentralManager.js'
|
|
||||||
import process from 'process'
|
import process from 'process'
|
||||||
import config from '../../tools/ConfigManager.js'
|
import config from '../../tools/ConfigManager.js'
|
||||||
import log from 'loglevel'
|
import log from 'loglevel'
|
||||||
|
import { createHeartRateManager } from './hrm/HeartRateManager.js'
|
||||||
|
|
||||||
log.setLevel(config.loglevel.default)
|
log.setLevel(config.loglevel.default)
|
||||||
const centralManager = createCentralManager()
|
const heartRateManager = createHeartRateManager()
|
||||||
|
|
||||||
centralManager.on('heartrateMeasurement', (heartrateMeasurement) => {
|
heartRateManager.on('heartRateMeasurement', (heartRateMeasurement) => {
|
||||||
process.send(heartrateMeasurement)
|
process.send(heartRateMeasurement)
|
||||||
})
|
})
|
||||||
|
|
@ -49,7 +49,7 @@ NobleBindings.prototype.onDisconnComplete = function (handle, reason) {
|
||||||
const noble = new Noble(new NobleBindings())
|
const noble = new Noble(new NobleBindings())
|
||||||
// END of noble patch
|
// END of noble patch
|
||||||
|
|
||||||
function createCentralManager () {
|
function createHeartRateManager () {
|
||||||
const emitter = new EventEmitter()
|
const emitter = new EventEmitter()
|
||||||
let batteryLevel
|
let batteryLevel
|
||||||
|
|
||||||
|
|
@ -64,10 +64,10 @@ function createCentralManager () {
|
||||||
|
|
||||||
noble.on('discover', (peripheral) => {
|
noble.on('discover', (peripheral) => {
|
||||||
noble.stopScanning()
|
noble.stopScanning()
|
||||||
connectHeartratePeripheral(peripheral)
|
connectHeartRatePeripheral(peripheral)
|
||||||
})
|
})
|
||||||
|
|
||||||
function connectHeartratePeripheral (peripheral) {
|
function connectHeartRatePeripheral (peripheral) {
|
||||||
// connect to the heart rate sensor
|
// connect to the heart rate sensor
|
||||||
peripheral.connect((error) => {
|
peripheral.connect((error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -75,7 +75,7 @@ function createCentralManager () {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.info(`heart rate peripheral connected, name: '${peripheral.advertisement?.localName}', id: ${peripheral.id}`)
|
log.info(`heart rate peripheral connected, name: '${peripheral.advertisement?.localName}', id: ${peripheral.id}`)
|
||||||
subscribeToHeartrateMeasurement(peripheral)
|
subscribeToHeartRateMeasurement(peripheral)
|
||||||
})
|
})
|
||||||
|
|
||||||
peripheral.once('disconnect', () => {
|
peripheral.once('disconnect', () => {
|
||||||
|
|
@ -87,33 +87,33 @@ function createCentralManager () {
|
||||||
}
|
}
|
||||||
|
|
||||||
// see https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/
|
// see https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/
|
||||||
function subscribeToHeartrateMeasurement (peripheral) {
|
function subscribeToHeartRateMeasurement (peripheral) {
|
||||||
const heartrateMeasurementUUID = '2a37'
|
const heartRateMeasurementUUID = '2a37'
|
||||||
const batteryLevelUUID = '2a19'
|
const batteryLevelUUID = '2a19'
|
||||||
|
|
||||||
peripheral.discoverSomeServicesAndCharacteristics([], [heartrateMeasurementUUID, batteryLevelUUID],
|
peripheral.discoverSomeServicesAndCharacteristics([], [heartRateMeasurementUUID, batteryLevelUUID],
|
||||||
(error, services, characteristics) => {
|
(error, services, characteristics) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
log.error(error)
|
log.error(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const heartrateMeasurementCharacteristic = characteristics.find(
|
const heartRateMeasurementCharacteristic = characteristics.find(
|
||||||
characteristic => characteristic.uuid === heartrateMeasurementUUID
|
characteristic => characteristic.uuid === heartRateMeasurementUUID
|
||||||
)
|
)
|
||||||
|
|
||||||
const batteryLevelCharacteristic = characteristics.find(
|
const batteryLevelCharacteristic = characteristics.find(
|
||||||
characteristic => characteristic.uuid === batteryLevelUUID
|
characteristic => characteristic.uuid === batteryLevelUUID
|
||||||
)
|
)
|
||||||
|
|
||||||
if (heartrateMeasurementCharacteristic !== undefined) {
|
if (heartRateMeasurementCharacteristic !== undefined) {
|
||||||
heartrateMeasurementCharacteristic.notify(true, (error) => {
|
heartRateMeasurementCharacteristic.notify(true, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
log.error(error)
|
log.error(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
heartrateMeasurementCharacteristic.on('data', (data, isNotification) => {
|
heartRateMeasurementCharacteristic.on('data', (data, isNotification) => {
|
||||||
const buffer = Buffer.from(data)
|
const buffer = Buffer.from(data)
|
||||||
const flags = buffer.readUInt8(0)
|
const flags = buffer.readUInt8(0)
|
||||||
// bits of the feature flag:
|
// bits of the feature flag:
|
||||||
|
|
@ -121,7 +121,7 @@ function createCentralManager () {
|
||||||
// 1 + 2: Sensor Contact Status
|
// 1 + 2: Sensor Contact Status
|
||||||
// 3: Energy Expended Status
|
// 3: Energy Expended Status
|
||||||
// 4: RR-Interval
|
// 4: RR-Interval
|
||||||
const heartrateUint16LE = flags & 0b1
|
const heartRateUint16LE = flags & 0b1
|
||||||
|
|
||||||
// from the specs:
|
// from the specs:
|
||||||
// While most human applications require support for only 255 bpm or less, special
|
// 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
|
// If the Heart Rate Measurement Value is less than or equal to 255 bpm a UINT8 format
|
||||||
// should be used for power savings.
|
// should be used for power savings.
|
||||||
// If the Heart Rate Measurement Value exceeds 255 bpm a UINT16 format shall be used.
|
// If the Heart Rate Measurement Value exceeds 255 bpm a UINT16 format shall be used.
|
||||||
const heartrate = heartrateUint16LE ? buffer.readUInt16LE(1) : buffer.readUInt8(1)
|
const heartrate = heartRateUint16LE ? buffer.readUInt16LE(1) : buffer.readUInt8(1)
|
||||||
emitter.emit('heartrateMeasurement', { heartrate, batteryLevel })
|
emitter.emit('heartRateMeasurement', { heartrate, batteryLevel })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -155,4 +155,4 @@ function createCentralManager () {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createCentralManager }
|
export { createHeartRateManager }
|
||||||
|
|
@ -18,7 +18,6 @@ import { createPeripheralManager } from './peripherals/PeripheralManager.js'
|
||||||
import { replayRowingSession } from './tools/RowingRecorder.js'
|
import { replayRowingSession } from './tools/RowingRecorder.js'
|
||||||
import { createWorkoutRecorder } from './engine/WorkoutRecorder.js'
|
import { createWorkoutRecorder } from './engine/WorkoutRecorder.js'
|
||||||
import { createWorkoutUploader } from './engine/WorkoutUploader.js'
|
import { createWorkoutUploader } from './engine/WorkoutUploader.js'
|
||||||
import { createAntManager } from './peripherals/ant/AntManager.js'
|
|
||||||
const exec = promisify(child_process.exec)
|
const exec = promisify(child_process.exec)
|
||||||
|
|
||||||
// set the log levels
|
// set the log levels
|
||||||
|
|
@ -192,17 +191,17 @@ rowingStatistics.on('rowingStopped', (metrics) => {
|
||||||
workoutRecorder.writeRecordings()
|
workoutRecorder.writeRecordings()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (config.heartrateMonitorBLE) {
|
if (config.heartRateMonitorBLE) {
|
||||||
const bleCentralService = child_process.fork('./app/peripherals/ble/CentralService.js')
|
peripheralManager.startBleHeartRateService()
|
||||||
bleCentralService.on('message', (heartrateMeasurement) => {
|
peripheralManager.on('heartRateBleMeasurement', (heartRateMeasurement) => {
|
||||||
rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement)
|
rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.heartrateMonitorANT) {
|
if (config.heartRateMonitorANT) {
|
||||||
const antManager = createAntManager()
|
peripheralManager.startAntHeartRateService()
|
||||||
antManager.on('heartrateMeasurement', (heartrateMeasurement) => {
|
peripheralManager.on('heartRateAntMeasurement', (heartRateMeasurement) => {
|
||||||
rowingStatistics.handleHeartrateMeasurement(heartrateMeasurement)
|
rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue