incyclist-devices 1.4.39 → 1.4.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/DeviceSupport.d.ts +2 -1
- package/lib/DeviceSupport.js +2 -0
- package/lib/ble/ble-device.js +1 -1
- package/lib/ble/fm.d.ts +78 -0
- package/lib/ble/fm.js +248 -0
- package/lib/ble/incyclist-protocol.js +6 -1
- package/package.json +1 -1
package/lib/DeviceSupport.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { CyclingModeProperyType } from './CyclingMode';
|
|
|
11
11
|
import BleInterface from './ble/ble-interface';
|
|
12
12
|
import BleHrmDevice from './ble/hrm';
|
|
13
13
|
import BleCyclingPowerDevice from './ble/pwr';
|
|
14
|
+
import BleFitnessMachineDevice from './ble/fm';
|
|
14
15
|
declare const Protocols: {
|
|
15
16
|
SimulatorProtocol: typeof SimulatorProtocol;
|
|
16
17
|
DaumClassicProtocol: typeof DaumClassicProtocol;
|
|
@@ -18,4 +19,4 @@ declare const Protocols: {
|
|
|
18
19
|
KettlerRacerProtocol: typeof KettlerRacerProtocol;
|
|
19
20
|
BleProtocol: typeof BleProtocol;
|
|
20
21
|
};
|
|
21
|
-
export { DeviceProtocolBase, DeviceProtocol, DeviceRegistry, INTERFACE, DeviceAdapter as Device, Protocols, AntScanner, BleProtocol, CyclingModeProperyType, BleInterface, BleHrmDevice, BleCyclingPowerDevice };
|
|
22
|
+
export { DeviceProtocolBase, DeviceProtocol, DeviceRegistry, INTERFACE, DeviceAdapter as Device, Protocols, AntScanner, BleProtocol, CyclingModeProperyType, BleInterface, BleHrmDevice, BleCyclingPowerDevice, BleFitnessMachineDevice };
|
package/lib/DeviceSupport.js
CHANGED
|
@@ -33,6 +33,8 @@ const hrm_1 = __importDefault(require("./ble/hrm"));
|
|
|
33
33
|
exports.BleHrmDevice = hrm_1.default;
|
|
34
34
|
const pwr_1 = __importDefault(require("./ble/pwr"));
|
|
35
35
|
exports.BleCyclingPowerDevice = pwr_1.default;
|
|
36
|
+
const fm_1 = __importDefault(require("./ble/fm"));
|
|
37
|
+
exports.BleFitnessMachineDevice = fm_1.default;
|
|
36
38
|
const Protocols = {
|
|
37
39
|
SimulatorProtocol: Simulator_1.default,
|
|
38
40
|
DaumClassicProtocol: DaumClassicProtocol_1.default,
|
package/lib/ble/ble-device.js
CHANGED
|
@@ -105,7 +105,7 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
105
105
|
this.cleanupListeners();
|
|
106
106
|
if (!connected) {
|
|
107
107
|
this.logEvent({ message: 'connect: discover characteristics start' });
|
|
108
|
-
const res = yield peripheral.discoverSomeServicesAndCharacteristicsAsync(
|
|
108
|
+
const res = yield peripheral.discoverSomeServicesAndCharacteristicsAsync([], []);
|
|
109
109
|
const { characteristics } = res;
|
|
110
110
|
this.logEvent({ message: 'connect: discover characteristics result',
|
|
111
111
|
result: characteristics.map(c => ({ uuid: ble_1.uuid(c.uuid), properties: c.properties.join(','), service: ble_1.uuid(c._serviceUuid) }))
|
package/lib/ble/fm.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { BleDevice } from './ble-device';
|
|
3
|
+
import BleInterface from './ble-interface';
|
|
4
|
+
import BleProtocol from './incyclist-protocol';
|
|
5
|
+
import { BleDeviceClass } from './ble';
|
|
6
|
+
import DeviceAdapter, { DeviceData } from '../Device';
|
|
7
|
+
import { DeviceProtocol } from '../DeviceProtocol';
|
|
8
|
+
import { EventLogger } from 'gd-eventlog';
|
|
9
|
+
import CyclingMode from '../CyclingMode';
|
|
10
|
+
import { IncyclistBikeData } from '../CyclingMode';
|
|
11
|
+
declare type PowerData = {
|
|
12
|
+
instantaneousPower?: number;
|
|
13
|
+
balance?: number;
|
|
14
|
+
accTorque?: number;
|
|
15
|
+
time: number;
|
|
16
|
+
rpm: number;
|
|
17
|
+
raw?: string;
|
|
18
|
+
};
|
|
19
|
+
declare type IndoorBikeData = {
|
|
20
|
+
speed?: number;
|
|
21
|
+
averageSpeed?: number;
|
|
22
|
+
cadence?: number;
|
|
23
|
+
averageCadence?: number;
|
|
24
|
+
totalDistance?: number;
|
|
25
|
+
resistanceLevel?: number;
|
|
26
|
+
instantaneousPower?: number;
|
|
27
|
+
averagePower?: number;
|
|
28
|
+
expendedEnergy?: number;
|
|
29
|
+
heartrate?: number;
|
|
30
|
+
metabolicEquivalent?: number;
|
|
31
|
+
time?: number;
|
|
32
|
+
remainingTime?: number;
|
|
33
|
+
raw?: string;
|
|
34
|
+
};
|
|
35
|
+
export default class BleFitnessMachineDevice extends BleDevice {
|
|
36
|
+
static services: string[];
|
|
37
|
+
static characteristics: string[];
|
|
38
|
+
data: IndoorBikeData;
|
|
39
|
+
constructor(props?: any);
|
|
40
|
+
getProfile(): string;
|
|
41
|
+
getServiceUUids(): string[];
|
|
42
|
+
parseIndoorBikeData(_data: Uint8Array): IndoorBikeData;
|
|
43
|
+
onData(characteristic: string, data: Buffer): void;
|
|
44
|
+
write(characteristic: any, data: any): Promise<boolean>;
|
|
45
|
+
read(characteristic: any): Promise<Buffer>;
|
|
46
|
+
reset(): void;
|
|
47
|
+
}
|
|
48
|
+
export declare class FmAdapter extends DeviceAdapter {
|
|
49
|
+
device: BleFitnessMachineDevice;
|
|
50
|
+
ignore: boolean;
|
|
51
|
+
ble: BleInterface;
|
|
52
|
+
protocol: DeviceProtocol;
|
|
53
|
+
paused: boolean;
|
|
54
|
+
logger: EventLogger;
|
|
55
|
+
mode: CyclingMode;
|
|
56
|
+
distanceInternal: number;
|
|
57
|
+
prevDataTS: number;
|
|
58
|
+
constructor(device: BleDeviceClass, protocol: BleProtocol);
|
|
59
|
+
isBike(): boolean;
|
|
60
|
+
isHrm(): boolean;
|
|
61
|
+
isPower(): boolean;
|
|
62
|
+
getProfile(): string;
|
|
63
|
+
getName(): string;
|
|
64
|
+
getDisplayName(): string;
|
|
65
|
+
getCyclingMode(): CyclingMode;
|
|
66
|
+
getDefaultCyclingMode(): CyclingMode;
|
|
67
|
+
getPort(): string;
|
|
68
|
+
setIgnoreBike(ignore: any): void;
|
|
69
|
+
setIgnorePower(ignore: any): void;
|
|
70
|
+
onDeviceData(deviceData: PowerData): void;
|
|
71
|
+
mapData(deviceData: IndoorBikeData): IncyclistBikeData;
|
|
72
|
+
transformData(bikeData: IncyclistBikeData): DeviceData;
|
|
73
|
+
start(props?: any): Promise<any>;
|
|
74
|
+
stop(): Promise<boolean>;
|
|
75
|
+
pause(): Promise<boolean>;
|
|
76
|
+
resume(): Promise<boolean>;
|
|
77
|
+
}
|
|
78
|
+
export {};
|
package/lib/ble/fm.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const ble_device_1 = require("./ble-device");
|
|
16
|
+
const ble_interface_1 = __importDefault(require("./ble-interface"));
|
|
17
|
+
const Device_1 = __importDefault(require("../Device"));
|
|
18
|
+
const gd_eventlog_1 = require("gd-eventlog");
|
|
19
|
+
const power_meter_1 = __importDefault(require("../modes/power-meter"));
|
|
20
|
+
const bit = (nr) => (1 << nr);
|
|
21
|
+
const IndoorBikeDataFlag = {
|
|
22
|
+
MoreData: bit(0),
|
|
23
|
+
AverageSpeedPresent: bit(1),
|
|
24
|
+
InstantaneousCadence: bit(2),
|
|
25
|
+
AverageCadencePresent: bit(3),
|
|
26
|
+
TotalDistancePresent: bit(4),
|
|
27
|
+
ResistanceLevelPresent: bit(5),
|
|
28
|
+
InstantaneousPowerPresent: bit(6),
|
|
29
|
+
AveragePowerPresent: bit(7),
|
|
30
|
+
ExpendedEnergyPresent: bit(8),
|
|
31
|
+
HeartRatePresent: bit(9),
|
|
32
|
+
MetabolicEquivalentPresent: bit(10),
|
|
33
|
+
ElapsedTimePresent: bit(11),
|
|
34
|
+
RemainingTimePresent: bit(12)
|
|
35
|
+
};
|
|
36
|
+
class BleFitnessMachineDevice extends ble_device_1.BleDevice {
|
|
37
|
+
constructor(props) {
|
|
38
|
+
super(props);
|
|
39
|
+
this.data = {};
|
|
40
|
+
}
|
|
41
|
+
getProfile() {
|
|
42
|
+
return 'Smart Trainer';
|
|
43
|
+
}
|
|
44
|
+
getServiceUUids() {
|
|
45
|
+
return BleFitnessMachineDevice.services;
|
|
46
|
+
}
|
|
47
|
+
parseIndoorBikeData(_data) {
|
|
48
|
+
const data = Buffer.from(_data);
|
|
49
|
+
const flags = data.readUInt16LE(0);
|
|
50
|
+
let offset = 2;
|
|
51
|
+
if ((flags & IndoorBikeDataFlag.MoreData) === 0) {
|
|
52
|
+
this.data.speed = data.readUInt16LE(offset) / 100;
|
|
53
|
+
offset += 2;
|
|
54
|
+
}
|
|
55
|
+
if (flags & IndoorBikeDataFlag.AverageSpeedPresent) {
|
|
56
|
+
this.data.averageSpeed = data.readUInt16LE(offset);
|
|
57
|
+
offset += 2;
|
|
58
|
+
}
|
|
59
|
+
if (flags & IndoorBikeDataFlag.InstantaneousCadence) {
|
|
60
|
+
this.data.cadence = data.readUInt16LE(offset) / 2;
|
|
61
|
+
offset += 2;
|
|
62
|
+
}
|
|
63
|
+
if (flags & IndoorBikeDataFlag.AverageCadencePresent) {
|
|
64
|
+
this.data.averageCadence = data.readUInt16LE(offset);
|
|
65
|
+
offset += 2;
|
|
66
|
+
}
|
|
67
|
+
if (flags & IndoorBikeDataFlag.TotalDistancePresent) {
|
|
68
|
+
this.data.totalDistance = data.readUInt16LE(offset);
|
|
69
|
+
offset += 2;
|
|
70
|
+
}
|
|
71
|
+
if (flags & IndoorBikeDataFlag.ResistanceLevelPresent) {
|
|
72
|
+
this.data.resistanceLevel = data.readUInt16LE(offset);
|
|
73
|
+
offset += 2;
|
|
74
|
+
}
|
|
75
|
+
if (flags & IndoorBikeDataFlag.InstantaneousPowerPresent) {
|
|
76
|
+
this.data.instantaneousPower = data.readUInt16LE(offset);
|
|
77
|
+
offset += 2;
|
|
78
|
+
}
|
|
79
|
+
if (flags & IndoorBikeDataFlag.AveragePowerPresent) {
|
|
80
|
+
this.data.averagePower = data.readUInt16LE(offset);
|
|
81
|
+
offset += 2;
|
|
82
|
+
}
|
|
83
|
+
if (flags & IndoorBikeDataFlag.ExpendedEnergyPresent) {
|
|
84
|
+
this.data.expendedEnergy = data.readUInt16LE(offset);
|
|
85
|
+
offset += 2;
|
|
86
|
+
}
|
|
87
|
+
if (flags & IndoorBikeDataFlag.HeartRatePresent) {
|
|
88
|
+
this.data.heartrate = data.readUInt16LE(offset);
|
|
89
|
+
offset += 2;
|
|
90
|
+
}
|
|
91
|
+
if (flags & IndoorBikeDataFlag.MetabolicEquivalentPresent) {
|
|
92
|
+
this.data.metabolicEquivalent = data.readUInt16LE(offset);
|
|
93
|
+
offset += 2;
|
|
94
|
+
}
|
|
95
|
+
if (flags & IndoorBikeDataFlag.ElapsedTimePresent) {
|
|
96
|
+
this.data.time = data.readUInt16LE(offset);
|
|
97
|
+
offset += 2;
|
|
98
|
+
}
|
|
99
|
+
if (flags & IndoorBikeDataFlag.RemainingTimePresent) {
|
|
100
|
+
this.data.remainingTime = data.readUInt16LE(offset);
|
|
101
|
+
offset += 2;
|
|
102
|
+
}
|
|
103
|
+
return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
|
|
104
|
+
}
|
|
105
|
+
onData(characteristic, data) {
|
|
106
|
+
console.log(characteristic.toLocaleLowerCase(), data);
|
|
107
|
+
if (characteristic.toLocaleLowerCase() === '2ad2') {
|
|
108
|
+
const res = this.parseIndoorBikeData(data);
|
|
109
|
+
this.emit('data', res);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
write(characteristic, data) {
|
|
113
|
+
console.log('write', characteristic, data);
|
|
114
|
+
return Promise.resolve(true);
|
|
115
|
+
}
|
|
116
|
+
read(characteristic) {
|
|
117
|
+
console.log('read', characteristic);
|
|
118
|
+
return Promise.resolve(Buffer.from([]));
|
|
119
|
+
}
|
|
120
|
+
reset() {
|
|
121
|
+
this.data = {};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.default = BleFitnessMachineDevice;
|
|
125
|
+
BleFitnessMachineDevice.services = ['1826'];
|
|
126
|
+
BleFitnessMachineDevice.characteristics = ['2acc', '2ad2', '2ad6', '2ad8', '2ad9', '2ada'];
|
|
127
|
+
ble_interface_1.default.register('BleFitnessMachineDevice', 'fm', BleFitnessMachineDevice, BleFitnessMachineDevice.services);
|
|
128
|
+
class FmAdapter extends Device_1.default {
|
|
129
|
+
constructor(device, protocol) {
|
|
130
|
+
super(protocol);
|
|
131
|
+
this.ignore = false;
|
|
132
|
+
this.paused = false;
|
|
133
|
+
this.distanceInternal = 0;
|
|
134
|
+
this.device = device;
|
|
135
|
+
this.ble = protocol.ble;
|
|
136
|
+
this.mode = this.getDefaultCyclingMode();
|
|
137
|
+
this.logger = new gd_eventlog_1.EventLogger('BLE-FM');
|
|
138
|
+
}
|
|
139
|
+
isBike() { return true; }
|
|
140
|
+
isHrm() { return false; }
|
|
141
|
+
isPower() { return true; }
|
|
142
|
+
getProfile() {
|
|
143
|
+
return 'Power Meter';
|
|
144
|
+
}
|
|
145
|
+
getName() {
|
|
146
|
+
return `${this.device.name}`;
|
|
147
|
+
}
|
|
148
|
+
getDisplayName() {
|
|
149
|
+
return this.getName();
|
|
150
|
+
}
|
|
151
|
+
getCyclingMode() {
|
|
152
|
+
if (!this.mode)
|
|
153
|
+
this.mode = this.getDefaultCyclingMode();
|
|
154
|
+
return this.mode;
|
|
155
|
+
}
|
|
156
|
+
getDefaultCyclingMode() {
|
|
157
|
+
return new power_meter_1.default(this);
|
|
158
|
+
}
|
|
159
|
+
getPort() {
|
|
160
|
+
return 'ble';
|
|
161
|
+
}
|
|
162
|
+
setIgnoreBike(ignore) {
|
|
163
|
+
this.ignore = ignore;
|
|
164
|
+
}
|
|
165
|
+
setIgnorePower(ignore) {
|
|
166
|
+
this.ignore = ignore;
|
|
167
|
+
}
|
|
168
|
+
onDeviceData(deviceData) {
|
|
169
|
+
if (this.prevDataTS && Date.now() - this.prevDataTS < 1000)
|
|
170
|
+
return;
|
|
171
|
+
this.prevDataTS = Date.now();
|
|
172
|
+
this.logger.logEvent({ message: 'onDeviceData', data: deviceData });
|
|
173
|
+
let incyclistData = this.mapData(deviceData);
|
|
174
|
+
incyclistData = this.getCyclingMode().updateData(incyclistData);
|
|
175
|
+
const data = this.transformData(incyclistData);
|
|
176
|
+
if (this.onDataFn && !this.ignore && !this.paused)
|
|
177
|
+
this.onDataFn(data);
|
|
178
|
+
}
|
|
179
|
+
mapData(deviceData) {
|
|
180
|
+
const data = {
|
|
181
|
+
isPedalling: false,
|
|
182
|
+
power: 0,
|
|
183
|
+
pedalRpm: undefined,
|
|
184
|
+
speed: 0,
|
|
185
|
+
heartrate: 0,
|
|
186
|
+
distanceInternal: 0,
|
|
187
|
+
slope: undefined,
|
|
188
|
+
time: undefined
|
|
189
|
+
};
|
|
190
|
+
data.power = (deviceData.instantaneousPower !== undefined ? deviceData.instantaneousPower : data.power);
|
|
191
|
+
data.pedalRpm = (deviceData.cadence !== undefined ? deviceData.cadence : data.pedalRpm);
|
|
192
|
+
data.time = (deviceData.time !== undefined ? deviceData.time : data.time);
|
|
193
|
+
data.isPedalling = data.pedalRpm > 0 || (data.pedalRpm === undefined && data.power > 0);
|
|
194
|
+
return data;
|
|
195
|
+
}
|
|
196
|
+
transformData(bikeData) {
|
|
197
|
+
if (this.ignore) {
|
|
198
|
+
return {};
|
|
199
|
+
}
|
|
200
|
+
if (bikeData === undefined)
|
|
201
|
+
return;
|
|
202
|
+
let distance = 0;
|
|
203
|
+
if (this.distanceInternal !== undefined && bikeData.distanceInternal !== undefined) {
|
|
204
|
+
distance = Math.round(bikeData.distanceInternal - this.distanceInternal);
|
|
205
|
+
}
|
|
206
|
+
if (bikeData.distanceInternal !== undefined)
|
|
207
|
+
this.distanceInternal = bikeData.distanceInternal;
|
|
208
|
+
let data = {
|
|
209
|
+
speed: bikeData.speed,
|
|
210
|
+
slope: bikeData.slope,
|
|
211
|
+
power: bikeData.power !== undefined ? Math.round(bikeData.power) : undefined,
|
|
212
|
+
cadence: bikeData.pedalRpm !== undefined ? Math.round(bikeData.pedalRpm) : undefined,
|
|
213
|
+
distance,
|
|
214
|
+
timestamp: Date.now()
|
|
215
|
+
};
|
|
216
|
+
return data;
|
|
217
|
+
}
|
|
218
|
+
start(props) {
|
|
219
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
220
|
+
this.logger.logEvent({ message: 'start requested', profile: this.getProfile(), props });
|
|
221
|
+
try {
|
|
222
|
+
const bleDevice = yield this.ble.connectDevice(this.device);
|
|
223
|
+
if (bleDevice) {
|
|
224
|
+
this.device = bleDevice;
|
|
225
|
+
bleDevice.on('data', (data) => {
|
|
226
|
+
this.onDeviceData(data);
|
|
227
|
+
});
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
this.logger.logEvent({ message: 'start result: error', error: err.message, profile: this.getProfile() });
|
|
233
|
+
throw new Error(`could not start device, reason:${err.message}`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
stop() {
|
|
238
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
239
|
+
this.logger.logEvent({ message: 'stop requested', profile: this.getProfile() });
|
|
240
|
+
this.distanceInternal = 0;
|
|
241
|
+
this.device.reset();
|
|
242
|
+
return this.device.disconnect();
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
pause() { this.paused = true; return Promise.resolve(true); }
|
|
246
|
+
resume() { this.paused = false; return Promise.resolve(true); }
|
|
247
|
+
}
|
|
248
|
+
exports.FmAdapter = FmAdapter;
|
|
@@ -24,9 +24,10 @@ const DeviceProtocol_1 = __importStar(require("../DeviceProtocol"));
|
|
|
24
24
|
const DeviceRegistry_1 = __importDefault(require("../DeviceRegistry"));
|
|
25
25
|
const ble_1 = require("./ble");
|
|
26
26
|
const ble_interface_1 = __importDefault(require("./ble-interface"));
|
|
27
|
+
const fm_1 = __importStar(require("./fm"));
|
|
27
28
|
const hrm_1 = __importStar(require("./hrm"));
|
|
28
29
|
const pwr_1 = __importStar(require("./pwr"));
|
|
29
|
-
const supportedDeviceTypes = [hrm_1.default, pwr_1.default];
|
|
30
|
+
const supportedDeviceTypes = [hrm_1.default, pwr_1.default, fm_1.default];
|
|
30
31
|
class BleProtocol extends DeviceProtocol_1.default {
|
|
31
32
|
constructor(binding) {
|
|
32
33
|
super();
|
|
@@ -58,6 +59,10 @@ class BleProtocol extends DeviceProtocol_1.default {
|
|
|
58
59
|
case 'hr':
|
|
59
60
|
case 'heartrate monitor':
|
|
60
61
|
return new hrm_1.HrmAdapter(fromDevice ? bleDevice : new hrm_1.default(props()), this);
|
|
62
|
+
case 'fm':
|
|
63
|
+
case 'smart trainer':
|
|
64
|
+
case 'fitness machine':
|
|
65
|
+
return new fm_1.FmAdapter(fromDevice ? bleDevice : new fm_1.default(props()), this);
|
|
61
66
|
case 'cp':
|
|
62
67
|
case 'power meter':
|
|
63
68
|
return new pwr_1.PwrAdapter(fromDevice ? bleDevice : new pwr_1.default(props()), this);
|