incyclist-devices 1.4.46 → 1.4.49

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.
@@ -6,6 +6,13 @@ interface BleDeviceConstructProps extends BleDeviceProps {
6
6
  log?: boolean;
7
7
  logger?: EventLogger;
8
8
  }
9
+ declare type CommandQueueItem = {
10
+ uuid: string;
11
+ data: Buffer;
12
+ resolve: any;
13
+ reject: any;
14
+ timeout: any;
15
+ };
9
16
  export declare abstract class BleDevice extends BleDeviceClass {
10
17
  id: string;
11
18
  address: string;
@@ -19,6 +26,7 @@ export declare abstract class BleDevice extends BleDeviceClass {
19
26
  deviceInfo: BleDeviceInfo;
20
27
  isInitialized: boolean;
21
28
  subscribedCharacteristics: string[];
29
+ writeQueue: CommandQueueItem[];
22
30
  constructor(props?: BleDeviceConstructProps);
23
31
  logEvent(event: any): void;
24
32
  setInterface(ble: BleInterfaceClass): void;
@@ -32,8 +40,8 @@ export declare abstract class BleDevice extends BleDeviceClass {
32
40
  connect(props?: ConnectProps): Promise<boolean>;
33
41
  disconnect(): Promise<boolean>;
34
42
  abstract getProfile(): string;
35
- abstract onData(characteristic: string, data: Buffer): void;
36
- write(characteristicUuid: string, data: Buffer, withoutResponse?: boolean): Promise<boolean>;
43
+ onData(characteristic: string, data: Buffer): void;
44
+ write(characteristicUuid: string, data: Buffer, withoutResponse?: boolean): Promise<ArrayBuffer>;
37
45
  read(characteristicUuid: string): Promise<Uint8Array>;
38
46
  getDeviceInfo(): Promise<BleDeviceInfo>;
39
47
  }
@@ -26,6 +26,7 @@ class BleDevice extends ble_1.BleDeviceClass {
26
26
  this.characteristics = [];
27
27
  this.subscribedCharacteristics = [];
28
28
  this.isInitialized = false;
29
+ this.writeQueue = [];
29
30
  if (props.peripheral) {
30
31
  const { id, address, advertisement, state } = props.peripheral;
31
32
  this.peripheral = props.peripheral;
@@ -103,6 +104,7 @@ class BleDevice extends ble_1.BleDeviceClass {
103
104
  return Promise.resolve(true);
104
105
  return this.getDeviceInfo().then(() => {
105
106
  this.emit('deviceInfo', this.deviceInfo);
107
+ this.logEvent(Object.assign({ message: 'ftms device init done' }, this.deviceInfo));
106
108
  this.isInitialized = true;
107
109
  return true;
108
110
  });
@@ -235,9 +237,20 @@ class BleDevice extends ble_1.BleDeviceClass {
235
237
  }
236
238
  });
237
239
  }
240
+ onData(characteristic, data) {
241
+ if (this.writeQueue.length > 0) {
242
+ const writeIdx = this.writeQueue.findIndex(i => i.uuid === characteristic.toLocaleLowerCase());
243
+ if (writeIdx !== -1) {
244
+ const writeItem = this.writeQueue[writeIdx];
245
+ this.writeQueue.splice(writeIdx, 1);
246
+ if (writeItem.resolve)
247
+ writeItem.resolve(data);
248
+ }
249
+ }
250
+ }
238
251
  write(characteristicUuid, data, withoutResponse = false) {
239
252
  return __awaiter(this, void 0, void 0, function* () {
240
- if (this.subscribedCharacteristics.find(c => c === characteristicUuid) === undefined) {
253
+ if (!withoutResponse && this.subscribedCharacteristics.find(c => c === characteristicUuid) === undefined) {
241
254
  const connector = this.ble.getConnector(this.peripheral);
242
255
  connector.on(characteristicUuid, (uuid, data) => {
243
256
  this.onData(uuid, data);
@@ -251,11 +264,18 @@ class BleDevice extends ble_1.BleDeviceClass {
251
264
  reject(new Error('Characteristic not found'));
252
265
  return;
253
266
  }
267
+ if (withoutResponse) {
268
+ characteristic.write(data, withoutResponse);
269
+ resolve(new ArrayBuffer(0));
270
+ return;
271
+ }
272
+ const writeId = this.writeQueue.length;
273
+ this.writeQueue.push({ uuid: characteristicUuid.toLocaleLowerCase(), data, resolve, reject, timeout: Date.now() + 1000 });
254
274
  characteristic.write(data, withoutResponse, (err) => {
255
- if (err)
275
+ if (err) {
276
+ this.writeQueue.splice(writeId, 1);
256
277
  reject(err);
257
- else
258
- resolve(true);
278
+ }
259
279
  });
260
280
  });
261
281
  });
@@ -1,18 +1,11 @@
1
1
  import CyclingMode, { CyclingModeProperty, IncyclistBikeData, UpdateRequest } from "../CyclingMode";
2
2
  import PowerBasedCyclingModeBase from "../modes/power-base";
3
3
  import { FmAdapter } from "./fm";
4
- export declare type ERGEvent = {
5
- rpmUpdated?: boolean;
6
- gearUpdated?: boolean;
7
- starting?: boolean;
8
- tsStart?: number;
9
- };
10
- export default class ERGCyclingMode extends PowerBasedCyclingModeBase implements CyclingMode {
4
+ export default class BleERGCyclingMode extends PowerBasedCyclingModeBase implements CyclingMode {
11
5
  prevRequest: UpdateRequest;
12
6
  hasBikeUpdate: boolean;
13
7
  chain: number[];
14
8
  cassette: number[];
15
- event: ERGEvent;
16
9
  constructor(adapter: FmAdapter, props?: any);
17
10
  getName(): string;
18
11
  getDescription(): string;
@@ -13,11 +13,10 @@ const config = {
13
13
  { key: 'startPower', name: 'Starting Power', description: 'Initial power in Watts at start of training', type: CyclingMode_1.CyclingModeProperyType.Integer, default: 50, min: 25, max: 800 },
14
14
  ]
15
15
  };
16
- class ERGCyclingMode extends power_base_1.default {
16
+ class BleERGCyclingMode extends power_base_1.default {
17
17
  constructor(adapter, props) {
18
18
  super(adapter, props);
19
19
  this.hasBikeUpdate = false;
20
- this.event = {};
21
20
  this.initLogger('ERGMode');
22
21
  }
23
22
  getName() {
@@ -34,7 +33,7 @@ class ERGCyclingMode extends power_base_1.default {
34
33
  }
35
34
  getBikeInitRequest() {
36
35
  const startPower = this.getSetting('startPower');
37
- return { targetPower: startPower };
36
+ return { slope: 0, targetPower: startPower };
38
37
  }
39
38
  sendBikeUpdate(request) {
40
39
  const getData = () => {
@@ -43,25 +42,18 @@ class ERGCyclingMode extends power_base_1.default {
43
42
  const { pedalRpm, slope, power, speed } = this.data;
44
43
  return { pedalRpm, slope, power, speed };
45
44
  };
46
- this.logger.logEvent({ message: "processing update request", request, prev: this.prevRequest, data: getData(), event: this.event });
45
+ this.logger.logEvent({ message: "processing update request", request, prev: this.prevRequest, data: getData() });
47
46
  let newRequest = {};
48
47
  try {
49
48
  if (!request || request.reset || Object.keys(request).length === 0) {
50
49
  this.prevRequest = {};
51
- return request || {};
50
+ return request.reset ? { reset: true } : {};
52
51
  }
53
52
  const prevData = this.data || {};
54
53
  if (request.targetPower !== undefined) {
55
54
  delete request.slope;
56
55
  delete request.refresh;
57
56
  }
58
- if (this.event.starting && request.targetPower === undefined) {
59
- newRequest.targetPower = this.getSetting('startPower');
60
- if (this.event.tsStart && Date.now() - this.event.tsStart > 5000) {
61
- delete this.event.starting;
62
- delete this.event.tsStart;
63
- }
64
- }
65
57
  if (request.refresh) {
66
58
  delete request.refresh;
67
59
  newRequest.targetPower = this.prevRequest.targetPower;
@@ -70,14 +62,12 @@ class ERGCyclingMode extends power_base_1.default {
70
62
  if (!this.data)
71
63
  this.data = {};
72
64
  this.data.slope = request.slope;
65
+ delete request.slope;
73
66
  }
74
67
  if (request.maxPower !== undefined && request.minPower !== undefined && request.maxPower === request.minPower) {
75
68
  request.targetPower = request.maxPower;
76
- }
77
- if (request.targetPower !== undefined) {
78
69
  newRequest.targetPower = request.targetPower;
79
70
  }
80
- delete request.slope;
81
71
  if (request.maxPower !== undefined) {
82
72
  if (newRequest.targetPower !== undefined && newRequest.targetPower > request.maxPower) {
83
73
  newRequest.targetPower = request.maxPower;
@@ -89,9 +79,8 @@ class ERGCyclingMode extends power_base_1.default {
89
79
  newRequest.targetPower = request.minPower;
90
80
  }
91
81
  newRequest.minPower = request.minPower;
92
- }
93
- if (newRequest.targetPower !== undefined && prevData.power !== undefined && newRequest.targetPower === prevData.power) {
94
- delete newRequest.targetPower;
82
+ if (prevData.power && prevData.power < request.minPower)
83
+ newRequest.targetPower = request.minPower;
95
84
  }
96
85
  this.prevRequest = JSON.parse(JSON.stringify(request));
97
86
  }
@@ -106,11 +95,6 @@ class ERGCyclingMode extends power_base_1.default {
106
95
  const prevRequest = this.prevRequest || {};
107
96
  const data = this.data || {};
108
97
  const bikeType = this.getSetting('bikeType').toLowerCase();
109
- delete this.event.rpmUpdated;
110
- if (prevData === {} || prevData.speed === undefined || prevData.speed === 0) {
111
- this.event.starting = true;
112
- this.event.tsStart = Date.now();
113
- }
114
98
  try {
115
99
  const rpm = bikeData.pedalRpm || 0;
116
100
  let power = bikeData.power || 0;
@@ -127,15 +111,12 @@ class ERGCyclingMode extends power_base_1.default {
127
111
  data.distanceInternal = Math.round(distanceInternal + distance);
128
112
  data.slope = slope;
129
113
  data.pedalRpm = rpm;
130
- if (data.time !== undefined && !(this.event.starting && !bikeData.pedalRpm))
114
+ if (data.time !== undefined && data.speed > 0)
131
115
  data.time += t;
132
116
  else
133
117
  data.time = 0;
134
118
  data.heartrate = bikeData.heartrate;
135
119
  data.isPedalling = bikeData.isPedalling;
136
- if (rpm && rpm !== prevData.pedalRpm) {
137
- this.event.rpmUpdated = true;
138
- }
139
120
  }
140
121
  catch (err) {
141
122
  this.logger.logEvent({ message: 'error', fn: 'updateData()', error: err.message || err });
@@ -145,4 +126,4 @@ class ERGCyclingMode extends power_base_1.default {
145
126
  return data;
146
127
  }
147
128
  }
148
- exports.default = ERGCyclingMode;
129
+ exports.default = BleERGCyclingMode;
@@ -1,18 +1,9 @@
1
1
  import CyclingMode, { CyclingModeProperty, IncyclistBikeData, UpdateRequest } from "../CyclingMode";
2
2
  import PowerBasedCyclingModeBase from "../modes/power-base";
3
3
  import { FmAdapter } from "./fm";
4
- export declare type ERGEvent = {
5
- rpmUpdated?: boolean;
6
- gearUpdated?: boolean;
7
- starting?: boolean;
8
- tsStart?: number;
9
- };
10
- export default class ERGCyclingMode extends PowerBasedCyclingModeBase implements CyclingMode {
4
+ export default class FtmsCyclingMode extends PowerBasedCyclingModeBase implements CyclingMode {
11
5
  prevRequest: UpdateRequest;
12
6
  hasBikeUpdate: boolean;
13
- chain: number[];
14
- cassette: number[];
15
- event: ERGEvent;
16
7
  constructor(adapter: FmAdapter, props?: any);
17
8
  getName(): string;
18
9
  getDescription(): string;
@@ -9,16 +9,14 @@ const config = {
9
9
  name: "Smart Trainer",
10
10
  description: "Calculates speed based on power and slope. Slope is set to the device",
11
11
  properties: [
12
- { key: 'bikeType', name: 'Bike Type', description: '', type: CyclingMode_1.CyclingModeProperyType.SingleSelect, options: ['Race', 'Mountain', 'Triathlon'], default: 'Race' },
13
- { key: 'startPower', name: 'Starting Power', description: 'Initial power in Watts at start of training', type: CyclingMode_1.CyclingModeProperyType.Integer, default: 50, min: 25, max: 800 },
12
+ { key: 'bikeType', name: 'Bike Type', description: '', type: CyclingMode_1.CyclingModeProperyType.SingleSelect, options: ['Race', 'Mountain', 'Triathlon'], default: 'Race' }
14
13
  ]
15
14
  };
16
- class ERGCyclingMode extends power_base_1.default {
15
+ class FtmsCyclingMode extends power_base_1.default {
17
16
  constructor(adapter, props) {
18
17
  super(adapter, props);
19
18
  this.hasBikeUpdate = false;
20
- this.event = {};
21
- this.initLogger('ERGMode');
19
+ this.initLogger('FtmsMode');
22
20
  }
23
21
  getName() {
24
22
  return config.name;
@@ -33,71 +31,32 @@ class ERGCyclingMode extends power_base_1.default {
33
31
  return config.properties.find(p => p.name === name);
34
32
  }
35
33
  getBikeInitRequest() {
36
- const startPower = this.getSetting('startPower');
37
- return { targetPower: startPower };
34
+ return { slope: 0 };
38
35
  }
39
36
  sendBikeUpdate(request) {
40
37
  const getData = () => {
41
38
  if (!this.data)
42
39
  return {};
43
- const { pedalRpm, slope, power, speed } = this.data;
44
- return { pedalRpm, slope, power, speed };
40
+ const { gear, pedalRpm, slope, power, speed } = this.data;
41
+ return { gear, pedalRpm, slope, power, speed };
45
42
  };
46
- this.logger.logEvent({ message: "processing update request", request, prev: this.prevRequest, data: getData(), event: this.event });
43
+ const event = {};
44
+ if (this.data === undefined)
45
+ event.noData = true;
46
+ if (request.slope !== undefined && (event.noData || Math.abs(request.slope - this.data.slope) >= 0.1))
47
+ event.slopeUpdate = true;
48
+ if (this.prevRequest === undefined)
49
+ event.initialCall = true;
50
+ this.logger.logEvent({ message: "processing update request", request, prev: this.prevRequest, data: getData(), event });
47
51
  let newRequest = {};
48
- try {
49
- if (!request || request.reset || Object.keys(request).length === 0) {
50
- this.prevRequest = {};
51
- return request || {};
52
- }
53
- const prevData = this.data || {};
54
- if (request.targetPower !== undefined) {
55
- delete request.slope;
56
- delete request.refresh;
57
- }
58
- if (this.event.starting && request.targetPower === undefined) {
59
- newRequest.targetPower = this.getSetting('startPower');
60
- if (this.event.tsStart && Date.now() - this.event.tsStart > 5000) {
61
- delete this.event.starting;
62
- delete this.event.tsStart;
63
- }
64
- }
65
- if (request.refresh) {
66
- delete request.refresh;
67
- newRequest.targetPower = this.prevRequest.targetPower;
68
- }
69
- if (request.slope !== undefined) {
70
- if (!this.data)
71
- this.data = {};
72
- this.data.slope = request.slope;
73
- }
74
- if (request.maxPower !== undefined && request.minPower !== undefined && request.maxPower === request.minPower) {
75
- request.targetPower = request.maxPower;
76
- }
77
- if (request.targetPower !== undefined) {
78
- newRequest.targetPower = request.targetPower;
79
- }
80
- delete request.slope;
81
- if (request.maxPower !== undefined) {
82
- if (newRequest.targetPower !== undefined && newRequest.targetPower > request.maxPower) {
83
- newRequest.targetPower = request.maxPower;
84
- }
85
- newRequest.maxPower = request.maxPower;
86
- }
87
- if (request.minPower !== undefined) {
88
- if (newRequest.targetPower !== undefined && newRequest.targetPower < request.minPower) {
89
- newRequest.targetPower = request.minPower;
90
- }
91
- newRequest.minPower = request.minPower;
92
- }
93
- if (newRequest.targetPower !== undefined && prevData.power !== undefined && newRequest.targetPower === prevData.power) {
94
- delete newRequest.targetPower;
95
- }
96
- this.prevRequest = JSON.parse(JSON.stringify(request));
52
+ if (request.slope === undefined && request.refresh && this.prevRequest) {
53
+ return this.prevRequest;
97
54
  }
98
- catch (err) {
99
- this.logger.logEvent({ message: "error", fn: 'sendBikeUpdate()', error: err.message || err, stack: err.stack });
55
+ if (request.slope !== undefined) {
56
+ newRequest.slope = parseFloat(request.slope.toFixed(1));
57
+ this.data.slope = newRequest.slope;
100
58
  }
59
+ this.prevRequest = JSON.parse(JSON.stringify(newRequest));
101
60
  return newRequest;
102
61
  }
103
62
  updateData(bikeData) {
@@ -106,11 +65,6 @@ class ERGCyclingMode extends power_base_1.default {
106
65
  const prevRequest = this.prevRequest || {};
107
66
  const data = this.data || {};
108
67
  const bikeType = this.getSetting('bikeType').toLowerCase();
109
- delete this.event.rpmUpdated;
110
- if (prevData === {} || prevData.speed === undefined || prevData.speed === 0) {
111
- this.event.starting = true;
112
- this.event.tsStart = Date.now();
113
- }
114
68
  try {
115
69
  const rpm = bikeData.pedalRpm || 0;
116
70
  let power = bikeData.power || 0;
@@ -127,15 +81,12 @@ class ERGCyclingMode extends power_base_1.default {
127
81
  data.distanceInternal = Math.round(distanceInternal + distance);
128
82
  data.slope = slope;
129
83
  data.pedalRpm = rpm;
130
- if (data.time !== undefined && !(this.event.starting && !bikeData.pedalRpm))
84
+ if (data.time !== undefined)
131
85
  data.time += t;
132
86
  else
133
87
  data.time = 0;
134
88
  data.heartrate = bikeData.heartrate;
135
89
  data.isPedalling = bikeData.isPedalling;
136
- if (rpm && rpm !== prevData.pedalRpm) {
137
- this.event.rpmUpdated = true;
138
- }
139
90
  }
140
91
  catch (err) {
141
92
  this.logger.logEvent({ message: 'error', fn: 'updateData()', error: err.message || err });
@@ -145,4 +96,4 @@ class ERGCyclingMode extends power_base_1.default {
145
96
  return data;
146
97
  }
147
98
  }
148
- exports.default = ERGCyclingMode;
99
+ exports.default = FtmsCyclingMode;
package/lib/ble/fm.d.ts CHANGED
@@ -31,6 +31,9 @@ declare type IndoorBikeData = {
31
31
  time?: number;
32
32
  remainingTime?: number;
33
33
  raw?: string;
34
+ targetPower?: number;
35
+ targetInclination?: number;
36
+ status?: string;
34
37
  };
35
38
  declare type IndoorBikeFeatures = {
36
39
  fitnessMachine: number;
@@ -43,6 +46,10 @@ export default class BleFitnessMachineDevice extends BleDevice {
43
46
  features: IndoorBikeFeatures;
44
47
  hasControl: boolean;
45
48
  isCPSubscribed: boolean;
49
+ crr: number;
50
+ cw: number;
51
+ windSpeed: number;
52
+ wheelSize: number;
46
53
  constructor(props?: any);
47
54
  init(): Promise<boolean>;
48
55
  onDisconnect(): void;
@@ -52,11 +59,25 @@ export default class BleFitnessMachineDevice extends BleDevice {
52
59
  isPower(): boolean;
53
60
  isHrm(): boolean;
54
61
  parseHrm(_data: Uint8Array): IndoorBikeData;
62
+ setCrr(crr: number): void;
63
+ getCrr(): number;
64
+ setCw(cw: number): void;
65
+ getCw(): number;
66
+ setWindSpeed(windSpeed: number): void;
67
+ getWindSpeed(): number;
55
68
  parseIndoorBikeData(_data: Uint8Array): IndoorBikeData;
69
+ parseFitnessMachineStatus(_data: Uint8Array): IndoorBikeData;
56
70
  getFitnessMachineFeatures(): Promise<IndoorBikeFeatures>;
57
71
  onData(characteristic: string, data: Buffer): void;
72
+ writeFtmsMessage(requestedOpCode: any, data: any): Promise<number>;
58
73
  requestControl(): Promise<boolean>;
59
- setTargetPower(power: number): Promise<void>;
74
+ setTargetPower(power: number): Promise<boolean>;
75
+ setSlope(slope: any): Promise<boolean>;
76
+ setTargetInclination(inclination: number): Promise<boolean>;
77
+ setIndoorBikeSimulation(windSpeed: number, gradient: number, crr: number, cw: number): Promise<boolean>;
78
+ startRequest(): Promise<boolean>;
79
+ stopRequest(): Promise<boolean>;
80
+ PauseRequest(): Promise<boolean>;
60
81
  reset(): void;
61
82
  }
62
83
  export declare class FmAdapter extends DeviceAdapter {
@@ -66,7 +87,7 @@ export declare class FmAdapter extends DeviceAdapter {
66
87
  protocol: DeviceProtocol;
67
88
  paused: boolean;
68
89
  logger: EventLogger;
69
- mode: CyclingMode;
90
+ cyclingMode: CyclingMode;
70
91
  distanceInternal: number;
71
92
  prevDataTS: number;
72
93
  constructor(device: BleDeviceClass, protocol: BleProtocol);
@@ -77,6 +98,8 @@ export declare class FmAdapter extends DeviceAdapter {
77
98
  getProfile(): string;
78
99
  getName(): string;
79
100
  getDisplayName(): string;
101
+ getSupportedCyclingModes(): Array<any>;
102
+ setCyclingMode(mode: string | CyclingMode, settings?: any): void;
80
103
  getCyclingMode(): CyclingMode;
81
104
  getDefaultCyclingMode(): CyclingMode;
82
105
  getPort(): string;
@@ -87,6 +110,7 @@ export declare class FmAdapter extends DeviceAdapter {
87
110
  transformData(bikeData: IncyclistBikeData): DeviceData;
88
111
  start(props?: any): Promise<any>;
89
112
  stop(): Promise<boolean>;
113
+ sendUpdate(request: any): Promise<void>;
90
114
  pause(): Promise<boolean>;
91
115
  resume(): Promise<boolean>;
92
116
  }
package/lib/ble/fm.js CHANGED
@@ -18,7 +18,15 @@ const ble_interface_1 = __importDefault(require("./ble-interface"));
18
18
  const Device_1 = __importDefault(require("../Device"));
19
19
  const gd_eventlog_1 = require("gd-eventlog");
20
20
  const power_meter_1 = __importDefault(require("../modes/power-meter"));
21
+ const ble_st_mode_1 = __importDefault(require("./ble-st-mode"));
22
+ const ble_erg_mode_1 = __importDefault(require("./ble-erg-mode"));
21
23
  const FTMS_CP = '2ad9';
24
+ const cwABike = {
25
+ race: 0.35,
26
+ triathlon: 0.29,
27
+ mountain: 0.57
28
+ };
29
+ const cRR = 0.0036;
22
30
  const bit = (nr) => (1 << nr);
23
31
  const IndoorBikeDataFlag = {
24
32
  MoreData: bit(0),
@@ -79,6 +87,10 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
79
87
  this.features = undefined;
80
88
  this.hasControl = false;
81
89
  this.isCPSubscribed = false;
90
+ this.crr = 0.0033;
91
+ this.cw = 0.6;
92
+ this.windSpeed = 0;
93
+ this.wheelSize = 2100;
82
94
  this.data = {};
83
95
  }
84
96
  init() {
@@ -138,6 +150,12 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
138
150
  }
139
151
  return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
140
152
  }
153
+ setCrr(crr) { this.crr = crr; }
154
+ getCrr() { return this.crr; }
155
+ setCw(cw) { this.cw = cw; }
156
+ getCw() { return this.cw; }
157
+ setWindSpeed(windSpeed) { this.windSpeed = windSpeed; }
158
+ getWindSpeed() { return this.windSpeed; }
141
159
  parseIndoorBikeData(_data) {
142
160
  const data = Buffer.from(_data);
143
161
  const flags = data.readUInt16LE(0);
@@ -159,19 +177,22 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
159
177
  offset += 2;
160
178
  }
161
179
  if (flags & IndoorBikeDataFlag.TotalDistancePresent) {
162
- this.data.totalDistance = data.readUInt16LE(offset);
180
+ const dvLow = data.readUInt8(offset);
181
+ offset += 1;
182
+ const dvHigh = data.readUInt16LE(offset);
163
183
  offset += 2;
184
+ this.data.totalDistance = (dvHigh << 8) + dvLow;
164
185
  }
165
186
  if (flags & IndoorBikeDataFlag.ResistanceLevelPresent) {
166
- this.data.resistanceLevel = data.readUInt16LE(offset);
187
+ this.data.resistanceLevel = data.readInt16LE(offset);
167
188
  offset += 2;
168
189
  }
169
190
  if (flags & IndoorBikeDataFlag.InstantaneousPowerPresent) {
170
- this.data.instantaneousPower = data.readUInt16LE(offset);
191
+ this.data.instantaneousPower = data.readInt16LE(offset);
171
192
  offset += 2;
172
193
  }
173
194
  if (flags & IndoorBikeDataFlag.AveragePowerPresent) {
174
- this.data.averagePower = data.readUInt16LE(offset);
195
+ this.data.averagePower = data.readInt16LE(offset);
175
196
  offset += 2;
176
197
  }
177
198
  if (flags & IndoorBikeDataFlag.ExpendedEnergyPresent) {
@@ -196,6 +217,43 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
196
217
  }
197
218
  return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
198
219
  }
220
+ parseFitnessMachineStatus(_data) {
221
+ const data = Buffer.from(_data);
222
+ const OpCode = data.readUInt8(0);
223
+ switch (OpCode) {
224
+ case 8:
225
+ this.data.targetPower = data.readInt16LE(1);
226
+ break;
227
+ case 6:
228
+ this.data.targetInclination = data.readInt16LE(1) / 10;
229
+ break;
230
+ case 4:
231
+ this.data.status = "STARTED";
232
+ break;
233
+ case 3:
234
+ case 2:
235
+ this.data.status = "STOPPED";
236
+ break;
237
+ case 20:
238
+ const spinDownStatus = data.readUInt8(1);
239
+ switch (spinDownStatus) {
240
+ case 1:
241
+ this.data.status = "SPIN DOWN REQUESTED";
242
+ break;
243
+ case 2:
244
+ this.data.status = "SPIN DOWN SUCCESS";
245
+ break;
246
+ case 3:
247
+ this.data.status = "SPIN DOWN ERROR";
248
+ break;
249
+ case 4:
250
+ this.data.status = "STOP PEDALING";
251
+ break;
252
+ default: break;
253
+ }
254
+ }
255
+ return Object.assign(Object.assign({}, this.data), { raw: data.toString('hex') });
256
+ }
199
257
  getFitnessMachineFeatures() {
200
258
  return __awaiter(this, void 0, void 0, function* () {
201
259
  if (this.features)
@@ -215,14 +273,42 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
215
273
  });
216
274
  }
217
275
  onData(characteristic, data) {
218
- if (characteristic.toLocaleLowerCase() === '2ad2') {
219
- const res = this.parseIndoorBikeData(data);
220
- this.emit('data', res);
276
+ super.onData(characteristic, data);
277
+ const uuid = characteristic.toLocaleLowerCase();
278
+ let res = undefined;
279
+ switch (uuid) {
280
+ case '2ad2':
281
+ res = this.parseIndoorBikeData(data);
282
+ break;
283
+ case '2a37':
284
+ res = this.parseHrm(data);
285
+ break;
286
+ case '2ada':
287
+ res = this.parseFitnessMachineStatus(data);
288
+ break;
289
+ default:
290
+ break;
221
291
  }
222
- if (characteristic.toLocaleLowerCase() === '2a37') {
223
- const res = this.parseHrm(data);
292
+ if (res)
224
293
  this.emit('data', res);
225
- }
294
+ }
295
+ writeFtmsMessage(requestedOpCode, data) {
296
+ return __awaiter(this, void 0, void 0, function* () {
297
+ try {
298
+ const res = yield this.write(FTMS_CP, data);
299
+ const responseData = Buffer.from(res);
300
+ const opCode = responseData.readUInt8(0);
301
+ const request = responseData.readUInt8(1);
302
+ const result = responseData.readUInt8(2);
303
+ if (opCode !== 128 || request !== requestedOpCode)
304
+ throw new Error('Illegal response ');
305
+ return result;
306
+ }
307
+ catch (err) {
308
+ this.logEvent({ message: 'writeFtmsMessage failed', opCode: requestedOpCode, reason: err.message });
309
+ return 4;
310
+ }
311
+ });
226
312
  }
227
313
  requestControl() {
228
314
  return __awaiter(this, void 0, void 0, function* () {
@@ -230,21 +316,109 @@ class BleFitnessMachineDevice extends ble_device_1.BleDevice {
230
316
  return true;
231
317
  const data = Buffer.alloc(1);
232
318
  data.writeUInt8(0, 0);
233
- const success = yield this.write(FTMS_CP, data);
234
- if (success)
319
+ const res = yield this.writeFtmsMessage(0, data);
320
+ if (res === 1) {
235
321
  this.hasControl = true;
322
+ }
236
323
  return this.hasControl;
237
324
  });
238
325
  }
239
326
  setTargetPower(power) {
240
327
  return __awaiter(this, void 0, void 0, function* () {
328
+ this.logEvent({ message: 'setTargetPower', power, skip: (this.data.targetPower !== undefined && this.data.targetPower === power) });
329
+ if (this.data.targetPower !== undefined && this.data.targetPower === power)
330
+ return true;
241
331
  const hasControl = yield this.requestControl();
242
- if (!hasControl)
243
- throw new Error('setTargetPower not possible - control is disabled');
332
+ if (!hasControl) {
333
+ this.logEvent({ message: 'setTargetPower failed', reason: 'control is disabled' });
334
+ return false;
335
+ }
244
336
  const data = Buffer.alloc(3);
245
337
  data.writeUInt8(5, 0);
246
338
  data.writeInt16LE(Math.round(power), 1);
247
- const res = yield this.write(FTMS_CP, data);
339
+ const res = yield this.writeFtmsMessage(5, data);
340
+ return (res === 1);
341
+ });
342
+ }
343
+ setSlope(slope) {
344
+ return __awaiter(this, void 0, void 0, function* () {
345
+ this.logEvent({ message: 'setSlope', slope });
346
+ const { windSpeed, crr, cw } = this;
347
+ return yield this.setIndoorBikeSimulation(windSpeed, slope, crr, cw);
348
+ });
349
+ }
350
+ setTargetInclination(inclination) {
351
+ return __awaiter(this, void 0, void 0, function* () {
352
+ if (this.data.targetInclination !== undefined && this.data.targetInclination === inclination)
353
+ return true;
354
+ const hasControl = yield this.requestControl();
355
+ if (!hasControl) {
356
+ this.logEvent({ message: 'setTargetInclination failed', reason: 'control is disabled' });
357
+ return false;
358
+ }
359
+ const data = Buffer.alloc(3);
360
+ data.writeUInt8(3, 0);
361
+ data.writeInt16LE(Math.round(inclination * 10), 1);
362
+ const res = yield this.writeFtmsMessage(3, data);
363
+ return (res === 1);
364
+ });
365
+ }
366
+ setIndoorBikeSimulation(windSpeed, gradient, crr, cw) {
367
+ return __awaiter(this, void 0, void 0, function* () {
368
+ const hasControl = yield this.requestControl();
369
+ if (!hasControl) {
370
+ this.logEvent({ message: 'setTargetInclination failed', reason: 'control is disabled' });
371
+ return false;
372
+ }
373
+ const data = Buffer.alloc(7);
374
+ data.writeUInt8(17, 0);
375
+ data.writeInt16LE(Math.round(windSpeed * 1000), 1);
376
+ data.writeInt16LE(Math.round(gradient * 100), 3);
377
+ data.writeUInt8(Math.round(crr * 10000), 5);
378
+ data.writeUInt8(Math.round(cw * 100), 6);
379
+ const res = yield this.writeFtmsMessage(17, data);
380
+ return (res === 1);
381
+ });
382
+ }
383
+ startRequest() {
384
+ return __awaiter(this, void 0, void 0, function* () {
385
+ const hasControl = yield this.requestControl();
386
+ if (!hasControl) {
387
+ this.logEvent({ message: 'startRequest failed', reason: 'control is disabled' });
388
+ return false;
389
+ }
390
+ const data = Buffer.alloc(1);
391
+ data.writeUInt8(7, 0);
392
+ const res = yield this.writeFtmsMessage(7, data);
393
+ return (res === 1);
394
+ });
395
+ }
396
+ stopRequest() {
397
+ return __awaiter(this, void 0, void 0, function* () {
398
+ const hasControl = yield this.requestControl();
399
+ if (!hasControl) {
400
+ this.logEvent({ message: 'stopRequest failed', reason: 'control is disabled' });
401
+ return false;
402
+ }
403
+ const data = Buffer.alloc(2);
404
+ data.writeUInt8(8, 0);
405
+ data.writeUInt8(1, 1);
406
+ const res = yield this.writeFtmsMessage(8, data);
407
+ return (res === 1);
408
+ });
409
+ }
410
+ PauseRequest() {
411
+ return __awaiter(this, void 0, void 0, function* () {
412
+ const hasControl = yield this.requestControl();
413
+ if (!hasControl) {
414
+ this.logEvent({ message: 'PauseRequest failed', reason: 'control is disabled' });
415
+ return false;
416
+ }
417
+ const data = Buffer.alloc(2);
418
+ data.writeUInt8(8, 0);
419
+ data.writeUInt8(2, 1);
420
+ const res = yield this.writeFtmsMessage(8, data);
421
+ return (res === 1);
248
422
  });
249
423
  }
250
424
  reset() {
@@ -263,7 +437,7 @@ class FmAdapter extends Device_1.default {
263
437
  this.distanceInternal = 0;
264
438
  this.device = device;
265
439
  this.ble = protocol.ble;
266
- this.mode = this.getDefaultCyclingMode();
440
+ this.cyclingMode = this.getDefaultCyclingMode();
267
441
  this.logger = new gd_eventlog_1.EventLogger('BLE-FM');
268
442
  }
269
443
  isBike() { return this.device.isBike(); }
@@ -284,13 +458,33 @@ class FmAdapter extends Device_1.default {
284
458
  getDisplayName() {
285
459
  return this.getName();
286
460
  }
461
+ getSupportedCyclingModes() {
462
+ return [ble_st_mode_1.default, ble_erg_mode_1.default, power_meter_1.default];
463
+ }
464
+ setCyclingMode(mode, settings) {
465
+ let selectedMode;
466
+ if (typeof mode === 'string') {
467
+ const supported = this.getSupportedCyclingModes();
468
+ const CyclingModeClass = supported.find(M => { const m = new M(this); return m.getName() === mode; });
469
+ if (CyclingModeClass) {
470
+ this.cyclingMode = new CyclingModeClass(this, settings);
471
+ return;
472
+ }
473
+ selectedMode = this.getDefaultCyclingMode();
474
+ }
475
+ else {
476
+ selectedMode = mode;
477
+ }
478
+ this.cyclingMode = selectedMode;
479
+ this.cyclingMode.setSettings(settings);
480
+ }
287
481
  getCyclingMode() {
288
- if (!this.mode)
289
- this.mode = this.getDefaultCyclingMode();
290
- return this.mode;
482
+ if (!this.cyclingMode)
483
+ this.cyclingMode = this.getDefaultCyclingMode();
484
+ return this.cyclingMode;
291
485
  }
292
486
  getDefaultCyclingMode() {
293
- return new power_meter_1.default(this);
487
+ return new ble_st_mode_1.default(this);
294
488
  }
295
489
  getPort() {
296
490
  return 'ble';
@@ -360,6 +554,22 @@ class FmAdapter extends Device_1.default {
360
554
  const bleDevice = yield this.ble.connectDevice(this.device);
361
555
  if (bleDevice) {
362
556
  this.device = bleDevice;
557
+ const mode = this.getCyclingMode();
558
+ if (mode && mode.getSetting('bikeType')) {
559
+ const bikeType = mode.getSetting('bikeType').toLowerCase();
560
+ this.device.setCrr(cRR);
561
+ switch (bikeType) {
562
+ case 'race':
563
+ this.device.setCw(cwABike.race);
564
+ break;
565
+ case 'triathlon':
566
+ this.device.setCw(cwABike.triathlon);
567
+ break;
568
+ case 'mountain':
569
+ this.device.setCw(cwABike.mountain);
570
+ break;
571
+ }
572
+ }
363
573
  bleDevice.on('data', (data) => {
364
574
  this.onDeviceData(data);
365
575
  });
@@ -380,6 +590,19 @@ class FmAdapter extends Device_1.default {
380
590
  return this.device.disconnect();
381
591
  });
382
592
  }
593
+ sendUpdate(request) {
594
+ return __awaiter(this, void 0, void 0, function* () {
595
+ if (this.paused || !this.device)
596
+ return;
597
+ const requested = this.getCyclingMode().sendBikeUpdate(request);
598
+ if (requested.slope !== undefined) {
599
+ yield this.device.setSlope(requested.slope);
600
+ }
601
+ if (requested.targetPower !== undefined) {
602
+ yield this.device.setTargetPower(requested.targetPower);
603
+ }
604
+ });
605
+ }
383
606
  pause() { this.paused = true; return Promise.resolve(true); }
384
607
  resume() { this.paused = false; return Promise.resolve(true); }
385
608
  }
package/lib/ble/hrm.js CHANGED
@@ -51,6 +51,7 @@ class BleHrmDevice extends ble_device_1.BleDevice {
51
51
  return { heartrate, rr, raw: data.toString('hex') };
52
52
  }
53
53
  onData(characteristic, data) {
54
+ super.onData(characteristic, data);
54
55
  if (characteristic.toLocaleLowerCase() === '2a37') {
55
56
  const res = this.parseHrm(data);
56
57
  this.emit('data', res);
package/lib/ble/pwr.d.ts CHANGED
@@ -72,6 +72,7 @@ export declare class PwrAdapter extends DeviceAdapter {
72
72
  transformData(bikeData: IncyclistBikeData): DeviceData;
73
73
  start(props?: any): Promise<any>;
74
74
  stop(): Promise<boolean>;
75
+ sendUpdate(request: any): Promise<void>;
75
76
  pause(): Promise<boolean>;
76
77
  resume(): Promise<boolean>;
77
78
  }
package/lib/ble/pwr.js CHANGED
@@ -109,6 +109,7 @@ class BleCyclingPowerDevice extends ble_device_1.BleDevice {
109
109
  return { instantaneousPower, balance, accTorque, rpm, time, raw: data.toString('hex') };
110
110
  }
111
111
  onData(characteristic, data) {
112
+ super.onData(characteristic, data);
112
113
  if (characteristic.toLocaleLowerCase() === '2a63') {
113
114
  const res = this.parsePower(data);
114
115
  this.emit('data', res);
@@ -254,6 +255,13 @@ class PwrAdapter extends Device_1.default {
254
255
  return this.device.disconnect();
255
256
  });
256
257
  }
258
+ sendUpdate(request) {
259
+ return __awaiter(this, void 0, void 0, function* () {
260
+ if (this.paused)
261
+ return;
262
+ this.getCyclingMode().sendBikeUpdate(request);
263
+ });
264
+ }
257
265
  pause() { this.paused = true; return Promise.resolve(true); }
258
266
  resume() { this.paused = false; return Promise.resolve(true); }
259
267
  }
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.config = void 0;
7
7
  const power_base_1 = __importDefault(require("./power-base"));
8
+ const MIN_SPEED = 10;
8
9
  exports.config = {
9
10
  name: 'PowerMeter',
10
11
  description: 'Power and cadence are taken from device. Speed is calculated from power and current slope\nThis mode will not respect maximum power and/or workout limits',
@@ -54,12 +55,18 @@ class PowerMeterCyclingMode extends power_base_1.default {
54
55
  const m = this.getWeight();
55
56
  let t = this.getTimeSinceLastUpdate();
56
57
  const { speed, distance } = this.calculateSpeedAndDistance(power, slope, m, t);
57
- data.speed = speed;
58
58
  data.power = Math.round(power);
59
- data.distanceInternal = Math.round(distanceInternal + distance);
60
59
  data.slope = slope;
60
+ if (power === 0 && speed < MIN_SPEED) {
61
+ data.speed = Math.round(prevData.speed - 1) < 0 ? 0 : Math.round(prevData.speed - 1);
62
+ data.distanceInternal = Math.round(distanceInternal + data.speed / 3.6 * t);
63
+ }
64
+ else {
65
+ data.speed = (power === 0 && speed < MIN_SPEED) ? 0 : speed;
66
+ data.distanceInternal = (power === 0 && speed < MIN_SPEED) ? Math.round(distanceInternal) : Math.round(distanceInternal + distance);
67
+ }
61
68
  if (props.log)
62
- this.logger.logEvent({ message: "updateData result", data, bikeData, prevSpeed: prevData.speed });
69
+ this.logger.logEvent({ message: "updateData result", data, bikeData, prevSpeed: prevData.speed, stopped: speed < MIN_SPEED });
63
70
  this.data = data;
64
71
  }
65
72
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incyclist-devices",
3
- "version": "1.4.46",
3
+ "version": "1.4.49",
4
4
  "dependencies": {
5
5
  "@serialport/parser-byte-length": "^9.0.1",
6
6
  "@serialport/parser-delimiter": "^9.0.1",