@hangtime/grip-connect 0.9.0 → 0.10.1

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.
Files changed (127) hide show
  1. package/README.md +24 -15
  2. package/dist/cjs/index.d.ts +3 -1
  3. package/dist/cjs/index.d.ts.map +1 -1
  4. package/dist/cjs/index.js +5 -1
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/interfaces/callback.interface.d.ts +37 -31
  7. package/dist/cjs/interfaces/callback.interface.d.ts.map +1 -1
  8. package/dist/cjs/interfaces/device/kilterboard.interface.d.ts +3 -2
  9. package/dist/cjs/interfaces/device/kilterboard.interface.d.ts.map +1 -1
  10. package/dist/cjs/interfaces/device.interface.d.ts +4 -2
  11. package/dist/cjs/interfaces/device.interface.d.ts.map +1 -1
  12. package/dist/cjs/models/device/climbro.model.d.ts.map +1 -1
  13. package/dist/cjs/models/device/climbro.model.js +4 -8
  14. package/dist/cjs/models/device/climbro.model.js.map +1 -1
  15. package/dist/cjs/models/device/entralpi.model.d.ts.map +1 -1
  16. package/dist/cjs/models/device/entralpi.model.js +5 -9
  17. package/dist/cjs/models/device/entralpi.model.js.map +1 -1
  18. package/dist/cjs/models/device/forceboard.model.d.ts +1 -0
  19. package/dist/cjs/models/device/forceboard.model.d.ts.map +1 -1
  20. package/dist/cjs/models/device/forceboard.model.js +10 -15
  21. package/dist/cjs/models/device/forceboard.model.js.map +1 -1
  22. package/dist/cjs/models/device/kilterboard.model.d.ts +5 -3
  23. package/dist/cjs/models/device/kilterboard.model.d.ts.map +1 -1
  24. package/dist/cjs/models/device/kilterboard.model.js +52 -16
  25. package/dist/cjs/models/device/kilterboard.model.js.map +1 -1
  26. package/dist/cjs/models/device/motherboard.model.d.ts +7 -0
  27. package/dist/cjs/models/device/motherboard.model.d.ts.map +1 -1
  28. package/dist/cjs/models/device/motherboard.model.js +28 -15
  29. package/dist/cjs/models/device/motherboard.model.js.map +1 -1
  30. package/dist/cjs/models/device/pb-700bt.model.d.ts +63 -0
  31. package/dist/cjs/models/device/pb-700bt.model.d.ts.map +1 -0
  32. package/dist/cjs/models/device/pb-700bt.model.js +247 -0
  33. package/dist/cjs/models/device/pb-700bt.model.js.map +1 -0
  34. package/dist/cjs/models/device/progressor.model.d.ts.map +1 -1
  35. package/dist/cjs/models/device/progressor.model.js +4 -8
  36. package/dist/cjs/models/device/progressor.model.js.map +1 -1
  37. package/dist/cjs/models/device/smartboard-pro.model.d.ts +8 -0
  38. package/dist/cjs/models/device/smartboard-pro.model.d.ts.map +1 -1
  39. package/dist/cjs/models/device/smartboard-pro.model.js +62 -6
  40. package/dist/cjs/models/device/smartboard-pro.model.js.map +1 -1
  41. package/dist/cjs/models/device/wh-c06.model.d.ts.map +1 -1
  42. package/dist/cjs/models/device/wh-c06.model.js +5 -9
  43. package/dist/cjs/models/device/wh-c06.model.js.map +1 -1
  44. package/dist/cjs/models/device.model.d.ts +81 -13
  45. package/dist/cjs/models/device.model.d.ts.map +1 -1
  46. package/dist/cjs/models/device.model.js +138 -6
  47. package/dist/cjs/models/device.model.js.map +1 -1
  48. package/dist/cjs/models/index.d.ts +2 -1
  49. package/dist/cjs/models/index.d.ts.map +1 -1
  50. package/dist/cjs/models/index.js +4 -1
  51. package/dist/cjs/models/index.js.map +1 -1
  52. package/dist/cjs/utils.d.ts +16 -0
  53. package/dist/cjs/utils.d.ts.map +1 -0
  54. package/dist/cjs/utils.js +54 -0
  55. package/dist/cjs/utils.js.map +1 -0
  56. package/dist/index.d.ts +3 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +2 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/interfaces/callback.interface.d.ts +37 -31
  61. package/dist/interfaces/callback.interface.d.ts.map +1 -1
  62. package/dist/interfaces/device/kilterboard.interface.d.ts +3 -2
  63. package/dist/interfaces/device/kilterboard.interface.d.ts.map +1 -1
  64. package/dist/interfaces/device.interface.d.ts +4 -2
  65. package/dist/interfaces/device.interface.d.ts.map +1 -1
  66. package/dist/models/device/climbro.model.d.ts.map +1 -1
  67. package/dist/models/device/climbro.model.js +4 -8
  68. package/dist/models/device/climbro.model.js.map +1 -1
  69. package/dist/models/device/entralpi.model.d.ts.map +1 -1
  70. package/dist/models/device/entralpi.model.js +5 -9
  71. package/dist/models/device/entralpi.model.js.map +1 -1
  72. package/dist/models/device/forceboard.model.d.ts +1 -0
  73. package/dist/models/device/forceboard.model.d.ts.map +1 -1
  74. package/dist/models/device/forceboard.model.js +10 -15
  75. package/dist/models/device/forceboard.model.js.map +1 -1
  76. package/dist/models/device/kilterboard.model.d.ts +5 -3
  77. package/dist/models/device/kilterboard.model.d.ts.map +1 -1
  78. package/dist/models/device/kilterboard.model.js +52 -16
  79. package/dist/models/device/kilterboard.model.js.map +1 -1
  80. package/dist/models/device/motherboard.model.d.ts +7 -0
  81. package/dist/models/device/motherboard.model.d.ts.map +1 -1
  82. package/dist/models/device/motherboard.model.js +28 -15
  83. package/dist/models/device/motherboard.model.js.map +1 -1
  84. package/dist/models/device/pb-700bt.model.d.ts +63 -0
  85. package/dist/models/device/pb-700bt.model.d.ts.map +1 -0
  86. package/dist/models/device/pb-700bt.model.js +243 -0
  87. package/dist/models/device/pb-700bt.model.js.map +1 -0
  88. package/dist/models/device/progressor.model.d.ts.map +1 -1
  89. package/dist/models/device/progressor.model.js +4 -8
  90. package/dist/models/device/progressor.model.js.map +1 -1
  91. package/dist/models/device/smartboard-pro.model.d.ts +8 -0
  92. package/dist/models/device/smartboard-pro.model.d.ts.map +1 -1
  93. package/dist/models/device/smartboard-pro.model.js +62 -6
  94. package/dist/models/device/smartboard-pro.model.js.map +1 -1
  95. package/dist/models/device/wh-c06.model.d.ts.map +1 -1
  96. package/dist/models/device/wh-c06.model.js +5 -9
  97. package/dist/models/device/wh-c06.model.js.map +1 -1
  98. package/dist/models/device.model.d.ts +81 -13
  99. package/dist/models/device.model.d.ts.map +1 -1
  100. package/dist/models/device.model.js +160 -15
  101. package/dist/models/device.model.js.map +1 -1
  102. package/dist/models/index.d.ts +2 -1
  103. package/dist/models/index.d.ts.map +1 -1
  104. package/dist/models/index.js +2 -1
  105. package/dist/models/index.js.map +1 -1
  106. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  107. package/dist/utils.d.ts +16 -0
  108. package/dist/utils.d.ts.map +1 -0
  109. package/dist/utils.js +50 -0
  110. package/dist/utils.js.map +1 -0
  111. package/package.json +4 -1
  112. package/src/index.ts +13 -0
  113. package/src/interfaces/callback.interface.ts +41 -31
  114. package/src/interfaces/device/kilterboard.interface.ts +2 -2
  115. package/src/interfaces/device.interface.ts +4 -2
  116. package/src/models/device/climbro.model.ts +4 -8
  117. package/src/models/device/entralpi.model.ts +5 -9
  118. package/src/models/device/forceboard.model.ts +11 -16
  119. package/src/models/device/kilterboard.model.ts +56 -19
  120. package/src/models/device/motherboard.model.ts +32 -15
  121. package/src/models/device/pb-700bt.model.ts +263 -0
  122. package/src/models/device/progressor.model.ts +4 -8
  123. package/src/models/device/smartboard-pro.model.ts +73 -6
  124. package/src/models/device/wh-c06.model.ts +5 -9
  125. package/src/models/device.model.ts +191 -17
  126. package/src/models/index.ts +3 -1
  127. package/src/utils.ts +57 -0
