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:
parent
a3cd6e6f39
commit
17d6a74332
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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' }))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue