incyclist-devices 1.4.37 → 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.d.ts +1 -6
- package/lib/ble/ble-device.js +59 -14
- package/lib/ble/ble-interface.d.ts +6 -0
- package/lib/ble/ble-interface.js +229 -79
- package/lib/ble/ble.d.ts +9 -0
- package/lib/ble/ble.js +10 -0
- package/lib/ble/fm.d.ts +78 -0
- package/lib/ble/fm.js +248 -0
- package/lib/ble/hrm.js +3 -2
- package/lib/ble/incyclist-protocol.js +6 -1
- package/lib/ble/pwr.d.ts +1 -0
- package/lib/ble/pwr.js +7 -3
- 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.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { EventLogger } from "gd-eventlog";
|
|
3
3
|
import { BleInterfaceClass, BleDeviceClass, BlePeripheral, BleDeviceProps, ConnectProps } from "./ble";
|
|
4
|
-
interface ConnectState {
|
|
5
|
-
isConnecting: boolean;
|
|
6
|
-
isConnected: boolean;
|
|
7
|
-
isDisconnecting: boolean;
|
|
8
|
-
}
|
|
9
4
|
interface BleDeviceConstructProps extends BleDeviceProps {
|
|
10
5
|
log?: boolean;
|
|
11
6
|
logger?: EventLogger;
|
|
@@ -19,13 +14,13 @@ export declare abstract class BleDevice extends BleDeviceClass {
|
|
|
19
14
|
peripheral?: BlePeripheral;
|
|
20
15
|
characteristics: any[];
|
|
21
16
|
state?: string;
|
|
22
|
-
connectState: ConnectState;
|
|
23
17
|
logger?: EventLogger;
|
|
24
18
|
constructor(props?: BleDeviceConstructProps);
|
|
25
19
|
logEvent(event: any): void;
|
|
26
20
|
setInterface(ble: BleInterfaceClass): void;
|
|
27
21
|
private cleanupListeners;
|
|
28
22
|
private onDisconnect;
|
|
23
|
+
waitForConnectFinished(timeout: any): Promise<unknown>;
|
|
29
24
|
connect(props?: ConnectProps): Promise<boolean>;
|
|
30
25
|
disconnect(): Promise<boolean>;
|
|
31
26
|
abstract getProfile(): string;
|
package/lib/ble/ble-device.js
CHANGED
|
@@ -11,11 +11,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const gd_eventlog_1 = require("gd-eventlog");
|
|
13
13
|
const ble_1 = require("./ble");
|
|
14
|
+
const CONNECT_WAIT_TIMEOUT = 10000;
|
|
14
15
|
class BleDevice extends ble_1.BleDeviceClass {
|
|
15
16
|
constructor(props) {
|
|
16
17
|
super();
|
|
17
18
|
this.characteristics = [];
|
|
18
|
-
this.connectState = { isConnecting: false, isConnected: false, isDisconnecting: false };
|
|
19
19
|
this.id = props.id;
|
|
20
20
|
this.address = props.address;
|
|
21
21
|
this.name = props.name;
|
|
@@ -43,7 +43,7 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
43
43
|
this.logger.logEvent(event);
|
|
44
44
|
}
|
|
45
45
|
if (process.env.BLE_DEBUG) {
|
|
46
|
-
console.log(event);
|
|
46
|
+
console.log('~~~BLE:', event);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
setInterface(ble) {
|
|
@@ -55,9 +55,9 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
55
55
|
}
|
|
56
56
|
else {
|
|
57
57
|
this.characteristics.forEach(c => {
|
|
58
|
+
c.unsubscribe();
|
|
58
59
|
c.removeAllListeners('data');
|
|
59
60
|
});
|
|
60
|
-
this.characteristics = [];
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
onDisconnect() {
|
|
@@ -67,6 +67,27 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
67
67
|
}
|
|
68
68
|
this.emit('disconnected');
|
|
69
69
|
}
|
|
70
|
+
waitForConnectFinished(timeout) {
|
|
71
|
+
const waitStart = Date.now();
|
|
72
|
+
const waitTimeout = waitStart + timeout;
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const waitIv = setInterval(() => {
|
|
75
|
+
try {
|
|
76
|
+
if (this.connectState.isConnecting && Date.now() > waitTimeout) {
|
|
77
|
+
clearInterval(waitIv);
|
|
78
|
+
return reject(new Error('connection already in progress'));
|
|
79
|
+
}
|
|
80
|
+
if (!this.connectState.isConnecting) {
|
|
81
|
+
clearInterval(waitIv);
|
|
82
|
+
return resolve(true);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.log('~~~ error', err);
|
|
87
|
+
}
|
|
88
|
+
}, 100);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
70
91
|
connect(props) {
|
|
71
92
|
return __awaiter(this, void 0, void 0, function* () {
|
|
72
93
|
const connectPeripheral = (peripheral) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -80,30 +101,36 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
80
101
|
this.logEvent({ message: 'cannot connect', error: err.message || err });
|
|
81
102
|
}
|
|
82
103
|
}
|
|
83
|
-
this.connectState.isConnecting = false;
|
|
84
|
-
this.connectState.isConnected = true;
|
|
85
|
-
this.state = "connected";
|
|
86
|
-
this.emit('connected');
|
|
87
|
-
this.cleanupListeners();
|
|
88
|
-
this.ble.addConnectedDevice(this);
|
|
89
|
-
this.peripheral.once('disconnect', () => { this.onDisconnect(); });
|
|
90
104
|
try {
|
|
105
|
+
this.cleanupListeners();
|
|
91
106
|
if (!connected) {
|
|
92
|
-
|
|
107
|
+
this.logEvent({ message: 'connect: discover characteristics start' });
|
|
108
|
+
const res = yield peripheral.discoverSomeServicesAndCharacteristicsAsync([], []);
|
|
109
|
+
const { characteristics } = res;
|
|
110
|
+
this.logEvent({ message: 'connect: discover characteristics result',
|
|
111
|
+
result: characteristics.map(c => ({ uuid: ble_1.uuid(c.uuid), properties: c.properties.join(','), service: ble_1.uuid(c._serviceUuid) }))
|
|
112
|
+
});
|
|
93
113
|
this.characteristics = characteristics;
|
|
94
114
|
}
|
|
95
115
|
else {
|
|
96
116
|
this.characteristics = connected.characteristics;
|
|
97
117
|
}
|
|
118
|
+
this.connectState.isConnecting = false;
|
|
119
|
+
this.connectState.isConnected = true;
|
|
120
|
+
this.state = "connected";
|
|
121
|
+
this.emit('connected');
|
|
122
|
+
this.ble.addConnectedDevice(this);
|
|
123
|
+
this.peripheral.once('disconnect', () => { this.onDisconnect(); });
|
|
98
124
|
this.characteristics.forEach(c => {
|
|
99
125
|
if (c.properties.find(p => p === 'notify')) {
|
|
100
126
|
c.on('data', (data, _isNotification) => {
|
|
101
127
|
this.onData(ble_1.uuid(c.uuid), data);
|
|
102
128
|
});
|
|
103
129
|
if (!connected) {
|
|
130
|
+
this.logEvent({ message: 'subscribe', device: this.name, address: this.address, service: c._serviceUuid, characteristic: c.uuid });
|
|
104
131
|
c.subscribe((err) => {
|
|
105
132
|
if (err)
|
|
106
|
-
this.logEvent({ message: 'cannot subscribe', error: err.message || err });
|
|
133
|
+
this.logEvent({ message: 'cannot subscribe', device: this.name, address: this.address, service: c._serviceUuid, characteristic: c.uuid, error: err.message || err });
|
|
107
134
|
});
|
|
108
135
|
}
|
|
109
136
|
}
|
|
@@ -111,9 +138,25 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
111
138
|
}
|
|
112
139
|
catch (err) {
|
|
113
140
|
this.logEvent({ message: 'cannot connect', error: err.message || err });
|
|
141
|
+
this.connectState.isConnecting = false;
|
|
142
|
+
this.connectState.isConnected = false;
|
|
114
143
|
}
|
|
115
144
|
});
|
|
116
145
|
try {
|
|
146
|
+
if (this.connectState.isConnecting) {
|
|
147
|
+
yield this.waitForConnectFinished(CONNECT_WAIT_TIMEOUT);
|
|
148
|
+
}
|
|
149
|
+
if (this.connectState.isConnected) {
|
|
150
|
+
this.characteristics.forEach(c => {
|
|
151
|
+
if (c.properties.find(p => p === 'notify')) {
|
|
152
|
+
c.on('data', (data, _isNotification) => {
|
|
153
|
+
this.onData(ble_1.uuid(c.uuid), data);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
this.connectState.isConnecting = true;
|
|
117
160
|
if (this.peripheral) {
|
|
118
161
|
const { id, address, advertisement } = this.peripheral;
|
|
119
162
|
const name = advertisement === null || advertisement === void 0 ? void 0 : advertisement.localName;
|
|
@@ -124,8 +167,8 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
124
167
|
}
|
|
125
168
|
else {
|
|
126
169
|
const { id, name, address } = this;
|
|
170
|
+
let error;
|
|
127
171
|
if (this.address || this.id || this.name) {
|
|
128
|
-
this.connectState.isConnecting = true;
|
|
129
172
|
this.logEvent({ message: 'connect requested', mode: 'device', device: { id, name, address } });
|
|
130
173
|
try {
|
|
131
174
|
if (this.ble.isScanning()) {
|
|
@@ -140,9 +183,11 @@ class BleDevice extends ble_1.BleDeviceClass {
|
|
|
140
183
|
}
|
|
141
184
|
}
|
|
142
185
|
catch (err) {
|
|
186
|
+
console.log('~~~ error', err);
|
|
187
|
+
error = err;
|
|
143
188
|
}
|
|
144
189
|
}
|
|
145
|
-
this.logEvent({ message: 'connect result: failure', mode: 'device', device: { id, name, address } });
|
|
190
|
+
this.logEvent({ message: 'connect result: failure', mode: 'device', device: { id, name, address }, error: error.message, stack: error.stack });
|
|
146
191
|
this.connectState.isConnecting = false;
|
|
147
192
|
this.connectState.isConnected = false;
|
|
148
193
|
return false;
|
|
@@ -3,6 +3,8 @@ import { EventLogger } from 'gd-eventlog';
|
|
|
3
3
|
import { BleInterfaceClass, ConnectProps, ScanProps, BleDeviceClass, BlePeripheral, BleBinding } from './ble';
|
|
4
4
|
export interface ScanState {
|
|
5
5
|
isScanning: boolean;
|
|
6
|
+
isConnecting: boolean;
|
|
7
|
+
isBackgroundScan: boolean;
|
|
6
8
|
timeout?: NodeJS.Timeout;
|
|
7
9
|
}
|
|
8
10
|
export interface ConnectState {
|
|
@@ -26,6 +28,7 @@ export default class BleInterface extends BleInterfaceClass {
|
|
|
26
28
|
connectState: ConnectState;
|
|
27
29
|
devices: BleDeviceInfo[];
|
|
28
30
|
logger: EventLogger;
|
|
31
|
+
deviceCache: any[];
|
|
29
32
|
static deviceClasses: BleDeviceClassInfo[];
|
|
30
33
|
static _instance: BleInterface;
|
|
31
34
|
static getInstance(props?: {
|
|
@@ -48,7 +51,10 @@ export default class BleInterface extends BleInterfaceClass {
|
|
|
48
51
|
getDevicesFromServices(deviceTypes: (typeof BleDeviceClass)[], services: string | string[]): (typeof BleDeviceClass)[];
|
|
49
52
|
getServicesFromDeviceTypes(deviceTypes: (typeof BleDeviceClass)[]): string[];
|
|
50
53
|
getServicesFromDevice(device: BleDeviceClass): string[];
|
|
54
|
+
waitForConnectFinished(timeout: any): Promise<unknown>;
|
|
51
55
|
connectDevice(requested: BleDeviceClass, timeout?: number): Promise<BleDeviceClass>;
|
|
56
|
+
waitForScanFinished(timeout: any): Promise<unknown>;
|
|
57
|
+
addPeripheralToCache(peripheral: BlePeripheral): void;
|
|
52
58
|
scan(props: ScanProps): Promise<BleDeviceClass[]>;
|
|
53
59
|
stopScan(): Promise<boolean>;
|
|
54
60
|
isScanning(): boolean;
|
package/lib/ble/ble-interface.js
CHANGED
|
@@ -12,12 +12,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
const gd_eventlog_1 = require("gd-eventlog");
|
|
13
13
|
const utils_1 = require("../utils");
|
|
14
14
|
const ble_1 = require("./ble");
|
|
15
|
+
const CONNECT_TIMEOUT = 5000;
|
|
15
16
|
class BleInterface extends ble_1.BleInterfaceClass {
|
|
16
17
|
constructor(props = {}) {
|
|
17
18
|
super(props);
|
|
18
|
-
this.scanState = { isScanning: false, timeout: undefined };
|
|
19
|
+
this.scanState = { isScanning: false, isConnecting: false, timeout: undefined, isBackgroundScan: false };
|
|
19
20
|
this.connectState = { isConnecting: false, isConnected: false, isInitSuccess: false };
|
|
20
21
|
this.devices = [];
|
|
22
|
+
this.deviceCache = [];
|
|
21
23
|
if (props.logger)
|
|
22
24
|
this.logger = props.logger;
|
|
23
25
|
else if (props.log) {
|
|
@@ -51,7 +53,7 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
51
53
|
this.logger.logEvent(event);
|
|
52
54
|
}
|
|
53
55
|
if (process.env.BLE_DEBUG) {
|
|
54
|
-
console.log(event);
|
|
56
|
+
console.log('~~BLE:', event);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
onStateChange(state) {
|
|
@@ -67,11 +69,21 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
67
69
|
}
|
|
68
70
|
connect(props = {}) {
|
|
69
71
|
const timeout = props.timeout || 2000;
|
|
72
|
+
const runBackgroundScan = () => {
|
|
73
|
+
this.scanState.isBackgroundScan = true;
|
|
74
|
+
this.scan({ timeout: 5000, isBackgroundScan: true })
|
|
75
|
+
.then(() => {
|
|
76
|
+
this.scanState.isBackgroundScan = false;
|
|
77
|
+
})
|
|
78
|
+
.catch(() => {
|
|
79
|
+
this.scanState.isBackgroundScan = false;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
70
82
|
return new Promise((resolve, reject) => {
|
|
71
83
|
if (this.connectState.isConnected) {
|
|
72
84
|
return resolve(true);
|
|
73
85
|
}
|
|
74
|
-
this.logEvent({ message: 'connect request' });
|
|
86
|
+
this.logEvent({ message: 'connect request', });
|
|
75
87
|
if (!this.getBinding())
|
|
76
88
|
return Promise.reject(new Error('no binding defined'));
|
|
77
89
|
this.connectState.timeout = setTimeout(() => {
|
|
@@ -114,7 +126,9 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
114
126
|
this.connectState.isConnected = true;
|
|
115
127
|
this.connectState.isConnecting = false;
|
|
116
128
|
this.logEvent({ message: 'connect result: success' });
|
|
117
|
-
|
|
129
|
+
resolve(true);
|
|
130
|
+
runBackgroundScan();
|
|
131
|
+
return;
|
|
118
132
|
}
|
|
119
133
|
else {
|
|
120
134
|
this.getBinding().once('error', (err) => {
|
|
@@ -133,6 +147,7 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
133
147
|
this.connectState.isConnected = true;
|
|
134
148
|
this.connectState.isConnecting = false;
|
|
135
149
|
this.logEvent({ message: 'connect result: success' });
|
|
150
|
+
runBackgroundScan();
|
|
136
151
|
return resolve(true);
|
|
137
152
|
}
|
|
138
153
|
else {
|
|
@@ -229,16 +244,37 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
229
244
|
});
|
|
230
245
|
return services;
|
|
231
246
|
}
|
|
232
|
-
|
|
247
|
+
waitForConnectFinished(timeout) {
|
|
248
|
+
const waitStart = Date.now();
|
|
249
|
+
const waitTimeout = waitStart + timeout;
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
const waitIv = setInterval(() => {
|
|
252
|
+
if (this.scanState.isConnecting && Date.now() > waitTimeout) {
|
|
253
|
+
clearInterval(waitIv);
|
|
254
|
+
return reject(new Error('Connecting already in progress'));
|
|
255
|
+
}
|
|
256
|
+
if (!this.scanState.isConnecting) {
|
|
257
|
+
clearInterval(waitIv);
|
|
258
|
+
return resolve(true);
|
|
259
|
+
}
|
|
260
|
+
}, 100);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
connectDevice(requested, timeout = CONNECT_TIMEOUT) {
|
|
233
264
|
return __awaiter(this, void 0, void 0, function* () {
|
|
234
|
-
const { id, name, address } = requested;
|
|
235
|
-
|
|
265
|
+
const { id, name, address, getProfile } = requested;
|
|
266
|
+
const profile = getProfile && typeof (getProfile) === 'function' ? getProfile() : undefined;
|
|
267
|
+
this.logEvent({ message: 'connectDevice', id, name, address, profile, isbusy: this.scanState.isConnecting });
|
|
268
|
+
if (this.scanState.isConnecting) {
|
|
269
|
+
yield this.waitForConnectFinished(10000);
|
|
270
|
+
}
|
|
271
|
+
this.scanState.isConnecting = true;
|
|
236
272
|
let devices = [];
|
|
237
273
|
let retry = false;
|
|
238
274
|
let retryCount = 0;
|
|
239
275
|
do {
|
|
240
276
|
if (retryCount > 0) {
|
|
241
|
-
this.logEvent({ message: 'retry connect device', retryCount });
|
|
277
|
+
this.logEvent({ message: 'retry connect device', id, name, address, profile, retryCount });
|
|
242
278
|
}
|
|
243
279
|
try {
|
|
244
280
|
devices = yield this.scan({ timeout, device: requested });
|
|
@@ -249,28 +285,66 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
249
285
|
}
|
|
250
286
|
catch (err) {
|
|
251
287
|
if (err.message === 'scanning already in progress') {
|
|
252
|
-
this.logEvent({ message: 'scan busy' });
|
|
253
288
|
yield utils_1.sleep(1000);
|
|
254
289
|
retryCount++;
|
|
255
290
|
retry = retryCount < 5;
|
|
256
291
|
continue;
|
|
257
292
|
}
|
|
293
|
+
this.scanState.isConnecting = false;
|
|
258
294
|
throw err;
|
|
259
295
|
}
|
|
260
296
|
} while (devices.length === 0 && retry);
|
|
261
|
-
if (devices.length === 0)
|
|
297
|
+
if (devices.length === 0) {
|
|
298
|
+
this.logEvent({ message: 'connectDevice failure', id, name, address, profile, error: 'device not found' });
|
|
299
|
+
this.scanState.isConnecting = false;
|
|
262
300
|
throw new Error('device not found');
|
|
301
|
+
}
|
|
263
302
|
if (devices[0]) {
|
|
303
|
+
this.logEvent({ message: 'connectDevice connecting', id, name, address, profile });
|
|
264
304
|
const connected = yield devices[0].connect();
|
|
305
|
+
this.scanState.isConnecting = false;
|
|
265
306
|
if (connected) {
|
|
307
|
+
this.logEvent({ message: 'connectDevice success', id, name, address, profile });
|
|
266
308
|
return devices[0];
|
|
267
309
|
}
|
|
268
310
|
else {
|
|
311
|
+
this.logEvent({ message: 'connectDevice failure', id, name, address, profile });
|
|
269
312
|
throw new Error('connect failed');
|
|
270
313
|
}
|
|
271
314
|
}
|
|
272
315
|
});
|
|
273
316
|
}
|
|
317
|
+
waitForScanFinished(timeout) {
|
|
318
|
+
const waitStart = Date.now();
|
|
319
|
+
const waitTimeout = waitStart + timeout;
|
|
320
|
+
return new Promise((resolve, reject) => {
|
|
321
|
+
const waitIv = setInterval(() => {
|
|
322
|
+
if (this.scanState.isScanning && Date.now() > waitTimeout) {
|
|
323
|
+
clearInterval(waitIv);
|
|
324
|
+
return reject(new Error('scanning already in progress'));
|
|
325
|
+
}
|
|
326
|
+
if (!this.scanState.isScanning) {
|
|
327
|
+
clearInterval(waitIv);
|
|
328
|
+
return resolve(true);
|
|
329
|
+
}
|
|
330
|
+
}, 100);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
addPeripheralToCache(peripheral) {
|
|
334
|
+
try {
|
|
335
|
+
this.logEvent({ message: 'adding device to cache', device: { address: peripheral.address, name: peripheral.advertisement ? peripheral.advertisement.localName : '' } });
|
|
336
|
+
const existing = this.deviceCache.find(p => p.address === peripheral.address);
|
|
337
|
+
if (!existing)
|
|
338
|
+
this.deviceCache.push(peripheral);
|
|
339
|
+
else {
|
|
340
|
+
if (peripheral.advertisement && peripheral.advertisement.localName !== '' && existing.advertisement && existing.advertisement.localName === '')
|
|
341
|
+
existing.advertisement.localName = peripheral.advertisement.localName;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
console.log('~~~ error', err);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
274
348
|
scan(props) {
|
|
275
349
|
return __awaiter(this, void 0, void 0, function* () {
|
|
276
350
|
const { timeout = 5000, deviceTypes = [], device } = props;
|
|
@@ -282,99 +356,175 @@ class BleInterface extends ble_1.BleInterfaceClass {
|
|
|
282
356
|
if (!this.isConnected()) {
|
|
283
357
|
yield this.connect();
|
|
284
358
|
}
|
|
359
|
+
this.logEvent({ message: 'scan()', props, scanState: this.scanState, cache: this.deviceCache.map(p => ({ name: p.advertisement ? p.advertisement.localName : '', address: p.address })) });
|
|
360
|
+
if (!props.isBackgroundScan && this.scanState.isBackgroundScan) {
|
|
361
|
+
yield this.stopScan();
|
|
362
|
+
this.scanState.isBackgroundScan = false;
|
|
363
|
+
}
|
|
285
364
|
const detectedPeripherals = {};
|
|
365
|
+
let opStr;
|
|
286
366
|
if (scanForDevice) {
|
|
367
|
+
opStr = 'search device';
|
|
287
368
|
const { id, address, name } = device;
|
|
288
369
|
this.logEvent({ message: 'search device request', device: { id, address, name }, deviceTypes });
|
|
289
370
|
}
|
|
290
|
-
else
|
|
371
|
+
else {
|
|
372
|
+
opStr = 'scan';
|
|
291
373
|
this.logEvent({ message: 'scan start', services });
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
374
|
+
}
|
|
375
|
+
if (this.scanState.isScanning) {
|
|
376
|
+
try {
|
|
377
|
+
yield this.waitForScanFinished(timeout);
|
|
296
378
|
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
this.logEvent({ message: `${opStr} result: already scanning` });
|
|
381
|
+
return Promise.reject(err);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
297
385
|
this.scanState.isScanning = true;
|
|
298
386
|
if (scanForDevice && device instanceof ble_1.BleDeviceClass) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.logEvent({ message:
|
|
303
|
-
|
|
387
|
+
if (this.devices && this.devices.length > 0) {
|
|
388
|
+
const connectedDevices = this.devices.map(i => ({ name: i.device.name, address: i.device.address, isConnected: i.isConnected, connectState: i.device.getConnectState() }));
|
|
389
|
+
const { name, address } = device;
|
|
390
|
+
this.logEvent({ message: `${opStr}: check if already registered`, device: { name, address }, connectedDevices });
|
|
391
|
+
const existing = this.devices.find(i => (i.device.address === device.address || i.device.name === device.name));
|
|
392
|
+
if (existing) {
|
|
393
|
+
const d = device;
|
|
394
|
+
const linkedDevice = existing.device;
|
|
395
|
+
d.peripheral = existing.device.peripheral;
|
|
396
|
+
if (d.setInterface && typeof (d.setInterface) === 'function')
|
|
397
|
+
d.setInterface(this);
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
let connectState = linkedDevice.getConnectState();
|
|
400
|
+
this.logEvent({ message: `${opStr}: device already registered`, device: device.name, address: device.address, connectState });
|
|
401
|
+
if (connectState.isConnecting) {
|
|
402
|
+
const waitStart = Date.now();
|
|
403
|
+
const waitTimeout = waitStart + timeout;
|
|
404
|
+
const waitIv = setInterval(() => {
|
|
405
|
+
try {
|
|
406
|
+
connectState = linkedDevice.getConnectState();
|
|
407
|
+
if (connectState.isConnecting && Date.now() > waitTimeout) {
|
|
408
|
+
clearInterval(waitIv);
|
|
409
|
+
this.scanState.isScanning = false;
|
|
410
|
+
return resolve([]);
|
|
411
|
+
}
|
|
412
|
+
if (!connectState.isConnecting) {
|
|
413
|
+
clearInterval(waitIv);
|
|
414
|
+
this.scanState.isScanning = false;
|
|
415
|
+
return resolve([device]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
console.log('~~~ error', err);
|
|
420
|
+
}
|
|
421
|
+
}, 100);
|
|
422
|
+
}
|
|
423
|
+
else if (connectState.isConnected) {
|
|
424
|
+
this.scanState.isScanning = false;
|
|
425
|
+
resolve([device]);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
this.scanState.isScanning = false;
|
|
429
|
+
resolve([]);
|
|
430
|
+
}
|
|
431
|
+
}, 100);
|
|
432
|
+
}
|
|
304
433
|
}
|
|
305
434
|
}
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
this.logEvent({ message: '
|
|
309
|
-
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
435
|
+
const onPeripheralFound = (peripheral, fromCache = false) => {
|
|
436
|
+
if (fromCache)
|
|
437
|
+
this.logEvent({ message: 'adding from Cache', peripheral: peripheral.address });
|
|
438
|
+
if (!peripheral || !peripheral.advertisement)
|
|
439
|
+
return;
|
|
440
|
+
if (!detectedPeripherals[peripheral.id]) {
|
|
441
|
+
if (process.env.BLE_DEBUG)
|
|
442
|
+
console.log('discovered', peripheral);
|
|
443
|
+
detectedPeripherals[peripheral.id] = peripheral;
|
|
444
|
+
this.addPeripheralToCache(peripheral);
|
|
445
|
+
let DeviceClasses;
|
|
446
|
+
if (scanForDevice && (!deviceTypes || deviceTypes.length === 0)) {
|
|
447
|
+
const classes = BleInterface.deviceClasses.map(c => c.Class);
|
|
448
|
+
DeviceClasses = this.getDevicesFromServices(classes, peripheral.advertisement.serviceUuids);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
DeviceClasses = this.getDevicesFromServices(deviceTypes, peripheral.advertisement.serviceUuids);
|
|
452
|
+
}
|
|
453
|
+
DeviceClasses.forEach(DeviceClass => {
|
|
454
|
+
let cntFound = 0;
|
|
455
|
+
if (!DeviceClass)
|
|
456
|
+
return;
|
|
457
|
+
if (scanForDevice && cntFound > 0)
|
|
458
|
+
return;
|
|
459
|
+
const C = DeviceClass;
|
|
460
|
+
const d = new C({ peripheral });
|
|
461
|
+
if (device && device.getProfile && device.getProfile() !== d.getProfile())
|
|
462
|
+
return;
|
|
463
|
+
d.setInterface(this);
|
|
464
|
+
if (scanForDevice) {
|
|
465
|
+
if ((device.id && device.id !== '' && d.id === device.id) ||
|
|
466
|
+
(device.address && device.address !== '' && d.address === device.address) ||
|
|
467
|
+
(device.name && device.name !== '' && d.name === device.name))
|
|
468
|
+
cntFound++;
|
|
323
469
|
}
|
|
324
|
-
else
|
|
325
|
-
|
|
470
|
+
else
|
|
471
|
+
cntFound++;
|
|
472
|
+
const existing = this.devices.find(i => i.device.id === d.id && i.device.getProfile() === d.getProfile());
|
|
473
|
+
if (!scanForDevice && cntFound > 0 && !existing) {
|
|
474
|
+
this.logEvent({ message: `${opStr}: device found`, device: d.name, address: d.address, services: d.services.join(',') });
|
|
475
|
+
this.devices.push({ device: d, isConnected: false });
|
|
476
|
+
this.emit('device', d);
|
|
326
477
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
478
|
+
if (scanForDevice && cntFound > 0) {
|
|
479
|
+
if (fromCache) {
|
|
480
|
+
resolve([d]);
|
|
330
481
|
return;
|
|
331
|
-
if (scanForDevice && cntFound > 0)
|
|
332
|
-
return;
|
|
333
|
-
const C = DeviceClass;
|
|
334
|
-
const d = new C({ peripheral });
|
|
335
|
-
if (device.getProfile && device.getProfile() !== d.getProfile())
|
|
336
|
-
return;
|
|
337
|
-
d.setInterface(this);
|
|
338
|
-
if (scanForDevice) {
|
|
339
|
-
if ((device.id && device.id !== '' && d.id === device.id) ||
|
|
340
|
-
(device.address && device.address !== '' && d.address === device.address) ||
|
|
341
|
-
(device.name && device.name !== '' && d.name === device.name))
|
|
342
|
-
cntFound++;
|
|
343
482
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
if (scanForDevice && cntFound > 0) {
|
|
353
|
-
if (this.scanState.timeout) {
|
|
354
|
-
clearTimeout(this.scanState.timeout);
|
|
355
|
-
this.scanState.timeout = null;
|
|
356
|
-
bleBinding.stopScanning(() => {
|
|
357
|
-
this.getBinding().removeAllListeners('discover');
|
|
358
|
-
this.scanState.isScanning = false;
|
|
359
|
-
resolve([d]);
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
483
|
+
if (this.scanState.timeout) {
|
|
484
|
+
clearTimeout(this.scanState.timeout);
|
|
485
|
+
this.scanState.timeout = null;
|
|
486
|
+
this.logEvent({ message: `${opStr}: stop scanning`, requested: scanForDevice ? { name: device.name, address: device.address } : undefined, });
|
|
487
|
+
bleBinding.stopScanning(() => {
|
|
488
|
+
this.getBinding().removeAllListeners('discover');
|
|
489
|
+
this.scanState.isScanning = false;
|
|
363
490
|
resolve([d]);
|
|
364
|
-
}
|
|
491
|
+
});
|
|
365
492
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
493
|
+
else {
|
|
494
|
+
resolve([d]);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
this.logEvent({ message: `${opStr}: start scanning`, requested: scanForDevice ? { name: device.name, address: device.address } : undefined, timeout });
|
|
503
|
+
this.deviceCache.forEach(peripheral => {
|
|
504
|
+
onPeripheralFound(peripheral, true);
|
|
505
|
+
});
|
|
506
|
+
bleBinding.startScanning([], true, (err) => {
|
|
507
|
+
if (err) {
|
|
508
|
+
this.logEvent({ message: `${opStr} result: error`, requested: scanForDevice ? { name: device.name, address: device.address } : undefined, error: err.message });
|
|
509
|
+
this.scanState.isScanning = false;
|
|
510
|
+
return reject(err);
|
|
511
|
+
}
|
|
512
|
+
bleBinding.on('discover', (p) => {
|
|
513
|
+
console.log('~~~ discovered:', p.address, p.advertisement ? p.advertisement.localName : '');
|
|
514
|
+
onPeripheralFound(p);
|
|
370
515
|
});
|
|
371
516
|
});
|
|
372
517
|
this.scanState.timeout = setTimeout(() => {
|
|
373
518
|
this.scanState.timeout = null;
|
|
374
|
-
this.logEvent({ message:
|
|
519
|
+
this.logEvent({ message: `${opStr} result: devices found`, requested: scanForDevice ? { name: device.name, address: device.address } : undefined, devices: this.devices.map(i => i.device.name + (!i.device.name || i.device.name === '') ? `addr=${i.device.address}` : '') });
|
|
375
520
|
this.getBinding().removeAllListeners('discover');
|
|
521
|
+
this.logEvent({ message: `${opStr}: stop scanning`, requested: scanForDevice ? { name: device.name, address: device.address } : undefined, });
|
|
376
522
|
bleBinding.stopScanning(() => {
|
|
377
523
|
this.scanState.isScanning = false;
|
|
524
|
+
if (scanForDevice) {
|
|
525
|
+
reject(new Error('device not found'));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
378
528
|
resolve(this.devices.map(i => i.device));
|
|
379
529
|
});
|
|
380
530
|
}, timeout);
|
package/lib/ble/ble.d.ts
CHANGED
|
@@ -8,12 +8,20 @@ export interface BleDeviceIdentifier {
|
|
|
8
8
|
address?: string;
|
|
9
9
|
name?: string;
|
|
10
10
|
}
|
|
11
|
+
export interface ConnectState {
|
|
12
|
+
isConnecting: boolean;
|
|
13
|
+
isConnected: boolean;
|
|
14
|
+
isDisconnecting: boolean;
|
|
15
|
+
}
|
|
11
16
|
export declare abstract class BleDeviceClass extends EventEmitter {
|
|
12
17
|
static services: string[];
|
|
13
18
|
id?: string;
|
|
14
19
|
address?: string;
|
|
15
20
|
name?: string;
|
|
16
21
|
peripheral?: BlePeripheral;
|
|
22
|
+
connectState: ConnectState;
|
|
23
|
+
getConnectState(): ConnectState;
|
|
24
|
+
isConnected(): boolean;
|
|
17
25
|
abstract getProfile(): string;
|
|
18
26
|
abstract getServiceUUids(): string[];
|
|
19
27
|
abstract connect(props?: ConnectProps): Promise<boolean>;
|
|
@@ -29,6 +37,7 @@ export declare type ScanProps = {
|
|
|
29
37
|
timeout?: number;
|
|
30
38
|
deviceTypes?: (typeof BleDeviceClass)[];
|
|
31
39
|
device?: BleDeviceClass;
|
|
40
|
+
isBackgroundScan?: boolean;
|
|
32
41
|
};
|
|
33
42
|
export declare class BleBindingWrapper {
|
|
34
43
|
protected binding: BleBinding;
|
package/lib/ble/ble.js
CHANGED
|
@@ -5,6 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const events_1 = __importDefault(require("events"));
|
|
7
7
|
class BleDeviceClass extends events_1.default {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.connectState = { isConnecting: false, isConnected: false, isDisconnecting: false };
|
|
11
|
+
}
|
|
12
|
+
getConnectState() {
|
|
13
|
+
return this.connectState;
|
|
14
|
+
}
|
|
15
|
+
isConnected() {
|
|
16
|
+
return this.connectState.isConnected;
|
|
17
|
+
}
|
|
8
18
|
}
|
|
9
19
|
exports.BleDeviceClass = BleDeviceClass;
|
|
10
20
|
BleDeviceClass.services = [];
|
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;
|
package/lib/ble/hrm.js
CHANGED
|
@@ -99,7 +99,7 @@ class HrmAdapter extends Device_1.default {
|
|
|
99
99
|
}
|
|
100
100
|
start(props) {
|
|
101
101
|
return __awaiter(this, void 0, void 0, function* () {
|
|
102
|
-
this.logger.logEvent({ message: 'start requested', props });
|
|
102
|
+
this.logger.logEvent({ message: 'start requested', profile: this.getProfile(), props });
|
|
103
103
|
try {
|
|
104
104
|
const bleDevice = yield this.ble.connectDevice(this.device);
|
|
105
105
|
if (bleDevice) {
|
|
@@ -112,13 +112,14 @@ class HrmAdapter extends Device_1.default {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
catch (err) {
|
|
115
|
-
this.logger.logEvent({ message: 'start result: error', error: err.message });
|
|
115
|
+
this.logger.logEvent({ message: 'start result: error', error: err.message, profile: this.getProfile() });
|
|
116
116
|
throw new Error(`could not start device, reason:${err.message}`);
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
stop() {
|
|
121
121
|
return __awaiter(this, void 0, void 0, function* () {
|
|
122
|
+
this.logger.logEvent({ message: 'stop requested', profile: this.getProfile() });
|
|
122
123
|
return this.device.disconnect();
|
|
123
124
|
});
|
|
124
125
|
}
|
|
@@ -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);
|
package/lib/ble/pwr.d.ts
CHANGED
package/lib/ble/pwr.js
CHANGED
|
@@ -30,7 +30,7 @@ class BleCyclingPowerDevice extends ble_device_1.BleDevice {
|
|
|
30
30
|
this.prevCrankData = undefined;
|
|
31
31
|
}
|
|
32
32
|
getProfile() {
|
|
33
|
-
return '
|
|
33
|
+
return 'Power Meter';
|
|
34
34
|
}
|
|
35
35
|
getServiceUUids() {
|
|
36
36
|
return BleCyclingPowerDevice.services;
|
|
@@ -166,6 +166,9 @@ class PwrAdapter extends Device_1.default {
|
|
|
166
166
|
this.ignore = ignore;
|
|
167
167
|
}
|
|
168
168
|
onDeviceData(deviceData) {
|
|
169
|
+
if (this.prevDataTS && Date.now() - this.prevDataTS < 1000)
|
|
170
|
+
return;
|
|
171
|
+
this.prevDataTS = Date.now();
|
|
169
172
|
this.logger.logEvent({ message: 'onDeviceData', data: deviceData });
|
|
170
173
|
let incyclistData = this.mapData(deviceData);
|
|
171
174
|
incyclistData = this.getCyclingMode().updateData(incyclistData);
|
|
@@ -214,7 +217,7 @@ class PwrAdapter extends Device_1.default {
|
|
|
214
217
|
}
|
|
215
218
|
start(props) {
|
|
216
219
|
return __awaiter(this, void 0, void 0, function* () {
|
|
217
|
-
this.logger.logEvent({ message: 'start requested', props });
|
|
220
|
+
this.logger.logEvent({ message: 'start requested', profile: this.getProfile(), props });
|
|
218
221
|
try {
|
|
219
222
|
const bleDevice = yield this.ble.connectDevice(this.device);
|
|
220
223
|
if (bleDevice) {
|
|
@@ -226,13 +229,14 @@ class PwrAdapter extends Device_1.default {
|
|
|
226
229
|
}
|
|
227
230
|
}
|
|
228
231
|
catch (err) {
|
|
229
|
-
this.logger.logEvent({ message: 'start result: error', error: err.message });
|
|
232
|
+
this.logger.logEvent({ message: 'start result: error', error: err.message, profile: this.getProfile() });
|
|
230
233
|
throw new Error(`could not start device, reason:${err.message}`);
|
|
231
234
|
}
|
|
232
235
|
});
|
|
233
236
|
}
|
|
234
237
|
stop() {
|
|
235
238
|
return __awaiter(this, void 0, void 0, function* () {
|
|
239
|
+
this.logger.logEvent({ message: 'stop requested', profile: this.getProfile() });
|
|
236
240
|
this.distanceInternal = 0;
|
|
237
241
|
this.device.reset();
|
|
238
242
|
return this.device.disconnect();
|