openrowingmonitor/app/ble/FitnessMachineControlPointC...

148 lines
5.0 KiB
JavaScript

'use strict'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
The connected Central can remotly control some parameters or our rowing monitor via this Control Point
So far tested on:
- Fulgaz: uses setIndoorBikeSimulationParameters
- Zwift: uses startOrResume and setIndoorBikeSimulationParameters
*/
import bleno from '@abandonware/bleno'
import log from 'loglevel'
// see https://www.bluetooth.com/specifications/specs/fitness-machine-service-1-0 for details
const ControlPointOpCode = {
requestControl: 0x00,
reset: 0x01,
setTargetSpeed: 0x02,
setTargetInclincation: 0x03,
setTargetResistanceLevel: 0x04,
setTargetPower: 0x05,
setTargetHeartRate: 0x06,
startOrResume: 0x07,
stopOrPause: 0x08,
setTargetedExpendedEnergy: 0x09,
setTargetedNumberOfSteps: 0x0A,
setTargetedNumberOfStrides: 0x0B,
setTargetedDistance: 0x0C,
setTargetedTrainingTime: 0x0D,
setTargetedTimeInTwoHeartRateZones: 0x0E,
setTargetedTimeInThreeHeartRateZones: 0x0F,
setTargetedTimeInFiveHeartRateZones: 0x10,
setIndoorBikeSimulationParameters: 0x11,
setWheelCircumference: 0x12,
spinDownControl: 0x13,
setTargetedCadence: 0x14,
responseCode: 0x80
}
const ResultCode = {
reserved: 0x00,
success: 0x01,
opCodeNotSupported: 0x02,
invalidParameter: 0x03,
operationFailed: 0x04,
controlNotPermitted: 0x05
}
export default class FitnessMachineControlPointCharacteristic extends bleno.Characteristic {
constructor (controlPointCallback) {
super({
// Fitness Machine Control Point
uuid: '2AD9',
value: null,
properties: ['write']
})
this.controlled = false
if (!controlPointCallback) { throw new Error('controlPointCallback required') }
this.controlPointCallback = controlPointCallback
}
// Central sends a command to the Control Point
// todo: handle offset and withoutResponse properly
onWriteRequest (data, offset, withoutResponse, callback) {
const opCode = data.readUInt8(0)
switch (opCode) {
case ControlPointOpCode.requestControl:
if (!this.controlled) {
if (this.controlPointCallback({ name: 'requestControl' })) {
log.debug('requestControl sucessful')
this.controlled = true
callback(this.buildResponse(opCode, ResultCode.success))
} else {
callback(this.buildResponse(opCode, ResultCode.operationFailed))
}
} else {
callback(this.buildResponse(opCode, ResultCode.controlNotPermitted))
}
break
case ControlPointOpCode.reset:
this.handleSimpleCommand(ControlPointOpCode.reset, 'reset', callback)
// as per spec the reset command shall also reset the control
this.controlled = false
break
case ControlPointOpCode.startOrResume:
this.handleSimpleCommand(ControlPointOpCode.startOrResume, 'startOrResume', callback)
break
case ControlPointOpCode.stopOrPause: {
const controlParameter = data.readUInt8(1)
if (controlParameter === 1) {
this.handleSimpleCommand(ControlPointOpCode.stopOrPause, 'stop', callback)
} else if (controlParameter === 2) {
this.handleSimpleCommand(ControlPointOpCode.stopOrPause, 'pause', callback)
} else {
log.error(`stopOrPause with invalid controlParameter: ${controlParameter}`)
}
break
}
// todo: Most tested bike apps use these to simulate a bike ride. Not sure how we can use these in our rower
// since there is no adjustable resistance on the rowing machine
case ControlPointOpCode.setIndoorBikeSimulationParameters: {
const windspeed = data.readInt16LE(1) * 0.001
const grade = data.readInt16LE(3) * 0.01
const crr = data.readUInt8(5) * 0.0001
const cw = data.readUInt8(6) * 0.01
if (this.controlPointCallback({ name: 'simulation', value: { windspeed, grade, crr, cw } })) {
callback(this.buildResponse(opCode, ResultCode.success))
} else {
callback(this.buildResponse(opCode, ResultCode.operationFailed))
}
break
}
default:
log.info(`opCode ${opCode} is not supported`)
callback(this.buildResponse(opCode, ResultCode.opCodeNotSupported))
}
}
handleSimpleCommand (opCode, opName, callback) {
if (this.controlled) {
if (this.controlPointCallback({ name: opName })) {
const response = this.buildResponse(opCode, ResultCode.success)
callback(response)
} else {
callback(this.buildResponse(opCode, ResultCode.operationFailed))
}
} else {
log.info(`initating command '${opName}' requires 'requestControl'`)
callback(this.buildResponse(opCode, ResultCode.controlNotPermitted))
}
}
// build the response message as defined by the spec
buildResponse (opCode, resultCode) {
const buffer = Buffer.alloc(3)
buffer.writeUInt8(0x80, 0)
buffer.writeUInt8(opCode, 1)
buffer.writeUInt8(resultCode, 2)
return buffer
}
}