diff --git a/app/WebServer.js b/app/WebServer.js index 225f8f7..e2a318e 100644 --- a/app/WebServer.js +++ b/app/WebServer.js @@ -30,6 +30,7 @@ function createWebServer () { wss.on('connection', function connection (ws) { log.debug('websocket client connected') + emitter.emit('clientConnected', ws) ws.on('message', function incoming (data) { try { const message = JSON.parse(data) diff --git a/app/ble/FtmsPeripheral.js b/app/ble/FtmsPeripheral.js index 434cca2..afabd8f 100644 --- a/app/ble/FtmsPeripheral.js +++ b/app/ble/FtmsPeripheral.js @@ -26,17 +26,7 @@ function createFtmsPeripheral (options) { const deviceInformationService = new DeviceInformationService() bleno.on('stateChange', (state) => { - if (state === 'poweredOn') { - bleno.startAdvertising( - peripheralName, - [fitnessMachineService.uuid, deviceInformationService.uuid], - (error) => { - if (error) log.error(error) - } - ) - } else { - bleno.stopAdvertising() - } + triggerAdvertising(state) }) bleno.on('advertisingStart', (error) => { @@ -70,9 +60,6 @@ function createFtmsPeripheral (options) { bleno.on('advertisingStartError', (event) => { log.debug('advertisingStartError', event) }) - bleno.on('advertisingStop', (event) => { - log.debug('advertisingStop', event) - }) bleno.on('servicesSetError', (event) => { log.debug('servicesSetError', event) }) @@ -89,6 +76,29 @@ function createFtmsPeripheral (options) { return obj.res } + function destroy () { + return new Promise((resolve) => { + bleno.disconnect() + bleno.removeAllListeners() + bleno.stopAdvertising(resolve) + }) + } + + function triggerAdvertising (eventState) { + const activeState = eventState || bleno.state + if (activeState === 'poweredOn') { + bleno.startAdvertising( + peripheralName, + [fitnessMachineService.uuid, deviceInformationService.uuid], + (error) => { + if (error) log.error(error) + } + ) + } else { + bleno.stopAdvertising() + } + } + // deliver current rowing metrics via BLE function notifyData (data) { fitnessMachineService.notifyData(data) @@ -100,8 +110,10 @@ function createFtmsPeripheral (options) { } return Object.assign(emitter, { + triggerAdvertising, notifyData, - notifyStatus + notifyStatus, + destroy }) } diff --git a/app/ble/PeripheralManager.js b/app/ble/PeripheralManager.js new file mode 100644 index 0000000..70ecaec --- /dev/null +++ b/app/ble/PeripheralManager.js @@ -0,0 +1,95 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + This manager creates the different Bluetooth Low Energy (BLE) Peripherals and allows + switching between them +*/ +import config from '../config.js' +import { createFtmsPeripheral } from './FtmsPeripheral.js' +import { createPm5Peripheral } from './Pm5Peripheral.js' +import log from 'loglevel' +import EventEmitter from 'node:events' + +const modes = ['FTMS', 'FTMSBIKE', 'PM5'] +function createPeripheralManager () { + const emitter = new EventEmitter() + let peripheral + let mode + + createPeripheral(config.bluetoothMode) + + function getPeripheral () { + return peripheral + } + + function getPeripheralMode () { + return mode + } + + function switchPeripheralMode (newMode) { + // if now mode was passed, select the next one from the list + if (newMode === undefined) { + newMode = modes[(modes.indexOf(mode) + 1) % modes.length] + } + createPeripheral(newMode) + } + + function notifyMetrics (type, metrics) { + peripheral.notifyData(metrics) + } + + function notifyStatus (status) { + peripheral.notifyStatus(status) + } + + async function createPeripheral (newMode) { + if (peripheral) { + await peripheral.destroy() + peripheral.off('controlPoint', emitControlPointEvent) + } + + if (newMode === 'PM5') { + log.info('bluetooth profile: Concept2 PM5') + peripheral = createPm5Peripheral() + mode = 'PM5' + } else if (newMode === 'FTMSBIKE') { + log.info('bluetooth profile: FTMS Indoor Bike') + peripheral = createFtmsPeripheral({ + simulateIndoorBike: true + }) + mode = 'FTMSBIKE' + } else { + log.info('bluetooth profile: FTMS Rower') + peripheral = createFtmsPeripheral({ + simulateIndoorBike: false + }) + mode = 'FTMS' + } + // todo: re-emitting is not that great, maybe use callbacks instead + peripheral.on('control', emitControlPointEvent) + + peripheral.triggerAdvertising() + + emitter.emit('control', { + req: { + name: 'peripheralMode', + peripheralMode: mode + } + }) + } + + function emitControlPointEvent (event) { + emitter.emit('control', event) + } + + return Object.assign(emitter, { + getPeripheral, + getPeripheralMode, + switchPeripheralMode, + notifyMetrics, + notifyStatus + }) +} + +export { createPeripheralManager } diff --git a/app/ble/Pm5Peripheral.js b/app/ble/Pm5Peripheral.js index b76986d..f080f72 100644 --- a/app/ble/Pm5Peripheral.js +++ b/app/ble/Pm5Peripheral.js @@ -26,17 +26,7 @@ function createPm5Peripheral (options) { const rowingService = new Pm5RowingService() bleno.on('stateChange', (state) => { - if (state === 'poweredOn') { - bleno.startAdvertising( - peripheralName, - [gapService.uuid], - (error) => { - if (error) log.error(error) - } - ) - } else { - bleno.stopAdvertising() - } + triggerAdvertising(state) }) bleno.on('advertisingStart', (error) => { @@ -70,12 +60,6 @@ function createPm5Peripheral (options) { bleno.on('advertisingStartError', (event) => { log.debug('advertisingStartError', event) }) - bleno.on('advertisingStop', (event) => { - log.debug('advertisingStop', event) - }) - bleno.on('servicesSet', (event) => { - log.debug('servicesSet') - }) bleno.on('servicesSetError', (event) => { log.debug('servicesSetError', event) }) @@ -83,6 +67,29 @@ function createPm5Peripheral (options) { log.debug('rssiUpdate', event) }) + function destroy () { + return new Promise((resolve) => { + bleno.disconnect() + bleno.removeAllListeners() + bleno.stopAdvertising(resolve) + }) + } + + function triggerAdvertising (eventState) { + const activeState = eventState || bleno.state + if (activeState === 'poweredOn') { + bleno.startAdvertising( + peripheralName, + [gapService.uuid], + (error) => { + if (error) log.error(error) + } + ) + } else { + bleno.stopAdvertising() + } + } + // deliver current rowing metrics via BLE function notifyData (data) { rowingService.notify(data) @@ -95,8 +102,10 @@ function createPm5Peripheral (options) { } return Object.assign(emitter, { + triggerAdvertising, notifyData, - notifyStatus + notifyStatus, + destroy }) } diff --git a/app/client/app.js b/app/client/app.js index 751d9f4..2d076d6 100644 --- a/app/client/app.js +++ b/app/client/app.js @@ -7,7 +7,19 @@ */ // eslint-disable-next-line no-unused-vars export function createApp () { - const fields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', 'power', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted'] + const fields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', 'power', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted', 'peripheralMode'] + const fieldFormatter = { + peripheralMode: (value) => { + if (value === 'PM5') { + return 'Concept2 PM5' + } else if (value === 'FTMSBIKE') { + return 'FTMS (Bike)' + } else { + return 'FTMS (Rower)' + } + }, + distanceTotal: (value) => value >= 10000 ? { value: (value / 1000).toFixed(1), unit: 'km' } : { value, unit: 'm' } + } let socket initWebsocket() @@ -41,7 +53,13 @@ export function createApp () { for (const [key, value] of Object.entries(data)) { if (fields.includes(key)) { - document.getElementById(key).innerHTML = value + const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value + if (valueFormatted.value && valueFormatted.unit) { + document.getElementById(key).innerHTML = valueFormatted.value + document.getElementById(`${key}Unit`).innerHTML = valueFormatted.unit + } else { + document.getElementById(key).innerHTML = valueFormatted + } } } } catch (err) { @@ -100,8 +118,13 @@ export function createApp () { if (socket)socket.send(JSON.stringify({ command: 'reset' })) } + function switchPeripheralMode () { + if (socket)socket.send(JSON.stringify({ command: 'switchPeripheralMode' })) + } + return { toggleFullscreen, - reset + reset, + switchPeripheralMode } } diff --git a/app/client/index.html b/app/client/index.html index c268edb..04296d6 100644 --- a/app/client/index.html +++ b/app/client/index.html @@ -25,7 +25,7 @@