@hangtime/grip-connect 0.12.0 → 0.13.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 (180) hide show
  1. package/README.md +20 -9
  2. package/dist/cjs/index.d.ts +2 -2
  3. package/dist/cjs/index.d.ts.map +1 -1
  4. package/dist/cjs/index.js +3 -3
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/interfaces/command.interface.d.ts +110 -21
  7. package/dist/cjs/interfaces/command.interface.d.ts.map +1 -1
  8. package/dist/cjs/interfaces/device/aurora.interface.d.ts +17 -0
  9. package/dist/cjs/interfaces/device/aurora.interface.d.ts.map +1 -0
  10. package/dist/cjs/interfaces/device/{kilterboard.interface.js → aurora.interface.js} +1 -1
  11. package/dist/cjs/interfaces/device/aurora.interface.js.map +1 -0
  12. package/dist/cjs/interfaces/device/cts500.interface.d.ts +96 -0
  13. package/dist/cjs/interfaces/device/cts500.interface.d.ts.map +1 -0
  14. package/dist/cjs/interfaces/device/cts500.interface.js +3 -0
  15. package/dist/cjs/interfaces/device/cts500.interface.js.map +1 -0
  16. package/dist/cjs/interfaces/device/forceboard.interface.d.ts +2 -2
  17. package/dist/cjs/interfaces/device/forceboard.interface.d.ts.map +1 -1
  18. package/dist/cjs/interfaces/device/motherboard.interface.d.ts +1 -1
  19. package/dist/cjs/interfaces/device/pb-700bt.interface.d.ts +53 -0
  20. package/dist/cjs/interfaces/device/pb-700bt.interface.d.ts.map +1 -0
  21. package/dist/cjs/interfaces/device/pb-700bt.interface.js +3 -0
  22. package/dist/cjs/interfaces/device/pb-700bt.interface.js.map +1 -0
  23. package/dist/cjs/interfaces/device/progressor.interface.d.ts +2 -2
  24. package/dist/cjs/interfaces/device/progressor.interface.d.ts.map +1 -1
  25. package/dist/cjs/interfaces/index.d.ts +4 -1
  26. package/dist/cjs/interfaces/index.d.ts.map +1 -1
  27. package/dist/cjs/interfaces/nordic.interface.d.ts +47 -0
  28. package/dist/cjs/interfaces/nordic.interface.d.ts.map +1 -0
  29. package/dist/cjs/interfaces/nordic.interface.js +3 -0
  30. package/dist/cjs/interfaces/nordic.interface.js.map +1 -0
  31. package/dist/cjs/models/device/{kilterboard.model.d.ts → aurora.model.d.ts} +82 -40
  32. package/dist/cjs/models/device/aurora.model.d.ts.map +1 -0
  33. package/dist/cjs/models/device/aurora.model.js +407 -0
  34. package/dist/cjs/models/device/aurora.model.js.map +1 -0
  35. package/dist/cjs/models/device/climbro.model.js +1 -1
  36. package/dist/cjs/models/device/climbro.model.js.map +1 -1
  37. package/dist/cjs/models/device/cts500.model.d.ts +173 -0
  38. package/dist/cjs/models/device/cts500.model.d.ts.map +1 -0
  39. package/dist/cjs/models/device/cts500.model.js +596 -0
  40. package/dist/cjs/models/device/cts500.model.js.map +1 -0
  41. package/dist/cjs/models/device/forceboard.model.d.ts +2 -2
  42. package/dist/cjs/models/device/forceboard.model.d.ts.map +1 -1
  43. package/dist/cjs/models/device/forceboard.model.js +9 -16
  44. package/dist/cjs/models/device/forceboard.model.js.map +1 -1
  45. package/dist/cjs/models/device/motherboard.model.d.ts +4 -1
  46. package/dist/cjs/models/device/motherboard.model.d.ts.map +1 -1
  47. package/dist/cjs/models/device/motherboard.model.js +26 -10
  48. package/dist/cjs/models/device/motherboard.model.js.map +1 -1
  49. package/dist/cjs/models/device/pb-700bt.model.d.ts +2 -1
  50. package/dist/cjs/models/device/pb-700bt.model.d.ts.map +1 -1
  51. package/dist/cjs/models/device/pb-700bt.model.js.map +1 -1
  52. package/dist/cjs/models/device/progressor.model.d.ts +2 -2
  53. package/dist/cjs/models/device/progressor.model.d.ts.map +1 -1
  54. package/dist/cjs/models/device/progressor.model.js +4 -20
  55. package/dist/cjs/models/device/progressor.model.js.map +1 -1
  56. package/dist/cjs/models/device/wh-c06.model.d.ts +2 -0
  57. package/dist/cjs/models/device/wh-c06.model.d.ts.map +1 -1
  58. package/dist/cjs/models/device/wh-c06.model.js +45 -34
  59. package/dist/cjs/models/device/wh-c06.model.js.map +1 -1
  60. package/dist/cjs/models/device.model.d.ts +25 -5
  61. package/dist/cjs/models/device.model.d.ts.map +1 -1
  62. package/dist/cjs/models/device.model.js +94 -24
  63. package/dist/cjs/models/device.model.js.map +1 -1
  64. package/dist/cjs/models/index.d.ts +3 -1
  65. package/dist/cjs/models/index.d.ts.map +1 -1
  66. package/dist/cjs/models/index.js +8 -4
  67. package/dist/cjs/models/index.js.map +1 -1
  68. package/dist/cjs/models/nordic.model.d.ts +128 -0
  69. package/dist/cjs/models/nordic.model.d.ts.map +1 -0
  70. package/dist/cjs/models/nordic.model.js +405 -0
  71. package/dist/cjs/models/nordic.model.js.map +1 -0
  72. package/dist/cjs/package.json +3 -0
  73. package/dist/index.d.ts +2 -2
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/interfaces/command.interface.d.ts +110 -21
  78. package/dist/interfaces/command.interface.d.ts.map +1 -1
  79. package/dist/interfaces/device/aurora.interface.d.ts +17 -0
  80. package/dist/interfaces/device/aurora.interface.d.ts.map +1 -0
  81. package/dist/interfaces/device/aurora.interface.js +2 -0
  82. package/dist/interfaces/device/aurora.interface.js.map +1 -0
  83. package/dist/interfaces/device/cts500.interface.d.ts +96 -0
  84. package/dist/interfaces/device/cts500.interface.d.ts.map +1 -0
  85. package/dist/interfaces/device/cts500.interface.js +2 -0
  86. package/dist/interfaces/device/cts500.interface.js.map +1 -0
  87. package/dist/interfaces/device/forceboard.interface.d.ts +2 -2
  88. package/dist/interfaces/device/forceboard.interface.d.ts.map +1 -1
  89. package/dist/interfaces/device/motherboard.interface.d.ts +1 -1
  90. package/dist/interfaces/device/pb-700bt.interface.d.ts +53 -0
  91. package/dist/interfaces/device/pb-700bt.interface.d.ts.map +1 -0
  92. package/dist/interfaces/device/pb-700bt.interface.js +2 -0
  93. package/dist/interfaces/device/pb-700bt.interface.js.map +1 -0
  94. package/dist/interfaces/device/progressor.interface.d.ts +2 -2
  95. package/dist/interfaces/device/progressor.interface.d.ts.map +1 -1
  96. package/dist/interfaces/index.d.ts +4 -1
  97. package/dist/interfaces/index.d.ts.map +1 -1
  98. package/dist/interfaces/nordic.interface.d.ts +47 -0
  99. package/dist/interfaces/nordic.interface.d.ts.map +1 -0
  100. package/dist/interfaces/nordic.interface.js +2 -0
  101. package/dist/interfaces/nordic.interface.js.map +1 -0
  102. package/dist/models/device/{kilterboard.model.d.ts → aurora.model.d.ts} +82 -40
  103. package/dist/models/device/aurora.model.d.ts.map +1 -0
  104. package/dist/models/device/aurora.model.js +401 -0
  105. package/dist/models/device/aurora.model.js.map +1 -0
  106. package/dist/models/device/climbro.model.js +1 -1
  107. package/dist/models/device/climbro.model.js.map +1 -1
  108. package/dist/models/device/cts500.model.d.ts +173 -0
  109. package/dist/models/device/cts500.model.d.ts.map +1 -0
  110. package/dist/models/device/cts500.model.js +592 -0
  111. package/dist/models/device/cts500.model.js.map +1 -0
  112. package/dist/models/device/forceboard.model.d.ts +2 -2
  113. package/dist/models/device/forceboard.model.d.ts.map +1 -1
  114. package/dist/models/device/forceboard.model.js +9 -16
  115. package/dist/models/device/forceboard.model.js.map +1 -1
  116. package/dist/models/device/motherboard.model.d.ts +4 -1
  117. package/dist/models/device/motherboard.model.d.ts.map +1 -1
  118. package/dist/models/device/motherboard.model.js +26 -10
  119. package/dist/models/device/motherboard.model.js.map +1 -1
  120. package/dist/models/device/pb-700bt.model.d.ts +2 -1
  121. package/dist/models/device/pb-700bt.model.d.ts.map +1 -1
  122. package/dist/models/device/pb-700bt.model.js.map +1 -1
  123. package/dist/models/device/progressor.model.d.ts +2 -2
  124. package/dist/models/device/progressor.model.d.ts.map +1 -1
  125. package/dist/models/device/progressor.model.js +4 -20
  126. package/dist/models/device/progressor.model.js.map +1 -1
  127. package/dist/models/device/wh-c06.model.d.ts +2 -0
  128. package/dist/models/device/wh-c06.model.d.ts.map +1 -1
  129. package/dist/models/device/wh-c06.model.js +44 -34
  130. package/dist/models/device/wh-c06.model.js.map +1 -1
  131. package/dist/models/device.model.d.ts +25 -5
  132. package/dist/models/device.model.d.ts.map +1 -1
  133. package/dist/models/device.model.js +93 -24
  134. package/dist/models/device.model.js.map +1 -1
  135. package/dist/models/index.d.ts +3 -1
  136. package/dist/models/index.d.ts.map +1 -1
  137. package/dist/models/index.js +3 -1
  138. package/dist/models/index.js.map +1 -1
  139. package/dist/models/nordic.model.d.ts +128 -0
  140. package/dist/models/nordic.model.d.ts.map +1 -0
  141. package/dist/models/nordic.model.js +393 -0
  142. package/dist/models/nordic.model.js.map +1 -0
  143. package/package.json +47 -43
  144. package/src/index.ts +6 -3
  145. package/src/interfaces/command.interface.ts +131 -21
  146. package/src/interfaces/device/aurora.interface.ts +18 -0
  147. package/src/interfaces/device/cts500.interface.ts +113 -0
  148. package/src/interfaces/device/forceboard.interface.ts +2 -2
  149. package/src/interfaces/device/motherboard.interface.ts +1 -1
  150. package/src/interfaces/device/pb-700bt.interface.ts +61 -0
  151. package/src/interfaces/device/progressor.interface.ts +2 -2
  152. package/src/interfaces/index.ts +8 -2
  153. package/src/interfaces/nordic.interface.ts +47 -0
  154. package/src/models/device/aurora.model.ts +497 -0
  155. package/src/models/device/climbro.model.ts +1 -1
  156. package/src/models/device/cts500.model.ts +709 -0
  157. package/src/models/device/forceboard.model.ts +9 -16
  158. package/src/models/device/motherboard.model.ts +51 -9
  159. package/src/models/device/pb-700bt.model.ts +2 -1
  160. package/src/models/device/progressor.model.ts +4 -20
  161. package/src/models/device/wh-c06.model.ts +54 -42
  162. package/src/models/device.model.ts +104 -24
  163. package/src/models/index.ts +5 -1
  164. package/src/models/nordic.model.ts +468 -0
  165. package/dist/cjs/interfaces/device/kilterboard.interface.d.ts +0 -17
  166. package/dist/cjs/interfaces/device/kilterboard.interface.d.ts.map +0 -1
  167. package/dist/cjs/interfaces/device/kilterboard.interface.js.map +0 -1
  168. package/dist/cjs/models/device/kilterboard.model.d.ts.map +0 -1
  169. package/dist/cjs/models/device/kilterboard.model.js +0 -327
  170. package/dist/cjs/models/device/kilterboard.model.js.map +0 -1
  171. package/dist/interfaces/device/kilterboard.interface.d.ts +0 -17
  172. package/dist/interfaces/device/kilterboard.interface.d.ts.map +0 -1
  173. package/dist/interfaces/device/kilterboard.interface.js +0 -2
  174. package/dist/interfaces/device/kilterboard.interface.js.map +0 -1
  175. package/dist/models/device/kilterboard.model.d.ts.map +0 -1
  176. package/dist/models/device/kilterboard.model.js +0 -323
  177. package/dist/models/device/kilterboard.model.js.map +0 -1
  178. package/dist/tsconfig.cjs.tsbuildinfo +0 -1
  179. package/src/interfaces/device/kilterboard.interface.ts +0 -12
  180. package/src/models/device/kilterboard.model.ts +0 -347
