Enable rotation of the heart rate monitor modes

Add button to the GUI so the user can switch between heart rate monitor
modes (BLE, ANT, OFF). Update peripheralManager to handle switching and
implement necessary changes to the structure of the peripheralManager.
This commit is contained in:
Abász 2022-12-18 15:19:47 +01:00
parent a3cd6e6f39
commit 17d6a74332
10 changed files with 178 additions and 73 deletions

View File

@ -7,7 +7,7 @@
import { AppElement, html, css } from './AppElement.js'
import { customElement, state } from 'lit/decorators.js'
import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload } from '../lib/icons.js'
import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload, icon_heartbeat } from '../lib/icons.js'
import './AppDialog.js'
@customElement('dashboard-actions')
@ -66,7 +66,9 @@ export class DashboardActions extends AppElement {
<button @click=${this.reset}>${icon_undo}</button>
${this.renderOptionalButtons()}
<button @click=${this.switchBlePeripheralMode}>${icon_bluetooth}</button>
<div class="peripheral-mode">${this.peripheralMode()}</div>
<div class="peripheral-mode">${this.blePeripheralMode()}</div>
<button @click=${this.switchHrmPeripheralMode}>${icon_heartbeat}</button>
<div class="peripheral-mode">${this.appState?.config?.hrmPeripheralMode}</div>
${this.dialog ? this.dialog : ''}
`
}
@ -101,8 +103,8 @@ export class DashboardActions extends AppElement {
return buttons
}
peripheralMode () {
const value = this.appState?.config?.peripheralMode
blePeripheralMode () {
const value = this.appState?.config?.blePeripheralMode
switch (value) {
case 'PM5':
@ -139,6 +141,10 @@ export class DashboardActions extends AppElement {
this.sendEvent('triggerAction', { command: 'switchBlePeripheralMode' })
}
switchHrmPeripheralMode () {
this.sendEvent('triggerAction', { command: 'switchHrmMode' })
}
uploadTraining () {
this.dialog = html`
<app-dialog @close=${dialogClosed}>

View File

@ -134,6 +134,10 @@ export function createApp (app) {
if (socket)socket.send(JSON.stringify({ command: 'switchBlePeripheralMode' }))
break
}
case 'switchHrmMode': {
if (socket)socket.send(JSON.stringify({ command: 'switchHrmMode' }))
break
}
case 'reset': {
resetFields()
if (socket)socket.send(JSON.stringify({ command: 'reset' }))

View File

@ -11,8 +11,10 @@ export const APP_STATE = {
// contains all the rowing metrics that are delivered from the backend
metrics: {},
config: {
// currently can be FTMS, FTMSBIKE, PM5, CSC, CPS
peripheralMode: '',
// currently can be FTMS, FTMSBIKE, PM5, CSC, CPS, OFF
blePeripheralMode: '',
// currently can be ANT, BLE, OFF
hrmPeripheralMode: '',
// true if upload to strava is enabled
stravaUploadEnabled: false,
// true if remote device shutdown is enabled

View File

@ -12,17 +12,23 @@ 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'
import { createBleHrmPeripheral } from './ble/HrmPeripheral.js'
const bleModes = ['FTMS', 'FTMSBIKE', 'PM5', 'CSC', 'CPS', 'OFF']
const hrmModes = ['ANT', 'BLE', 'OFF']
function createPeripheralManager () {
const emitter = new EventEmitter()
let _antManager
let blePeripheral
let bleMode
let hrmPeripheral
let hrmMode
createBlePeripheral(config.bluetoothMode)
createHrmPeripheral(config.heartRateMode)
function getBlePeripheral () {
return blePeripheral
@ -32,6 +38,14 @@ function createPeripheralManager () {
return bleMode
}
function getHrmPeripheral () {
return hrmPeripheral
}
function getHrmPeripheralMode () {
return hrmMode
}
function switchBlePeripheralMode (newMode) {
// if now mode was passed, select the next one from the list
if (newMode === undefined) {
@ -102,22 +116,55 @@ function createPeripheralManager () {
})
}
function startBleHeartRateService () {
const hrmPeripheral = child_process.fork('./app/peripherals/ble/HrmPeripheral.js')
hrmPeripheral.on('message', (heartRateMeasurement) => {
emitter.emit('heartRateBleMeasurement', heartRateMeasurement)
})
function switchHrmMode (newMode) {
if (newMode === undefined) {
newMode = hrmModes[(hrmModes.indexOf(hrmMode) + 1) % hrmModes.length]
}
createHrmPeripheral(newMode)
}
function startAntHeartRateService () {
if (!this._antManager) {
this._antManager = new AntManager()
async function createHrmPeripheral (newMode) {
if (hrmPeripheral) {
await hrmPeripheral.destroy()
hrmPeripheral.removeAllListeners()
if (_antManager && newMode !== 'ANT') { await _antManager.closeAntStick() }
}
const antHrm = createAntHrmPeripheral(this._antManager)
switch (newMode) {
case 'ANT':
log.info('heart rate profile: ANT')
if (!_antManager) {
_antManager = new AntManager()
}
antHrm.on('heartRateMeasurement', (heartRateMeasurement) => {
emitter.emit('heartRateAntMeasurement', heartRateMeasurement)
hrmPeripheral = createAntHrmPeripheral(_antManager)
hrmMode = 'ANT'
await hrmPeripheral.attach()
break
case 'BLE':
log.info('heart rate profile: BLE')
hrmPeripheral = createBleHrmPeripheral()
hrmMode = 'BLE'
break
default:
log.info('heart rate profile: Off')
hrmMode = 'OFF'
}
if (hrmMode.toLocaleLowerCase() !== 'OFF'.toLocaleLowerCase()) {
hrmPeripheral.on('heartRateMeasurement', (heartRateMeasurement) => {
emitter.emit('heartRateMeasurement', heartRateMeasurement)
})
}
emitter.emit('control', {
req: {
name: 'hrmPeripheralMode',
peripheralMode: hrmMode
}
})
}
@ -126,10 +173,11 @@ function createPeripheralManager () {
}
return Object.assign(emitter, {
startAntHeartRateService,
startBleHeartRateService,
getBlePeripheral,
getBlePeripheralMode,
getHrmPeripheral,
getHrmPeripheralMode,
switchHrmMode,
switchBlePeripheralMode,
notifyMetrics,
notifyStatus

View File

@ -9,9 +9,12 @@
- 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'
export default class AntManager {
_isStickOpen = false
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
@ -22,10 +25,31 @@ export default class AntManager {
}
openAntStick () {
if (!this._stick.open()) {
return false
}
return this._stick
return new Promise((resolve, reject) => {
if (!this._stick.open()) {
reject(new Error('Error opening Ant Stick'))
}
this._stick.once('startup', () => {
log.info('ANT+ stick found')
this._isStickOpen = true
resolve(this._stick)
})
})
}
closeAntStick () {
return new Promise(resolve => {
this._stick.once('shutdown', () => {
log.info('ANT+ stick is closed')
this._isStickOpen = false
resolve()
})
this._stick.close()
})
}
isStickOpen () {
return this._isStickOpen
}
getAntStick () {

View File

@ -7,7 +7,6 @@
*/
import EventEmitter from 'node:events'
import Ant from 'ant-plus'
import log from 'loglevel'
function createAntHrmPeripheral (antManager) {
const emitter = new EventEmitter()
@ -19,32 +18,28 @@ function createAntHrmPeripheral (antManager) {
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 attach () {
return new Promise(resolve => {
heartRateSensor.once('attached', () => {
resolve()
})
heartRateSensor.attach(0, 0)
})
}
function destroy () {
return new Promise((resolve) => {
heartRateSensor.detach()
heartRateSensor.on('detached', () => {
antStick.removeAllListeners()
heartRateSensor.once('detached', () => {
heartRateSensor.removeAllListeners()
resolve()
})
heartRateSensor.detach()
})
}
return Object.assign(emitter, {
destroy
destroy,
attach
})
}

View File

@ -5,14 +5,29 @@
Starts the central manager in a forked thread since noble does not like
to run in the same thread as bleno
*/
import process from 'process'
import config from '../../tools/ConfigManager.js'
import log from 'loglevel'
import { createHeartRateManager } from './hrm/HeartRateManager.js'
import EventEmitter from 'node:events'
import child_process from 'child_process'
log.setLevel(config.loglevel.default)
const heartRateManager = createHeartRateManager()
function createBleHrmPeripheral () {
const emitter = new EventEmitter()
heartRateManager.on('heartRateMeasurement', (heartRateMeasurement) => {
process.send(heartRateMeasurement)
})
const bleHrmProcess = child_process.fork('./app/peripherals/ble/hrm/HrmService.js')
bleHrmProcess.on('message', (heartRateMeasurement) => {
emitter.emit('heartRateMeasurement', heartRateMeasurement)
})
function destroy () {
return new Promise(resolve => {
bleHrmProcess.kill()
bleHrmProcess.removeAllListeners()
resolve()
})
}
return Object.assign(emitter, {
destroy
})
}
export { createBleHrmPeripheral }

View File

@ -0,0 +1,17 @@
'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Starts the central manager in a forked thread since noble does not like
to run in the same thread as bleno
*/
import process from 'process'
import log from 'loglevel'
import config from '../../../tools/ConfigManager.js'
import { createHeartRateManager } from './HeartRateManager.js'
log.setLevel(config.loglevel.default)
const heartRateManager = createHeartRateManager()
heartRateManager.on('heartRateMeasurement', (heartRateMeasurement) => {
process.send(heartRateMeasurement)
})

View File

@ -102,11 +102,19 @@ peripheralManager.on('control', (event) => {
webServer.notifyClients('config', getConfig())
event.res = true
break
case 'hrmPeripheralMode':
webServer.notifyClients('config', getConfig())
event.res = true
break
default:
log.info('unhandled Command', event.req)
}
})
peripheralManager.on('heartRateMeasurement', (heartRateMeasurement) => {
rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement)
})
function pauseWorkout () {
rowingStatistics.pause()
}
@ -191,20 +199,6 @@ rowingStatistics.on('rowingStopped', (metrics) => {
workoutRecorder.writeRecordings()
})
if (config.heartRateMonitorBLE) {
peripheralManager.startBleHeartRateService()
peripheralManager.on('heartRateBleMeasurement', (heartRateMeasurement) => {
rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement)
})
}
if (config.heartRateMonitorANT) {
peripheralManager.startAntHeartRateService()
peripheralManager.on('heartRateAntMeasurement', (heartRateMeasurement) => {
rowingStatistics.handleHeartRateMeasurement(heartRateMeasurement)
})
}
workoutUploader.on('authorizeStrava', (data, client) => {
webServer.notifyClient(client, 'authorizeStrava', data)
})
@ -219,6 +213,9 @@ webServer.on('messageReceived', async (message, client) => {
case 'switchBlePeripheralMode':
peripheralManager.switchBlePeripheralMode()
break
case 'switchHrmMode':
peripheralManager.switchHrmMode()
break
case 'reset':
resetWorkout()
break
@ -243,7 +240,8 @@ webServer.on('clientConnected', (client) => {
// todo: extract this into some kind of state manager
function getConfig () {
return {
peripheralMode: peripheralManager.getBlePeripheralMode(),
blePeripheralMode: peripheralManager.getBlePeripheralMode(),
hrmPeripheralMode: peripheralManager.getHrmPeripheralMode(),
stravaUploadEnabled: !!config.stravaClientId && !!config.stravaClientSecret,
shutdownEnabled: !!config.shutdownCommand
}

View File

@ -81,15 +81,11 @@ export default {
// - OFF: Turns Bluetooth advertisement off
bluetoothMode: 'FTMS',
// Turn this on if you want support for Bluetooth Low Energy heart rate monitors
// Will currenty connect to the first device found
heartrateMonitorBLE: true,
// Turn this on if you want support for ANT+ heart rate monitors
// You will need an ANT+ USB stick for this to work, the following models might work:
// - Garmin USB or USB2 ANT+ or an off-brand clone of it (ID 0x1008)
// - Garmin mini ANT+ (ID 0x1009)
heartrateMonitorANT: false,
// Selects the heart rate monitor mode. Supported modes:
// - BLE: Use Bluetooth Low Energy to connect Heart Rate Monitor (Will currently connect to the first device found)
// - ANT: Use Ant+ to connect Heart Rate Monitor
// - OFF: turns of Heart Rate Monitor discovery
heartRateMode: 'BLE',
// Defines the name that is used to announce the FTMS Rower via Bluetooth Low Energy (BLE)
// Some rowing training applications expect that the rowing device is announced with a certain name