@hangtime/grip-connect 0.5.9 → 0.5.11
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/README.md +3 -2
- package/deno.json +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/models/base.model.js +1 -1
- package/dist/models/device/entralpi.model.js +2 -0
- package/dist/models/device/forceboard.model.js +2 -0
- package/dist/models/device/kilterboard.model.js +1 -1
- package/dist/models/device/motherboard.model.js +2 -0
- package/dist/models/device/progressor.model.js +2 -0
- package/dist/models/device/wh-c06.model.js +7 -2
- package/dist/models/device.model.d.ts +27 -1
- package/dist/models/device.model.js +80 -49
- package/dist/models/index.js +1 -0
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/models/base.model.ts +1 -1
- package/src/models/device/entralpi.model.ts +3 -1
- package/src/models/device/forceboard.model.ts +3 -1
- package/src/models/device/kilterboard.model.ts +1 -1
- package/src/models/device/motherboard.model.ts +3 -1
- package/src/models/device/progressor.model.ts +3 -1
- package/src/models/device/wh-c06.model.ts +9 -4
- package/src/models/device.model.ts +86 -52
- package/src/models/index.ts +2 -0
package/README.md
CHANGED
|
@@ -35,7 +35,8 @@ Learn more: [Documentation](https://stevie-ray.github.io/hangtime-grip-connect/)
|
|
|
35
35
|
|
|
36
36
|
## Install
|
|
37
37
|
|
|
38
|
-
This project can be found in the [NPM
|
|
38
|
+
This project can be found in the [NPM](https://www.npmjs.com/package/@hangtime/grip-connect) and
|
|
39
|
+
[JSR](https://jsr.io/@hangtime/grip-connect) package registries.
|
|
39
40
|
|
|
40
41
|
```sh [npm]
|
|
41
42
|
$ npm install @hangtime/grip-connect
|
|
@@ -113,7 +114,7 @@ document.querySelector("#motherboard").addEventListener("click", async () => {
|
|
|
113
114
|
- By default [watchAdvertisements](https://chromestatus.com/feature/5180688812736512) isn't supported . For Chrome,
|
|
114
115
|
enable it at `chrome://flags/#enable-experimental-web-platform-features`.
|
|
115
116
|
- ✅ [Kilter Board](https://stevie-ray.github.io/hangtime-grip-connect/devices/kilterboard.html)
|
|
116
|
-
- ✅ [Entralpi](https://stevie-ray.github.io/hangtime-grip-connect/devices/entralpi.html) / Lefu Scale
|
|
117
|
+
- ✅ [Entralpi](https://stevie-ray.github.io/hangtime-grip-connect/devices/entralpi.html) / Lefu / Unique CW275 Scale
|
|
117
118
|
- ✅ [PitchSix Force Board](https://stevie-ray.github.io/hangtime-grip-connect/devices/forceboard.html)
|
|
118
119
|
- ➡️ [Climbro](https://stevie-ray.github.io/hangtime-grip-connect/devices/climbro.html)
|
|
119
120
|
- ➡️ [Smartboard Climbing - mySmartBoard](https://stevie-ray.github.io/hangtime-grip-connect/devices/mysmartboard.html)
|
package/deno.json
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Climbro, Entralpi, ForceBoard, KilterBoard, Motherboard, mySmartBoard,
|
|
1
|
+
export { Climbro, Entralpi, ForceBoard, KilterBoard, Motherboard, mySmartBoard, Progressor, WHC06, } from "./models/index";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Climbro, Entralpi, ForceBoard, KilterBoard, Motherboard, mySmartBoard,
|
|
1
|
+
export { Climbro, Entralpi, ForceBoard, KilterBoard, Motherboard, mySmartBoard, Progressor, WHC06, } from "./models/index";
|
|
@@ -152,6 +152,8 @@ export class Entralpi extends Device {
|
|
|
152
152
|
handleNotifications = (characteristic) => {
|
|
153
153
|
const value = characteristic.value;
|
|
154
154
|
if (value) {
|
|
155
|
+
// Update timestamp
|
|
156
|
+
this.updateTimestamp();
|
|
155
157
|
if (value.buffer) {
|
|
156
158
|
const receivedTime = Date.now();
|
|
157
159
|
const receivedData = (value.getUint16(0) / 100).toFixed(1);
|
|
@@ -181,6 +181,8 @@ export class ForceBoard extends Device {
|
|
|
181
181
|
handleNotifications = (characteristic) => {
|
|
182
182
|
const value = characteristic.value;
|
|
183
183
|
if (value) {
|
|
184
|
+
// Update timestamp
|
|
185
|
+
this.updateTimestamp();
|
|
184
186
|
if (value.buffer) {
|
|
185
187
|
const receivedTime = Date.now();
|
|
186
188
|
const dataArray = new Uint8Array(value.buffer);
|
|
@@ -154,7 +154,7 @@ export class KilterBoard extends Device {
|
|
|
154
154
|
- 0x2
|
|
155
155
|
- *packets
|
|
156
156
|
- 0x3
|
|
157
|
-
|
|
157
|
+
|
|
158
158
|
First byte is always 1, the second is a number of packets, then checksum, then 2, packets themselves, and finally 3.
|
|
159
159
|
*/
|
|
160
160
|
return [1, data.length, this.checksum(data), 2, ...data, 3];
|
|
@@ -191,6 +191,8 @@ export class Motherboard extends Device {
|
|
|
191
191
|
handleNotifications = (characteristic) => {
|
|
192
192
|
const value = characteristic.value;
|
|
193
193
|
if (value) {
|
|
194
|
+
// Update timestamp
|
|
195
|
+
this.updateTimestamp();
|
|
194
196
|
if (value.buffer) {
|
|
195
197
|
for (let i = 0; i < value.byteLength; i++) {
|
|
196
198
|
this.receiveBuffer.push(value.getUint8(i));
|
|
@@ -116,6 +116,8 @@ export class Progressor extends Device {
|
|
|
116
116
|
handleNotifications = (characteristic) => {
|
|
117
117
|
const value = characteristic.value;
|
|
118
118
|
if (value) {
|
|
119
|
+
// Update timestamp
|
|
120
|
+
this.updateTimestamp();
|
|
119
121
|
if (value.buffer) {
|
|
120
122
|
const receivedTime = Date.now();
|
|
121
123
|
// Read the first byte of the buffer to determine the kind of message
|
|
@@ -72,6 +72,8 @@ export class WHC06 extends Device {
|
|
|
72
72
|
if (!this.bluetooth.gatt) {
|
|
73
73
|
throw new Error("GATT is not available on this device");
|
|
74
74
|
}
|
|
75
|
+
// Update timestamp
|
|
76
|
+
this.updateTimestamp();
|
|
75
77
|
// Device has no services / characteristics, so we directly call onSuccess
|
|
76
78
|
onSuccess();
|
|
77
79
|
this.bluetooth.addEventListener("advertisementreceived", (event) => {
|
|
@@ -152,10 +154,13 @@ export class WHC06 extends Device {
|
|
|
152
154
|
clearTimeout(this.advertisementTimeout);
|
|
153
155
|
}
|
|
154
156
|
// Set a new timeout to stop tracking if no advertisement is received
|
|
155
|
-
this.advertisementTimeout =
|
|
157
|
+
this.advertisementTimeout = globalThis.setTimeout(() => {
|
|
156
158
|
// Mimic a disconnect
|
|
157
159
|
const disconnectedEvent = new Event("gattserverdisconnected");
|
|
158
|
-
Object.defineProperty(disconnectedEvent, "target", {
|
|
160
|
+
Object.defineProperty(disconnectedEvent, "target", {
|
|
161
|
+
value: this.bluetooth,
|
|
162
|
+
writable: false,
|
|
163
|
+
});
|
|
159
164
|
// Print error to the console
|
|
160
165
|
console.error(`No advertisement received for ${this.advertisementTimeoutTime} seconds, stopping tracking..`);
|
|
161
166
|
this.onDisconnected(disconnectedEvent);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseModel } from "./../models/base.model";
|
|
2
2
|
import type { IDevice, Service } from "../interfaces/device.interface";
|
|
3
|
-
import type { NotifyCallback, WriteCallback
|
|
3
|
+
import type { ActiveCallback, NotifyCallback, WriteCallback } from "../interfaces/callback.interface";
|
|
4
4
|
import type { DownloadPacket } from "../interfaces/download.interface";
|
|
5
5
|
import type { Commands } from "../interfaces/command.interface";
|
|
6
6
|
export declare abstract class Device extends BaseModel implements IDevice {
|
|
@@ -138,6 +138,22 @@ export declare abstract class Device extends BaseModel implements IDevice {
|
|
|
138
138
|
* @protected
|
|
139
139
|
*/
|
|
140
140
|
protected activeCallback: ActiveCallback;
|
|
141
|
+
/**
|
|
142
|
+
* Event listener for handling the 'gattserverdisconnected' event.
|
|
143
|
+
* This listener delegates the event to the `onDisconnected` method.
|
|
144
|
+
*
|
|
145
|
+
* @private
|
|
146
|
+
* @type {(event: Event) => void}
|
|
147
|
+
*/
|
|
148
|
+
private onDisconnectedListener;
|
|
149
|
+
/**
|
|
150
|
+
* A map that stores notification event listeners keyed by characteristic UUIDs.
|
|
151
|
+
* This allows for proper addition and removal of event listeners associated with each characteristic.
|
|
152
|
+
*
|
|
153
|
+
* @private
|
|
154
|
+
* @type {Map<string, EventListener>}
|
|
155
|
+
*/
|
|
156
|
+
private notificationListeners;
|
|
141
157
|
constructor(device: Partial<IDevice>);
|
|
142
158
|
/**
|
|
143
159
|
* Sets the callback function to be called when the activity status changes,
|
|
@@ -364,6 +380,16 @@ export declare abstract class Device extends BaseModel implements IDevice {
|
|
|
364
380
|
* console.log('Calibrated sample:', calibratedSample);
|
|
365
381
|
*/
|
|
366
382
|
protected applyTare(sample: number): number;
|
|
383
|
+
/**
|
|
384
|
+
* Updates the timestamp of the last device interaction.
|
|
385
|
+
* This method sets the updatedAt property to the current date and time.
|
|
386
|
+
* @protected
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* device.updateTimestamp();
|
|
390
|
+
* console.log('Last updated:', device.updatedAt);
|
|
391
|
+
*/
|
|
392
|
+
protected updateTimestamp: () => void;
|
|
367
393
|
/**
|
|
368
394
|
* Writes a message to the specified characteristic of a Bluetooth device and optionally provides a callback to handle responses.
|
|
369
395
|
* @param {string} serviceId - The service UUID of the Bluetooth device containing the target characteristic.
|
|
@@ -51,7 +51,10 @@ export class Device extends BaseModel {
|
|
|
51
51
|
/**
|
|
52
52
|
* Configuration for threshold and duration.
|
|
53
53
|
*/
|
|
54
|
-
activeConfig = {
|
|
54
|
+
activeConfig = {
|
|
55
|
+
threshold: 2.5,
|
|
56
|
+
duration: 1000,
|
|
57
|
+
};
|
|
55
58
|
/**
|
|
56
59
|
* Maximum mass recorded from the device, initialized to "0".
|
|
57
60
|
* @type {string}
|
|
@@ -134,6 +137,22 @@ export class Device extends BaseModel {
|
|
|
134
137
|
* @protected
|
|
135
138
|
*/
|
|
136
139
|
activeCallback = (data) => console.log(data);
|
|
140
|
+
/**
|
|
141
|
+
* Event listener for handling the 'gattserverdisconnected' event.
|
|
142
|
+
* This listener delegates the event to the `onDisconnected` method.
|
|
143
|
+
*
|
|
144
|
+
* @private
|
|
145
|
+
* @type {(event: Event) => void}
|
|
146
|
+
*/
|
|
147
|
+
onDisconnectedListener = (event) => this.onDisconnected(event);
|
|
148
|
+
/**
|
|
149
|
+
* A map that stores notification event listeners keyed by characteristic UUIDs.
|
|
150
|
+
* This allows for proper addition and removal of event listeners associated with each characteristic.
|
|
151
|
+
*
|
|
152
|
+
* @private
|
|
153
|
+
* @type {Map<string, EventListener>}
|
|
154
|
+
*/
|
|
155
|
+
notificationListeners = new Map();
|
|
137
156
|
constructor(device) {
|
|
138
157
|
super(device);
|
|
139
158
|
this.filters = device.filters || [];
|
|
@@ -144,6 +163,8 @@ export class Device extends BaseModel {
|
|
|
144
163
|
this.massAverage = "0";
|
|
145
164
|
this.massTotalSum = 0;
|
|
146
165
|
this.dataPointCount = 0;
|
|
166
|
+
this.createdAt = new Date();
|
|
167
|
+
this.updatedAt = new Date();
|
|
147
168
|
}
|
|
148
169
|
/**
|
|
149
170
|
* Sets the callback function to be called when the activity status changes,
|
|
@@ -189,28 +210,19 @@ export class Device extends BaseModel {
|
|
|
189
210
|
*/
|
|
190
211
|
activityCheck = (input) => {
|
|
191
212
|
return new Promise((resolve) => {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (this.
|
|
200
|
-
this.
|
|
201
|
-
if (this.activeCallback) {
|
|
202
|
-
this.activeCallback(activeNow);
|
|
203
|
-
}
|
|
213
|
+
const startValue = input;
|
|
214
|
+
const { threshold, duration } = this.activeConfig;
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
// After waiting for `duration`, check if still active (for a real scenario, you might store a last known input)
|
|
217
|
+
const activeNow = startValue > threshold;
|
|
218
|
+
if (this.isActive !== activeNow) {
|
|
219
|
+
this.isActive = activeNow;
|
|
220
|
+
if (this.activeCallback) {
|
|
221
|
+
this.activeCallback(activeNow);
|
|
204
222
|
}
|
|
205
|
-
resolve();
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
// Continue checking until the duration is met
|
|
209
|
-
requestAnimationFrame(checkActivity);
|
|
210
223
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
checkActivity();
|
|
224
|
+
resolve();
|
|
225
|
+
}, duration);
|
|
214
226
|
});
|
|
215
227
|
};
|
|
216
228
|
/**
|
|
@@ -227,6 +239,9 @@ export class Device extends BaseModel {
|
|
|
227
239
|
*/
|
|
228
240
|
connect = async (onSuccess = () => console.log("Connected successfully"), onError = (error) => console.error(error)) => {
|
|
229
241
|
try {
|
|
242
|
+
if (typeof navigator === "undefined" || !("bluetooth" in navigator)) {
|
|
243
|
+
throw new Error("Web Bluetooth API not supported in this environment.");
|
|
244
|
+
}
|
|
230
245
|
// Request device and set up connection
|
|
231
246
|
const deviceServices = this.getAllServiceUUIDs();
|
|
232
247
|
this.bluetooth = await navigator.bluetooth.requestDevice({
|
|
@@ -236,9 +251,7 @@ export class Device extends BaseModel {
|
|
|
236
251
|
if (!this.bluetooth.gatt) {
|
|
237
252
|
throw new Error("GATT is not available on this device");
|
|
238
253
|
}
|
|
239
|
-
this.bluetooth.addEventListener("gattserverdisconnected",
|
|
240
|
-
this.onDisconnected(event);
|
|
241
|
-
});
|
|
254
|
+
this.bluetooth.addEventListener("gattserverdisconnected", this.onDisconnectedListener);
|
|
242
255
|
this.server = await this.bluetooth.gatt.connect();
|
|
243
256
|
if (this.server.connected) {
|
|
244
257
|
await this.onConnected(onSuccess);
|
|
@@ -262,22 +275,23 @@ export class Device extends BaseModel {
|
|
|
262
275
|
*/
|
|
263
276
|
disconnect = () => {
|
|
264
277
|
if (this.isConnected()) {
|
|
278
|
+
this.updateTimestamp();
|
|
265
279
|
// Remove all notification listeners
|
|
266
280
|
this.services.forEach((service) => {
|
|
267
281
|
service.characteristics.forEach((char) => {
|
|
282
|
+
// TODO: remove device-specific logic
|
|
268
283
|
if (char.characteristic && char.id === "rx") {
|
|
269
284
|
char.characteristic.stopNotifications();
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
285
|
+
const listener = this.notificationListeners.get(char.uuid);
|
|
286
|
+
if (listener) {
|
|
287
|
+
char.characteristic.removeEventListener("characteristicvaluechanged", listener);
|
|
288
|
+
this.notificationListeners.delete(char.uuid);
|
|
289
|
+
}
|
|
276
290
|
}
|
|
277
291
|
});
|
|
278
292
|
});
|
|
279
293
|
// Remove disconnect listener
|
|
280
|
-
this.bluetooth?.removeEventListener("gattserverdisconnected", this.
|
|
294
|
+
this.bluetooth?.removeEventListener("gattserverdisconnected", this.onDisconnectedListener);
|
|
281
295
|
// Safely attempt to disconnect the device's GATT server, if available
|
|
282
296
|
this.bluetooth?.gatt?.disconnect();
|
|
283
297
|
// Reset properties
|
|
@@ -367,6 +381,10 @@ export class Device extends BaseModel {
|
|
|
367
381
|
* device.download('json');
|
|
368
382
|
*/
|
|
369
383
|
download = (format = "csv") => {
|
|
384
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
385
|
+
console.warn("Download is not supported outside a browser environment.");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
370
388
|
let content = "";
|
|
371
389
|
let mimeType = "";
|
|
372
390
|
let fileName = "";
|
|
@@ -391,7 +409,7 @@ export class Device extends BaseModel {
|
|
|
391
409
|
// Create a Blob object containing the data
|
|
392
410
|
const blob = new Blob([content], { type: mimeType });
|
|
393
411
|
// Create a URL for the Blob
|
|
394
|
-
const url =
|
|
412
|
+
const url = globalThis.URL.createObjectURL(blob);
|
|
395
413
|
// Create a link element
|
|
396
414
|
const link = document.createElement("a");
|
|
397
415
|
// Set link attributes
|
|
@@ -403,7 +421,7 @@ export class Device extends BaseModel {
|
|
|
403
421
|
link.click();
|
|
404
422
|
// Clean up: remove the link and revoke the URL
|
|
405
423
|
document.body.removeChild(link);
|
|
406
|
-
|
|
424
|
+
globalThis.URL.revokeObjectURL(url);
|
|
407
425
|
};
|
|
408
426
|
/**
|
|
409
427
|
* Returns UUIDs of all services associated with the device.
|
|
@@ -453,15 +471,11 @@ export class Device extends BaseModel {
|
|
|
453
471
|
*/
|
|
454
472
|
handleNotifications = (characteristic) => {
|
|
455
473
|
const value = characteristic.value;
|
|
456
|
-
if (!value)
|
|
474
|
+
if (!value)
|
|
457
475
|
return;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
else {
|
|
463
|
-
console.log(value);
|
|
464
|
-
}
|
|
476
|
+
this.updateTimestamp();
|
|
477
|
+
// Received notification data
|
|
478
|
+
console.log(value);
|
|
465
479
|
};
|
|
466
480
|
/**
|
|
467
481
|
* Checks if a Bluetooth device is connected.
|
|
@@ -508,6 +522,7 @@ export class Device extends BaseModel {
|
|
|
508
522
|
* });
|
|
509
523
|
*/
|
|
510
524
|
onConnected = async (onSuccess) => {
|
|
525
|
+
this.updateTimestamp();
|
|
511
526
|
if (!this.server) {
|
|
512
527
|
throw new Error("GATT server is not available");
|
|
513
528
|
}
|
|
@@ -528,15 +543,17 @@ export class Device extends BaseModel {
|
|
|
528
543
|
const element = matchingService.characteristics.find((char) => char.uuid === matchingCharacteristic.uuid);
|
|
529
544
|
if (element) {
|
|
530
545
|
element.characteristic = matchingCharacteristic;
|
|
531
|
-
//
|
|
546
|
+
// TODO: remove device-specific logic
|
|
532
547
|
if (element.id === "rx") {
|
|
533
548
|
matchingCharacteristic.startNotifications();
|
|
534
|
-
|
|
549
|
+
const listener = (event) => {
|
|
535
550
|
const target = event.target;
|
|
536
551
|
if (target && target.value) {
|
|
537
552
|
this.handleNotifications(target);
|
|
538
553
|
}
|
|
539
|
-
}
|
|
554
|
+
};
|
|
555
|
+
matchingCharacteristic.addEventListener("characteristicvaluechanged", listener);
|
|
556
|
+
this.notificationListeners.set(element.uuid, listener);
|
|
540
557
|
}
|
|
541
558
|
}
|
|
542
559
|
}
|
|
@@ -558,9 +575,8 @@ export class Device extends BaseModel {
|
|
|
558
575
|
* device.onDisconnected(event);
|
|
559
576
|
*/
|
|
560
577
|
onDisconnected = (event) => {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
console.warn(`Device ${device.name} is disconnected.`);
|
|
578
|
+
console.warn(`Device ${event.target.name} is disconnected.`);
|
|
579
|
+
this.disconnect();
|
|
564
580
|
};
|
|
565
581
|
/**
|
|
566
582
|
* Reads the value of the specified characteristic from the device.
|
|
@@ -581,8 +597,9 @@ export class Device extends BaseModel {
|
|
|
581
597
|
// Get the characteristic from the service
|
|
582
598
|
const characteristic = this.getCharacteristic(serviceId, characteristicId);
|
|
583
599
|
if (!characteristic) {
|
|
584
|
-
throw new Error(
|
|
600
|
+
throw new Error(`Characteristic "${characteristicId}" not found in service "${serviceId}"`);
|
|
585
601
|
}
|
|
602
|
+
this.updateTimestamp();
|
|
586
603
|
// Decode the value based on characteristicId and serviceId
|
|
587
604
|
let decodedValue;
|
|
588
605
|
const decoder = new TextDecoder("utf-8");
|
|
@@ -620,6 +637,7 @@ export class Device extends BaseModel {
|
|
|
620
637
|
tare(duration = 5000) {
|
|
621
638
|
if (this.tareActive)
|
|
622
639
|
return false;
|
|
640
|
+
this.updateTimestamp();
|
|
623
641
|
this.tareActive = true;
|
|
624
642
|
this.tareDuration = duration;
|
|
625
643
|
this.tareSamples = [];
|
|
@@ -654,6 +672,18 @@ export class Device extends BaseModel {
|
|
|
654
672
|
// Return the current tare-adjusted value
|
|
655
673
|
return this.tareCurrent;
|
|
656
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Updates the timestamp of the last device interaction.
|
|
677
|
+
* This method sets the updatedAt property to the current date and time.
|
|
678
|
+
* @protected
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* device.updateTimestamp();
|
|
682
|
+
* console.log('Last updated:', device.updatedAt);
|
|
683
|
+
*/
|
|
684
|
+
updateTimestamp = () => {
|
|
685
|
+
this.updatedAt = new Date();
|
|
686
|
+
};
|
|
657
687
|
/**
|
|
658
688
|
* Writes a message to the specified characteristic of a Bluetooth device and optionally provides a callback to handle responses.
|
|
659
689
|
* @param {string} serviceId - The service UUID of the Bluetooth device containing the target characteristic.
|
|
@@ -679,8 +709,9 @@ export class Device extends BaseModel {
|
|
|
679
709
|
// Get the characteristic from the service
|
|
680
710
|
const characteristic = this.getCharacteristic(serviceId, characteristicId);
|
|
681
711
|
if (!characteristic) {
|
|
682
|
-
throw new Error(
|
|
712
|
+
throw new Error(`Characteristic "${characteristicId}" not found in service "${serviceId}"`);
|
|
683
713
|
}
|
|
714
|
+
this.updateTimestamp();
|
|
684
715
|
// Convert the message to Uint8Array if it's a string
|
|
685
716
|
const valueToWrite = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
686
717
|
// Write the value to the characteristic
|
package/dist/models/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hangtime/grip-connect",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
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
|
+
"type": "module",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
7
8
|
"scripts": {
|
package/src/index.ts
CHANGED
package/src/models/base.model.ts
CHANGED
|
@@ -8,7 +8,7 @@ export abstract class BaseModel {
|
|
|
8
8
|
updatedAt?: Date
|
|
9
9
|
|
|
10
10
|
constructor(base: IBase) {
|
|
11
|
-
this.id = base.id
|
|
11
|
+
this.id = base.id ?? globalThis.crypto?.randomUUID()
|
|
12
12
|
|
|
13
13
|
this.createdAt = base.createdAt
|
|
14
14
|
this.updatedAt = base.updatedAt
|
|
@@ -155,10 +155,12 @@ export class Entralpi extends Device implements IEntralpi {
|
|
|
155
155
|
*
|
|
156
156
|
* @param {BluetoothRemoteGATTCharacteristic} characteristic - The notification event.
|
|
157
157
|
*/
|
|
158
|
-
handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
158
|
+
override handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
159
159
|
const value: DataView | undefined = characteristic.value
|
|
160
160
|
|
|
161
161
|
if (value) {
|
|
162
|
+
// Update timestamp
|
|
163
|
+
this.updateTimestamp()
|
|
162
164
|
if (value.buffer) {
|
|
163
165
|
const receivedTime: number = Date.now()
|
|
164
166
|
const receivedData: string = (value.getUint16(0) / 100).toFixed(1)
|
|
@@ -182,9 +182,11 @@ export class ForceBoard extends Device implements IForceBoard {
|
|
|
182
182
|
*
|
|
183
183
|
* @param {BluetoothRemoteGATTCharacteristic} characteristic - The notification event.
|
|
184
184
|
*/
|
|
185
|
-
handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
185
|
+
override handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
186
186
|
const value: DataView | undefined = characteristic.value
|
|
187
187
|
if (value) {
|
|
188
|
+
// Update timestamp
|
|
189
|
+
this.updateTimestamp()
|
|
188
190
|
if (value.buffer) {
|
|
189
191
|
const receivedTime: number = Date.now()
|
|
190
192
|
const dataArray = new Uint8Array(value.buffer)
|
|
@@ -161,7 +161,7 @@ export class KilterBoard extends Device implements IKilterBoard {
|
|
|
161
161
|
- 0x2
|
|
162
162
|
- *packets
|
|
163
163
|
- 0x3
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
First byte is always 1, the second is a number of packets, then checksum, then 2, packets themselves, and finally 3.
|
|
166
166
|
*/
|
|
167
167
|
return [1, data.length, this.checksum(data), 2, ...data, 3]
|
|
@@ -203,10 +203,12 @@ export class Motherboard extends Device implements IMotherboard {
|
|
|
203
203
|
*
|
|
204
204
|
* @param {BluetoothRemoteGATTCharacteristic} characteristic - The notification event.
|
|
205
205
|
*/
|
|
206
|
-
handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
206
|
+
override handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
207
207
|
const value: DataView | undefined = characteristic.value
|
|
208
208
|
|
|
209
209
|
if (value) {
|
|
210
|
+
// Update timestamp
|
|
211
|
+
this.updateTimestamp()
|
|
210
212
|
if (value.buffer) {
|
|
211
213
|
for (let i = 0; i < value.byteLength; i++) {
|
|
212
214
|
this.receiveBuffer.push(value.getUint8(i))
|
|
@@ -122,10 +122,12 @@ export class Progressor extends Device implements IProgressor {
|
|
|
122
122
|
*
|
|
123
123
|
* @param {BluetoothRemoteGATTCharacteristic} characteristic - The notification event.
|
|
124
124
|
*/
|
|
125
|
-
handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
125
|
+
override handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
126
126
|
const value: DataView | undefined = characteristic.value
|
|
127
127
|
|
|
128
128
|
if (value) {
|
|
129
|
+
// Update timestamp
|
|
130
|
+
this.updateTimestamp()
|
|
129
131
|
if (value.buffer) {
|
|
130
132
|
const receivedTime: number = Date.now()
|
|
131
133
|
// Read the first byte of the buffer to determine the kind of message
|
|
@@ -68,7 +68,7 @@ export class WHC06 extends Device implements IWHC06 {
|
|
|
68
68
|
* @param {Function} [onSuccess] - Optional callback function to execute on successful connection. Default logs success.
|
|
69
69
|
* @param {Function} [onError] - Optional callback function to execute on error. Default logs the error.
|
|
70
70
|
*/
|
|
71
|
-
connect = async (
|
|
71
|
+
override connect = async (
|
|
72
72
|
onSuccess: () => void = () => console.log("Connected successfully"),
|
|
73
73
|
onError: (error: Error) => void = (error) => console.error(error),
|
|
74
74
|
): Promise<void> => {
|
|
@@ -86,6 +86,8 @@ export class WHC06 extends Device implements IWHC06 {
|
|
|
86
86
|
if (!this.bluetooth.gatt) {
|
|
87
87
|
throw new Error("GATT is not available on this device")
|
|
88
88
|
}
|
|
89
|
+
// Update timestamp
|
|
90
|
+
this.updateTimestamp()
|
|
89
91
|
|
|
90
92
|
// Device has no services / characteristics, so we directly call onSuccess
|
|
91
93
|
onSuccess()
|
|
@@ -166,7 +168,7 @@ export class WHC06 extends Device implements IWHC06 {
|
|
|
166
168
|
* For the WH-C06 device, the `gatt.connected` property remains `false` even after the device is connected.
|
|
167
169
|
* @returns {boolean} A boolean indicating whether the device is connected.
|
|
168
170
|
*/
|
|
169
|
-
isConnected = (): boolean => {
|
|
171
|
+
override isConnected = (): boolean => {
|
|
170
172
|
return !!this.bluetooth
|
|
171
173
|
}
|
|
172
174
|
|
|
@@ -180,10 +182,13 @@ export class WHC06 extends Device implements IWHC06 {
|
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
// Set a new timeout to stop tracking if no advertisement is received
|
|
183
|
-
this.advertisementTimeout =
|
|
185
|
+
this.advertisementTimeout = globalThis.setTimeout(() => {
|
|
184
186
|
// Mimic a disconnect
|
|
185
187
|
const disconnectedEvent = new Event("gattserverdisconnected")
|
|
186
|
-
Object.defineProperty(disconnectedEvent, "target", {
|
|
188
|
+
Object.defineProperty(disconnectedEvent, "target", {
|
|
189
|
+
value: this.bluetooth,
|
|
190
|
+
writable: false,
|
|
191
|
+
})
|
|
187
192
|
// Print error to the console
|
|
188
193
|
console.error(`No advertisement received for ${this.advertisementTimeoutTime} seconds, stopping tracking..`)
|
|
189
194
|
this.onDisconnected(disconnectedEvent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseModel } from "./../models/base.model"
|
|
2
2
|
import type { IDevice, Service } from "../interfaces/device.interface"
|
|
3
|
-
import type {
|
|
3
|
+
import type { ActiveCallback, massObject, NotifyCallback, WriteCallback } from "../interfaces/callback.interface"
|
|
4
4
|
import type { DownloadPacket } from "../interfaces/download.interface"
|
|
5
5
|
import type { Commands } from "../interfaces/command.interface"
|
|
6
6
|
|
|
@@ -61,7 +61,10 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
61
61
|
/**
|
|
62
62
|
* Configuration for threshold and duration.
|
|
63
63
|
*/
|
|
64
|
-
private activeConfig: { threshold: number; duration: number } = {
|
|
64
|
+
private activeConfig: { threshold: number; duration: number } = {
|
|
65
|
+
threshold: 2.5,
|
|
66
|
+
duration: 1000,
|
|
67
|
+
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
67
70
|
* Maximum mass recorded from the device, initialized to "0".
|
|
@@ -158,6 +161,24 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
158
161
|
*/
|
|
159
162
|
protected activeCallback: ActiveCallback = (data: boolean) => console.log(data)
|
|
160
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Event listener for handling the 'gattserverdisconnected' event.
|
|
166
|
+
* This listener delegates the event to the `onDisconnected` method.
|
|
167
|
+
*
|
|
168
|
+
* @private
|
|
169
|
+
* @type {(event: Event) => void}
|
|
170
|
+
*/
|
|
171
|
+
private onDisconnectedListener = (event: Event) => this.onDisconnected(event)
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A map that stores notification event listeners keyed by characteristic UUIDs.
|
|
175
|
+
* This allows for proper addition and removal of event listeners associated with each characteristic.
|
|
176
|
+
*
|
|
177
|
+
* @private
|
|
178
|
+
* @type {Map<string, EventListener>}
|
|
179
|
+
*/
|
|
180
|
+
private notificationListeners = new Map<string, EventListener>()
|
|
181
|
+
|
|
161
182
|
constructor(device: Partial<IDevice>) {
|
|
162
183
|
super(device)
|
|
163
184
|
|
|
@@ -170,6 +191,9 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
170
191
|
this.massAverage = "0"
|
|
171
192
|
this.massTotalSum = 0
|
|
172
193
|
this.dataPointCount = 0
|
|
194
|
+
|
|
195
|
+
this.createdAt = new Date()
|
|
196
|
+
this.updatedAt = new Date()
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
/**
|
|
@@ -218,29 +242,19 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
218
242
|
*/
|
|
219
243
|
protected activityCheck = (input: number): Promise<void> => {
|
|
220
244
|
return new Promise((resolve) => {
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.isActive = activeNow
|
|
231
|
-
if (this.activeCallback) {
|
|
232
|
-
this.activeCallback(activeNow)
|
|
233
|
-
}
|
|
245
|
+
const startValue = input
|
|
246
|
+
const { threshold, duration } = this.activeConfig
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
// After waiting for `duration`, check if still active (for a real scenario, you might store a last known input)
|
|
249
|
+
const activeNow = startValue > threshold
|
|
250
|
+
if (this.isActive !== activeNow) {
|
|
251
|
+
this.isActive = activeNow
|
|
252
|
+
if (this.activeCallback) {
|
|
253
|
+
this.activeCallback(activeNow)
|
|
234
254
|
}
|
|
235
|
-
resolve()
|
|
236
|
-
} else {
|
|
237
|
-
// Continue checking until the duration is met
|
|
238
|
-
requestAnimationFrame(checkActivity)
|
|
239
255
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Start the activity check
|
|
243
|
-
checkActivity()
|
|
256
|
+
resolve()
|
|
257
|
+
}, duration)
|
|
244
258
|
})
|
|
245
259
|
}
|
|
246
260
|
|
|
@@ -261,6 +275,9 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
261
275
|
onError: (error: Error) => void = (error) => console.error(error),
|
|
262
276
|
): Promise<void> => {
|
|
263
277
|
try {
|
|
278
|
+
if (typeof navigator === "undefined" || !("bluetooth" in navigator)) {
|
|
279
|
+
throw new Error("Web Bluetooth API not supported in this environment.")
|
|
280
|
+
}
|
|
264
281
|
// Request device and set up connection
|
|
265
282
|
const deviceServices = this.getAllServiceUUIDs()
|
|
266
283
|
|
|
@@ -273,9 +290,7 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
273
290
|
throw new Error("GATT is not available on this device")
|
|
274
291
|
}
|
|
275
292
|
|
|
276
|
-
this.bluetooth.addEventListener("gattserverdisconnected",
|
|
277
|
-
this.onDisconnected(event)
|
|
278
|
-
})
|
|
293
|
+
this.bluetooth.addEventListener("gattserverdisconnected", this.onDisconnectedListener)
|
|
279
294
|
|
|
280
295
|
this.server = await this.bluetooth.gatt.connect()
|
|
281
296
|
|
|
@@ -301,22 +316,23 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
301
316
|
*/
|
|
302
317
|
disconnect = (): void => {
|
|
303
318
|
if (this.isConnected()) {
|
|
319
|
+
this.updateTimestamp()
|
|
304
320
|
// Remove all notification listeners
|
|
305
321
|
this.services.forEach((service) => {
|
|
306
322
|
service.characteristics.forEach((char) => {
|
|
323
|
+
// TODO: remove device-specific logic
|
|
307
324
|
if (char.characteristic && char.id === "rx") {
|
|
308
325
|
char.characteristic.stopNotifications()
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
})
|
|
326
|
+
const listener = this.notificationListeners.get(char.uuid)
|
|
327
|
+
if (listener) {
|
|
328
|
+
char.characteristic.removeEventListener("characteristicvaluechanged", listener)
|
|
329
|
+
this.notificationListeners.delete(char.uuid)
|
|
330
|
+
}
|
|
315
331
|
}
|
|
316
332
|
})
|
|
317
333
|
})
|
|
318
334
|
// Remove disconnect listener
|
|
319
|
-
this.bluetooth?.removeEventListener("gattserverdisconnected", this.
|
|
335
|
+
this.bluetooth?.removeEventListener("gattserverdisconnected", this.onDisconnectedListener)
|
|
320
336
|
// Safely attempt to disconnect the device's GATT server, if available
|
|
321
337
|
this.bluetooth?.gatt?.disconnect()
|
|
322
338
|
// Reset properties
|
|
@@ -412,6 +428,10 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
412
428
|
* device.download('json');
|
|
413
429
|
*/
|
|
414
430
|
download = (format: "csv" | "json" | "xml" = "csv"): void => {
|
|
431
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
432
|
+
console.warn("Download is not supported outside a browser environment.")
|
|
433
|
+
return
|
|
434
|
+
}
|
|
415
435
|
let content = ""
|
|
416
436
|
let mimeType = ""
|
|
417
437
|
let fileName = ""
|
|
@@ -439,7 +459,7 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
439
459
|
const blob = new Blob([content], { type: mimeType })
|
|
440
460
|
|
|
441
461
|
// Create a URL for the Blob
|
|
442
|
-
const url =
|
|
462
|
+
const url = globalThis.URL.createObjectURL(blob)
|
|
443
463
|
|
|
444
464
|
// Create a link element
|
|
445
465
|
const link = document.createElement("a")
|
|
@@ -456,7 +476,7 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
456
476
|
|
|
457
477
|
// Clean up: remove the link and revoke the URL
|
|
458
478
|
document.body.removeChild(link)
|
|
459
|
-
|
|
479
|
+
globalThis.URL.revokeObjectURL(url)
|
|
460
480
|
}
|
|
461
481
|
|
|
462
482
|
/**
|
|
@@ -514,16 +534,11 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
514
534
|
*/
|
|
515
535
|
protected handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
|
|
516
536
|
const value = characteristic.value
|
|
537
|
+
if (!value) return
|
|
517
538
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (value.buffer) {
|
|
523
|
-
console.log(value)
|
|
524
|
-
} else {
|
|
525
|
-
console.log(value)
|
|
526
|
-
}
|
|
539
|
+
this.updateTimestamp()
|
|
540
|
+
// Received notification data
|
|
541
|
+
console.log(value)
|
|
527
542
|
}
|
|
528
543
|
|
|
529
544
|
/**
|
|
@@ -573,6 +588,8 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
573
588
|
* });
|
|
574
589
|
*/
|
|
575
590
|
protected onConnected = async (onSuccess: () => void): Promise<void> => {
|
|
591
|
+
this.updateTimestamp()
|
|
592
|
+
|
|
576
593
|
if (!this.server) {
|
|
577
594
|
throw new Error("GATT server is not available")
|
|
578
595
|
}
|
|
@@ -600,15 +617,17 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
600
617
|
if (element) {
|
|
601
618
|
element.characteristic = matchingCharacteristic
|
|
602
619
|
|
|
603
|
-
//
|
|
620
|
+
// TODO: remove device-specific logic
|
|
604
621
|
if (element.id === "rx") {
|
|
605
622
|
matchingCharacteristic.startNotifications()
|
|
606
|
-
|
|
623
|
+
const listener = (event: Event) => {
|
|
607
624
|
const target = event.target as BluetoothRemoteGATTCharacteristic
|
|
608
625
|
if (target && target.value) {
|
|
609
626
|
this.handleNotifications(target)
|
|
610
627
|
}
|
|
611
|
-
}
|
|
628
|
+
}
|
|
629
|
+
matchingCharacteristic.addEventListener("characteristicvaluechanged", listener)
|
|
630
|
+
this.notificationListeners.set(element.uuid, listener)
|
|
612
631
|
}
|
|
613
632
|
}
|
|
614
633
|
} else {
|
|
@@ -631,9 +650,8 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
631
650
|
* device.onDisconnected(event);
|
|
632
651
|
*/
|
|
633
652
|
protected onDisconnected = (event: Event): void => {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
console.warn(`Device ${device.name} is disconnected.`)
|
|
653
|
+
console.warn(`Device ${(event.target as BluetoothDevice).name} is disconnected.`)
|
|
654
|
+
this.disconnect()
|
|
637
655
|
}
|
|
638
656
|
|
|
639
657
|
/**
|
|
@@ -655,8 +673,9 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
655
673
|
// Get the characteristic from the service
|
|
656
674
|
const characteristic = this.getCharacteristic(serviceId, characteristicId)
|
|
657
675
|
if (!characteristic) {
|
|
658
|
-
throw new Error(
|
|
676
|
+
throw new Error(`Characteristic "${characteristicId}" not found in service "${serviceId}"`)
|
|
659
677
|
}
|
|
678
|
+
this.updateTimestamp()
|
|
660
679
|
// Decode the value based on characteristicId and serviceId
|
|
661
680
|
let decodedValue: string
|
|
662
681
|
const decoder = new TextDecoder("utf-8")
|
|
@@ -697,6 +716,7 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
697
716
|
*/
|
|
698
717
|
tare(duration = 5000): boolean {
|
|
699
718
|
if (this.tareActive) return false
|
|
719
|
+
this.updateTimestamp()
|
|
700
720
|
this.tareActive = true
|
|
701
721
|
this.tareDuration = duration
|
|
702
722
|
this.tareSamples = []
|
|
@@ -735,6 +755,19 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
735
755
|
return this.tareCurrent
|
|
736
756
|
}
|
|
737
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Updates the timestamp of the last device interaction.
|
|
760
|
+
* This method sets the updatedAt property to the current date and time.
|
|
761
|
+
* @protected
|
|
762
|
+
*
|
|
763
|
+
* @example
|
|
764
|
+
* device.updateTimestamp();
|
|
765
|
+
* console.log('Last updated:', device.updatedAt);
|
|
766
|
+
*/
|
|
767
|
+
protected updateTimestamp = (): void => {
|
|
768
|
+
this.updatedAt = new Date()
|
|
769
|
+
}
|
|
770
|
+
|
|
738
771
|
/**
|
|
739
772
|
* Writes a message to the specified characteristic of a Bluetooth device and optionally provides a callback to handle responses.
|
|
740
773
|
* @param {string} serviceId - The service UUID of the Bluetooth device containing the target characteristic.
|
|
@@ -766,8 +799,9 @@ export abstract class Device extends BaseModel implements IDevice {
|
|
|
766
799
|
// Get the characteristic from the service
|
|
767
800
|
const characteristic = this.getCharacteristic(serviceId, characteristicId)
|
|
768
801
|
if (!characteristic) {
|
|
769
|
-
throw new Error(
|
|
802
|
+
throw new Error(`Characteristic "${characteristicId}" not found in service "${serviceId}"`)
|
|
770
803
|
}
|
|
804
|
+
this.updateTimestamp()
|
|
771
805
|
// Convert the message to Uint8Array if it's a string
|
|
772
806
|
const valueToWrite: Uint8Array = typeof message === "string" ? new TextEncoder().encode(message) : message
|
|
773
807
|
// Write the value to the characteristic
|