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, {
|
||||
handleHeartrateMeasurement,
|
||||
handleHeartRateMeasurement: handleHeartrateMeasurement,
|
||||
handleRotationImpulse,
|
||||
setIntervalParameters,
|
||||
pause: pauseTraining,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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)
|
||||
})
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue