adds selection of ble profile to frontend

This commit is contained in:
Lars Berning 2021-03-20 21:12:04 +00:00
parent d90fa9ea1f
commit 2e3654fabd
8 changed files with 200 additions and 64 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@
<div class="label">Distance</div>
<div class="content">
<span class="value" id="distanceTotal"></span>
<span class="unit">m</span>
<span class="unit" id="distanceTotalUnit">m</span>
</div>
</div>
<div class="col">
@ -73,6 +73,7 @@
<div class="col">
<div class="content">
<button onclick="app.toggleFullscreen()">Fullscreen</button>
<button onclick="app.switchPeripheralMode()" id="peripheralMode">Bluetooth Mode</button>
<button onclick="app.reset()">Reset</button>
</div>
</div>

View File

@ -9,11 +9,10 @@
import { fork } from 'child_process'
import log from 'loglevel'
import config from './config.js'
import { createFtmsPeripheral } from './ble/FtmsPeripheral.js'
import { createPm5Peripheral } from './ble/Pm5Peripheral.js'
import { createRowingEngine } from './engine/RowingEngine.js'
import { createRowingStatistics } from './engine/RowingStatistics.js'
import { createWebServer } from './WebServer.js'
import { createPeripheralManager } from './ble/PeripheralManager.js'
// eslint-disable-next-line no-unused-vars
import { recordRowingSession, replayRowingSession } from './tools/RowingRecorder.js'
@ -27,42 +26,31 @@ for (const [loggerName, logLevel] of Object.entries(config.loglevel)) {
log.info(`==== Open Rowing Monitor ${process.env.npm_package_version} ====\n`)
let peripheral
if (config.bluetoothMode === 'PM5') {
log.info('bluetooth profile: Concept2 PM5')
peripheral = createPm5Peripheral()
} else if (config.bluetoothMode === 'FTMSBIKE') {
log.info('bluetooth profile: FTMS Indoor Bike')
peripheral = createFtmsPeripheral({
simulateIndoorBike: true
})
} else {
log.info('bluetooth profile: FTMS Rower')
peripheral = createFtmsPeripheral({
simulateIndoorBike: false
})
}
const peripheralManager = createPeripheralManager()
peripheral.on('controlPoint', (event) => {
peripheralManager.on('control', (event) => {
if (event?.req?.name === 'requestControl') {
event.res = true
} else if (event?.req?.name === 'reset') {
log.debug('reset requested')
rowingStatistics.reset()
peripheral.notifyStatus({ name: 'reset' })
peripheralManager.notifyStatus({ name: 'reset' })
event.res = true
// todo: we could use these controls once we implement a concept of a rowing session
} else if (event?.req?.name === 'stop') {
log.debug('stop requested')
peripheral.notifyStatus({ name: 'stoppedOrPausedByUser' })
peripheralManager.notifyStatus({ name: 'stoppedOrPausedByUser' })
event.res = true
} else if (event?.req?.name === 'pause') {
log.debug('pause requested')
peripheral.notifyStatus({ name: 'stoppedOrPausedByUser' })
peripheralManager.notifyStatus({ name: 'stoppedOrPausedByUser' })
event.res = true
} else if (event?.req?.name === 'startOrResume') {
log.debug('startOrResume requested')
peripheral.notifyStatus({ name: 'startedOrResumedByUser' })
peripheralManager.notifyStatus({ name: 'startedOrResumedByUser' })
event.res = true
} else if (event?.req?.name === 'peripheralMode') {
webServer.notifyClients({ peripheralMode: event.req.peripheralMode })
event.res = true
} else {
log.info('unhandled Command', event.req)
@ -99,7 +87,7 @@ rowingStatistics.on('strokeFinished', (data) => {
strokeState: data.strokeState
}
webServer.notifyClients(metrics)
peripheral.notifyData(metrics)
peripheralManager.notifyMetrics('strokeFinished', metrics)
})
rowingStatistics.on('rowingPaused', (data) => {
@ -120,7 +108,7 @@ rowingStatistics.on('rowingPaused', (data) => {
strokeState: 'RECOVERY'
}
webServer.notifyClients(metrics)
peripheral.notifyData(metrics)
peripheralManager.notifyMetrics('rowingPaused', metrics)
})
rowingStatistics.on('durationUpdate', (data) => {
@ -133,12 +121,18 @@ const webServer = createWebServer()
webServer.on('messageReceived', (message) => {
if (message.command === 'reset') {
rowingStatistics.reset()
peripheral.notifyStatus({ name: 'reset' })
peripheralManager.notifyStatus({ name: 'reset' })
} if (message.command === 'switchPeripheralMode') {
peripheralManager.switchPeripheralMode()
} else {
log.warn(`invalid command received: ${message}`)
}
})
webServer.on('clientConnected', () => {
webServer.notifyClients({ peripheralMode: peripheralManager.getPeripheralMode() })
})
// recordRowingSession('recordings/wrx700_2magnets.csv')
/*
replayRowingSession(rowingEngine.handleRotationImpulse, {

View File

@ -5,9 +5,10 @@ This is the very minimalistic Backlog for further development of this project.
## Soon
* refactor Stroke Phase Handling in RowingStatistics and pm5Peripheral
* calculate the missing energy averages for FTMS
* figure out where to set the Service Advertising Data (FTMS.pdf p 15)
* Web UI: hint, when screen is not in always on mode
* Web UI: replace fullscreen button with exit Button when started from home screen
* investigate: occasionally stroke rate is too high - seems to happen after rowing pause
* figure out where to set the Service Advertising Data (FTMS.pdf p 15)
* investigate bug: crash, when one unsubscribe to BLE "Generic Attribute", probably a bleno bug "handleAttribute.emit is not a function"
* what value should we use for split, if we are in a rowing pause? technically should be infinity...
* set up a Raspberry Pi with the installation instructions to see if they are correct