@@ -1,8 +1,15 @@
1
1
  import { BaseModel } from "./../models/base.model.js"
2
2
  import type { IDevice, Service } from "../interfaces/device.interface.js"
3
- import type { ActiveCallback, massObject, NotifyCallback, WriteCallback } from "../interfaces/callback.interface.js"
3
+ import type {
4
+ ActiveCallback,
5
+ ForceMeasurement,
6
+ ForceUnit,
7
+ NotifyCallback,
8
+ WriteCallback,
9
+ } from "../interfaces/callback.interface.js"
4
10
  import type { DownloadPacket } from "../interfaces/download.interface.js"
5
11
  import type { Commands } from "../interfaces/command.interface.js"
12
+ import { convertForce, convertForceMeasurement } from "../utils.js"
6
13
 
7
14
  export abstract class Device extends BaseModel implements IDevice {
8
15
  /**
@@ -67,26 +74,62 @@ export abstract class Device extends BaseModel implements IDevice {
67
74
  }
68
75
 
69
76
  /**
70
- * Maximum mass recorded from the device, initialized to "0".
71
- * @type {string}
77
+ * Highest instantaneous force (peak) recorded in the session; may be negative.
78
+ * Initialized to Number.NEGATIVE_INFINITY so the first sample sets the peak.
79
+ * @type {number}
72
80
  * @protected
73
81
  */
74
- protected massMax: string
82
+ protected peak: number
75
83
 
76
84
  /**
77
- * Average mass calculated from the device data, initialized to "0".
78
- * @type {string}
85
+ * Mean (average) force over the session, initialized to 0.
86
+ * @type {number}
79
87
  * @protected
80
88
  */
81
- protected massAverage: string
89
+ protected mean: number
82
90
 
83
91
  /**
84
- * Total sum of all mass data points recorded from the device.
85
- * Used to calculate the average mass.
92
+ * Display unit for force measurements (output unit for notify callbacks).
93
+ * @type {ForceUnit}
94
+ * @protected
95
+ */
96
+ protected unit: ForceUnit
97
+
98
+ /**
99
+ * Unit of the values streamed by the device (kg for most devices, lbs for ForceBoard).
100
+ * @type {ForceUnit}
101
+ * @protected
102
+ */
103
+ protected streamUnit: ForceUnit = "kg"
104
+
105
+ /**
106
+ * Optional sampling rate in Hz when known or calculated from notification timestamps.
107
+ * @type {number | undefined}
108
+ * @protected
109
+ */
110
+ protected samplingRateHz?: number
111
+
112
+ /**
113
+ * Start time of the current rate measurement interval.
114
+ * @type {number}
115
+ * @private
116
+ */
117
+ private rateIntervalStart = 0
118
+
119
+ /**
120
+ * Number of samples in the current rate measurement interval.
121
+ * @type {number}
122
+ * @private
123
+ */
124
+ private rateIntervalSamples = 0
125
+
126
+ /**
127
+ * Running sum of force values for the session.
128
+ * Used to calculate mean (average) force.
86
129
  * @type {number}
87
130
  * @protected
88
131
  */
89
- protected massTotalSum: number
132
+ protected sum: number
90
133
 
91
134
  /**
92
135
  * Number of data points received from the device.
@@ -135,13 +178,13 @@ export abstract class Device extends BaseModel implements IDevice {
135
178
  private tareDuration = 5000
136
179
 
137
180
  /**
138
- * Optional callback for handling write operations.
181
+ * Optional callback for handling mass/force data notifications.
139
182
  * @callback NotifyCallback
140
- * @param {massObject} data - The data passed to the callback.
183
+ * @param {ForceMeasurement} data - The force measurement passed to the callback.
141
184
  * @type {NotifyCallback | undefined}
142
185
  * @protected
143
186
  */
144
- protected notifyCallback: NotifyCallback = (data: massObject) => console.log(data)
187
+ protected notifyCallback: NotifyCallback = (data: ForceMeasurement) => console.log(data)
145
188
 
146
189
  /**
147
190
  * Optional callback for handling write operations.
@@ -189,15 +232,121 @@ export abstract class Device extends BaseModel implements IDevice {
189
232
  this.bluetooth = device.bluetooth
190
233
  }
191
234
 
192
- this.massMax = "0"
193
- this.massAverage = "0"
194
- this.massTotalSum = 0
235
+ this.peak = Number.NEGATIVE_INFINITY
236
+ this.mean = 0
237
+ this.sum = 0
195
238
  this.dataPointCount = 0
239
+ this.unit = "kg"
240
+
241
+ // Reset sampling rate calculation state
242
+ this.rateIntervalStart = 0
243
+ this.rateIntervalSamples = 0
196
244
 
197
245
  this.createdAt = new Date()
198
246
  this.updatedAt = new Date()
199
247
  }
200
248
 
249
+ /**
250
+ * Builds a ForceMeasurement for a single zone (e.g. left/center/right).
251
+ * With one argument, current/peak/mean are all set to that value.
252
+ * With three arguments, uses the given current, peak, and mean for the zone.
253
+ * @param valueOrCurrent - Force value, or current force for this zone
254
+ * @param peak - Optional peak for this zone (required if mean is provided)
255
+ * @param mean - Optional mean for this zone
256
+ * @returns ForceMeasurement (no nested distribution)
257
+ * @protected
258
+ */
259
+ protected buildZoneMeasurement(valueOrCurrent: number, peak?: number, mean?: number): ForceMeasurement {
260
+ const useFullStats = peak !== undefined && mean !== undefined
261
+ const current = valueOrCurrent
262
+ const zonePeak = useFullStats ? (peak === 0 && current < 0 ? current : peak) : valueOrCurrent
263
+ const zoneMean = useFullStats ? mean : valueOrCurrent
264
+ const zone: ForceMeasurement = {
265
+ unit: this.unit,
266
+ timestamp: Date.now(),
267
+ current,
268
+ peak: zonePeak,
269
+ mean: zoneMean,
270
+ }
271
+ if (this.samplingRateHz !== undefined) {
272
+ zone.samplingRateHz = this.samplingRateHz
273
+ }
274
+ return zone
275
+ }
276
+
277
+ /**
278
+ * Interval duration (ms) for sampling rate calculation.
279
+ * @private
280
+ * @readonly
281
+ */
282
+ private static readonly RATE_INTERVAL_MS = 1000
283
+
284
+ /**
285
+ * Calculates sampling rate: samples per second.
286
+ * Uses fixed intervals to avoid sliding window edge effects.
287
+ * @private
288
+ */
289
+ private updateSamplingRate(): void {
290
+ const now = Date.now()
291
+
292
+ if (this.rateIntervalStart === 0) {
293
+ this.rateIntervalStart = now
294
+ }
295
+
296
+ this.rateIntervalSamples++
297
+
298
+ const elapsed = now - this.rateIntervalStart
299
+ if (elapsed >= Device.RATE_INTERVAL_MS) {
300
+ this.samplingRateHz = Math.round((this.rateIntervalSamples / elapsed) * 1000)
301
+ this.rateIntervalStart = now
302
+ this.rateIntervalSamples = 0
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Builds a ForceMeasurement payload with unit and timestamp for notify callbacks.
308
+ * @param current - Current force at this sample
309
+ * @param distribution - Optional zone distribution: numbers (converted via buildZoneMeasurement) or full ForceMeasurement per zone
310
+ * @returns ForceMeasurement
311
+ * @protected
312
+ */
313
+ protected buildForceMeasurement(
314
+ current: number,
315
+ distribution?: {
316
+ left?: ForceMeasurement
317
+ center?: ForceMeasurement
318
+ right?: ForceMeasurement
319
+ },
320
+ ): ForceMeasurement {
321
+ this.updateSamplingRate()
322
+ const payload: ForceMeasurement = {
323
+ unit: this.unit,
324
+ timestamp: Date.now(),
325
+ current: convertForce(current, this.streamUnit, this.unit),
326
+ peak: convertForce(this.peak, this.streamUnit, this.unit),
327
+ mean: convertForce(this.mean, this.streamUnit, this.unit),
328
+ }
329
+ if (this.samplingRateHz !== undefined) {
330
+ payload.samplingRateHz = this.samplingRateHz
331
+ }
332
+ if (
333
+ distribution !== undefined &&
334
+ (distribution.left !== undefined || distribution.center !== undefined || distribution.right !== undefined)
335
+ ) {
336
+ payload.distribution = {}
337
+ if (distribution.left !== undefined) {
338
+ payload.distribution.left = convertForceMeasurement(distribution.left, this.streamUnit, this.unit)
339
+ }
340
+ if (distribution.center !== undefined) {
341
+ payload.distribution.center = convertForceMeasurement(distribution.center, this.streamUnit, this.unit)
342
+ }
343
+ if (distribution.right !== undefined) {
344
+ payload.distribution.right = convertForceMeasurement(distribution.right, this.streamUnit, this.unit)
345
+ }
346
+ }
347
+ return payload
348
+ }
349
+
201
350
  /**
202
351
  * Sets the callback function to be called when the activity status changes,
203
352
  * and optionally sets the configuration for threshold and duration.
@@ -276,6 +425,28 @@ export abstract class Device extends BaseModel implements IDevice {
276
425
 
277
426
  const bluetooth = await this.getBluetooth()
278
427
 
428
+ // Experiment: Reconnect to known devices, enable these Chrome flags:
429
+ // - chrome://flags/#enable-experimental-web-platform-features → enables getDevices() API
430
+ // - chrome://flags/#enable-web-bluetooth-new-permissions-backend → ensures it returns all permitted devices, not just connected ones
431
+ // let reconnectDevice: BluetoothDevice | undefined
432
+ // if (typeof bluetooth.getDevices === "function") {
433
+ // const devices: BluetoothDevice[] = await bluetooth.getDevices()
434
+ // if (devices.length > 0 && this.filters.length > 0) {
435
+ // reconnectDevice = devices.find((device) => {
436
+ // if (!device.name) return false
437
+ // const d = device
438
+ // return this.filters.some(
439
+ // (f) => (f.name && d.name === f.name) || (f.namePrefix && d.name?.startsWith(f.namePrefix)),
440
+ // )
441
+ // })
442
+ // }
443
+ // if (reconnectDevice) {
444
+ // this.bluetooth = reconnectDevice
445
+ // // It's currently impossible to call this.bluetooth.gatt.connect() here.
446
+ // // After restarting the Browser, it will always give: "Bluetooth Device is no longer in range."
447
+ // }
448
+ // }
449
+
279
450
  this.bluetooth = await bluetooth.requestDevice({
280
451
  filters: this.filters,
281
452
  optionalServices: deviceServices,
@@ -540,6 +711,7 @@ export abstract class Device extends BaseModel implements IDevice {
540
711
  /**
541
712
  * Sets the callback function to be called when notifications are received.
542
713
  * @param {NotifyCallback} callback - The callback function to be set.
714
+ * @param {ForceUnit} [unit="kg"] - Optional display unit for force values in the callback payload.
543
715
  * @returns {void}
544
716
  * @public
545
717
  *
@@ -547,8 +719,10 @@ export abstract class Device extends BaseModel implements IDevice {
547
719
  * device.notify((data) => {
548
720
  * console.log('Received notification:', data);
549
721
  * });
722
+ * device.notify((data) => { ... }, 'lbs');
550
723
  */
551
- notify = (callback: NotifyCallback): void => {
724
+ notify = (callback: NotifyCallback, unit?: ForceUnit): void => {
725
+ this.unit = unit ?? "kg"
552
726
  this.notifyCallback = callback
553
727
  }
554
728
 
@@ -4,12 +4,14 @@ export { Entralpi } from "./device/entralpi.model.js"
4
4
 
5
5
  export { ForceBoard } from "./device/forceboard.model.js"
6
6
 
7
- export { KilterBoard } from "./device/kilterboard.model.js"
7
+ export { KilterBoard, KilterBoardPlacementRoles } from "./device/kilterboard.model.js"
8
8
 
9
9
  export { Motherboard } from "./device/motherboard.model.js"
10
10
 
11
11
  export { mySmartBoard } from "./device/mysmartboard.model.js"
12
12
 
13
+ export { PB700BT } from "./device/pb-700bt.model.js"
14
+
13
15
  export { Progressor } from "./device/progressor.model.js"
14
16
 
15
17
  export { SmartBoardPro } from "./device/smartboard-pro.model.js"
package/src/utils.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { ForceMeasurement, ForceUnit } from "./interfaces/callback.interface.js"
2
+
3
+ /** 1 kg = this many lbs (force-equivalent) */
4
+ const KG_TO_LBS = 2.20462262185
5
+
6
+ /**
7
+ * Converts a force value between kg and lbs.
8
+ * @param value - The numeric force value in the source unit.
9
+ * @param from - The unit of the input value.
10
+ * @param to - The unit for the output value.
11
+ * @returns The force value in the target unit.
12
+ */
13
+ export function convertForce(value: number, from: ForceUnit, to: ForceUnit): number {
14
+ if (from === to) return value
15
+ return from === "kg" ? value * KG_TO_LBS : value / KG_TO_LBS
16
+ }
17
+
18
+ /**
19
+ * Converts all numeric force fields in a ForceMeasurement from one unit to another.
20
+ * Recurses one level into distribution.left / center / right.
21
+ * Used internally by the device model when building notify payloads.
22
+ */
23
+ export function convertForceMeasurement(
24
+ measurement: ForceMeasurement,
25
+ from: ForceUnit,
26
+ to: ForceUnit,
27
+ ): ForceMeasurement {
28
+ if (from === to) return measurement
29
+ const out: ForceMeasurement = {
30
+ unit: to,
31
+ timestamp: measurement.timestamp,
32
+ current: convertForce(measurement.current, from, to),
33
+ peak: convertForce(measurement.peak, from, to),
34
+ mean: convertForce(measurement.mean, from, to),
35
+ }
36
+ if (measurement.samplingRateHz !== undefined) {
37
+ out.samplingRateHz = measurement.samplingRateHz
38
+ }
39
+ if (
40
+ measurement.distribution &&
41
+ (measurement.distribution.left !== undefined ||
42
+ measurement.distribution.center !== undefined ||
43
+ measurement.distribution.right !== undefined)
44
+ ) {
45
+ out.distribution = {}
46
+ if (measurement.distribution.left !== undefined) {
47
+ out.distribution.left = convertForceMeasurement(measurement.distribution.left, from, to)
48
+ }
49
+ if (measurement.distribution.center !== undefined) {
50
+ out.distribution.center = convertForceMeasurement(measurement.distribution.center, from, to)
51
+ }
52
+ if (measurement.distribution.right !== undefined) {
53
+ out.distribution.right = convertForceMeasurement(measurement.distribution.right, from, to)
54
+ }
55
+ }
56
+ return out
57
+ }