@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 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 package registry](https://www.npmjs.com/package/@hangtime/grip-connect).
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
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@hangtime/grip-connect",
3
+ "version": "0.5.11",
4
+ "license": "BSD-2-Clause",
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ }
8
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { Climbro, Entralpi, ForceBoard, KilterBoard, Motherboard, mySmartBoard, WHC06, Progressor, } from "./models/index";
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, WHC06, Progressor, } from "./models/index";
1
+ export { Climbro, Entralpi, ForceBoard, KilterBoard, Motherboard, mySmartBoard, Progressor, WHC06, } from "./models/index";
@@ -3,7 +3,7 @@ export class BaseModel {
3
3
  createdAt;
4
4
  updatedAt;
5
5
  constructor(base) {
6
- this.id = base.id ? base.id : self.crypto.randomUUID();
6
+ this.id = base.id ?? globalThis.crypto?.randomUUID();
7
7
  this.createdAt = base.createdAt;
8
8
  this.updatedAt = base.updatedAt;
9
9
  }
@@ -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 = window.setTimeout(() => {
157
+ this.advertisementTimeout = globalThis.setTimeout(() => {
156
158
  // Mimic a disconnect
157
159
  const disconnectedEvent = new Event("gattserverdisconnected");
158
- Object.defineProperty(disconnectedEvent, "target", { value: this.bluetooth, writable: false });
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, ActiveCallback } from "../interfaces/callback.interface";
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 = { threshold: 2.5, duration: 1000 };
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 startTime = Date.now();
193
- const checkActivity = () => {
194
- const currentTime = Date.now();
195
- const elapsedTime = currentTime - startTime;
196
- if (elapsedTime >= this.activeConfig.duration) {
197
- // Determine the activity status based on the most recent input
198
- const activeNow = input > this.activeConfig.threshold;
199
- if (this.isActive !== activeNow) {
200
- this.isActive = activeNow;
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
- // Start the activity check
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", (event) => {
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
- char.characteristic.removeEventListener("characteristicvaluechanged", (event) => {
271
- const target = event.target;
272
- if (target && target.value) {
273
- this.handleNotifications(target);
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.onDisconnected);
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 = window.URL.createObjectURL(blob);
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
- window.URL.revokeObjectURL(url);
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
- if (value.buffer) {
460
- console.log(value);
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
- // notify
546
+ // TODO: remove device-specific logic
532
547
  if (element.id === "rx") {
533
548
  matchingCharacteristic.startNotifications();
534
- matchingCharacteristic.addEventListener("characteristicvaluechanged", (event) => {
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
- this.bluetooth = undefined;
562
- const device = event.target;
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("Characteristic is undefined");
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("Characteristic is undefined");
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
@@ -1,3 +1,4 @@
1
+ // @ts-types="npm:@types/web-bluetooth@^0.0.20"
1
2
  export { Climbro } from "./device/climbro.model";
2
3
  export { Entralpi } from "./device/entralpi.model";
3
4
  export { ForceBoard } from "./device/forceboard.model";
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@hangtime/grip-connect",
3
- "version": "0.5.9",
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
@@ -5,6 +5,6 @@ export {
5
5
  KilterBoard,
6
6
  Motherboard,
7
7
  mySmartBoard,
8
- WHC06,
9
8
  Progressor,
9
+ WHC06,
10
10
  } from "./models/index"
@@ -8,7 +8,7 @@ export abstract class BaseModel {
8
8
  updatedAt?: Date
9
9
 
10
10
  constructor(base: IBase) {
11
- this.id = base.id ? base.id : self.crypto.randomUUID()
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 = window.setTimeout(() => {
185
+ this.advertisementTimeout = globalThis.setTimeout(() => {
184
186
  // Mimic a disconnect
185
187
  const disconnectedEvent = new Event("gattserverdisconnected")
186
- Object.defineProperty(disconnectedEvent, "target", { value: this.bluetooth, writable: false })
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 { NotifyCallback, massObject, WriteCallback, ActiveCallback } from "../interfaces/callback.interface"
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 } = { threshold: 2.5, duration: 1000 }
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 startTime = Date.now()
222
- const checkActivity = () => {
223
- const currentTime = Date.now()
224
- const elapsedTime = currentTime - startTime
225
-
226
- if (elapsedTime >= this.activeConfig.duration) {
227
- // Determine the activity status based on the most recent input
228
- const activeNow = input > this.activeConfig.threshold
229
- if (this.isActive !== activeNow) {
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", (event) => {
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
- char.characteristic.removeEventListener("characteristicvaluechanged", (event: Event) => {
310
- const target = event.target as BluetoothRemoteGATTCharacteristic
311
- if (target && target.value) {
312
- this.handleNotifications(target)
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.onDisconnected)
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 = window.URL.createObjectURL(blob)
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
- window.URL.revokeObjectURL(url)
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
- if (!value) {
519
- return
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
- // notify
620
+ // TODO: remove device-specific logic
604
621
  if (element.id === "rx") {
605
622
  matchingCharacteristic.startNotifications()
606
- matchingCharacteristic.addEventListener("characteristicvaluechanged", (event: Event) => {
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
- this.bluetooth = undefined
635
- const device = event.target as BluetoothDevice
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("Characteristic is undefined")
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("Characteristic is undefined")
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
@@ -1,3 +1,5 @@
1
+ // @ts-types="npm:@types/web-bluetooth@^0.0.20"
2
+
1
3
  export { Climbro } from "./device/climbro.model"
2
4
 
3
5
  export { Entralpi } from "./device/entralpi.model"