openrowingmonitor/app/peripherals/ant/FEPeripheral.js

219 lines
8.1 KiB
JavaScript

'use strict'
import log from 'loglevel'
import { Messages } from 'incyclist-ant-plus'
import { PeripheralConstants } from '../PeripheralConstants.js'
/*
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
Creates a Bluetooth Low Energy (BLE) Peripheral with all the Services that are required for
a Cycling Speed and Cadence Profile
*/
function createFEPeripheral (antManager) {
const antStick = antManager.getAntStick()
const deviceType = 0x11 // Ant FE-C device
const deviceNumber = 1
const deviceId = parseInt(PeripheralConstants.serial, 10) & 0xFFFF
const channel = 1
const broadcastPeriod = 8192 // 8192/32768 ~4hz
const broadcastInterval = broadcastPeriod / 32768 * 1000 // millisecond
const rfChannel = 57 // 2457 MHz
let dataPageCount = 0
let commonPageCount = 0
let timer
let sessionData = {
accumulatedStrokes: 0,
accumulatedDistance: 0,
accumulatedTime: 0,
accumulatedPower: 0,
cycleLinearVelocity: 0,
strokeRate: 0,
instantaneousPower: 0,
distancePerStroke: 0,
fitnessEquipmentState: fitnessEquipmentStates.inUse,
sessionStatus: 'WaitingForStart'
}
async function attach () {
if (!antManager.isStickOpen()) { await antManager.openAntStick() }
const messages = [
Messages.assignChannel(channel, 'transmit'),
Messages.setDevice(channel, deviceId, deviceType, deviceNumber),
Messages.setFrequency(channel, rfChannel),
Messages.setPeriod(channel, broadcastPeriod),
Messages.openChannel(channel)
]
log.info(`ANT+ FE server start [deviceId=${deviceId} channel=${channel}]`)
for (const message of messages) {
antStick.write(message)
}
timer = setInterval(onBroadcastInterval, broadcastInterval)
}
function destroy () {
return new Promise((resolve) => {
clearInterval(timer)
log.info(`ANT+ FE server stopped [deviceId=${deviceId} channel=${channel}]`)
const messages = [
Messages.closeChannel(channel),
Messages.unassignChannel(channel)
]
for (const message of messages) {
antStick.write(message)
}
resolve()
})
}
function onBroadcastInterval () {
dataPageCount++
let data
switch (true) {
case dataPageCount === 65 || dataPageCount === 66:
if (commonPageCount % 2 === 0) { // 0x50 - Common Page for Manufacturers Identification (approx twice a minute)
data = [
channel,
0x50, // Page 80
0xFF, // Reserved
0xFF, // Reserved
parseInt(PeripheralConstants.hardwareRevision, 10) & 0xFF, // Hardware Revision
...Messages.intToLEHexArray(40, 2), // Manufacturer ID (value 255 = Development ID, value 40 = concept2)
0x0001 // Model Number
]
}
if (commonPageCount % 2 === 1) { // 0x51 - Common Page for Product Information (approx twice a minute)
data = [
channel,
0x51, // Page 81
0xFF, // Reserved
parseInt(PeripheralConstants.firmwareRevision.slice(-2), 10), // SW Revision (Supplemental)
parseInt(PeripheralConstants.firmwareRevision[0], 10), // SW Version
...Messages.intToLEHexArray(parseInt(PeripheralConstants.serial, 10), 4) // Serial Number (None)
]
}
if (dataPageCount === 66) {
commonPageCount++
dataPageCount = 0
}
break
case dataPageCount % 8 === 4: // 0x11 - General Settings Page (once a second)
case dataPageCount % 8 === 7:
data = [
channel,
0x11, // Page 17
0xFF, // Reserved
0xFF, // Reserved
...Messages.intToLEHexArray(sessionData.distancePerStroke, 1), // Stroke Length in 0.01 m
0x7FFF, // Incline (Not Used)
0x00, // Resistance (DF may be reported if conversion to the % is worked out (value in % with a resolution of 0.5%).
...Messages.intToLEHexArray(feCapabilitiesBitField, 1)
]
if (sessionData.sessionStatus === 'Rowing') {
log.trace(`Page 17 Data Sent. Event=${dataPageCount}. Stroke Length=${sessionData.distancePerStroke}.`)
log.trace(`Hex Stroke Length=0x${sessionData.distancePerStroke.toString(16)}.`)
}
break
case dataPageCount % 8 === 3: // 0x16 - Specific Rower Data (once a second)
case dataPageCount % 8 === 0:
data = [
channel,
0x16, // Page 22
0xFF, // Reserved
0xFF, // Reserved
...Messages.intToLEHexArray(sessionData.accumulatedStrokes, 1), // Stroke Count
...Messages.intToLEHexArray(sessionData.strokeRate, 1), // Cadence / Stroke Rate
...Messages.intToLEHexArray(sessionData.instantaneousPower, 2), // Instant Power (2 bytes)
...Messages.intToLEHexArray((sessionData.fitnessEquipmentState + rowingCapabilitiesBitField), 1)
]
if (sessionData.sessionStatus === 'Rowing') {
log.trace(`Page 22 Data Sent. Event=${dataPageCount}. Strokes=${sessionData.accumulatedStrokes}. Stroke Rate=${sessionData.strokeRate}. Power=${sessionData.instantaneousPower}`)
log.trace(`Hex Strokes=0x${sessionData.accumulatedStrokes.toString(16)}. Hex Stroke Rate=0x${sessionData.strokeRate.toString(16)}. Hex Power=0x${Messages.intToLEHexArray(sessionData.instantaneousPower, 2)}.`)
}
break
case dataPageCount % 4 === 2: // 0x10 - General FE Data (twice a second)
default:
data = [
channel,
0x10, // Page 16
0x16, // Rowing Machine (22)
...Messages.intToLEHexArray(sessionData.accumulatedTime, 1), // elapsed time
...Messages.intToLEHexArray(sessionData.accumulatedDistance, 1), // distance travelled
...Messages.intToLEHexArray(sessionData.cycleLinearVelocity, 2), // speed in 0.001 m/s
0xFF, // heart rate not being sent
...Messages.intToLEHexArray((sessionData.fitnessEquipmentState + feCapabilitiesBitField), 1)
]
if (sessionData.sessionStatus === 'Rowing') {
log.trace(`Page 16 Data Sent. Event=${dataPageCount}. Time=${sessionData.accumulatedTime}. Distance=${sessionData.accumulatedDistance}. Speed=${sessionData.cycleLinearVelocity}.`)
log.trace(`Hex Time=0x${sessionData.accumulatedTime.toString(16)}. Hex Distance=0x${sessionData.accumulatedDistance.toString(16)}. Hex Speed=0x${Messages.intToLEHexArray(sessionData.cycleLinearVelocity, 2)}.`)
}
break
}
const message = Messages.broadcastData(data)
antStick.write(message)
}
function notifyData (type, data) {
if (type === 'strokeFinished' || type === 'metricsUpdate') {
sessionData = {
...sessionData,
accumulatedDistance: data.totalLinearDistance & 0xFF,
accumulatedStrokes: data.totalNumberOfStrokes & 0xFF,
accumulatedTime: Math.trunc(data.totalMovingTime * 4) & 0xFF,
cycleLinearVelocity: Math.round(data.cycleLinearVelocity * 1000),
strokeRate: Math.round(data.cycleStrokeRate) & 0xFF,
instantaneousPower: Math.round(data.cyclePower) & 0xFFFF,
distancePerStroke: Math.round(data.cycleDistance * 100),
sessionStatus: data.sessionStatus
}
}
}
// FE does not have status characteristic
function notifyStatus (status) {
}
return {
notifyData,
notifyStatus,
attach,
destroy
}
}
const fitnessEquipmentStates = {
asleep: (1 << 0x04),
ready: (2 << 0x04),
inUse: (3 << 0x04),
finished: (4 << 0x04),
lapToggleBit: (8 << 0x04)
}
const fitnessEquipmentCapabilities = {
hrDataSourceHandContactSensors: (0x03 << 0),
hrDataSourceEmSensors: (0x02 << 0),
hrDataSourceAntSensors: (0x01 << 0),
hrDataSourceInvalid: (0x00 << 0),
distanceTraveledEnabled: (0x01 << 2),
virtualSpeed: (0x01 << 3),
realSpeed: (0x00 << 3)
}
const rowingMachineCapabilities = {
accumulatedStrokesEnabled: (0x01 << 0)
}
const feCapabilitiesBitField = fitnessEquipmentCapabilities.hrDataSourceInvalid | fitnessEquipmentCapabilities.distanceTraveledEnabled | fitnessEquipmentCapabilities.realSpeed
const rowingCapabilitiesBitField = rowingMachineCapabilities.accumulatedStrokesEnabled
export { createFEPeripheral }