@hangtime/grip-connect 0.5.9 → 0.5.10

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.
@@ -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);
@@ -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) => {
@@ -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.
@@ -134,6 +134,22 @@ export class Device extends BaseModel {
134
134
  * @protected
135
135
  */
136
136
  activeCallback = (data) => console.log(data);
137
+ /**
138
+ * Event listener for handling the 'gattserverdisconnected' event.
139
+ * This listener delegates the event to the `onDisconnected` method.
140
+ *
141
+ * @private
142
+ * @type {(event: Event) => void}
143
+ */
144
+ onDisconnectedListener = (event) => this.onDisconnected(event);
145
+ /**
146
+ * A map that stores notification event listeners keyed by characteristic UUIDs.
147
+ * This allows for proper addition and removal of event listeners associated with each characteristic.
148
+ *
149
+ * @private
150
+ * @type {Map<string, EventListener>}
151
+ */
152
+ notificationListeners = new Map();
137
153
  constructor(device) {
138
154
  super(device);
139
155
  this.filters = device.filters || [];
@@ -144,6 +160,8 @@ export class Device extends BaseModel {
144
160
  this.massAverage = "0";
145
161
  this.massTotalSum = 0;
146
162
  this.dataPointCount = 0;
163
+ this.createdAt = new Date();
164
+ this.updatedAt = new Date();
147
165
  }
148
166
  /**
149
167
  * Sets the callback function to be called when the activity status changes,
@@ -189,28 +207,19 @@ export class Device extends BaseModel {
189
207
  */
190
208
  activityCheck = (input) => {
191
209
  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
- }
210
+ const startValue = input;
211
+ const { threshold, duration } = this.activeConfig;
212
+ setTimeout(() => {
213
+ // After waiting for `duration`, check if still active (for a real scenario, you might store a last known input)
214
+ const activeNow = startValue > threshold;
215
+ if (this.isActive !== activeNow) {
216
+ this.isActive = activeNow;
217
+ if (this.activeCallback) {
218
+ this.activeCallback(activeNow);
204
219
  }
205
- resolve();
206
- }
207
- else {
208
- // Continue checking until the duration is met
209
- requestAnimationFrame(checkActivity);
210
220
  }
211
- };
212
- // Start the activity check
213
- checkActivity();
221
+ resolve();
222
+ }, duration);
214
223
  });
215
224
  };
216
225
  /**
@@ -227,6 +236,9 @@ export class Device extends BaseModel {
227
236
  */
