101 lines
4.0 KiB
JavaScript
101 lines
4.0 KiB
JavaScript
'use strict'
|
|
/*
|
|
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
|
|
|
This implements the Indoor Bike Data Characteristic as defined by the Bluetooth SIG
|
|
Currently hardly any applications exist that support these FTMS Characteristic for Rowing.
|
|
So we use this to simulate an FTMS Indoor Bike characteristic.
|
|
Of course we can not deliver rowing specific parameters like this (such as stroke rate), but
|
|
this allows us to use the open rowing monitor with bike training platforms such as
|
|
Zwift, Sufferfest, RGT Cycling, Kinomap, Bkool, Rouvy and more...
|
|
So far tested on:
|
|
- Kinomap.com: uses Power and Speed
|
|
- Fulgaz: uses Power and Speed
|
|
- Zwift: uses Power
|
|
- RGT Cycling: connects Power but then disconnects again (seems something is missing here)
|
|
|
|
From specs:
|
|
The Server should notify this characteristic at a regular interval, typically once per second
|
|
while in a connection and the interval is not configurable by the Client
|
|
*/
|
|
import bleno from '@abandonware/bleno'
|
|
import log from 'loglevel'
|
|
import BufferBuilder from '../BufferBuilder.js'
|
|
|
|
export default class IndoorBikeDataCharacteristic extends bleno.Characteristic {
|
|
constructor () {
|
|
super({
|
|
// Indoor Bike Data
|
|
uuid: '2AD2',
|
|
value: null,
|
|
properties: ['notify']
|
|
})
|
|
this._updateValueCallback = null
|
|
this._subscriberMaxValueSize = null
|
|
}
|
|
|
|
onSubscribe (maxValueSize, updateValueCallback) {
|
|
log.debug(`IndoorBikeDataCharacteristic - central subscribed with maxSize: ${maxValueSize}`)
|
|
this._updateValueCallback = updateValueCallback
|
|
this._subscriberMaxValueSize = maxValueSize
|
|
return this.RESULT_SUCCESS
|
|
}
|
|
|
|
onUnsubscribe () {
|
|
log.debug('IndoorBikeDataCharacteristic - central unsubscribed')
|
|
this._updateValueCallback = null
|
|
this._subscriberMaxValueSize = null
|
|
return this.RESULT_UNLIKELY_ERROR
|
|
}
|
|
|
|
notify (data) {
|
|
// ignore events without the mandatory fields
|
|
if (!('speed' in data)) {
|
|
log.error('can not deliver bike data without mandatory fields')
|
|
return this.RESULT_SUCCESS
|
|
}
|
|
|
|
if (this._updateValueCallback) {
|
|
const bufferBuilder = new BufferBuilder()
|
|
// Field flags as defined in the Bluetooth Documentation
|
|
// Instantaneous speed (default), Instantaneous Cadence (2), Total Distance (4),
|
|
// Instantaneous Power (6), Total / Expended Energy (8), Heart Rate (9), Elapsed Time (11)
|
|
// 01010100
|
|
bufferBuilder.writeUInt8(0x54)
|
|
// 00001011
|
|
bufferBuilder.writeUInt8(0x0B)
|
|
|
|
// see https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-3/
|
|
// for some of the data types
|
|
// Instantaneous Speed in km/h
|
|
bufferBuilder.writeUInt16LE(Math.round(data.speed * 100))
|
|
// Instantaneous Cadence in rotations per minute (we use this to communicate the strokes per minute)
|
|
bufferBuilder.writeUInt16LE(Math.round(data.strokesPerMinute * 2))
|
|
// Total Distance in meters
|
|
bufferBuilder.writeUInt24LE(Math.round(data.distanceTotal))
|
|
// Instantaneous Power in watts
|
|
bufferBuilder.writeUInt16LE(Math.round(data.power))
|
|
// Energy
|
|
// Total energy in kcal
|
|
bufferBuilder.writeUInt16LE(Math.round(data.caloriesTotal))
|
|
// Energy per hour
|
|
// The Energy per Hour field represents the average expended energy of a user during a
|
|
// period of one hour.
|
|
bufferBuilder.writeUInt16LE(Math.round(data.caloriesPerHour))
|
|
// Energy per minute
|
|
bufferBuilder.writeUInt8(Math.round(data.caloriesPerMinute))
|
|
// Heart Rate: Beats per minute with a resolution of 1
|
|
bufferBuilder.writeUInt8(Math.round(data.heartrate))
|
|
// Elapsed Time: Seconds with a resolution of 1
|
|
bufferBuilder.writeUInt16LE(Math.round(data.durationTotal))
|
|
|
|
const buffer = bufferBuilder.getBuffer()
|
|
if (buffer.length > this._subscriberMaxValueSize) {
|
|
log.warn(`IndoorBikeDataCharacteristic - notification of ${buffer.length} bytes is too large for the subscriber`)
|
|
}
|
|
this._updateValueCallback(bufferBuilder.getBuffer())
|
|
}
|
|
return this.RESULT_SUCCESS
|
|
}
|
|
}
|