@@ -1,11 +1,11 @@
1
- import { Device } from "../device.model.js"
1
+ import { NordicDfuDevice, createNordicDfuService } from "../nordic.model.js"
2
2
  import type { IForceBoard } from "../../interfaces/device/forceboard.interface.js"
3
3
 
4
4
  /**
5
5
  * Represents a PitchSix Force Board device.
6
6
  * {@link https://pitchsix.com}
7
7
  */
8
- export class ForceBoard extends Device implements IForceBoard {
8
+ export class ForceBoard extends NordicDfuDevice implements IForceBoard {
9
9
  protected override streamUnit = "lbs" as const
10
10
 
11
11
  constructor() {
@@ -46,18 +46,7 @@ export class ForceBoard extends Device implements IForceBoard {
46
46
  },
47
47
  ],
48
48
  },
49
- {
50
- name: "Nordic Device Firmware Update (DFU) Service",
51
- id: "dfu",
52
- uuid: "0000fe59-0000-1000-8000-00805f9b34fb",
53
- characteristics: [
54
- {
55
- name: "Buttonless DFU",
56
- id: "dfu",
57
- uuid: "8ec90003-f315-4f60-9fb8-838830daea50",
58
- },
59
- ],
60
- },
49
+ createNordicDfuService(),
61
50
  {
62
51
  name: "",
63
52
  id: "",
@@ -83,7 +72,7 @@ export class ForceBoard extends Device implements IForceBoard {
83
72
  ],
84
73
  },
85
74
  {
86
- name: "Temperature Serivce",
75
+ name: "Temperature Service",
87
76
  id: "temperature",
88
77
  uuid: "3a90328c-c266-4c76-b05a-6af6104a0b13",
89
78
  characteristics: [
@@ -193,7 +182,7 @@ export class ForceBoard extends Device implements IForceBoard {
193
182
  this.updateTimestamp()
194
183
  if (value.buffer) {
195
184
  const receivedTime: number = Date.now()
196
- const dataArray = new Uint8Array(value.buffer)
185
+ const dataArray = new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
197
186
 
198
187
  const numSamples = (dataArray[0] << 8) | dataArray[1]
199
188
  this.currentSamplesPerPacket = numSamples
@@ -265,7 +254,11 @@ export class ForceBoard extends Device implements IForceBoard {
265
254
  */
266
255
  stream = async (duration = 0): Promise<void> => {
267
256
  this.resetPacketTracking()
257
+ this.resetSessionData()
268
258
  await this.write("weight", "tx", this.commands.START_WEIGHT_MEAS, duration)
259
+ if (duration !== 0) {
260
+ await this.stop()
261
+ }
269
262
  }
270
263
 
271
264
  /**
@@ -40,10 +40,13 @@ export class Motherboard extends Device implements IMotherboard {
40
40
 
41
41
  /** Per-zone peak and running sum for left/center/right (used for distribution stats). */
42
42
  private leftPeak = Number.NEGATIVE_INFINITY
43
+ private leftMin = Number.POSITIVE_INFINITY
43
44
  private leftSum = 0
44
45
  private centerPeak = Number.NEGATIVE_INFINITY
46
+ private centerMin = Number.POSITIVE_INFINITY
45
47
  private centerSum = 0
46
48
  private rightPeak = Number.NEGATIVE_INFINITY
49
+ private rightMin = Number.POSITIVE_INFINITY
47
50
  private rightSum = 0
48
51
 
49
52
  constructor() {
@@ -295,12 +298,15 @@ export class Motherboard extends Device implements IMotherboard {
295
298
  this.dataPointCount++
296
299
  this.mean = this.sum / this.dataPointCount
297
300
 
298
- // Per-zone peak and sum for distribution
301
+ // Per-zone peak, min and sum for distribution
299
302
  this.leftPeak = Math.max(this.leftPeak, leftClamped)
303
+ this.leftMin = Math.min(this.leftMin, leftClamped)
300
304
  this.leftSum += leftClamped
301
305
  this.centerPeak = Math.max(this.centerPeak, centerClamped)
306
+ this.centerMin = Math.min(this.centerMin, centerClamped)
302
307
  this.centerSum += centerClamped
303
308
  this.rightPeak = Math.max(this.rightPeak, rightClamped)
309
+ this.rightMin = Math.min(this.rightMin, rightClamped)
304
310
  this.rightSum += rightClamped
305
311
 
306
312
  // Add data to downloadable Array (distribution = per-zone measurements)
@@ -310,13 +316,24 @@ export class Motherboard extends Device implements IMotherboard {
310
316
  battRaw: packet.battRaw,
311
317
  sampleIndex: packet.sampleIndex,
312
318
  distribution: {
313
- left: this.buildZoneMeasurement(leftClamped, this.leftPeak, this.leftSum / this.dataPointCount),
319
+ left: this.buildZoneMeasurement(
320
+ leftClamped,
321
+ this.leftPeak,
322
+ this.leftSum / this.dataPointCount,
323
+ this.leftMin,
324
+ ),
314
325
  center: this.buildZoneMeasurement(
315
326
  centerClamped,
316
327
  this.centerPeak,
317
328
  this.centerSum / this.dataPointCount,
329
+ this.centerMin,
330
+ ),
331
+ right: this.buildZoneMeasurement(
332
+ rightClamped,
333
+ this.rightPeak,
334
+ this.rightSum / this.dataPointCount,
335
+ this.rightMin,
318
336
  ),
319
- right: this.buildZoneMeasurement(rightClamped, this.rightPeak, this.rightSum / this.dataPointCount),
320
337
  },
321
338
  }),
322
339
  )
@@ -324,12 +341,27 @@ export class Motherboard extends Device implements IMotherboard {
324
341
  // Check if device is being used
325
342
  this.activityCheck(center)
326
343
 
327
- // Notify with weight data (distribution zones have proper peak/mean per zone)
344
+ // Notify with weight data (distribution zones have proper peak/mean/min per zone)
328
345
  this.notifyCallback(
329
346
  this.buildForceMeasurement(totalCurrent, {
330
- left: this.buildZoneMeasurement(leftClamped, this.leftPeak, this.leftSum / this.dataPointCount),
331
- center: this.buildZoneMeasurement(centerClamped, this.centerPeak, this.centerSum / this.dataPointCount),
332
- right: this.buildZoneMeasurement(rightClamped, this.rightPeak, this.rightSum / this.dataPointCount),
347
+ left: this.buildZoneMeasurement(
348
+ leftClamped,
349
+ this.leftPeak,
350
+ this.leftSum / this.dataPointCount,
351
+ this.leftMin,
352
+ ),
353
+ center: this.buildZoneMeasurement(
354
+ centerClamped,
355
+ this.centerPeak,
356
+ this.centerSum / this.dataPointCount,
357
+ this.centerMin,
358
+ ),
359
+ right: this.buildZoneMeasurement(
360
+ rightClamped,
361
+ this.rightPeak,
362
+ this.rightSum / this.dataPointCount,
363
+ this.rightMin,
364
+ ),
333
365
  }),
334
366
  )
335
367
  } else if (this.writeLast === this.commands.GET_CALIBRATION) {
@@ -359,7 +391,7 @@ export class Motherboard extends Device implements IMotherboard {
359
391
  /**
360
392
  * Sets the LED color based on a single color option. Defaults to turning the LEDs off if no configuration is provided.
361
393
  * @param {"green" | "red" | "orange"} [config] - Optional color or array of climb placements for the LEDs. Ignored if placements are provided.
362
- * @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Kilter Board if LED settings were applied, or `undefined` if no action was taken or for the Motherboard.
394
+ * @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for Aurora-compatible LED settings, or `undefined` if no action was taken or for the Motherboard.
363
395
  */
364
396
  led = async (config?: "green" | "red" | "orange"): Promise<number[] | undefined> => {
365
397
  if (this.isConnected()) {
@@ -413,7 +445,17 @@ export class Motherboard extends Device implements IMotherboard {
413
445
  */
414
446
  stream = async (duration = 0): Promise<void> => {
415
447
  this.resetPacketTracking()
416
- this.downloadPackets.length = 0
448
+ this.resetSessionData()
449
+ // Reset per-zone session stats for the distribution measurements
450
+ this.leftPeak = Number.NEGATIVE_INFINITY
451
+ this.leftMin = Number.POSITIVE_INFINITY
452
+ this.leftSum = 0
453
+ this.centerPeak = Number.NEGATIVE_INFINITY
454
+ this.centerMin = Number.POSITIVE_INFINITY
455
+ this.centerSum = 0
456
+ this.rightPeak = Number.NEGATIVE_INFINITY
457
+ this.rightMin = Number.POSITIVE_INFINITY
458
+ this.rightSum = 0
417
459
  // Read calibration data if not already available
418
460
  if (!this.calibrationData[0].length) {
419
461
  await this.calibration()
@@ -1,10 +1,11 @@
1
1
  import { Device } from "../device.model.js"
2
+ import type { IPB700BT } from "../../interfaces/device/pb-700bt.interface.js"
2
3
 
3
4
  /**
4
5
  * Represents a NSD PB-700BT device.
5
6
  * {@link https://www.nsd.com.tw/}
6
7
  */
7
- export class PB700BT extends Device {
8
+ export class PB700BT extends Device implements IPB700BT {
8
9
  constructor() {
9
10
  super({
10
11
  filters: [{ name: "NSD Workout" }],
@@ -1,4 +1,4 @@
1
- import { Device } from "../device.model.js"
1
+ import { NordicDfuDevice, createNordicDfuService } from "../nordic.model.js"
2
2
  import type { IProgressor } from "../../interfaces/device/progressor.interface.js"
3
3
 
4
4
  /**
@@ -113,7 +113,7 @@ function parseCalibrationTableRecordPayload(payload: Uint8Array, index: number):
113
113
  return `${String(index).padStart(2, "0")}: ${hex} | raw ${lowerRaw.toLocaleString()}..${upperRaw.toLocaleString()} | slope ${formatCalibrationFloat(slope)} | intercept ${formatCalibrationFloat(intercept)}`
114
114
  }
115
115
 
116
- export class Progressor extends Device implements IProgressor {
116
+ export class Progressor extends NordicDfuDevice implements IProgressor {
117
117
  /** Device timestamps (µs) of recent samples (samples in last 1s device time). */
118
118
  private recentSampleTimestamps: number[] = []
119
119
  /** 1-based index for multi-packet calibration-table export responses. */
@@ -140,18 +140,7 @@ export class Progressor extends Device implements IProgressor {
140
140
  },
141
141
  ],
142
142
  },
143
- {
144
- name: "Nordic Device Firmware Update (DFU) Service",
145
- id: "dfu",
146
- uuid: "0000fe59-0000-1000-8000-00805f9b34fb",
147
- characteristics: [
148
- {
149
- name: "Buttonless DFU",
150
- id: "dfu",
151
- uuid: "8ec90003-f315-4f60-9fb8-838830daea50",
152
- },
153
- ],
154
- },
143
+ createNordicDfuService(),
155
144
  ],
156
145
  // Tindeq API: opcode = single byte (ASCII char code = decimal 100–114 v2 firmware: 115-118)
157
146
  commands: {
@@ -467,12 +456,7 @@ export class Progressor extends Device implements IProgressor {
467
456
  */
468
457
  stream = async (duration = 0): Promise<void> => {
469
458
  // Reset download packets and session stats for fresh measurement
470
- this.downloadPackets.length = 0
471
- this.peak = Number.NEGATIVE_INFINITY
472
- this.mean = 0
473
- this.sum = 0
474
- this.dataPointCount = 0
475
- this.min = Number.POSITIVE_INFINITY
459
+ this.resetSessionData()
476
460
  this.resetPacketTracking()
477
461
  this.recentSampleTimestamps = []
478
462
  // Start streaming data
@@ -41,6 +41,45 @@ export class WHC06 extends Device implements IWHC06 {
41
41
  */
42
42
  private readonly advertisementTimeoutTime: number = 10
43
43
 
44
+ private readonly onAdvertisementReceived = (event: BluetoothAdvertisingEvent): void => {
45
+ const data = event.manufacturerData.get(WHC06.manufacturerId)
46
+ if (data) {
47
+ this.currentSamplesPerPacket = 1
48
+ this.recordPacketReceived()
49
+ const weight = (data.getUint8(WHC06.weightOffset) << 8) | data.getUint8(WHC06.weightOffset + 1)
50
+ // const stable = (data.getUint8(STABLE_OFFSET) & 0xf0) >> 4
51
+ // const unit = data.getUint8(STABLE_OFFSET) & 0x0f
52
+ const receivedTime: number = Date.now()
53
+ const receivedData = weight / 100
54
+
55
+ const numericData = receivedData - this.applyTare(receivedData)
56
+ const currentMassTotal = Math.max(-1000, numericData)
57
+
58
+ // Update session stats before building packet
59
+ this.peak = Math.max(this.peak, numericData)
60
+ this.min = Math.min(this.min, Math.max(-1000, numericData))
61
+ this.sum += currentMassTotal
62
+ this.dataPointCount++
63
+ this.mean = this.sum / this.dataPointCount
64
+
65
+ // Add data to downloadable Array
66
+ this.downloadPackets.push(
67
+ this.buildDownloadPacket(currentMassTotal, [numericData], {
68
+ timestamp: receivedTime,
69
+ sampleIndex: this.dataPointCount,
70
+ }),
71
+ )
72
+
73
+ // Check if device is being used
74
+ this.activityCheck(numericData)
75
+
76
+ // Notify with weight data
77
+ this.notifyCallback(this.buildForceMeasurement(currentMassTotal))
78
+ }
79
+ // Reset "still advertising" counter
80
+ this.resetAdvertisementTimeout()
81
+ }
82
+
44
83
  // /**
45
84
  // * Offset for the byte location in the manufacturer data to determine weight stability.
46
85
  // * @type {number}
@@ -93,47 +132,7 @@ export class WHC06 extends Device implements IWHC06 {
93
132
  // Update timestamp
94
133
  this.updateTimestamp()
95
134
 
96
- // Device has no services / characteristics, so we directly call onSuccess
97
- onSuccess()
98
-
99
- this.bluetooth.addEventListener("advertisementreceived", (event) => {
100
- const data = event.manufacturerData.get(WHC06.manufacturerId)
101
- if (data) {
102
- this.currentSamplesPerPacket = 1
103
- this.recordPacketReceived()
104
- const weight = (data.getUint8(WHC06.weightOffset) << 8) | data.getUint8(WHC06.weightOffset + 1)
105
- // const stable = (data.getUint8(STABLE_OFFSET) & 0xf0) >> 4
106
- // const unit = data.getUint8(STABLE_OFFSET) & 0x0f
107
- const receivedTime: number = Date.now()
108
- const receivedData = weight / 100
109
-
110
- const numericData = receivedData - this.applyTare(receivedData) * -1
111
- const currentMassTotal = Math.max(-1000, numericData)
112
-
113
- // Update session stats before building packet
114
- this.peak = Math.max(this.peak, numericData)
115
- this.min = Math.min(this.min, Math.max(-1000, numericData))
116
- this.sum += currentMassTotal
117
- this.dataPointCount++
118
- this.mean = this.sum / this.dataPointCount
119
-
120
- // Add data to downloadable Array
121
- this.downloadPackets.push(
122
- this.buildDownloadPacket(currentMassTotal, [numericData], {
123
- timestamp: receivedTime,
124
- sampleIndex: this.dataPointCount,
125
- }),
126
- )
127
-
128
- // Check if device is being used
129
- this.activityCheck(numericData)
130
-
131
- // Notify with weight data
132
- this.notifyCallback(this.buildForceMeasurement(currentMassTotal))
133
- }
134
- // Reset "still advertising" counter
135
- this.resetAdvertisementTimeout()
136
- })
135
+ this.bluetooth.addEventListener("advertisementreceived", this.onAdvertisementReceived)
137
136
 
138
137
  // When the companyIdentifier is provided we want to get manufacturerData using watchAdvertisements.
139
138
  if (optionalManufacturerData.length) {
@@ -148,7 +147,11 @@ export class WHC06 extends Device implements IWHC06 {
148
147
  )
149
148
  }
150
149
  }
150
+
151
+ // Device has no services / characteristics, so success means advertisement watching is ready.
152
+ onSuccess()
151
153
  } catch (error) {
154
+ this.disconnect()
152
155
  onError(error as Error)
153
156
  }
154
157
  }
@@ -167,7 +170,7 @@ export class WHC06 extends Device implements IWHC06 {
167
170
  */
168
171
  private resetAdvertisementTimeout = (): void => {
169
172
  // Clear the previous timeout
170
- if (this.advertisementTimeout) {
173
+ if (this.advertisementTimeout !== null) {
171
174
  clearTimeout(this.advertisementTimeout)
172
175
  }
173
176
 
@@ -184,4 +187,13 @@ export class WHC06 extends Device implements IWHC06 {
184
187
  this.onDisconnected(disconnectedEvent)
185
188
  }, this.advertisementTimeoutTime * 1000) // 10 seconds
186
189
  }
190
+
191
+ protected override onDisconnectCleanup(): void {
192
+ if (this.advertisementTimeout !== null) {
193
+ clearTimeout(this.advertisementTimeout)
194
+ this.advertisementTimeout = null
195
+ }
196
+ this.bluetooth?.removeEventListener("advertisementreceived", this.onAdvertisementReceived)
197
+ delete this.bluetooth
198
+ }
187
199
  }
@@ -213,6 +213,14 @@ export abstract class Device extends BaseModel implements IDevice {
213
213
  */
214
214
  private tareActive = false
215
215
 
216
+ /**
217
+ * The characteristic ID that accepts notifications.
218
+ * Switching to "buttonless" allows to enter DFU mode and update the firmware.
219
+ *
220
+ * @type {"rx" | "buttonless" | "control"}
221
+ */
222
+ protected notifyCharacteristicId: "rx" | "buttonless" = "rx"
223
+
216
224
  /**
217
225
  * Timestamp when the tare calibration process started.
218
226
  * @type {number | null}
@@ -276,6 +284,8 @@ export abstract class Device extends BaseModel implements IDevice {
276
284
  */
277
285
  private notificationListeners = new Map<string, EventListener>()
278
286
 
287
+ private activeCheckPending: { target: boolean; timeout: ReturnType<typeof setTimeout> } | undefined = undefined
288
+
279
289
  constructor(device: Partial<IDevice>) {
280
290
  super(device)
281
291
 
@@ -304,19 +314,20 @@ export abstract class Device extends BaseModel implements IDevice {
304
314
  /**
305
315
  * Builds a ForceMeasurement for a single zone (e.g. left/center/right).
306
316
  * With one argument, current/peak/mean are all set to that value.
307
- * With three arguments, uses the given current, peak, and mean for the zone.
317
+ * With three or four arguments, uses the given current, peak, mean, and min for the zone.
308
318
  * @param valueOrCurrent - Force value, or current force for this zone
309
319
  * @param peak - Optional peak for this zone (required if mean is provided)
310
320
  * @param mean - Optional mean for this zone
321
+ * @param min - Optional session minimum for this zone
311
322
  * @returns ForceMeasurement (no nested distribution)
312
323
  * @protected
313
324
  */
314
- protected buildZoneMeasurement(valueOrCurrent: number, peak?: number, mean?: number): ForceMeasurement {
325
+ protected buildZoneMeasurement(valueOrCurrent: number, peak?: number, mean?: number, min?: number): ForceMeasurement {
315
326
  const useFullStats = peak !== undefined && mean !== undefined
316
327
  const current = valueOrCurrent
317
328
  const zonePeak = useFullStats ? (peak === 0 && current < 0 ? current : peak) : valueOrCurrent
318
329
  const zoneMean = useFullStats ? mean : valueOrCurrent
319
- const zoneMin = useFullStats ? Math.min(zonePeak, current) : current
330
+ const zoneMin = useFullStats ? (min ?? Math.min(zonePeak, current)) : current
320
331
  return {
321
332
  unit: this.unit,
322
333
  timestamp: Date.now(),
@@ -493,6 +504,21 @@ export abstract class Device extends BaseModel implements IDevice {
493
504
  }
494
505
  }
495
506
 
507
+ /**
508
+ * Resets per-session measurement state: the download buffer and force statistics.
509
+ * Device models call this at the start of a new streaming session so stats from a
510
+ * previous session do not leak into the next one.
511
+ * @protected
512
+ */
513
+ protected resetSessionData(): void {
514
+ this.downloadPackets.length = 0
515
+ this.peak = Number.NEGATIVE_INFINITY
516
+ this.min = Number.POSITIVE_INFINITY
517
+ this.mean = 0
518
+ this.sum = 0
519
+ this.dataPointCount = 0
520
+ }
521
+
496
522
  /**
497
523
  * Checks if a dynamic value is active based on a threshold and duration.
498
524
  *
@@ -501,20 +527,37 @@ export abstract class Device extends BaseModel implements IDevice {
501
527
  * the previous state, the callback function is called with the updated activity status.
502
528
  *
503
529
  * @param {number} input - The dynamic value to check for activity status.
504
- * @returns {Promise<void>} A promise that resolves once the activity check is complete.
505
530
  *
506
531
  * @example
507
- * await device.activityCheck(5.0);
532
+ * device.activityCheck(5.0);
508
533
  */
509
- protected activityCheck = async (input: number): Promise<void> => {
510
- const startValue = input
534
+ protected activityCheck = (input: number): void => {
511
535
  const { threshold, duration } = this.activeConfig
512
- // After waiting for `duration`, check if still active.
513
- await new Promise((resolve) => setTimeout(resolve, duration))
514
- const activeNow = startValue > threshold
515
- if (this.isActive !== activeNow) {
516
- this.isActive = activeNow
517
- this.activeCallback?.(activeNow)
536
+ const activeNow = input > threshold
537
+
538
+ if (activeNow === this.isActive) {
539
+ if (this.activeCheckPending) {
540
+ clearTimeout(this.activeCheckPending.timeout)
541
+ this.activeCheckPending = undefined
542
+ }
543
+ return
544
+ }
545
+
546
+ if (this.activeCheckPending?.target === activeNow) {
547
+ return
548
+ }
549
+
550
+ if (this.activeCheckPending) {
551
+ clearTimeout(this.activeCheckPending.timeout)
552
+ }
553
+
554
+ this.activeCheckPending = {
555
+ target: activeNow,
556
+ timeout: setTimeout(() => {
557
+ this.isActive = activeNow
558
+ this.activeCallback(activeNow)
559
+ this.activeCheckPending = undefined
560
+ }, duration),
518
561
  }
519
562
  }
520
563
 
@@ -562,10 +605,19 @@ export abstract class Device extends BaseModel implements IDevice {
562
605
  // }
563
606
  // }
564
607
 
565
- this.bluetooth = await bluetooth.requestDevice({
566
- filters: this.filters,
567
- optionalServices: deviceServices,
568
- })
608
+ if (!this.bluetooth?.gatt) {
609
+ this.bluetooth = await bluetooth.requestDevice({
610
+ filters: this.filters,
611
+ optionalServices: deviceServices,
612
+ })
613
+ }
614
+
615
+ if (!this.bluetooth) {
616
+ throw new Error("Bluetooth device is not available")
617
+ }
618
+
619
+ // Hook for when a device has been selected.
620
+ this.onBluetoothDeviceSelected(this.bluetooth)
569
621
 
570
622
  if (!this.bluetooth.gatt) {
571
623
  throw new Error("GATT is not available on this device")
@@ -573,12 +625,15 @@ export abstract class Device extends BaseModel implements IDevice {
573
625
 
574
626
  this.bluetooth.addEventListener("gattserverdisconnected", this.onDisconnectedListener)
575
627
 
576
- this.server = await this.bluetooth.gatt.connect()
628
+ this.server = this.bluetooth.gatt.connected ? this.bluetooth.gatt : await this.bluetooth.gatt.connect()
577
629
 
578
- if (this.server.connected) {
579
- await this.onConnected(onSuccess)
630
+ if (!this.server.connected) {
631
+ throw new Error("GATT server did not connect")
580
632
  }
633
+
634
+ await this.onConnected(onSuccess)
581
635
  } catch (error) {
636
+ this.disconnect()
582
637
  onError(error as Error)
583
638
  }
584
639
  }
@@ -604,7 +659,7 @@ export abstract class Device extends BaseModel implements IDevice {
604
659
  // Remove all notification listeners and stop notifications if possible.
605
660
  this.services.forEach((service) => {
606
661
  service.characteristics.forEach((char) => {
607
- if (!char.characteristic || char.id !== "rx") return
662
+ if (!char.characteristic || char.id !== this.notifyCharacteristicId) return
608
663
 
609
664
  if (isConnected) {
610
665
  // Best effort only: avoid unhandled rejections when the device already disconnected.
@@ -629,6 +684,15 @@ export abstract class Device extends BaseModel implements IDevice {
629
684
  this.server = undefined
630
685
  this.writeLast = null
631
686
  this.isActive = false
687
+ if (this.activeCheckPending) {
688
+ clearTimeout(this.activeCheckPending.timeout)
689
+ this.activeCheckPending = undefined
690
+ }
691
+ this.onDisconnectCleanup()
692
+ }
693
+
694
+ protected onDisconnectCleanup(): void {
695
+ return
632
696
  }
633
697
 
634
698
  /**
@@ -820,6 +884,14 @@ export abstract class Device extends BaseModel implements IDevice {
820
884
  throw new Error("Bluetooth not available.")
821
885
  }
822
886
 
887
+ /**
888
+ * Hook for device-specific setup after a Bluetooth device has been selected.
889
+ */
890
+ protected onBluetoothDeviceSelected(device: BluetoothDevice): void {
891
+ void device
892
+ return
893
+ }
894
+
823
895
  /**
824
896
  * Handles notifications received from a characteristic.
825
897
  * @param {DataView} value - The notification event.
@@ -921,10 +993,10 @@ export abstract class Device extends BaseModel implements IDevice {
921
993
  if (descriptor) {
922
994
  // Assign the actual Bluetooth characteristic object to the descriptor so it can be used later
923
995
  descriptor.characteristic = matchingCharacteristic
924
- // Look for the "rx" characteristic id that accepts notifications
925
- if (descriptor.id === "rx") {
996
+ // Look for our default notify characteristic id that accepts notifications
997
+ if (descriptor.id === this.notifyCharacteristicId) {
926
998
  // Start receiving notifications for changes on this characteristic
927
- matchingCharacteristic.startNotifications()
999
+ await matchingCharacteristic.startNotifications()
928
1000
  // Triggered when the characteristic's value changes
929
1001
  const listener = (event: Event) => {
930
1002
  // Cast the event's target to a BluetoothRemoteGATTCharacteristic to access its properties
@@ -934,12 +1006,20 @@ export abstract class Device extends BaseModel implements IDevice {
934
1006
  this.handleNotifications(target.value)
935
1007
  }
936
1008
  }
1009
+ const existingListener = this.notificationListeners.get(descriptor.uuid)
1010
+ if (existingListener) {
1011
+ matchingCharacteristic.removeEventListener("characteristicvaluechanged", existingListener)
1012
+ }
937
1013
  // Attach the event listener to listen for changes in the characteristic's value
938
1014
  matchingCharacteristic.addEventListener("characteristicvaluechanged", listener)
939
1015
  // Store the listener so it can be referenced (for later removal)
940
1016
  this.notificationListeners.set(descriptor.uuid, listener)
941
1017
  }
942
1018
  }
1019
+ } else if (matchingService.id === "dfu") {
1020
+ // App mode exposes buttonless only, bootloader exposes control+packet only.
1021
+ delete characteristic.characteristic
1022
+ continue
943
1023
  } else {
944
1024
  throw new Error(`Characteristic ${characteristic.uuid} not found in service ${service.uuid}`)
945
1025
  }
@@ -1,10 +1,12 @@
1
1
  export { Climbro } from "./device/climbro.model.js"
2
2
 
3
+ export { CTS500 } from "./device/cts500.model.js"
4
+
3
5
  export { Entralpi } from "./device/entralpi.model.js"
4
6
 
5
7
  export { ForceBoard } from "./device/forceboard.model.js"
6
8
 
7
- export { KilterBoard, KilterBoardPlacementRoles } from "./device/kilterboard.model.js"
9
+ export { NordicDfuDevice, createNordicDfuService } from "./nordic.model.js"
8
10
 
9
11
  export { Motherboard } from "./device/motherboard.model.js"
10
12
 
@@ -17,3 +19,5 @@ export { Progressor } from "./device/progressor.model.js"
17
19
  export { SmartBoardPro } from "./device/smartboard-pro.model.js"
18
20
 
19
21
  export { WHC06 } from "./device/wh-c06.model.js"
22
+
23
+ export { AuroraBoard } from "./device/aurora.model.js"