ant-plus
Version:
A node module for ANT+
224 lines (183 loc) • 7.01 kB
text/typescript
/*
* 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);
}
}