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:
Abász 2022-12-12 23:20:39 +01:00
parent 3edfe22434
commit 7ef338d856
7 changed files with 118 additions and 73 deletions

View File

@ -417,7 +417,7 @@ function createRowingStatistics (config) {
}
return Object.assign(emitter, {
handleHeartrateMeasurement,
handleHeartRateMeasurement: handleHeartrateMeasurement,
handleRotationImpulse,
setIntervalParameters,
pause: pauseTraining,

View File

@ -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,

View File

@ -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 }

View File

@ -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 }

View File

@ -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)
})

View File

@ -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 }

View File

@ -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)
})
}