228
237
  connect = async (onSuccess = () => console.log("Connected successfully"), onError = (error) => console.error(error)) => {
229
238
  try {
239
+ if (typeof navigator === "undefined" || !("bluetooth" in navigator)) {
240
+ throw new Error("Web Bluetooth API not supported in this environment.");
241
+ }
230
242
  // Request device and set up connection
231
243
  const deviceServices = this.getAllServiceUUIDs();
232
244
  this.bluetooth = await navigator.bluetooth.requestDevice({
@@ -236,9 +248,7 @@ export class Device extends BaseModel {
236
248
  if (!this.bluetooth.gatt) {
237
249
  throw new Error("GATT is not available on this device");
238
250
  }
239
- this.bluetooth.addEventListener("gattserverdisconnected", (event) => {
240
- this.onDisconnected(event);
241
- });
251
+ this.bluetooth.addEventListener("gattserverdisconnected", this.onDisconnectedListener);
242
252
  this.server = await this.bluetooth.gatt.connect();
243
253
  if (this.server.connected) {
244
254
  await this.onConnected(onSuccess);
@@ -262,22 +272,23 @@ export class Device extends BaseModel {
262
272
  */
263
273
  disconnect = () => {
264
274
  if (this.isConnected()) {
275
+ this.updateTimestamp();
265
276
  // Remove all notification listeners
266
277
  this.services.forEach((service) => {
267
278
  service.characteristics.forEach((char) => {
279
+ // TODO: remove device-specific logic
268
280
  if (char.characteristic && char.id === "rx") {
269
281
  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
- });
282
+ const listener = this.notificationListeners.get(char.uuid);
283
+ if (listener) {
284
+ char.characteristic.removeEventListener("characteristicvaluechanged", listener);
285
+ this.notificationListeners.delete(char.uuid);
286
+ }
276
287
  }
277
288
  });
278
289
  });
279
290
  // Remove disconnect listener
280
- this.bluetooth?.removeEventListener("gattserverdisconnected", this.onDisconnected);
291
+ this.bluetooth?.removeEventListener("gattserverdisconnected", this.onDisconnectedListener);
281
292
  // Safely attempt to disconnect the device's GATT server, if available
282
293
  this.bluetooth?.gatt?.disconnect();
283
294
  // Reset properties
@@ -367,6 +378,10 @@ export class Device extends BaseModel {
367
378
  * device.download('json');
368
379
  */
369
380
  download = (format = "csv") => {
381
+ if (typeof document === "undefined" || typeof window === "undefined") {
382
+ console.warn("Download is not supported outside a browser environment.");
383
+ return;
384
+ }
370
385
  let content = "";
371
386
  let mimeType = "";
372
387
  let fileName = "";
@@ -453,15 +468,11 @@ export class Device extends BaseModel {
453
468
  */
454
469
  handleNotifications = (characteristic) => {
455
470
  const value = characteristic.value;
456
- if (!value) {
471
+ if (!value)
457
472
  return;
458
- }
459
- if (value.buffer) {
460
- console.log(value);
461
- }
462
- else {
463
- console.log(value);
464
- }
473
+ this.updateTimestamp();
474
+ // Received notification data
475
+ console.log(value);
465
476
  };
466
477
  /**
467
478
  * Checks if a Bluetooth device is connected.
@@ -508,6 +519,7 @@ export class Device extends BaseModel {
508
519
  * });
509
520
  */
510
521
  onConnected = async (onSuccess) => {
522
+ this.updateTimestamp();
511
523
  if (!this.server) {
512
524
  throw new Error("GATT server is not available");
513
525
  }
@@ -528,15 +540,17 @@ export class Device extends BaseModel {
528
540
  const element = matchingService.characteristics.find((char) => char.uuid === matchingCharacteristic.uuid);
529
541
  if (element) {
530
542
  element.characteristic = matchingCharacteristic;
531
- // notify
543
+ // TODO: remove device-specific logic
532
544
  if (element.id === "rx") {
533
545
  matchingCharacteristic.startNotifications();
534
- matchingCharacteristic.addEventListener("characteristicvaluechanged", (event) => {
546
+ const listener = (event) => {
535
547
  const target = event.target;
536
548
  if (target && target.value) {
537
549
  this.handleNotifications(target);
538
550
  }
539
- });
551
+ };
552
+ matchingCharacteristic.addEventListener("characteristicvaluechanged", listener);
553
+ this.notificationListeners.set(element.uuid, listener);
540
554
  }
541
555
  }
542
556
  }
@@ -558,9 +572,8 @@ export class Device extends BaseModel {
558
572
  * device.onDisconnected(event);
559
573
  */
560
574
  onDisconnected = (event) => {
561
- this.bluetooth = undefined;
562
- const device = event.target;
563
- console.warn(`Device ${device.name} is disconnected.`);
575
+ console.warn(`Device ${event.target.name} is disconnected.`);
576
+ this.disconnect();
564
577
  };
565
578
  /**
566
579
  * Reads the value of the specified characteristic from the device.
@@ -581,8 +594,9 @@ export class Device extends BaseModel {
581
594
  // Get the characteristic from the service
582
595
  const characteristic = this.getCharacteristic(serviceId, characteristicId);
583
596
  if (!characteristic) {
584
- throw new Error("Characteristic is undefined");
597
+ throw new Error(`Characteristic "${characteristicId}" not found in service "${serviceId}"`);
585
598
  }
599
+ this.updateTimestamp();
586
600
  // Decode the value based on characteristicId and serviceId
587
601
  let decodedValue;
588
602
  const decoder = new TextDecoder("utf-8");
@@ -620,6 +634,7 @@ export class Device extends BaseModel {
620
634
  tare(duration = 5000) {
621
635
  if (this.tareActive)
622
636
  return false;
637
+ this.updateTimestamp();
623
638
  this.tareActive = true;
624
639
  this.tareDuration = duration;
625
640
  this.tareSamples = [];
@@ -654,6 +669,18 @@ export class Device extends BaseModel {
654
669
  // Return the current tare-adjusted value
655
670
  return this.tareCurrent;
656
671
  }
672
+ /**
673
+ * Updates the timestamp of the last device interaction.
674
+ * This method sets the updatedAt property to the current date and time.
675
+ * @protected
676
+ *
677
+ * @example
678
+ * device.updateTimestamp();
679
+ * console.log('Last updated:', device.updatedAt);
680
+ */
681
+ updateTimestamp = () => {
682
+ this.updatedAt = new Date();
683
+ };
657
684
  /**
658
685
  * Writes a message to the specified characteristic of a Bluetooth device and optionally provides a callback to handle responses.
659
686
  * @param {string} serviceId - The service UUID of the Bluetooth device containing the target characteristic.
@@ -679,8 +706,9 @@ export class Device extends BaseModel {
679
706
  // Get the characteristic from the service
680
707
  const characteristic = this.getCharacteristic(serviceId, characteristicId);
681
708
  if (!characteristic) {
682
- throw new Error("Characteristic is undefined");
709
+ throw new Error(`Characteristic "${characteristicId}" not found in service "${serviceId}"`);
683
710
  }
711
+ this.updateTimestamp();
684
712
  // Convert the message to Uint8Array if it's a string
685
713
  const valueToWrite = typeof message === "string" ? new TextEncoder().encode(message) : message;
686
714
  // Write the value to the characteristic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hangtime/grip-connect",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
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",
@@ -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
@@ -159,6 +159,8 @@ export class Entralpi extends Device implements IEntralpi {
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)
@@ -185,6 +185,8 @@ export class ForceBoard extends Device implements IForceBoard {
185
185
  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)
@@ -207,6 +207,8 @@ export class Motherboard extends Device implements IMotherboard {
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))
@@ -126,6 +126,8 @@ export class Progressor extends Device implements IProgressor {
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
@@ -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()
@@ -158,6 +158,24 @@ export abstract class Device extends BaseModel implements IDevice {
158
158
  */
159
159
  protected activeCallback: ActiveCallback = (data: boolean) => console.log(data)
160
160
 
161
+ /**
162
+ * Event listener for handling the 'gattserverdisconnected' event.
163
+ * This listener delegates the event to the `onDisconnected` method.
164
+ *
165
+ * @private
166
+ * @type {(event: Event) => void}
167
+ */
168
+ private onDisconnectedListener = (event: Event) => this.onDisconnected(event)
169
+
170
+ /**
171
+ * A map that stores notification event listeners keyed by characteristic UUIDs.
172
+ * This allows for proper addition and removal of event listeners associated with each characteristic.
173
+ *
174
+ * @private
175
+ * @type {Map<string, EventListener>}
176
+ */
177
+ private notificationListeners = new Map<string, EventListener>()
178
+
161
179
  constructor(device: Partial<IDevice>) {
162
180
  super(device)
163
181
 
@@ -170,6 +188,9 @@ export abstract class Device extends BaseModel implements IDevice {
170
188
  this.massAverage = "0"
171
189
  this.massTotalSum = 0
172
190
  this.dataPointCount = 0
191
+
192
+ this.createdAt = new Date()
193
+ this.updatedAt = new Date()
173
194
  }
174
195
 
175
196
  /**
@@ -218,29 +239,19 @@ export abstract class Device extends BaseModel implements IDevice {
218
239
  */
219
240
  protected activityCheck = (input: number): Promise<void> => {
220
241
  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
- }
242
+ const startValue = input
243
+ const { threshold, duration } = this.activeConfig
244
+ setTimeout(() => {
245
+ // After waiting for `duration`, check if still active (for a real scenario, you might store a last known input)
246
+ const activeNow = startValue > threshold
247
+ if (this.isActive !== activeNow) {
248
+ this.isActive = activeNow
249
+ if (this.activeCallback) {
250
+ this.activeCallback(activeNow)
234
251
  }
235
- resolve()
236
- } else {
237
- // Continue checking until the duration is met
238
- requestAnimationFrame(checkActivity)
239
252
  }
240
- }
241
-
242
- // Start the activity check
243
- checkActivity()
253
+ resolve()
254
+ }, duration)
244
255
  })
245
256
  }
246
257
 
@@ -261,6 +272,9 @@ export abstract class Device extends BaseModel implements IDevice {
261
272
  onError: (error: Error) => void = (error) => console.error(error),
262
273
  ): Promise<void> => {
263
274
  try {
275
+ if (typeof navigator === "undefined" || !("bluetooth" in navigator)) {
276
+ throw new Error("Web Bluetooth API not supported in this environment.")
277
+ }
264
278
  // Request device and set up connection
265
279
  const deviceServices = this.getAllServiceUUIDs()
266
280
 
@@ -273,9 +287,7 @@ export abstract class Device extends BaseModel implements IDevice {
273
287
  throw new Error("GATT is not available on this device")
274
288
  }
275
289
 
276
- this.bluetooth.addEventListener("gattserverdisconnected", (event) => {
277
- this.onDisconnected(event)
278
- })
290
+ this.bluetooth.addEventListener("gattserverdisconnected", this.onDisconnectedListener)
279
291
 
280
292
  this.server = await this.bluetooth.gatt.connect()
281
293
 
@@ -301,22 +313,23 @@ export abstract class Device extends BaseModel implements IDevice {
301
313
  */
302
314
  disconnect = (): void => {
303
315
  if (this.isConnected()) {
316
+ this.updateTimestamp()
304
317
  // Remove all notification listeners
305
318
  this.services.forEach((service) => {
306
319
  service.characteristics.forEach((char) => {
320
+ // TODO: remove device-specific logic
307
321
  if (char.characteristic && char.id === "rx") {
308
322
  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
- })
323
+ const listener = this.notificationListeners.get(char.uuid)
324
+ if (listener) {
325
+ char.characteristic.removeEventListener("characteristicvaluechanged", listener)
326
+ this.notificationListeners.delete(char.uuid)
327
+ }
315
328
  }
316
329
  })
317
330
  })
318
331
  // Remove disconnect listener
319
- this.bluetooth?.removeEventListener("gattserverdisconnected", this.onDisconnected)
332
+ this.bluetooth?.removeEventListener("gattserverdisconnected", this.onDisconnectedListener)
320
333
  // Safely attempt to disconnect the device's GATT server, if available
321
334
  this.bluetooth?.gatt?.disconnect()
322
335
  // Reset properties
@@ -412,6 +425,10 @@ export abstract class Device extends BaseModel implements IDevice {
412
425
  * device.download('json');
413
426
  */
414
427
  download = (format: "csv" | "json" | "xml" = "csv"): void => {
428
+ if (typeof document === "undefined" || typeof window === "undefined") {
429
+ console.warn("Download is not supported outside a browser environment.")
430
+ return
431
+ }
415
432
  let content = ""
416
433
  let mimeType = ""
417
434
  let fileName = ""
@@ -514,16 +531,11 @@ export abstract class Device extends BaseModel implements IDevice {
514
531
  */
515
532
  protected handleNotifications = (characteristic: BluetoothRemoteGATTCharacteristic): void => {
516
533
  const value = characteristic.value
534
+ if (!value) return
517
535
 
518
- if (!value) {
519
- return
520
- }
521
-
522
- if (value.buffer) {
523
- console.log(value)
524
- } else {
525
- console.log(value)
526
- }
536
+ this.updateTimestamp()
537
+ // Received notification data
538
+ console.log(value)
527
539
  }
528
540
 
529
541
  /**
@@ -573,6 +585,8 @@ export abstract class Device extends BaseModel implements IDevice {
573
585
  * });
574
586
  */
575
587
  protected onConnected = async (onSuccess: () => void): Promise<void> => {
588
+ this.updateTimestamp()
589
+
576
590
  if (!this.server) {
577
591
  throw new Error("GATT server is not available")
578
592
  }
@@ -600,15 +614,17 @@ export abstract class Device extends BaseModel implements IDevice {
600
614
  if (element) {
601
615
  element.characteristic = matchingCharacteristic
602
616
 
603
- // notify
617
+ // TODO: remove device-specific logic
604
618
  if (element.id === "rx") {
605
619
  matchingCharacteristic.startNotifications()
606
- matchingCharacteristic.addEventListener("characteristicvaluechanged", (event: Event) => {
620
+ const listener = (event: Event) => {
607
621
  const target = event.target as BluetoothRemoteGATTCharacteristic
608
622
  if (target && target.value) {
609
623
  this.handleNotifications(target)
610
624
  }
611
- })
625
+ }
626
+ matchingCharacteristic.addEventListener("characteristicvaluechanged", listener)
627
+ this.notificationListeners.set(element.uuid, listener)
612
628
  }
613
629
  }
614
630
  } else {
@@ -631,9 +647,8 @@ export abstract class Device extends BaseModel implements IDevice {
631
647
  * device.onDisconnected(event);
632
648
  */
633
649
  protected onDisconnected = (event: Event): void => {
634
- this.bluetooth = undefined
635
- const device = event.target as BluetoothDevice
636
- console.warn(`Device ${device.name} is disconnected.`)
650
+ console.warn(`Device ${(event.target as BluetoothDevice).name} is disconnected.`)
651
+ this.disconnect()
637
652
  }
638
653
 
639
654
  /**
@@ -655,8 +670,9 @@ export abstract class Device extends BaseModel implements IDevice {
655
670
  // Get the characteristic from the service
656
671
  const characteristic = this.getCharacteristic(serviceId, characteristicId)
657
672
  if (!characteristic) {
658
- throw new Error("Characteristic is undefined")
673
+ throw new Error(`Characteristic "${characteristicId}" not found in service "${serviceId}"`)
659
674
  }
675
+ this.updateTimestamp()
660
676
  // Decode the value based on characteristicId and serviceId
661
677
  let decodedValue: string
662
678
  const decoder = new TextDecoder("utf-8")
@@ -697,6 +713,7 @@ export abstract class Device extends BaseModel implements IDevice {
697
713
  */
698
714
  tare(duration = 5000): boolean {
699
715
  if (this.tareActive) return false
716
+ this.updateTimestamp()
700
717
  this.tareActive = true
701
718
  this.tareDuration = duration
702
719
  this.tareSamples = []
@@ -735,6 +752,19 @@ export abstract class Device extends BaseModel implements IDevice {
735
752
  return this.tareCurrent
736
753
  }
737
754
 
755
+ /**
756
+ * Updates the timestamp of the last device interaction.
757
+ * This method sets the updatedAt property to the current date and time.
758
+ * @protected
759
+ *
760
+ * @example
761
+ * device.updateTimestamp();
762
+ * console.log('Last updated:', device.updatedAt);
763
+ */
764
+ protected updateTimestamp = (): void => {
765
+ this.updatedAt = new Date()
766
+ }
767
+
738
768
  /**
739
769
  * Writes a message to the specified characteristic of a Bluetooth device and optionally provides a callback to handle responses.
740
770
  * @param {string} serviceId - The service UUID of the Bluetooth device containing the target characteristic.
@@ -766,8 +796,9 @@ export abstract class Device extends BaseModel implements IDevice {
766
796
  // Get the characteristic from the service
767
797
  const characteristic = this.getCharacteristic(serviceId, characteristicId)
768
798
  if (!characteristic) {
769
- throw new Error("Characteristic is undefined")
799
+ throw new Error(`Characteristic "${characteristicId}" not found in service "${serviceId}"`)
770
800
  }
801
+ this.updateTimestamp()
771
802
  // Convert the message to Uint8Array if it's a string
772
803
  const valueToWrite: Uint8Array = typeof message === "string" ? new TextEncoder().encode(message) : message
773
804
  // Write the value to the characteristic