@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.
@@ -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
  }
@@ -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
- let numericData = Number(receivedData);
164
+ const convertedData = Number(receivedData);
163
165
  // Tare correction
164
- numericData -= applyTare(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
- let numericData = receivedData * 0.453592;
194
+ const convertedReceivedData = receivedData * 0.453592;
194
195
  // Tare correction
195
- numericData -= applyTare(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(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) => {
@@ -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 data = new DataView(buffer);
126
+ const rawData = new DataView(buffer);
127
127
  const receivedTime = Date.now();
128
- const [kind] = struct("<bb").unpack(data.buffer.slice(0, 2));
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(data.buffer.slice(2));
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
- // Add data to downloadable Array: sample and mass are the same
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: [weight],
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(weight)).toFixed(1);
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(weight));
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(weight);
153
+ checkActivity(numericData);
154
154
  this.notifyCallback({
155
155
  massMax: this.massMax,
156
156
  massAverage: this.massAverage,
157
- massTotal: Math.max(-1000, weight).toFixed(1),
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(data.buffer, 2).getUint32(0, true).toString();
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(data.buffer.slice(2));
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(data.buffer.slice(2));
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
- 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.4",
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
  }
@@ -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
- let numericData = Number(receivedData)
171
-
172
+ const convertedData = Number(receivedData)
172
173
  // Tare correction
173
- numericData -= applyTare(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
- let numericData = receivedData * 0.453592
200
-
199
+ const convertedReceivedData = receivedData * 0.453592
201
200
  // Tare correction
202
- numericData -= applyTare(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: 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.
@@ -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 data: DataView = new DataView(buffer)
136
+ const rawData: DataView = new DataView(buffer)
137
137
  const receivedTime: number = Date.now()
138
- const [kind] = struct("<bb").unpack(data.buffer.slice(0, 2))
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(data.buffer.slice(2))
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
- // Add data to downloadable Array: sample and mass are the same
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: [weight],
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(weight)).toFixed(1)
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(weight))
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(weight)
165
+ checkActivity(numericData)
166
166
 
167
167
  this.notifyCallback({
168
168
  massMax: this.massMax,
169
169
  massAverage: this.massAverage,
170
- massTotal: Math.max(-1000, weight).toFixed(1),
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(data.buffer, 2).getUint32(0, true).toString()
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(data.buffer.slice(2))
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(data.buffer.slice(2))
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
- 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
  }