@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.
- 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/motherboard.model.js +2 -0
- package/dist/models/device/progressor.model.js +2 -0
- package/dist/models/device/wh-c06.model.js +2 -0
- package/dist/models/device.model.d.ts +26 -0
- package/dist/models/device.model.js +74 -46
- package/package.json +1 -1
- package/src/models/base.model.ts +1 -1
- package/src/models/device/entralpi.model.ts +2 -0
- package/src/models/device/forceboard.model.ts +2 -0
- package/src/models/device/motherboard.model.ts +2 -0
- package/src/models/device/progressor.model.ts +2 -0
- package/src/models/device/wh-c06.model.ts +2 -0
- package/src/models/device.model.ts +79 -48
|
@@ -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
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (this.
|
|
200
|
-
this.
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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.
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
//
|
|
543
|
+
// TODO: remove device-specific logic
|
|
532
544
|
if (element.id === "rx") {
|
|
533
545
|
matchingCharacteristic.startNotifications();
|
|
534
|
-
|
|
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
|
-
|
|
562
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|
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
|
|
@@ -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
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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",
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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.
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
//
|
|
617
|
+
// TODO: remove device-specific logic
|
|
604
618
|
if (element.id === "rx") {
|
|
605
619
|
matchingCharacteristic.startNotifications()
|
|
606
|
-
|
|
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
|
-
|
|
635
|
-
|
|
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(
|
|
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(
|
|
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
|