UNPKG

ant-plus

Version:
224 lines (183 loc) 7.01 kB
/* * ANT+ profile: https://www.thisisant.com/developer/ant-plus/device-profiles/#521_tab * Spec sheet: https://www.thisisant.com/resources/bicycle-power/ */ import { Messages, SendCallback, AntPlusSensor, AntPlusScanner } from './ant'; class MuscleOxygenSensorState { constructor(deviceID: number) { this.DeviceID = deviceID; } _EventCount?: number; DeviceID: number; UTCTimeRequired?: boolean; SupportANTFS?: boolean; MeasurementInterval?: 0.25 | 0.5 | 1 | 2; TotalHemoglobinConcentration?: number | 'AmbientLightTooHigh' | 'Invalid'; PreviousSaturatedHemoglobinPercentage?: number | 'AmbientLightTooHigh' | 'Invalid'; CurrentSaturatedHemoglobinPercentage?: number | 'AmbientLightTooHigh' | 'Invalid'; HwVersion?: number; ManId?: number; ModelNum?: number; SwVersion?: number; SerialNumber?: number; OperatingTime?: number; BatteryVoltage?: number; BatteryStatus?: 'New' | 'Good' | 'Ok' | 'Low' | 'Critical' | 'Invalid'; } class MuscleOxygenScanState extends MuscleOxygenSensorState { Rssi: number; Threshold: number; } export class MuscleOxygenSensor extends AntPlusSensor { static deviceType = 0x1F; public attach(channel, deviceID): void { super.attach(channel, 'receive', deviceID, MuscleOxygenSensor.deviceType, 0, 255, 8192); this.state = new MuscleOxygenSensorState(deviceID); } private state: MuscleOxygenSensorState; protected updateState(deviceId, data) { this.state.DeviceID = deviceId; updateState(this, this.state, data); } private _sendTimeCmd(cmd: number, cbk?: SendCallback) { const now = new Date(); const utc = Math.round((now.getTime() - Date.UTC(1989, 11, 31, 0, 0, 0, 0)) / 1000); const offset = -Math.round(now.getTimezoneOffset() / 15); const payload = [0x10, cmd & 0xFF, 0xFF, offset & 0xFF, (utc >> 0) & 0xFF, (utc >> 8) & 0xFF, (utc >> 16) & 0xFF, (utc >> 24) & 0xFF]; const msg = Messages.acknowledgedData(this.channel, payload); this.send(msg, cbk); } public setUTCTime(cbk?: SendCallback) { this._sendTimeCmd(0x00, cbk); } public startSession(cbk?: SendCallback) { this._sendTimeCmd(0x01, cbk); } public stopSession(cbk?: SendCallback) { this._sendTimeCmd(0x02, cbk); } public setLap(cbk?: SendCallback) { this._sendTimeCmd(0x03, cbk); } } export class MuscleOxygenScanner extends AntPlusScanner { protected deviceType() { return MuscleOxygenSensor.deviceType; } private states: { [id: number]: MuscleOxygenScanState } = {}; protected createStateIfNew(deviceId) { if (!this.states[deviceId]) { this.states[deviceId] = new MuscleOxygenScanState(deviceId); } } protected updateRssiAndThreshold(deviceId, rssi, threshold) { this.states[deviceId].Rssi = rssi; this.states[deviceId].Threshold = threshold; } protected updateState(deviceId, data) { updateState(this, this.states[deviceId], data); } } function updateState( sensor: MuscleOxygenSensor | MuscleOxygenScanner, state: MuscleOxygenSensorState | MuscleOxygenScanState, data: Buffer) { const oldEventCount = state._EventCount || 0; const page = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA); switch (page) { case 0x01: { let eventCount = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 1); const notifications = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 2); const capabilities = data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 3); const total = data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 4) & 0xFFF; const previous = (data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 5) >> 4) & 0x3FF; const current = (data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 6) >> 6) & 0x3FF; if (eventCount !== oldEventCount) { state._EventCount = eventCount; if (oldEventCount > eventCount) { //Hit rollover value eventCount += 255; } } state.UTCTimeRequired = (notifications & 0x01) === 0x01; state.SupportANTFS = (capabilities & 0x01) === 0x01; switch ((capabilities >> 1) & 0x7) { case 1: state.MeasurementInterval = 0.25; break; case 2: state.MeasurementInterval = 0.5; break; case 3: state.MeasurementInterval = 1; break; case 4: state.MeasurementInterval = 2; break; default: delete state.MeasurementInterval; } switch (total) { case 0xFFE: state.TotalHemoglobinConcentration = 'AmbientLightTooHigh'; break; case 0xFFF: state.TotalHemoglobinConcentration = 'Invalid'; break; default: state.TotalHemoglobinConcentration = total; } switch (previous) { case 0x3FE: state.PreviousSaturatedHemoglobinPercentage = 'AmbientLightTooHigh'; break; case 0x3FF: state.PreviousSaturatedHemoglobinPercentage = 'Invalid'; break; default: state.PreviousSaturatedHemoglobinPercentage = previous; } switch (current) { case 0x3FE: state.CurrentSaturatedHemoglobinPercentage = 'AmbientLightTooHigh'; break; case 0x3FF: state.CurrentSaturatedHemoglobinPercentage = 'Invalid'; break; default: state.CurrentSaturatedHemoglobinPercentage = current; } break; } case 0x50: { state.HwVersion = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 3); state.ManId = data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 4); state.ModelNum = data.readUInt16LE(Messages.BUFFER_INDEX_MSG_DATA + 6); break; } case 0x51: { const swRevSup = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 2); const swRevMain = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 3); const serial = data.readInt32LE(Messages.BUFFER_INDEX_MSG_DATA + 4); state.SwVersion = swRevMain; if (swRevSup !== 0xFF) { state.SwVersion += swRevSup / 1000; } if (serial !== 0xFFFFFFFF) { state.SerialNumber = serial; } break; } case 0x52: { const batteryId = data.readUInt8(Messages.BUFFER_INDEX_MSG_DATA + 2); const operatingTime = data.readUInt32LE(Messages.BUFFER_INDEX_MSG_DATA + 3) & 0xFFFFFF; const batteryFrac = data.readInt32LE(Messages.BUFFER_INDEX_MSG_DATA + 6); const batteryStatus = data.readInt32LE(Messages.BUFFER_INDEX_MSG_DATA + 7); state.OperatingTime = operatingTime * (((batteryStatus & 0x80) === 0x80) ? 2 : 16); state.BatteryVoltage = (batteryStatus & 0x0F) + (batteryFrac / 256); const batteryFlags = (batteryStatus & 0x70) >>> 4; switch (batteryFlags) { case 1: state.BatteryStatus = 'New'; break; case 2: state.BatteryStatus = 'Good'; break; case 3: state.BatteryStatus = 'Ok'; break; case 4: state.BatteryStatus = 'Low'; break; case 5: state.BatteryStatus = 'Critical'; break; default: state.BatteryVoltage = undefined; state.BatteryStatus = 'Invalid'; break; } break; } default: return; } if (page !== 0x01 || state._EventCount !== oldEventCount) { sensor.emit('oxygenData', state); } }