@hangtime/grip-connect 0.5.5 → 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.
@@ -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 config - Optional color or array of climb placements.
73
- * @returns The prepared payload or undefined.
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?: ClimbPlacement[]): Promise<number[] | undefined>;
11
+ led(config: {
12
+ position: number;
13
+ role_id: number;
14
+ }[]): Promise<number[] | undefined>;
76
15
  }
@@ -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(data: number[]): number;
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(data: number[]): number[];
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(position: number): number[];
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(color: string): number;
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(position: number, ledColor: string): number[];
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(climbPlacementList: ClimbPlacement[]): number[];
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(n: number, list: number[]): number[][];
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: (buffer: number[]) => Uint8Array[];
124
+ private splitMessages;
133
125
  /**
134
126
  * Sends a series of messages to a device.
135
127
  */
136
- writeMessageSeries(messages: Uint8Array[]): Promise<void>;
128
+ private writeMessageSeries;
137
129
  /**
138
- * Configures the LEDs based on an array of climb placements. If a configuration is provided, it prepares and sends a payload to the device.
139
- * @param {ClimbPlacement[]} [config] - Optional color or array of climb placements for the LEDs. Ignored if placements are provided.
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?: ClimbPlacement[]) => Promise<number[] | undefined>;
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. If a configuration is provided, it prepares and sends a payload to the device.
277
- * @param {ClimbPlacement[]} [config] - Optional color or array of climb placements for the LEDs. Ignored if placements are provided.
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) => {
@@ -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
- let numericData = weight / 100;
75
+ const receivedTime = Date.now();
76
+ const receivedData = weight / 100;
60
77
  // Tare correction
61
- numericData -= applyTare(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.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 config - Optional color or array of climb placements.
82
- * @returns The prepared payload or undefined.
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?: ClimbPlacement[]): Promise<number[] | undefined>
11
+ led(config: { position: number; role_id: number }[]): Promise<number[] | undefined>
85
12
  }
@@ -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: ClimbPlacement[]) {
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. If a configuration is provided, it prepares and sends a payload to the device.
293
- * @param {ClimbPlacement[]} [config] - Optional color or array of climb placements for the LEDs. Ignored if placements are provided.
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?: ClimbPlacement[]): Promise<number[] | undefined> => {
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.
@@ -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
- let numericData = weight / 100
87
+ const receivedTime: number = Date.now()
88
+ const receivedData = weight / 100
76
89
 
77
90
  // Tare correction
78
- numericData -= applyTare(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
  }