begins implementation of the PM5 protocol
This commit is contained in:
parent
6c5c1b8cb8
commit
efe13f4804
|
|
@ -13,11 +13,11 @@
|
|||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import { EventEmitter } from 'events'
|
||||
import FitnessMachineService from './FitnessMachineService.js'
|
||||
import DeviceInformationService from './DeviceInformationService.js'
|
||||
import FitnessMachineService from './ftms/FitnessMachineService.js'
|
||||
import DeviceInformationService from './ftms/DeviceInformationService.js'
|
||||
import log from 'loglevel'
|
||||
|
||||
function createRowingMachinePeripheral (options) {
|
||||
function createFtmsPeripheral (options) {
|
||||
const emitter = new EventEmitter()
|
||||
|
||||
const peripheralName = options?.simulateIndoorBike ? 'OpenRowingBike' : 'OpenRowingMonitor'
|
||||
|
|
@ -107,4 +107,4 @@ function createRowingMachinePeripheral (options) {
|
|||
})
|
||||
}
|
||||
|
||||
export { createRowingMachinePeripheral }
|
||||
export { createFtmsPeripheral }
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
'use strict'
|
||||
/*
|
||||
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
||||
|
||||
Creates a Bluetooth Low Energy (BLE) Peripheral with all the Services that are used by the
|
||||
Concept2 PM5 rowing machine.
|
||||
|
||||
see: https://www.concept2.co.uk/files/pdf/us/monitors/PM5_BluetoothSmartInterfaceDefinition.pdf
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import { EventEmitter } from 'events'
|
||||
import { constants } from './pm5/Pm5Constants.js'
|
||||
import DeviceInformationService from './pm5/DeviceInformationService.js'
|
||||
import GapService from './pm5/GapService.js'
|
||||
import log from 'loglevel'
|
||||
import Pm5ControlService from './pm5/Pm5ControlService.js'
|
||||
import Pm5RowingService from './pm5/Pm5RowingService.js'
|
||||
|
||||
function createPm5Peripheral (options) {
|
||||
const emitter = new EventEmitter()
|
||||
|
||||
const peripheralName = constants.name
|
||||
const deviceInformationService = new DeviceInformationService()
|
||||
const gapService = new GapService()
|
||||
const controlService = new Pm5ControlService()
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
bleno.on('advertisingStart', (error) => {
|
||||
if (!error) {
|
||||
bleno.setServices(
|
||||
[gapService, deviceInformationService, controlService, rowingService],
|
||||
(error) => {
|
||||
if (error) log.error(error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
bleno.on('accept', (clientAddress) => {
|
||||
log.debug(`ble central connected: ${clientAddress}`)
|
||||
bleno.updateRssi()
|
||||
})
|
||||
|
||||
bleno.on('disconnect', (clientAddress) => {
|
||||
log.debug(`ble central disconnected: ${clientAddress}`)
|
||||
})
|
||||
|
||||
bleno.on('platform', (event) => {
|
||||
log.debug('platform', event)
|
||||
})
|
||||
bleno.on('addressChange', (event) => {
|
||||
log.debug('addressChange', event)
|
||||
})
|
||||
bleno.on('mtuChange', (event) => {
|
||||
log.debug('mtuChange', event)
|
||||
})
|
||||
bleno.on('advertisingStartError', (event) => {
|
||||
log.debug('advertisingStartError', event)
|
||||
})
|
||||
bleno.on('advertisingStop', (event) => {
|
||||
log.debug('advertisingStop', event)
|
||||
})
|
||||
bleno.on('servicesSet', (event) => {
|
||||
log.debug('servicesSet', event)
|
||||
})
|
||||
bleno.on('servicesSetError', (event) => {
|
||||
log.debug('servicesSetError', event)
|
||||
})
|
||||
bleno.on('rssiUpdate', (event) => {
|
||||
log.debug('rssiUpdate', event)
|
||||
})
|
||||
|
||||
// deliver current rowing metrics via BLE
|
||||
function notifyData (data) {
|
||||
// fitnessMachineService.notifyData(data)
|
||||
}
|
||||
|
||||
// deliver a status change via BLE
|
||||
function notifyStatus (status) {
|
||||
// fitnessMachineService.notifyStatus(status)
|
||||
}
|
||||
|
||||
return Object.assign(emitter, {
|
||||
notifyData,
|
||||
notifyStatus
|
||||
})
|
||||
}
|
||||
|
||||
export { createPm5Peripheral }
|
||||
|
|
@ -36,13 +36,13 @@ export default class IndoorBikeDataCharacteristic extends bleno.Characteristic {
|
|||
log.debug('IndooBikeDataCharacteristic - central subscribed')
|
||||
this._updateValueCallback = updateValueCallback
|
||||
return this.RESULT_SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
onUnsubscribe () {
|
||||
log.debug('IndooBikeDataCharacteristic - central unsubscribed')
|
||||
this._updateValueCallback = null
|
||||
return this.RESULT_UNLIKELY_ERROR
|
||||
};
|
||||
}
|
||||
|
||||
notify (data) {
|
||||
// ignore events without the mandatory fields
|
||||
|
|
@ -29,13 +29,13 @@ export default class RowerDataCharacteristic extends bleno.Characteristic {
|
|||
log.debug('RowerDataCharacteristic - central subscribed')
|
||||
this._updateValueCallback = updateValueCallback
|
||||
return this.RESULT_SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
onUnsubscribe () {
|
||||
log.debug('RowerDataCharacteristic - central unsubscribed')
|
||||
this._updateValueCallback = null
|
||||
return this.RESULT_UNLIKELY_ERROR
|
||||
};
|
||||
}
|
||||
|
||||
notify (data) {
|
||||
// ignore events without the mandatory fields
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
'use strict'
|
||||
/*
|
||||
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
||||
|
||||
Provides the required Device Information of the PM5
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import { constants, getFullUUID } from './Pm5Constants.js'
|
||||
import ValueReadCharacteristic from './ValueReadCharacteristic.js'
|
||||
|
||||
export default class DeviceInformationService extends bleno.PrimaryService {
|
||||
constructor () {
|
||||
super({
|
||||
// InformationenService uuid as defined by the PM5 specification
|
||||
uuid: getFullUUID('0010'),
|
||||
characteristics: [
|
||||
// C2 module number string
|
||||
new ValueReadCharacteristic(getFullUUID('0011'), constants.model, 'model'),
|
||||
// C2 serial number string
|
||||
new ValueReadCharacteristic(getFullUUID('0012'), constants.serial, 'serial'),
|
||||
// C2 hardware revision string
|
||||
new ValueReadCharacteristic(getFullUUID('0013'), constants.hardwareRevision, 'hardwareRevision'),
|
||||
// C2 firmware revision string
|
||||
new ValueReadCharacteristic(getFullUUID('0014'), constants.firmwareRevision, 'firmwareRevision'),
|
||||
// C2 manufacturer name string
|
||||
new ValueReadCharacteristic(getFullUUID('0015'), constants.manufacturer, 'manufacturer'),
|
||||
// Erg Machine Type
|
||||
new ValueReadCharacteristic(getFullUUID('0016'), constants.ergMachineType, 'ergMachineType')
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
'use strict'
|
||||
/*
|
||||
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
||||
|
||||
Provides all required GAP Characteristics of the PM5
|
||||
todo: not sure if this is correct, the normal GAP service has 0x1800
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import { constants, getFullUUID } from './Pm5Constants.js'
|
||||
import ValueReadCharacteristic from './ValueReadCharacteristic.js'
|
||||
|
||||
export default class GapService extends bleno.PrimaryService {
|
||||
constructor () {
|
||||
super({
|
||||
// GAP Service UUID of PM5
|
||||
uuid: getFullUUID('0000'),
|
||||
characteristics: [
|
||||
// GAP device name
|
||||
new ValueReadCharacteristic('2A00', constants.name),
|
||||
// GAP appearance
|
||||
new ValueReadCharacteristic('2A01', [0x00, 0x00]),
|
||||
// GAP peripheral privacy
|
||||
new ValueReadCharacteristic('2A02', [0x00]),
|
||||
// GAP reconnect address
|
||||
new ValueReadCharacteristic('2A03', '00:00:00:00:00:00'),
|
||||
// Peripheral preferred connection parameters
|
||||
new ValueReadCharacteristic('2A04', [0x18, 0x00, 0x18, 0x00, 0x00, 0x00, 0xE8, 0x03])
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
'use strict'
|
||||
/*
|
||||
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
||||
|
||||
Some PM5 specific constants
|
||||
*/
|
||||
const constants = {
|
||||
serial: '123456789',
|
||||
model: 'PM5',
|
||||
name: 'PM5 123456789',
|
||||
hardwareRevision: '633',
|
||||
// see https://www.concept2.com/service/monitors/pm5/firmware for available versions
|
||||
firmwareRevision: '207',
|
||||
manufacturer: 'Concept2',
|
||||
ergMachineType: [0x05]
|
||||
}
|
||||
|
||||
// PM5 uses 128bit UUIDs that are always prefixed and suffixed the same way
|
||||
function getFullUUID (uuid) {
|
||||
return `ce06${uuid}43e511e4916c0800200c9a66`
|
||||
}
|
||||
|
||||
export {
|
||||
getFullUUID,
|
||||
constants
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
'use strict'
|
||||
/*
|
||||
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
||||
|
||||
The Control service can be used to send control commands to the PM5 device
|
||||
todo: not yet implemented
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import { getFullUUID } from './Pm5Constants.js'
|
||||
import ValueReadCharacteristic from './ValueReadCharacteristic.js'
|
||||
|
||||
export default class PM5ControlService extends bleno.PrimaryService {
|
||||
constructor () {
|
||||
super({
|
||||
uuid: getFullUUID('0020'),
|
||||
characteristics: [
|
||||
new ValueReadCharacteristic(getFullUUID('0021'), 'Control Command'),
|
||||
new ValueReadCharacteristic(getFullUUID('0022'), 'Response to Control Command')
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
'use strict'
|
||||
/*
|
||||
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
||||
|
||||
This seems to be the central service to get information about the workout
|
||||
This Primary Service provides a lot of stuff that we most certainly do not need to simulate a
|
||||
simple PM5 service.
|
||||
|
||||
todo: figure out to which services some common applications subscribe and then just implement those
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import { getFullUUID } from './Pm5Constants.js'
|
||||
import ValueReadCharacteristic from './ValueReadCharacteristic.js'
|
||||
|
||||
export default class PM5RowingService extends bleno.PrimaryService {
|
||||
constructor () {
|
||||
super({
|
||||
uuid: getFullUUID('0030'),
|
||||
characteristics: [
|
||||
// C2 rowing general status
|
||||
new ValueReadCharacteristic(getFullUUID('0031'), 'rowing status', 'rowing status'),
|
||||
// C2 rowing additional status
|
||||
new ValueReadCharacteristic(getFullUUID('0032'), 'additional status', 'additional status'),
|
||||
// C2 rowing additional status 2
|
||||
new ValueReadCharacteristic(getFullUUID('0033'), 'additional status 2', 'additional status 2'),
|
||||
// C2 rowing general status and additional status samplerate
|
||||
new ValueReadCharacteristic(getFullUUID('0034'), 'samplerate', 'samplerate'),
|
||||
// C2 rowing stroke data
|
||||
new ValueReadCharacteristic(getFullUUID('0035'), 'stroke data', 'stroke data'),
|
||||
// C2 rowing additional stroke data
|
||||
new ValueReadCharacteristic(getFullUUID('0036'), 'additional stroke data', 'additional stroke data'),
|
||||
// C2 rowing split/interval data
|
||||
new ValueReadCharacteristic(getFullUUID('0037'), 'split data', 'split data'),
|
||||
// C2 rowing additional split/interval data
|
||||
new ValueReadCharacteristic(getFullUUID('0038'), 'additional split data', 'additional split data'),
|
||||
// C2 rowing end of workout summary data
|
||||
new ValueReadCharacteristic(getFullUUID('0039'), 'workout summary', 'workout summary'),
|
||||
// C2 rowing end of workout additional summary data
|
||||
new ValueReadCharacteristic(getFullUUID('003A'), 'additional workout summary', 'additional workout summary'),
|
||||
// C2 rowing heart rate belt information
|
||||
new ValueReadCharacteristic(getFullUUID('003B'), 'heart rate belt information', 'heart rate belt information'),
|
||||
// C2 force curve data
|
||||
new ValueReadCharacteristic(getFullUUID('003D'), 'force curve data', 'force curve data'),
|
||||
// C2 multiplexed information
|
||||
new ValueReadCharacteristic(getFullUUID('0080'), 'multiplexed information', 'multiplexed information')
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
'use strict'
|
||||
/*
|
||||
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
||||
|
||||
A simple Characteristic that gives access to a static value
|
||||
Currently used as placeholder on a lot of characteristics that are not yet implemented properly
|
||||
*/
|
||||
import bleno from '@abandonware/bleno'
|
||||
import log from 'loglevel'
|
||||
|
||||
export default class ValueReadCharacteristic extends bleno.Characteristic {
|
||||
constructor (uuid, value, description) {
|
||||
super({
|
||||
uuid: uuid,
|
||||
properties: ['read', 'notify'],
|
||||
value: null
|
||||
})
|
||||
this.uuid = uuid
|
||||
this._value = Buffer.isBuffer(value) ? value : Buffer.from(value)
|
||||
this.description = description
|
||||
this._updateValueCallback = null
|
||||
}
|
||||
|
||||
onReadRequest (offset, callback) {
|
||||
log.debug(`ValueReadRequest: ${this.description ? this.description : this.uuid}`)
|
||||
callback(this.RESULT_SUCCESS, this._value.slice(offset, this._value.length))
|
||||
}
|
||||
|
||||
onSubscribe (maxValueSize, updateValueCallback) {
|
||||
log.debug(`characteristic ${this.description ? this.description : this.uuid} - central subscribed`)
|
||||
this._updateValueCallback = updateValueCallback
|
||||
return this.RESULT_SUCCESS
|
||||
}
|
||||
|
||||
onUnsubscribe () {
|
||||
log.debug(`characteristic ${this.description ? this.description : this.uuid} - central unsubscribed`)
|
||||
this._updateValueCallback = null
|
||||
return this.RESULT_UNLIKELY_ERROR
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ import finalhandler from 'finalhandler'
|
|||
import http from 'http'
|
||||
import serveStatic from 'serve-static'
|
||||
import log from 'loglevel'
|
||||
import { createRowingMachinePeripheral } from './ble/RowingMachinePeripheral.js'
|
||||
import { createFtmsPeripheral } from './ble/FtmsPeripheral.js'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { createPm5Peripheral } from './ble/Pm5Peripheral.js'
|
||||
import { createRowingEngine } from './engine/RowingEngine.js'
|
||||
import { createRowingStatistics } from './engine/RowingStatistics.js'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
|
@ -21,10 +23,13 @@ import { recordRowingSession, replayRowingSession } from './tools/RowingRecorder
|
|||
// sets the global log level
|
||||
log.setLevel(log.levels.INFO)
|
||||
|
||||
const peripheral = createRowingMachinePeripheral({
|
||||
const peripheral = createFtmsPeripheral({
|
||||
simulateIndoorBike: false
|
||||
})
|
||||
|
||||
// the simulation of a C2 PM5 is not finished yet
|
||||
// const peripheral = createPm5Peripheral()
|
||||
|
||||
peripheral.on('controlPoint', (event) => {
|
||||
if (event?.req?.name === 'requestControl') {
|
||||
event.res = true
|
||||
|
|
|
|||
Loading…
Reference in New Issue