@hangtime/grip-connect 0.5.4 → 0.5.6
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/dist/interfaces/device/kilterboard.interface.d.ts +6 -67
- package/dist/models/device/entralpi.model.js +12 -2
- package/dist/models/device/forceboard.model.js +12 -3
- package/dist/models/device/kilterboard.model.d.ts +17 -23
- package/dist/models/device/kilterboard.model.js +4 -15
- package/dist/models/device/progressor.model.js +14 -14
- package/dist/models/device/wh-c06.model.d.ts +28 -1
- package/dist/models/device/wh-c06.model.js +60 -7
- package/package.json +1 -1
- package/src/interfaces/device/kilterboard.interface.ts +3 -76
- package/src/models/device/entralpi.model.ts +12 -3
- package/src/models/device/forceboard.model.ts +12 -5
- package/src/models/device/kilterboard.model.ts +14 -27
- package/src/models/device/progressor.model.ts +14 -14
- package/src/models/device/wh-c06.model.ts +62 -11
|
@@ -1,76 +1,15 @@
|
|
|
1
1
|
import type { IDevice } from "../device.interface";
|
|
2
|
-
/**
|
|
3
|
-
* Represents a climbing placement with a position and role identifier.
|
|
4
|
-
*/
|
|
5
|
-
export interface ClimbPlacement {
|
|
6
|
-
/** The position of the hold placement. */
|
|
7
|
-
position: number;
|
|
8
|
-
/** The role ID associated with the climb placement. */
|
|
9
|
-
role_id: number;
|
|
10
|
-
}
|
|
11
2
|
/**
|
|
12
3
|
* Interface representing the KilterBoard device, extending the base Device interface.
|
|
13
4
|
*/
|
|
14
5
|
export interface IKilterBoard extends IDevice {
|
|
15
|
-
/**
|
|
16
|
-
* Calculates the checksum for a byte array.
|
|
17
|
-
* @param data - The array of bytes to calculate the checksum for.
|
|
18
|
-
* @returns The calculated checksum value.
|
|
19
|
-
*/
|
|
20
|
-
checksum(data: number[]): number;
|
|
21
|
-
/**
|
|
22
|
-
* Wraps a byte array with header and footer bytes for transmission.
|
|
23
|
-
* @param data - The array of bytes to wrap.
|
|
24
|
-
* @returns The wrapped byte array.
|
|
25
|
-
*/
|
|
26
|
-
wrapBytes(data: number[]): number[];
|
|
27
|
-
/**
|
|
28
|
-
* Encodes a position into a byte array.
|
|
29
|
-
* @param position - The position to encode.
|
|
30
|
-
* @returns The encoded byte array representing the position.
|
|
31
|
-
*/
|
|
32
|
-
encodePosition(position: number): number[];
|
|
33
|
-
/**
|
|
34
|
-
* Encodes a color string into a numeric representation.
|
|
35
|
-
* @param color - The color string in hexadecimal format.
|
|
36
|
-
* @returns The encoded/compressed color value.
|
|
37
|
-
*/
|
|
38
|
-
encodeColor(color: string): number;
|
|
39
|
-
/**
|
|
40
|
-
* Encodes a placement into a byte array.
|
|
41
|
-
* @param position - The position to encode.
|
|
42
|
-
* @param ledColor - The color of the LED in hexadecimal format.
|
|
43
|
-
* @returns The encoded byte array representing the placement.
|
|
44
|
-
*/
|
|
45
|
-
encodePlacement(position: number, ledColor: string): number[];
|
|
46
|
-
/**
|
|
47
|
-
* Prepares byte arrays for transmission based on a list of climb placements.
|
|
48
|
-
* @param climbPlacementList - The list of climb placements.
|
|
49
|
-
* @returns The final byte array ready for transmission.
|
|
50
|
-
*/
|
|
51
|
-
prepBytesV3(climbPlacementList: ClimbPlacement[]): number[];
|
|
52
|
-
/**
|
|
53
|
-
* Splits a collection into slices of the specified length.
|
|
54
|
-
* @param n - Number of elements per slice.
|
|
55
|
-
* @param list - Array to be sliced.
|
|
56
|
-
* @returns The sliced array.
|
|
57
|
-
*/
|
|
58
|
-
splitEvery(n: number, list: number[]): number[][];
|
|
59
|
-
/**
|
|
60
|
-
* Splits a message into 20-byte chunks for Bluetooth transmission.
|
|
61
|
-
* @param buffer - The message to split.
|
|
62
|
-
* @returns The array of Uint8Arrays.
|
|
63
|
-
*/
|
|
64
|
-
splitMessages(buffer: number[]): Uint8Array[];
|
|
65
|
-
/**
|
|
66
|
-
* Sends a series of messages to the device.
|
|
67
|
-
* @param messages - Array of Uint8Arrays to send.
|
|
68
|
-
*/
|
|
69
|
-
writeMessageSeries(messages: Uint8Array[]): Promise<void>;
|
|
70
6
|
/**
|
|
71
7
|
* Configures the LEDs based on an array of climb placements.
|
|
72
|
-
* @param
|
|
73
|
-
* @returns
|
|
8
|
+
* @param {{ position: number; role_id: number }[]} config - Array of climb placements for the LEDs.
|
|
9
|
+
* @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Kilter Board if LED settings were applied, or `undefined` if no action was taken or for the Motherboard.
|
|
74
10
|
*/
|
|
75
|
-
led(config
|
|
11
|
+
led(config: {
|
|
12
|
+
position: number;
|
|
13
|
+
role_id: number;
|
|
14
|
+
}[]): Promise<number[] | undefined>;
|
|
76
15
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Device } from "../device.model";
|
|
2
2
|
import { applyTare } from "../../helpers/tare";
|
|
3
3
|
import { checkActivity } from "../../helpers/is-active";
|
|
4
|
+
import { DownloadPackets } from "../../helpers/download";
|
|
4
5
|
export class Entralpi extends Device {
|
|
5
6
|
constructor() {
|
|
6
7
|
super({
|
|
@@ -158,10 +159,19 @@ export class Entralpi extends Device {
|
|
|
158
159
|
if (value.buffer) {
|
|
159
160
|
const buffer = value.buffer;
|
|
160
161
|
const rawData = new DataView(buffer);
|
|
162
|
+
const receivedTime = Date.now();
|
|
161
163
|
const receivedData = (rawData.getUint16(0) / 100).toFixed(1);
|
|
162
|
-
|
|
164
|
+
const convertedData = Number(receivedData);
|
|
163
165
|
// Tare correction
|
|
164
|
-
numericData
|
|
166
|
+
const numericData = convertedData - applyTare(convertedData);
|
|
167
|
+
// Add data to downloadable Array
|
|
168
|
+
DownloadPackets.push({
|
|
169
|
+
received: receivedTime,
|
|
170
|
+
sampleNum: this.dataPointCount,
|
|
171
|
+
battRaw: 0,
|
|
172
|
+
samples: [convertedData],
|
|
173
|
+
masses: [numericData],
|
|
174
|
+
});
|
|
165
175
|
// Update massMax
|
|
166
176
|
this.massMax = Math.max(Number(this.massMax), numericData).toFixed(1);
|
|
167
177
|
// Update running sum and count
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Device } from "../device.model";
|
|
2
|
-
import { emptyDownloadPackets } from "../../helpers/download";
|
|
2
|
+
import { DownloadPackets, emptyDownloadPackets } from "../../helpers/download";
|
|
3
3
|
import { checkActivity } from "../../helpers/is-active";
|
|
4
4
|
import { applyTare } from "../../helpers/tare";
|
|
5
5
|
/**
|
|
@@ -188,11 +188,20 @@ export class ForceBoard extends Device {
|
|
|
188
188
|
if (value.buffer) {
|
|
189
189
|
const buffer = value.buffer;
|
|
190
190
|
const rawData = new DataView(buffer);
|
|
191
|
+
const receivedTime = Date.now();
|
|
191
192
|
const receivedData = rawData.getUint8(4); // Read the value at index 4
|
|
192
193
|
// Convert from LBS to KG
|
|
193
|
-
|
|
194
|
+
const convertedReceivedData = receivedData * 0.453592;
|
|
194
195
|
// Tare correction
|
|
195
|
-
numericData
|
|
196
|
+
const numericData = convertedReceivedData - applyTare(convertedReceivedData);
|
|
197
|
+
// Add data to downloadable Array
|
|
198
|
+
DownloadPackets.push({
|
|
199
|
+
received: receivedTime,
|
|
200
|
+
sampleNum: this.dataPointCount,
|
|
201
|
+
battRaw: 0,
|
|
202
|
+
samples: [convertedReceivedData],
|
|
203
|
+
masses: [numericData],
|
|
204
|
+
});
|
|
196
205
|
// Update massMax
|
|
197
206
|
this.massMax = Math.max(Number(this.massMax), numericData).toFixed(1);
|
|
198
207
|
// Update running sum and count
|
|
@@ -35,14 +35,6 @@ export declare const KilterBoardPlacementRoles: {
|
|
|
35
35
|
led_color: string;
|
|
36
36
|
screen_color: string;
|
|
37
37
|
}[];
|
|
38
|
-
/**
|
|
39
|
-
* Represents climbs_placements from the Kilter Board application
|
|
40
|
-
*/
|
|
41
|
-
declare class ClimbPlacement {
|
|
42
|
-
position: number;
|
|
43
|
-
role_id: number;
|
|
44
|
-
constructor(position: number, role_id: number);
|
|
45
|
-
}
|
|
46
38
|
/**
|
|
47
39
|
* Represents a Aurora Climbing device
|
|
48
40
|
* Kilter Board, Tension Board, Decoy Board, Touchstone Board, Grasshopper Board, Aurora Board, So iLL Board
|
|
@@ -79,13 +71,13 @@ export declare class KilterBoard extends Device implements IKilterBoard {
|
|
|
79
71
|
* @param data - The array of bytes to calculate the checksum for.
|
|
80
72
|
* @returns The calculated checksum value.
|
|
81
73
|
*/
|
|
82
|
-
checksum
|
|
74
|
+
private checksum;
|
|
83
75
|
/**
|
|
84
76
|
* Wraps a byte array with header and footer bytes for transmission.
|
|
85
77
|
* @param data - The array of bytes to wrap.
|
|
86
78
|
* @returns The wrapped byte array.
|
|
87
79
|
*/
|
|
88
|
-
wrapBytes
|
|
80
|
+
private wrapBytes;
|
|
89
81
|
/**
|
|
90
82
|
* Encodes a position into a byte array.
|
|
91
83
|
* The lowest 8 bits of the position get put in the first byte of the group.
|
|
@@ -93,27 +85,27 @@ export declare class KilterBoard extends Device implements IKilterBoard {
|
|
|
93
85
|
* @param position - The position to encode.
|
|
94
86
|
* @returns The encoded byte array representing the position.
|
|
95
87
|
*/
|
|
96
|
-
encodePosition
|
|
88
|
+
private encodePosition;
|
|
97
89
|
/**
|
|
98
90
|
* Encodes a color string into a numeric representation.
|
|
99
91
|
* The rgb color, 3 bits for the R and G components, 2 bits for the B component, with the 3 R bits occupying the high end of the byte and the 2 B bits in the low end (hence 3 G bits in the middle).
|
|
100
92
|
* @param color - The color string in hexadecimal format (e.g., 'FFFFFF').
|
|
101
93
|
* @returns The encoded /compressed color value.
|
|
102
94
|
*/
|
|
103
|
-
encodeColor
|
|
95
|
+
private encodeColor;
|
|
104
96
|
/**
|
|
105
97
|
* Encodes a placement (requires a 16-bit position and a 24-bit rgb color. ) into a byte array.
|
|
106
98
|
* @param position - The position to encode.
|
|
107
99
|
* @param ledColor - The color of the LED in hexadecimal format (e.g., 'FFFFFF').
|
|
108
100
|
* @returns The encoded byte array representing the placement.
|
|
109
101
|
*/
|
|
110
|
-
encodePlacement
|
|
102
|
+
private encodePlacement;
|
|
111
103
|
/**
|
|
112
104
|
* Prepares byte arrays for transmission based on a list of climb placements.
|
|
113
|
-
* @param climbPlacementList - The list of climb placements containing position and role ID.
|
|
114
|
-
* @returns The final byte array ready for transmission.
|
|
105
|
+
* @param {{ position: number; role_id: number }[]} climbPlacementList - The list of climb placements containing position and role ID.
|
|
106
|
+
* @returns {number[]} The final byte array ready for transmission.
|
|
115
107
|
*/
|
|
116
|
-
prepBytesV3
|
|
108
|
+
private prepBytesV3;
|
|
117
109
|
/**
|
|
118
110
|
* Splits a collection into slices of the specified length.
|
|
119
111
|
* https://github.com/ramda/ramda/blob/master/source/splitEvery.js
|
|
@@ -121,7 +113,7 @@ export declare class KilterBoard extends Device implements IKilterBoard {
|
|
|
121
113
|
* @param {Array} list
|
|
122
114
|
* @return {Array}
|
|
123
115
|
*/
|
|
124
|
-
splitEvery
|
|
116
|
+
private splitEvery;
|
|
125
117
|
/**
|
|
126
118
|
* The kilter board only supports messages of 20 bytes
|
|
127
119
|
* at a time. This method splits a full message into parts
|
|
@@ -129,16 +121,18 @@ export declare class KilterBoard extends Device implements IKilterBoard {
|
|
|
129
121
|
*
|
|
130
122
|
* @param buffer
|
|
131
123
|
*/
|
|
132
|
-
splitMessages
|
|
124
|
+
private splitMessages;
|
|
133
125
|
/**
|
|
134
126
|
* Sends a series of messages to a device.
|
|
135
127
|
*/
|
|
136
|
-
writeMessageSeries
|
|
128
|
+
private writeMessageSeries;
|
|
137
129
|
/**
|
|
138
|
-
* Configures the LEDs based on an array of climb placements.
|
|
139
|
-
* @param {
|
|
130
|
+
* Configures the LEDs based on an array of climb placements.
|
|
131
|
+
* @param {{ position: number; role_id: number }[]} config - Array of climb placements for the LEDs.
|
|
140
132
|
* @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Kilter Board if LED settings were applied, or `undefined` if no action was taken or for the Motherboard.
|
|
141
133
|
*/
|
|
142
|
-
led: (config
|
|
134
|
+
led: (config: {
|
|
135
|
+
position: number;
|
|
136
|
+
role_id: number;
|
|
137
|
+
}[]) => Promise<number[] | undefined>;
|
|
143
138
|
}
|
|
144
|
-
export {};
|
|
@@ -64,17 +64,6 @@ export const KilterBoardPlacementRoles = [
|
|
|
64
64
|
screen_color: "FFA500",
|
|
65
65
|
},
|
|
66
66
|
];
|
|
67
|
-
/**
|
|
68
|
-
* Represents climbs_placements from the Kilter Board application
|
|
69
|
-
*/
|
|
70
|
-
class ClimbPlacement {
|
|
71
|
-
position;
|
|
72
|
-
role_id;
|
|
73
|
-
constructor(position, role_id) {
|
|
74
|
-
this.position = position;
|
|
75
|
-
this.role_id = role_id;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
67
|
/**
|
|
79
68
|
* Represents a Aurora Climbing device
|
|
80
69
|
* Kilter Board, Tension Board, Decoy Board, Touchstone Board, Grasshopper Board, Aurora Board, So iLL Board
|
|
@@ -206,8 +195,8 @@ export class KilterBoard extends Device {
|
|
|
206
195
|
}
|
|
207
196
|
/**
|
|
208
197
|
* Prepares byte arrays for transmission based on a list of climb placements.
|
|
209
|
-
* @param climbPlacementList - The list of climb placements containing position and role ID.
|
|
210
|
-
* @returns The final byte array ready for transmission.
|
|
198
|
+
* @param {{ position: number; role_id: number }[]} climbPlacementList - The list of climb placements containing position and role ID.
|
|
199
|
+
* @returns {number[]} The final byte array ready for transmission.
|
|
211
200
|
*/
|
|
212
201
|
prepBytesV3(climbPlacementList) {
|
|
213
202
|
const resultArray = [];
|
|
@@ -273,8 +262,8 @@ export class KilterBoard extends Device {
|
|
|
273
262
|
}
|
|
274
263
|
}
|
|
275
264
|
/**
|
|
276
|
-
* Configures the LEDs based on an array of climb placements.
|
|
277
|
-
* @param {
|
|
265
|
+
* Configures the LEDs based on an array of climb placements.
|
|
266
|
+
* @param {{ position: number; role_id: number }[]} config - Array of climb placements for the LEDs.
|
|
278
267
|
* @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Kilter Board if LED settings were applied, or `undefined` if no action was taken or for the Motherboard.
|
|
279
268
|
*/
|
|
280
269
|
led = async (config) => {
|
|
@@ -123,38 +123,38 @@ export class Progressor extends Device {
|
|
|
123
123
|
if (value) {
|
|
124
124
|
if (value.buffer) {
|
|
125
125
|
const buffer = value.buffer;
|
|
126
|
-
const
|
|
126
|
+
const rawData = new DataView(buffer);
|
|
127
127
|
const receivedTime = Date.now();
|
|
128
|
-
const [kind] = struct("<bb").unpack(
|
|
128
|
+
const [kind] = struct("<bb").unpack(rawData.buffer.slice(0, 2));
|
|
129
129
|
if (kind === ProgressorResponses.WEIGHT_MEASURE) {
|
|
130
|
-
const iterable = struct("<fi").iter_unpack(
|
|
130
|
+
const iterable = struct("<fi").iter_unpack(rawData.buffer.slice(2));
|
|
131
131
|
// eslint-disable-next-line prefer-const
|
|
132
132
|
for (let [weight, seconds] of iterable) {
|
|
133
133
|
if (typeof weight === "number" && !isNaN(weight) && typeof seconds === "number" && !isNaN(seconds)) {
|
|
134
|
-
//
|
|
134
|
+
// Tare correction
|
|
135
|
+
const numericData = weight - applyTare(weight);
|
|
136
|
+
// Add data to downloadable Array
|
|
135
137
|
DownloadPackets.push({
|
|
136
138
|
received: receivedTime,
|
|
137
139
|
sampleNum: seconds,
|
|
138
140
|
battRaw: 0,
|
|
139
141
|
samples: [weight],
|
|
140
|
-
masses: [
|
|
142
|
+
masses: [numericData],
|
|
141
143
|
});
|
|
142
|
-
// Tare correction
|
|
143
|
-
weight -= applyTare(weight);
|
|
144
144
|
// Check for max weight
|
|
145
|
-
this.massMax = Math.max(Number(this.massMax), Number(
|
|
145
|
+
this.massMax = Math.max(Number(this.massMax), Number(numericData)).toFixed(1);
|
|
146
146
|
// Update running sum and count
|
|
147
|
-
const currentMassTotal = Math.max(-1000, Number(
|
|
147
|
+
const currentMassTotal = Math.max(-1000, Number(numericData));
|
|
148
148
|
this.massTotalSum += currentMassTotal;
|
|
149
149
|
this.dataPointCount++;
|
|
150
150
|
// Calculate the average dynamically
|
|
151
151
|
this.massAverage = (this.massTotalSum / this.dataPointCount).toFixed(1);
|
|
152
152
|
// Check if device is being used
|
|
153
|
-
checkActivity(
|
|
153
|
+
checkActivity(numericData);
|
|
154
154
|
this.notifyCallback({
|
|
155
155
|
massMax: this.massMax,
|
|
156
156
|
massAverage: this.massAverage,
|
|
157
|
-
massTotal: Math.max(-1000,
|
|
157
|
+
massTotal: Math.max(-1000, numericData).toFixed(1),
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
}
|
|
@@ -164,13 +164,13 @@ export class Progressor extends Device {
|
|
|
164
164
|
return;
|
|
165
165
|
let value = "";
|
|
166
166
|
if (this.writeLast === this.commands.GET_BATT_VLTG) {
|
|
167
|
-
value = new DataView(
|
|
167
|
+
value = new DataView(rawData.buffer, 2).getUint32(0, true).toString();
|
|
168
168
|
}
|
|
169
169
|
else if (this.writeLast === this.commands.GET_FW_VERSION) {
|
|
170
|
-
value = new TextDecoder().decode(
|
|
170
|
+
value = new TextDecoder().decode(rawData.buffer.slice(2));
|
|
171
171
|
}
|
|
172
172
|
else if (this.writeLast === this.commands.GET_ERR_INFO) {
|
|
173
|
-
value = new TextDecoder().decode(
|
|
173
|
+
value = new TextDecoder().decode(rawData.buffer.slice(2));
|
|
174
174
|
}
|
|
175
175
|
this.writeCallback(value);
|
|
176
176
|
}
|
|
@@ -7,11 +7,28 @@ import type { IWHC06 } from "../../interfaces/device/wh-c06.interface";
|
|
|
7
7
|
export declare class WHC06 extends Device implements IWHC06 {
|
|
8
8
|
/**
|
|
9
9
|
* Offset for the byte location in the manufacturer data to extract the weight.
|
|
10
|
-
* This value is constant across all instances of the class.
|
|
11
10
|
* @type {number}
|
|
12
11
|
* @constant
|
|
13
12
|
*/
|
|
14
13
|
private static readonly WEIGHT_OFFSET;
|
|
14
|
+
/**
|
|
15
|
+
* Company identifier for WH-C06, also used by 'TomTom International BV': https://www.bluetooth.com/specifications/assigned-numbers/
|
|
16
|
+
* @type {number}
|
|
17
|
+
* @constant
|
|
18
|
+
*/
|
|
19
|
+
private static readonly MANUFACTURER_ID;
|
|
20
|
+
/**
|
|
21
|
+
* To track disconnection timeout.
|
|
22
|
+
* @type {number|null}
|
|
23
|
+
* @constant
|
|
24
|
+
*/
|
|
25
|
+
private advertisementTimeout;
|
|
26
|
+
/**
|
|
27
|
+
* The limit in seconds when timeout is triggered
|
|
28
|
+
* @type {number|null}
|
|
29
|
+
* @constant
|
|
30
|
+
*/
|
|
31
|
+
private advertisementTimeoutTime;
|
|
15
32
|
constructor();
|
|
16
33
|
/**
|
|
17
34
|
* Connects to a Bluetooth device.
|
|
@@ -19,4 +36,14 @@ export declare class WHC06 extends Device implements IWHC06 {
|
|
|
19
36
|
* @param {Function} [onError] - Optional callback function to execute on error. Default logs the error.
|
|
20
37
|
*/
|
|
21
38
|
connect: (onSuccess?: () => void, onError?: (error: Error) => void) => Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Custom check if a Bluetooth device is connected.
|
|
41
|
+
* For the WH-C06 device, the `gatt.connected` property remains `false` even after the device is connected.
|
|
42
|
+
* @returns {boolean} A boolean indicating whether the device is connected.
|
|
43
|
+
*/
|
|
44
|
+
isConnected: () => boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Resets the timeout that checks if the device is still advertising.
|
|
47
|
+
*/
|
|
48
|
+
private resetAdvertisementTimeout;
|
|
22
49
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Device } from "../device.model";
|
|
2
2
|
import { applyTare } from "../../helpers/tare";
|
|
3
3
|
import { checkActivity } from "../../helpers/is-active";
|
|
4
|
+
import { DownloadPackets } from "../../helpers/download";
|
|
4
5
|
/**
|
|
5
6
|
* Represents a Weiheng - WH-C06 (or MAT Muscle Meter) device
|
|
6
7
|
* Enable 'Experimental Web Platform features' Chrome Flags.
|
|
@@ -8,11 +9,28 @@ import { checkActivity } from "../../helpers/is-active";
|
|
|
8
9
|
export class WHC06 extends Device {
|
|
9
10
|
/**
|
|
10
11
|
* Offset for the byte location in the manufacturer data to extract the weight.
|
|
11
|
-
* This value is constant across all instances of the class.
|
|
12
12
|
* @type {number}
|
|
13
13
|
* @constant
|
|
14
14
|
*/
|
|
15
15
|
static WEIGHT_OFFSET = 10;
|
|
16
|
+
/**
|
|
17
|
+
* Company identifier for WH-C06, also used by 'TomTom International BV': https://www.bluetooth.com/specifications/assigned-numbers/
|
|
18
|
+
* @type {number}
|
|
19
|
+
* @constant
|
|
20
|
+
*/
|
|
21
|
+
static MANUFACTURER_ID = 256;
|
|
22
|
+
/**
|
|
23
|
+
* To track disconnection timeout.
|
|
24
|
+
* @type {number|null}
|
|
25
|
+
* @constant
|
|
26
|
+
*/
|
|
27
|
+
advertisementTimeout = null;
|
|
28
|
+
/**
|
|
29
|
+
* The limit in seconds when timeout is triggered
|
|
30
|
+
* @type {number|null}
|
|
31
|
+
* @constant
|
|
32
|
+
*/
|
|
33
|
+
advertisementTimeoutTime = 10;
|
|
16
34
|
// private static readonly STABLE_OFFSET = 14
|
|
17
35
|
constructor() {
|
|
18
36
|
super({
|
|
@@ -45,20 +63,27 @@ export class WHC06 extends Device {
|
|
|
45
63
|
if (!this.bluetooth.gatt) {
|
|
46
64
|
throw new Error("GATT is not available on this device");
|
|
47
65
|
}
|
|
48
|
-
// Device has no services / characteristics
|
|
66
|
+
// Device has no services / characteristics, so we directly call onSuccess
|
|
49
67
|
onSuccess();
|
|
50
|
-
// WH-C06
|
|
51
|
-
const MANUFACTURER_ID = 256; // 0x0100
|
|
52
68
|
this.bluetooth.addEventListener("advertisementreceived", (event) => {
|
|
53
|
-
const data = event.manufacturerData.get(MANUFACTURER_ID);
|
|
69
|
+
const data = event.manufacturerData.get(WHC06.MANUFACTURER_ID);
|
|
54
70
|
if (data) {
|
|
55
71
|
// Handle recieved data
|
|
56
72
|
const weight = (data.getUint8(WHC06.WEIGHT_OFFSET) << 8) | data.getUint8(WHC06.WEIGHT_OFFSET + 1);
|
|
57
73
|
// const stable = (data.getUint8(STABLE_OFFSET) & 0xf0) >> 4
|
|
58
74
|
// const unit = data.getUint8(STABLE_OFFSET) & 0x0f
|
|
59
|
-
|
|
75
|
+
const receivedTime = Date.now();
|
|
76
|
+
const receivedData = weight / 100;
|
|
60
77
|
// Tare correction
|
|
61
|
-
numericData
|
|
78
|
+
const numericData = receivedData - applyTare(receivedData);
|
|
79
|
+
// Add data to downloadable Array
|
|
80
|
+
DownloadPackets.push({
|
|
81
|
+
received: receivedTime,
|
|
82
|
+
sampleNum: this.dataPointCount,
|
|
83
|
+
battRaw: 0,
|
|
84
|
+
samples: [numericData],
|
|
85
|
+
masses: [numericData],
|
|
86
|
+
});
|
|
62
87
|
// Update massMax
|
|
63
88
|
this.massMax = Math.max(Number(this.massMax), numericData).toFixed(1);
|
|
64
89
|
// Update running sum and count
|
|
@@ -76,6 +101,8 @@ export class WHC06 extends Device {
|
|
|
76
101
|
massTotal: Math.max(-1000, numericData).toFixed(1),
|
|
77
102
|
});
|
|
78
103
|
}
|
|
104
|
+
// Reset "still advertising" counter
|
|
105
|
+
this.resetAdvertisementTimeout();
|
|
79
106
|
});
|
|
80
107
|
// When the companyIdentifier is provided we want to get manufacturerData using watchAdvertisements.
|
|
81
108
|
if (optionalManufacturerData.length) {
|
|
@@ -94,4 +121,30 @@ export class WHC06 extends Device {
|
|
|
94
121
|
onError(error);
|
|
95
122
|
}
|
|
96
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Custom check if a Bluetooth device is connected.
|
|
126
|
+
* For the WH-C06 device, the `gatt.connected` property remains `false` even after the device is connected.
|
|
127
|
+
* @returns {boolean} A boolean indicating whether the device is connected.
|
|
128
|
+
*/
|
|
129
|
+
isConnected = () => {
|
|
130
|
+
return !!this.bluetooth;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Resets the timeout that checks if the device is still advertising.
|
|
134
|
+
*/
|
|
135
|
+
resetAdvertisementTimeout = () => {
|
|
136
|
+
// Clear the previous timeout
|
|
137
|
+
if (this.advertisementTimeout) {
|
|
138
|
+
clearTimeout(this.advertisementTimeout);
|
|
139
|
+
}
|
|
140
|
+
// Set a new timeout to stop tracking if no advertisement is received
|
|
141
|
+
this.advertisementTimeout = window.setTimeout(() => {
|
|
142
|
+
// Mimic a disconnect
|
|
143
|
+
const disconnectedEvent = new Event("gattserverdisconnected");
|
|
144
|
+
Object.defineProperty(disconnectedEvent, "target", { value: this.bluetooth, writable: false });
|
|
145
|
+
// Also display a e
|
|
146
|
+
throw new Error(`No advertisement received for ${this.advertisementTimeoutTime} seconds, stopping tracking..`);
|
|
147
|
+
this.onDisconnected(disconnectedEvent);
|
|
148
|
+
}, this.advertisementTimeoutTime * 1000); // 10 seconds
|
|
149
|
+
};
|
|
97
150
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hangtime/grip-connect",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Griptonite Motherboard, Tindeq Progressor, PitchSix Force Board, WHC-06, Entralpi, Climbro, mySmartBoard: Web Bluetooth API Force-Sensing strength analysis for climbers",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -1,85 +1,12 @@
|
|
|
1
1
|
import type { IDevice } from "../device.interface"
|
|
2
|
-
/**
|
|
3
|
-
* Represents a climbing placement with a position and role identifier.
|
|
4
|
-
*/
|
|
5
|
-
export interface ClimbPlacement {
|
|
6
|
-
/** The position of the hold placement. */
|
|
7
|
-
position: number
|
|
8
|
-
/** The role ID associated with the climb placement. */
|
|
9
|
-
role_id: number
|
|
10
|
-
}
|
|
11
2
|
/**
|
|
12
3
|
* Interface representing the KilterBoard device, extending the base Device interface.
|
|
13
4
|
*/
|
|
14
5
|
export interface IKilterBoard extends IDevice {
|
|
15
|
-
/**
|
|
16
|
-
* Calculates the checksum for a byte array.
|
|
17
|
-
* @param data - The array of bytes to calculate the checksum for.
|
|
18
|
-
* @returns The calculated checksum value.
|
|
19
|
-
*/
|
|
20
|
-
checksum(data: number[]): number
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Wraps a byte array with header and footer bytes for transmission.
|
|
24
|
-
* @param data - The array of bytes to wrap.
|
|
25
|
-
* @returns The wrapped byte array.
|
|
26
|
-
*/
|
|
27
|
-
wrapBytes(data: number[]): number[]
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Encodes a position into a byte array.
|
|
31
|
-
* @param position - The position to encode.
|
|
32
|
-
* @returns The encoded byte array representing the position.
|
|
33
|
-
*/
|
|
34
|
-
encodePosition(position: number): number[]
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Encodes a color string into a numeric representation.
|
|
38
|
-
* @param color - The color string in hexadecimal format.
|
|
39
|
-
* @returns The encoded/compressed color value.
|
|
40
|
-
*/
|
|
41
|
-
encodeColor(color: string): number
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Encodes a placement into a byte array.
|
|
45
|
-
* @param position - The position to encode.
|
|
46
|
-
* @param ledColor - The color of the LED in hexadecimal format.
|
|
47
|
-
* @returns The encoded byte array representing the placement.
|
|
48
|
-
*/
|
|
49
|
-
encodePlacement(position: number, ledColor: string): number[]
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Prepares byte arrays for transmission based on a list of climb placements.
|
|
53
|
-
* @param climbPlacementList - The list of climb placements.
|
|
54
|
-
* @returns The final byte array ready for transmission.
|
|
55
|
-
*/
|
|
56
|
-
prepBytesV3(climbPlacementList: ClimbPlacement[]): number[]
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Splits a collection into slices of the specified length.
|
|
60
|
-
* @param n - Number of elements per slice.
|
|
61
|
-
* @param list - Array to be sliced.
|
|
62
|
-
* @returns The sliced array.
|
|
63
|
-
*/
|
|
64
|
-
splitEvery(n: number, list: number[]): number[][]
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Splits a message into 20-byte chunks for Bluetooth transmission.
|
|
68
|
-
* @param buffer - The message to split.
|
|
69
|
-
* @returns The array of Uint8Arrays.
|
|
70
|
-
*/
|
|
71
|
-
splitMessages(buffer: number[]): Uint8Array[]
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Sends a series of messages to the device.
|
|
75
|
-
* @param messages - Array of Uint8Arrays to send.
|
|
76
|
-
*/
|
|
77
|
-
writeMessageSeries(messages: Uint8Array[]): Promise<void>
|
|
78
|
-
|
|
79
6
|
/**
|
|
80
7
|
* Configures the LEDs based on an array of climb placements.
|
|
81
|
-
* @param
|
|
82
|
-
* @returns
|
|
8
|
+
* @param {{ position: number; role_id: number }[]} config - Array of climb placements for the LEDs.
|
|
9
|
+
* @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Kilter Board if LED settings were applied, or `undefined` if no action was taken or for the Motherboard.
|
|
83
10
|
*/
|
|
84
|
-
led(config
|
|
11
|
+
led(config: { position: number; role_id: number }[]): Promise<number[] | undefined>
|
|
85
12
|
}
|
|
@@ -2,6 +2,7 @@ import { Device } from "../device.model"
|
|
|
2
2
|
import type { IEntralpi } from "../../interfaces/device/entralpi.interface"
|
|
3
3
|
import { applyTare } from "../../helpers/tare"
|
|
4
4
|
import { checkActivity } from "../../helpers/is-active"
|
|
5
|
+
import { DownloadPackets } from "../../helpers/download"
|
|
5
6
|
|
|
6
7
|
export class Entralpi extends Device implements IEntralpi {
|
|
7
8
|
constructor() {
|
|
@@ -165,12 +166,20 @@ export class Entralpi extends Device implements IEntralpi {
|
|
|
165
166
|
if (value.buffer) {
|
|
166
167
|
const buffer: ArrayBuffer = value.buffer
|
|
167
168
|
const rawData: DataView = new DataView(buffer)
|
|
169
|
+
const receivedTime: number = Date.now()
|
|
168
170
|
const receivedData: string = (rawData.getUint16(0) / 100).toFixed(1)
|
|
169
171
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
const convertedData = Number(receivedData)
|
|
172
173
|
// Tare correction
|
|
173
|
-
numericData
|
|
174
|
+
const numericData = convertedData - applyTare(convertedData)
|
|
175
|
+
// Add data to downloadable Array
|
|
176
|
+
DownloadPackets.push({
|
|
177
|
+
received: receivedTime,
|
|
178
|
+
sampleNum: this.dataPointCount,
|
|
179
|
+
battRaw: 0,
|
|
180
|
+
samples: [convertedData],
|
|
181
|
+
masses: [numericData],
|
|
182
|
+
})
|
|
174
183
|
|
|
175
184
|
// Update massMax
|
|
176
185
|
this.massMax = Math.max(Number(this.massMax), numericData).toFixed(1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Device } from "../device.model"
|
|
2
2
|
import type { IForceBoard } from "../../interfaces/device/forceboard.interface"
|
|
3
|
-
import { emptyDownloadPackets } from "../../helpers/download"
|
|
3
|
+
import { DownloadPackets, emptyDownloadPackets } from "../../helpers/download"
|
|
4
4
|
import { checkActivity } from "../../helpers/is-active"
|
|
5
5
|
import { applyTare } from "../../helpers/tare"
|
|
6
6
|
|
|
@@ -192,14 +192,21 @@ export class ForceBoard extends Device implements IForceBoard {
|
|
|
192
192
|
if (value.buffer) {
|
|
193
193
|
const buffer: ArrayBuffer = value.buffer
|
|
194
194
|
const rawData: DataView = new DataView(buffer)
|
|
195
|
+
const receivedTime: number = Date.now()
|
|
195
196
|
|
|
196
197
|
const receivedData = rawData.getUint8(4) // Read the value at index 4
|
|
197
|
-
|
|
198
198
|
// Convert from LBS to KG
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const convertedReceivedData = receivedData * 0.453592
|
|
201
200
|
// Tare correction
|
|
202
|
-
numericData
|
|
201
|
+
const numericData = convertedReceivedData - applyTare(convertedReceivedData)
|
|
202
|
+
// Add data to downloadable Array
|
|
203
|
+
DownloadPackets.push({
|
|
204
|
+
received: receivedTime,
|
|
205
|
+
sampleNum: this.dataPointCount,
|
|
206
|
+
battRaw: 0,
|
|
207
|
+
samples: [convertedReceivedData],
|
|
208
|
+
masses: [numericData],
|
|
209
|
+
})
|
|
203
210
|
|
|
204
211
|
// Update massMax
|
|
205
212
|
this.massMax = Math.max(Number(this.massMax), numericData).toFixed(1)
|
|
@@ -66,19 +66,6 @@ export const KilterBoardPlacementRoles = [
|
|
|
66
66
|
},
|
|
67
67
|
]
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Represents climbs_placements from the Kilter Board application
|
|
71
|
-
*/
|
|
72
|
-
class ClimbPlacement {
|
|
73
|
-
position: number
|
|
74
|
-
role_id: number
|
|
75
|
-
|
|
76
|
-
constructor(position: number, role_id: number) {
|
|
77
|
-
this.position = position
|
|
78
|
-
this.role_id = role_id
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
69
|
/**
|
|
83
70
|
* Represents a Aurora Climbing device
|
|
84
71
|
* Kilter Board, Tension Board, Decoy Board, Touchstone Board, Grasshopper Board, Aurora Board, So iLL Board
|
|
@@ -145,7 +132,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
145
132
|
* @param data - The array of bytes to calculate the checksum for.
|
|
146
133
|
* @returns The calculated checksum value.
|
|
147
134
|
*/
|
|
148
|
-
checksum(data: number[]) {
|
|
135
|
+
private checksum(data: number[]) {
|
|
149
136
|
let i = 0
|
|
150
137
|
for (const value of data) {
|
|
151
138
|
i = (i + value) & 255
|
|
@@ -157,7 +144,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
157
144
|
* @param data - The array of bytes to wrap.
|
|
158
145
|
* @returns The wrapped byte array.
|
|
159
146
|
*/
|
|
160
|
-
wrapBytes(data: number[]) {
|
|
147
|
+
private wrapBytes(data: number[]) {
|
|
161
148
|
if (data.length > this.MESSAGE_BODY_MAX_LENGTH) {
|
|
162
149
|
return []
|
|
163
150
|
}
|
|
@@ -180,7 +167,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
180
167
|
* @param position - The position to encode.
|
|
181
168
|
* @returns The encoded byte array representing the position.
|
|
182
169
|
*/
|
|
183
|
-
encodePosition(position: number) {
|
|
170
|
+
private encodePosition(position: number) {
|
|
184
171
|
const position1 = position & 255
|
|
185
172
|
const position2 = (position & 65280) >> 8
|
|
186
173
|
|
|
@@ -192,7 +179,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
192
179
|
* @param color - The color string in hexadecimal format (e.g., 'FFFFFF').
|
|
193
180
|
* @returns The encoded /compressed color value.
|
|
194
181
|
*/
|
|
195
|
-
encodeColor(color: string) {
|
|
182
|
+
private encodeColor(color: string) {
|
|
196
183
|
const substring = color.substring(0, 2)
|
|
197
184
|
const substring2 = color.substring(2, 4)
|
|
198
185
|
|
|
@@ -212,15 +199,15 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
212
199
|
* @param ledColor - The color of the LED in hexadecimal format (e.g., 'FFFFFF').
|
|
213
200
|
* @returns The encoded byte array representing the placement.
|
|
214
201
|
*/
|
|
215
|
-
encodePlacement(position: number, ledColor: string) {
|
|
202
|
+
private encodePlacement(position: number, ledColor: string) {
|
|
216
203
|
return [...this.encodePosition(position), this.encodeColor(ledColor)]
|
|
217
204
|
}
|
|
218
205
|
/**
|
|
219
206
|
* Prepares byte arrays for transmission based on a list of climb placements.
|
|
220
|
-
* @param climbPlacementList - The list of climb placements containing position and role ID.
|
|
221
|
-
* @returns The final byte array ready for transmission.
|
|
207
|
+
* @param {{ position: number; role_id: number }[]} climbPlacementList - The list of climb placements containing position and role ID.
|
|
208
|
+
* @returns {number[]} The final byte array ready for transmission.
|
|
222
209
|
*/
|
|
223
|
-
prepBytesV3(climbPlacementList:
|
|
210
|
+
private prepBytesV3(climbPlacementList: { position: number; role_id: number }[]) {
|
|
224
211
|
const resultArray: number[][] = []
|
|
225
212
|
let tempArray: number[] = [KilterBoardPacket.V3_MIDDLE]
|
|
226
213
|
|
|
@@ -260,7 +247,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
260
247
|
* @param {Array} list
|
|
261
248
|
* @return {Array}
|
|
262
249
|
*/
|
|
263
|
-
splitEvery(n: number, list: number[]) {
|
|
250
|
+
private splitEvery(n: number, list: number[]) {
|
|
264
251
|
if (n <= 0) {
|
|
265
252
|
throw new Error("First argument to splitEvery must be a positive integer")
|
|
266
253
|
}
|
|
@@ -278,22 +265,22 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
278
265
|
*
|
|
279
266
|
* @param buffer
|
|
280
267
|
*/
|
|
281
|
-
splitMessages = (buffer: number[]) =>
|
|
268
|
+
private splitMessages = (buffer: number[]) =>
|
|
282
269
|
this.splitEvery(this.MAX_BLUETOOTH_MESSAGE_SIZE, buffer).map((arr) => new Uint8Array(arr))
|
|
283
270
|
/**
|
|
284
271
|
* Sends a series of messages to a device.
|
|
285
272
|
*/
|
|
286
|
-
async writeMessageSeries(messages: Uint8Array[]) {
|
|
273
|
+
private async writeMessageSeries(messages: Uint8Array[]) {
|
|
287
274
|
for (const message of messages) {
|
|
288
275
|
await this.write("uart", "tx", message)
|
|
289
276
|
}
|
|
290
277
|
}
|
|
291
278
|
/**
|
|
292
|
-
* Configures the LEDs based on an array of climb placements.
|
|
293
|
-
* @param {
|
|
279
|
+
* Configures the LEDs based on an array of climb placements.
|
|
280
|
+
* @param {{ position: number; role_id: number }[]} config - Array of climb placements for the LEDs.
|
|
294
281
|
* @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Kilter Board if LED settings were applied, or `undefined` if no action was taken or for the Motherboard.
|
|
295
282
|
*/
|
|
296
|
-
led = async (config
|
|
283
|
+
led = async (config: { position: number; role_id: number }[]): Promise<number[] | undefined> => {
|
|
297
284
|
// Handle Kilterboard logic: process placements and send payload if connected
|
|
298
285
|
if (Array.isArray(config)) {
|
|
299
286
|
// Prepares byte arrays for transmission based on a list of climb placements.
|
|
@@ -133,28 +133,28 @@ export class Progressor extends Device implements IProgressor {
|
|
|
133
133
|
if (value) {
|
|
134
134
|
if (value.buffer) {
|
|
135
135
|
const buffer: ArrayBuffer = value.buffer
|
|
136
|
-
const
|
|
136
|
+
const rawData: DataView = new DataView(buffer)
|
|
137
137
|
const receivedTime: number = Date.now()
|
|
138
|
-
const [kind] = struct("<bb").unpack(
|
|
138
|
+
const [kind] = struct("<bb").unpack(rawData.buffer.slice(0, 2))
|
|
139
139
|
if (kind === ProgressorResponses.WEIGHT_MEASURE) {
|
|
140
|
-
const iterable: IterableIterator<unknown[]> = struct("<fi").iter_unpack(
|
|
140
|
+
const iterable: IterableIterator<unknown[]> = struct("<fi").iter_unpack(rawData.buffer.slice(2))
|
|
141
141
|
// eslint-disable-next-line prefer-const
|
|
142
142
|
for (let [weight, seconds] of iterable) {
|
|
143
143
|
if (typeof weight === "number" && !isNaN(weight) && typeof seconds === "number" && !isNaN(seconds)) {
|
|
144
|
-
//
|
|
144
|
+
// Tare correction
|
|
145
|
+
const numericData = weight - applyTare(weight)
|
|
146
|
+
// Add data to downloadable Array
|
|
145
147
|
DownloadPackets.push({
|
|
146
148
|
received: receivedTime,
|
|
147
149
|
sampleNum: seconds,
|
|
148
150
|
battRaw: 0,
|
|
149
151
|
samples: [weight],
|
|
150
|
-
masses: [
|
|
152
|
+
masses: [numericData],
|
|
151
153
|
})
|
|
152
|
-
// Tare correction
|
|
153
|
-
weight -= applyTare(weight)
|
|
154
154
|
// Check for max weight
|
|
155
|
-
this.massMax = Math.max(Number(this.massMax), Number(
|
|
155
|
+
this.massMax = Math.max(Number(this.massMax), Number(numericData)).toFixed(1)
|
|
156
156
|
// Update running sum and count
|
|
157
|
-
const currentMassTotal = Math.max(-1000, Number(
|
|
157
|
+
const currentMassTotal = Math.max(-1000, Number(numericData))
|
|
158
158
|
this.massTotalSum += currentMassTotal
|
|
159
159
|
this.dataPointCount++
|
|
160
160
|
|
|
@@ -162,12 +162,12 @@ export class Progressor extends Device implements IProgressor {
|
|
|
162
162
|
this.massAverage = (this.massTotalSum / this.dataPointCount).toFixed(1)
|
|
163
163
|
|
|
164
164
|
// Check if device is being used
|
|
165
|
-
checkActivity(
|
|
165
|
+
checkActivity(numericData)
|
|
166
166
|
|
|
167
167
|
this.notifyCallback({
|
|
168
168
|
massMax: this.massMax,
|
|
169
169
|
massAverage: this.massAverage,
|
|
170
|
-
massTotal: Math.max(-1000,
|
|
170
|
+
massTotal: Math.max(-1000, numericData).toFixed(1),
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
173
|
}
|
|
@@ -177,11 +177,11 @@ export class Progressor extends Device implements IProgressor {
|
|
|
177
177
|
let value = ""
|
|
178
178
|
|
|
179
179
|
if (this.writeLast === this.commands.GET_BATT_VLTG) {
|
|
180
|
-
value = new DataView(
|
|
180
|
+
value = new DataView(rawData.buffer, 2).getUint32(0, true).toString()
|
|
181
181
|
} else if (this.writeLast === this.commands.GET_FW_VERSION) {
|
|
182
|
-
value = new TextDecoder().decode(
|
|
182
|
+
value = new TextDecoder().decode(rawData.buffer.slice(2))
|
|
183
183
|
} else if (this.writeLast === this.commands.GET_ERR_INFO) {
|
|
184
|
-
value = new TextDecoder().decode(
|
|
184
|
+
value = new TextDecoder().decode(rawData.buffer.slice(2))
|
|
185
185
|
}
|
|
186
186
|
this.writeCallback(value)
|
|
187
187
|
} else if (kind === ProgressorResponses.LOW_BATTERY_WARNING) {
|
|
@@ -2,6 +2,7 @@ import { Device } from "../device.model"
|
|
|
2
2
|
import { applyTare } from "../../helpers/tare"
|
|
3
3
|
import { checkActivity } from "../../helpers/is-active"
|
|
4
4
|
import type { IWHC06 } from "../../interfaces/device/wh-c06.interface"
|
|
5
|
+
import { DownloadPackets } from "../../helpers/download"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Represents a Weiheng - WH-C06 (or MAT Muscle Meter) device
|
|
@@ -10,14 +11,30 @@ import type { IWHC06 } from "../../interfaces/device/wh-c06.interface"
|
|
|
10
11
|
export class WHC06 extends Device implements IWHC06 {
|
|
11
12
|
/**
|
|
12
13
|
* Offset for the byte location in the manufacturer data to extract the weight.
|
|
13
|
-
* This value is constant across all instances of the class.
|
|
14
14
|
* @type {number}
|
|
15
15
|
* @constant
|
|
16
16
|
*/
|
|
17
17
|
private static readonly WEIGHT_OFFSET = 10
|
|
18
|
+
/**
|
|
19
|
+
* Company identifier for WH-C06, also used by 'TomTom International BV': https://www.bluetooth.com/specifications/assigned-numbers/
|
|
20
|
+
* @type {number}
|
|
21
|
+
* @constant
|
|
22
|
+
*/
|
|
23
|
+
private static readonly MANUFACTURER_ID: number = 256
|
|
24
|
+
/**
|
|
25
|
+
* To track disconnection timeout.
|
|
26
|
+
* @type {number|null}
|
|
27
|
+
* @constant
|
|
28
|
+
*/
|
|
29
|
+
private advertisementTimeout: number | null = null
|
|
30
|
+
/**
|
|
31
|
+
* The limit in seconds when timeout is triggered
|
|
32
|
+
* @type {number|null}
|
|
33
|
+
* @constant
|
|
34
|
+
*/
|
|
35
|
+
private advertisementTimeoutTime = 10
|
|
18
36
|
|
|
19
37
|
// private static readonly STABLE_OFFSET = 14
|
|
20
|
-
|
|
21
38
|
constructor() {
|
|
22
39
|
super({
|
|
23
40
|
filters: [
|
|
@@ -33,7 +50,6 @@ export class WHC06 extends Device implements IWHC06 {
|
|
|
33
50
|
services: [],
|
|
34
51
|
})
|
|
35
52
|
}
|
|
36
|
-
|
|
37
53
|
/**
|
|
38
54
|
* Connects to a Bluetooth device.
|
|
39
55
|
* @param {Function} [onSuccess] - Optional callback function to execute on successful connection. Default logs success.
|
|
@@ -58,24 +74,30 @@ export class WHC06 extends Device implements IWHC06 {
|
|
|
58
74
|
throw new Error("GATT is not available on this device")
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
// Device has no services / characteristics
|
|
77
|
+
// Device has no services / characteristics, so we directly call onSuccess
|
|
62
78
|
onSuccess()
|
|
63
79
|
|
|
64
|
-
// WH-C06
|
|
65
|
-
const MANUFACTURER_ID = 256 // 0x0100
|
|
66
|
-
|
|
67
80
|
this.bluetooth.addEventListener("advertisementreceived", (event) => {
|
|
68
|
-
const data = event.manufacturerData.get(MANUFACTURER_ID)
|
|
81
|
+
const data = event.manufacturerData.get(WHC06.MANUFACTURER_ID)
|
|
69
82
|
if (data) {
|
|
70
83
|
// Handle recieved data
|
|
71
84
|
const weight = (data.getUint8(WHC06.WEIGHT_OFFSET) << 8) | data.getUint8(WHC06.WEIGHT_OFFSET + 1)
|
|
72
85
|
// const stable = (data.getUint8(STABLE_OFFSET) & 0xf0) >> 4
|
|
73
86
|
// const unit = data.getUint8(STABLE_OFFSET) & 0x0f
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
const receivedTime: number = Date.now()
|
|
88
|
+
const receivedData = weight / 100
|
|
76
89
|
|
|
77
90
|
// Tare correction
|
|
78
|
-
numericData
|
|
91
|
+
const numericData = receivedData - applyTare(receivedData)
|
|
92
|
+
|
|
93
|
+
// Add data to downloadable Array
|
|
94
|
+
DownloadPackets.push({
|
|
95
|
+
received: receivedTime,
|
|
96
|
+
sampleNum: this.dataPointCount,
|
|
97
|
+
battRaw: 0,
|
|
98
|
+
samples: [numericData],
|
|
99
|
+
masses: [numericData],
|
|
100
|
+
})
|
|
79
101
|
|
|
80
102
|
// Update massMax
|
|
81
103
|
this.massMax = Math.max(Number(this.massMax), numericData).toFixed(1)
|
|
@@ -98,6 +120,8 @@ export class WHC06 extends Device implements IWHC06 {
|
|
|
98
120
|
massTotal: Math.max(-1000, numericData).toFixed(1),
|
|
99
121
|
})
|
|
100
122
|
}
|
|
123
|
+
// Reset "still advertising" counter
|
|
124
|
+
this.resetAdvertisementTimeout()
|
|
101
125
|
})
|
|
102
126
|
|
|
103
127
|
// When the companyIdentifier is provided we want to get manufacturerData using watchAdvertisements.
|
|
@@ -117,4 +141,31 @@ export class WHC06 extends Device implements IWHC06 {
|
|
|
117
141
|
onError(error as Error)
|
|
118
142
|
}
|
|
119
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Custom check if a Bluetooth device is connected.
|
|
146
|
+
* For the WH-C06 device, the `gatt.connected` property remains `false` even after the device is connected.
|
|
147
|
+
* @returns {boolean} A boolean indicating whether the device is connected.
|
|
148
|
+
*/
|
|
149
|
+
isConnected = (): boolean => {
|
|
150
|
+
return !!this.bluetooth
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resets the timeout that checks if the device is still advertising.
|
|
154
|
+
*/
|
|
155
|
+
private resetAdvertisementTimeout = (): void => {
|
|
156
|
+
// Clear the previous timeout
|
|
157
|
+
if (this.advertisementTimeout) {
|
|
158
|
+
clearTimeout(this.advertisementTimeout)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Set a new timeout to stop tracking if no advertisement is received
|
|
162
|
+
this.advertisementTimeout = window.setTimeout(() => {
|
|
163
|
+
// Mimic a disconnect
|
|
164
|
+
const disconnectedEvent = new Event("gattserverdisconnected")
|
|
165
|
+
Object.defineProperty(disconnectedEvent, "target", { value: this.bluetooth, writable: false })
|
|
166
|
+
// Also display a e
|
|
167
|
+
throw new Error(`No advertisement received for ${this.advertisementTimeoutTime} seconds, stopping tracking..`)
|
|
168
|
+
this.onDisconnected(disconnectedEvent)
|
|
169
|
+
}, this.advertisementTimeoutTime * 1000) // 10 seconds
|
|
170
|
+
}
|
|
120
171
|
}
|