159 lines
5.2 KiB
JavaScript
159 lines
5.2 KiB
JavaScript
'use strict'
|
|
/*
|
|
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
|
|
|
This manager creates a Bluetooth Low Energy (BLE) Central that listens
|
|
and subscribes to heart rate services
|
|
*/
|
|
import log from 'loglevel'
|
|
import EventEmitter from 'node:events'
|
|
import Noble from '@abandonware/noble/lib/noble.js'
|
|
import NobleBindings from '@abandonware/noble/lib/hci-socket/bindings.js'
|
|
|
|
// We are using peripherals and centrals at the same time (with bleno and noble).
|
|
// The libraries do not play nice together in this scenario when they see peripherals
|
|
// from each other via the HCI-Socket.
|
|
// This is a quick patch for two handlers in noble that would otherwise throw warnings
|
|
// when they see a peripheral or handle that is managed by bleno
|
|
|
|
// START of noble patch
|
|
Noble.prototype.onRssiUpdate = function (peripheralUuid, rssi) {
|
|
const peripheral = this._peripherals[peripheralUuid]
|
|
|
|
if (peripheral) {
|
|
peripheral.rssi = rssi
|
|
peripheral.emit('rssiUpdate', rssi)
|
|
}
|
|
}
|
|
|
|
NobleBindings.prototype.onDisconnComplete = function (handle, reason) {
|
|
const uuid = this._handles[handle]
|
|
|
|
if (uuid) {
|
|
this._aclStreams[handle].push(null, null)
|
|
this._gatts[handle].removeAllListeners()
|
|
this._signalings[handle].removeAllListeners()
|
|
|
|
delete this._gatts[uuid]
|
|
delete this._gatts[handle]
|
|
delete this._signalings[uuid]
|
|
delete this._signalings[handle]
|
|
delete this._aclStreams[handle]
|
|
delete this._handles[uuid]
|
|
delete this._handles[handle]
|
|
|
|
this.emit('disconnect', uuid)
|
|
}
|
|
}
|
|
|
|
const noble = new Noble(new NobleBindings())
|
|
// END of noble patch
|
|
|
|
function createCentralManager () {
|
|
const emitter = new EventEmitter()
|
|
let batteryLevel
|
|
|
|
noble.on('stateChange', (state) => {
|
|
if (state === 'poweredOn') {
|
|
// search for heart rate service
|
|
noble.startScanning(['180d'], false)
|
|
} else {
|
|
noble.stopScanning()
|
|
}
|
|
})
|
|
|
|
noble.on('discover', (peripheral) => {
|
|
noble.stopScanning()
|
|
connectHeartratePeripheral(peripheral)
|
|
})
|
|
|
|
function connectHeartratePeripheral (peripheral) {
|
|
// connect to the heart rate sensor
|
|
peripheral.connect((error) => {
|
|
if (error) {
|
|
log.error(error)
|
|
return
|
|
}
|
|
log.info(`heart rate peripheral connected, name: '${peripheral.advertisement?.localName}', id: ${peripheral.id}`)
|
|
subscribeToHeartrateMeasurement(peripheral)
|
|
})
|
|
|
|
peripheral.once('disconnect', () => {
|
|
// todo: figure out if we have to dispose the peripheral somehow to prevent memory leaks
|
|
log.info('heart rate peripheral disconnected, searching new one')
|
|
batteryLevel = undefined
|
|
noble.startScanning(['180d'], false)
|
|
})
|
|
}
|
|
|
|
// see https://www.bluetooth.com/specifications/specs/heart-rate-service-1-0/
|
|
function subscribeToHeartrateMeasurement (peripheral) {
|
|
const heartrateMeasurementUUID = '2a37'
|
|
const batteryLevelUUID = '2a19'
|
|
|
|
peripheral.discoverSomeServicesAndCharacteristics([], [heartrateMeasurementUUID, batteryLevelUUID],
|
|
(error, services, characteristics) => {
|
|
if (error) {
|
|
log.error(error)
|
|
return
|
|
}
|
|
|
|
const heartrateMeasurementCharacteristic = characteristics.find(
|
|
characteristic => characteristic.uuid === heartrateMeasurementUUID
|
|
)
|
|
|
|
const batteryLevelCharacteristic = characteristics.find(
|
|
characteristic => characteristic.uuid === batteryLevelUUID
|
|
)
|
|
|
|
if (heartrateMeasurementCharacteristic !== undefined) {
|
|
heartrateMeasurementCharacteristic.notify(true, (error) => {
|
|
if (error) {
|
|
log.error(error)
|
|
return
|
|
}
|
|
|
|
heartrateMeasurementCharacteristic.on('data', (data, isNotification) => {
|
|
const buffer = Buffer.from(data)
|
|
const flags = buffer.readUInt8(0)
|
|
// bits of the feature flag:
|
|
// 0: Heart Rate Value Format
|
|
// 1 + 2: Sensor Contact Status
|
|
// 3: Energy Expended Status
|
|
// 4: RR-Interval
|
|
const heartrateUint16LE = flags & 0b1
|
|
|
|
// from the specs:
|
|
// While most human applications require support for only 255 bpm or less, special
|
|
// applications (e.g. animals) may require support for higher bpm values.
|
|
// If the Heart Rate Measurement Value is less than or equal to 255 bpm a UINT8 format
|
|
// should be used for power savings.
|
|
// If the Heart Rate Measurement Value exceeds 255 bpm a UINT16 format shall be used.
|
|
const heartrate = heartrateUint16LE ? buffer.readUInt16LE(1) : buffer.readUInt8(1)
|
|
emitter.emit('heartrateMeasurement', { heartrate, batteryLevel })
|
|
})
|
|
})
|
|
}
|
|
|
|
if (batteryLevelCharacteristic !== undefined) {
|
|
batteryLevelCharacteristic.notify(true, (error) => {
|
|
if (error) {
|
|
log.error(error)
|
|
return
|
|
}
|
|
|
|
batteryLevelCharacteristic.on('data', (data, isNotification) => {
|
|
const buffer = Buffer.from(data)
|
|
batteryLevel = buffer.readUInt8(0)
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
return Object.assign(emitter, {
|
|
})
|
|
}
|
|
|
|
export { createCentralManager }
|