adds selection of ble profile to frontend
This commit is contained in:
parent
d90fa9ea1f
commit
2e3654fabd
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue