@hangtime/grip-connect 0.8.7 → 0.10.0

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 (123) hide show
  1. package/README.md +15 -9
  2. package/dist/cjs/index.d.ts +2 -1
  3. package/dist/cjs/index.d.ts.map +1 -1
  4. package/dist/cjs/index.js +3 -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/climbro.interface.d.ts +5 -0
  9. package/dist/cjs/interfaces/device/climbro.interface.d.ts.map +1 -1
  10. package/dist/cjs/interfaces/device/kilterboard.interface.d.ts +3 -2
  11. package/dist/cjs/interfaces/device/kilterboard.interface.d.ts.map +1 -1
  12. package/dist/cjs/interfaces/device.interface.d.ts +2 -2
  13. package/dist/cjs/interfaces/device.interface.d.ts.map +1 -1
  14. package/dist/cjs/models/device/climbro.model.d.ts +32 -1
  15. package/dist/cjs/models/device/climbro.model.d.ts.map +1 -1
  16. package/dist/cjs/models/device/climbro.model.js +129 -3
  17. package/dist/cjs/models/device/climbro.model.js.map +1 -1
  18. package/dist/cjs/models/device/entralpi.model.d.ts.map +1 -1
  19. package/dist/cjs/models/device/entralpi.model.js +5 -9
  20. package/dist/cjs/models/device/entralpi.model.js.map +1 -1
  21. package/dist/cjs/models/device/forceboard.model.d.ts.map +1 -1
  22. package/dist/cjs/models/device/forceboard.model.js +5 -9
  23. package/dist/cjs/models/device/forceboard.model.js.map +1 -1
  24. package/dist/cjs/models/device/kilterboard.model.d.ts +5 -3
  25. package/dist/cjs/models/device/kilterboard.model.d.ts.map +1 -1
  26. package/dist/cjs/models/device/kilterboard.model.js +52 -16
  27. package/dist/cjs/models/device/kilterboard.model.js.map +1 -1
  28. package/dist/cjs/models/device/motherboard.model.d.ts +7 -0
  29. package/dist/cjs/models/device/motherboard.model.d.ts.map +1 -1
  30. package/dist/cjs/models/device/motherboard.model.js +28 -15
  31. package/dist/cjs/models/device/motherboard.model.js.map +1 -1
  32. package/dist/cjs/models/device/pb-700bt.model.d.ts +63 -0
  33. package/dist/cjs/models/device/pb-700bt.model.d.ts.map +1 -0
  34. package/dist/cjs/models/device/pb-700bt.model.js +247 -0
  35. package/dist/cjs/models/device/pb-700bt.model.js.map +1 -0
  36. package/dist/cjs/models/device/progressor.model.d.ts.map +1 -1
  37. package/dist/cjs/models/device/progressor.model.js +4 -8
  38. package/dist/cjs/models/device/progressor.model.js.map +1 -1
  39. package/dist/cjs/models/device/smartboard-pro.model.d.ts +8 -0
  40. package/dist/cjs/models/device/smartboard-pro.model.d.ts.map +1 -1
  41. package/dist/cjs/models/device/smartboard-pro.model.js +62 -6
  42. package/dist/cjs/models/device/smartboard-pro.model.js.map +1 -1
  43. package/dist/cjs/models/device/wh-c06.model.d.ts.map +1 -1
  44. package/dist/cjs/models/device/wh-c06.model.js +5 -9
  45. package/dist/cjs/models/device/wh-c06.model.js.map +1 -1
  46. package/dist/cjs/models/device.model.d.ts +72 -12
  47. package/dist/cjs/models/device.model.d.ts.map +1 -1
  48. package/dist/cjs/models/device.model.js +127 -5
  49. package/dist/cjs/models/device.model.js.map +1 -1
  50. package/dist/cjs/models/index.d.ts +2 -1
  51. package/dist/cjs/models/index.d.ts.map +1 -1
  52. package/dist/cjs/models/index.js +4 -1
  53. package/dist/cjs/models/index.js.map +1 -1
  54. package/dist/index.d.ts +2 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/interfaces/callback.interface.d.ts +37 -31
  59. package/dist/interfaces/callback.interface.d.ts.map +1 -1
  60. package/dist/interfaces/device/climbro.interface.d.ts +5 -0
  61. package/dist/interfaces/device/climbro.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 +2 -2
  65. package/dist/interfaces/device.interface.d.ts.map +1 -1
  66. package/dist/models/device/climbro.model.d.ts +32 -1
  67. package/dist/models/device/climbro.model.d.ts.map +1 -1
  68. package/dist/models/device/climbro.model.js +127 -3
  69. package/dist/models/device/climbro.model.js.map +1 -1
  70. package/dist/models/device/entralpi.model.d.ts.map +1 -1
  71. package/dist/models/device/entralpi.model.js +5 -9
  72. package/dist/models/device/entralpi.model.js.map +1 -1
  73. package/dist/models/device/forceboard.model.d.ts.map +1 -1
  74. package/dist/models/device/forceboard.model.js +5 -9
  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 +72 -12
  99. package/dist/models/device.model.d.ts.map +1 -1
  100. package/dist/models/device.model.js +149 -14
  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/package.json +4 -1
  108. package/src/index.ts +11 -0
  109. package/src/interfaces/callback.interface.ts +41 -31
  110. package/src/interfaces/device/climbro.interface.ts +8 -2
  111. package/src/interfaces/device/kilterboard.interface.ts +2 -2
  112. package/src/interfaces/device.interface.ts +2 -2
  113. package/src/models/device/climbro.model.ts +146 -3
  114. package/src/models/device/entralpi.model.ts +5 -9
  115. package/src/models/device/forceboard.model.ts +5 -9
  116. package/src/models/device/kilterboard.model.ts +56 -19
  117. package/src/models/device/motherboard.model.ts +32 -15
  118. package/src/models/device/pb-700bt.model.ts +263 -0
  119. package/src/models/device/progressor.model.ts +4 -8
  120. package/src/models/device/smartboard-pro.model.ts +73 -6
  121. package/src/models/device/wh-c06.model.ts +5 -9
  122. package/src/models/device.model.ts +179 -16
  123. package/src/models/index.ts +3 -1
@@ -185,22 +185,23 @@ export class KilterBoard extends Device implements IKilterBoard {
185
185
  /**
186
186
  * Encodes a color string into a numeric representation.
187
187
  * The rgb color, 3 bits for the R and G components, 2 bits for the B component, with the 3 R bits occupying the high end of the byte and the 2 B bits in the low end (hence 3 G bits in the middle).
188
+ * Format: 0bRRRGGGBB where RRR is 3 bits for red, GGG is 3 bits for green, BB is 2 bits for blue.
188
189
  * @param color - The color string in hexadecimal format (e.g., 'FFFFFF').
189
190
  * @returns The encoded /compressed color value.
190
191
  */
191
192
  private encodeColor(color: string): number {
192
- const substring = color.substring(0, 2)
193
- const substring2 = color.substring(2, 4)
193
+ const r = parseInt(color.substring(0, 2), 16)
194
+ const g = parseInt(color.substring(2, 4), 16)
195
+ const b = parseInt(color.substring(4, 6), 16)
194
196
 
195
- const parsedSubstring = parseInt(substring, 16) / 32
196
- const parsedSubstring2 = parseInt(substring2, 16) / 32
197
- const parsedResult = (parsedSubstring << 5) | (parsedSubstring2 << 2)
197
+ // Integer division: R and G divided by 32, B divided by 64
198
+ // Then pack into 0bRRRGGGBB format
199
+ const rBits = Math.floor(r / 32) // 0-7 (3 bits)
200
+ const gBits = Math.floor(g / 32) // 0-7 (3 bits)
201
+ const bBits = Math.floor(b / 64) // 0-3 (2 bits)
198
202
 
199
- const substring3 = color.substring(4, 6)
200
- const parsedSubstring3 = parseInt(substring3, 16) / 64
201
- const finalParsedResult = parsedResult | parsedSubstring3
202
-
203
- return finalParsedResult
203
+ // Pack: RRR in bits 7-5, GGG in bits 4-2, BB in bits 1-0
204
+ return (rBits << 5) | (gBits << 2) | bBits
204
205
  }
205
206
 
206
207
  /**
@@ -215,23 +216,59 @@ export class KilterBoard extends Device implements IKilterBoard {
215
216
 
216
217
  /**
217
218
  * Prepares byte arrays for transmission based on a list of climb placements.
218
- * @param {{ position: number; role_id: number }[]} climbPlacementList - The list of climb placements containing position and role ID.
219
+ * @param {{ position: number; role_id?: number; color?: string }[]} climbPlacementList - The list of climb placements containing position and either role ID or color.
219
220
  * @returns {number[]} The final byte array ready for transmission.
220
221
  */
221
- private prepBytesV3(climbPlacementList: { position: number; role_id: number }[]): number[] {
222
+ private prepBytesV3(climbPlacementList: { position: number; role_id?: number; color?: string }[]): number[] {
222
223
  const resultArray: number[][] = []
223
224
  let tempArray: number[] = [KilterBoardPacket.V3_MIDDLE]
224
225
 
225
- for (const climbPlacement of climbPlacementList) {
226
+ // Filter out any invalid placements and clean up objects before processing
227
+ const validPlacements = climbPlacementList
228
+ .filter((p) => {
229
+ // Explicitly check for color
230
+ const hasColor = p.color != null && typeof p.color === "string" && p.color.trim() !== ""
231
+ // Explicitly check for role_id - must be a number, not undefined or null
232
+ const hasRoleId = typeof p.role_id === "number" && !isNaN(p.role_id)
233
+
234
+ return hasColor || hasRoleId
235
+ })
236
+ .map((p) => {
237
+ // Create clean objects without undefined properties
238
+ if (p.color != null && typeof p.color === "string" && p.color.trim() !== "") {
239
+ return { position: p.position, color: p.color.trim() } as { position: number; color: string }
240
+ } else if (typeof p.role_id === "number" && !isNaN(p.role_id)) {
241
+ return { position: p.position, role_id: p.role_id } as { position: number; role_id: number }
242
+ }
243
+ // This should never happen due to the filter above, but TypeScript needs this
244
+ throw new Error("Invalid placement after filter")
245
+ })
246
+
247
+ for (const climbPlacement of validPlacements) {
226
248
  if (tempArray.length + 3 > KilterBoard.messageBodyMaxLength) {
227
249
  resultArray.push(tempArray)
228
250
  tempArray = [KilterBoardPacket.V3_MIDDLE]
229
251
  }
230
- const role = KilterBoardPlacementRoles.find((placement) => placement.id === climbPlacement.role_id)
231
- if (!role) {
232
- throw new Error(`Role with id ${climbPlacement.role_id} not found in placement_roles`)
252
+
253
+ let colorHex: string
254
+
255
+ // Type guard: check if this placement has a color property
256
+ if ("color" in climbPlacement && climbPlacement.color) {
257
+ // Use direct color if provided
258
+ colorHex = climbPlacement.color.replace("#", "").toUpperCase()
259
+ } else if ("role_id" in climbPlacement && typeof climbPlacement.role_id === "number") {
260
+ // Fall back to role_id if no color provided
261
+ const role = KilterBoardPlacementRoles.find((placement) => placement.id === climbPlacement.role_id)
262
+ if (!role) {
263
+ throw new Error(`Role with id ${climbPlacement.role_id} not found in placement_roles`)
264
+ }
265
+ colorHex = role.led_color
266
+ } else {
267
+ // This should never happen due to the filter above, but just in case
268
+ throw new Error(`Either role_id or color must be provided for position ${climbPlacement.position}`)
233
269
  }
234
- const encodedPlacement = this.encodePlacement(climbPlacement.position, role.led_color)
270
+
271
+ const encodedPlacement = this.encodePlacement(climbPlacement.position, colorHex)
235
272
  tempArray.push(...encodedPlacement)
236
273
  }
237
274
 
@@ -292,10 +329,10 @@ export class KilterBoard extends Device implements IKilterBoard {
292
329
 
293
330
  /**
294
331
  * Configures the LEDs based on an array of climb placements.
295
- * @param {{ position: number; role_id: number }[]} config - Array of climb placements for the LEDs.
332
+ * @param {{ position: number; role_id?: number; color?: string }[]} config - Array of climb placements for the LEDs. Either role_id or color (hex string) must be provided.
296
333
  * @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.
297
334
  */
298
- led = async (config: { position: number; role_id: number }[]): Promise<number[] | undefined> => {
335
+ led = async (config: { position: number; role_id?: number; color?: string }[]): Promise<number[] | undefined> => {
299
336
  // Handle Kilterboard logic: process placements and send payload if connected
300
337
  if (Array.isArray(config)) {
301
338
  // Prepares byte arrays for transmission based on a list of climb placements.
@@ -39,6 +39,14 @@ export class Motherboard extends Device implements IMotherboard {
39
39
  */
40
40
  private calibrationData: number[][][] = [[], [], [], []]
41
41
 
42
+ /** Per-zone peak and running sum for left/center/right (used for distribution stats). */
43
+ private leftPeak = Number.NEGATIVE_INFINITY
44
+ private leftSum = 0
45
+ private centerPeak = Number.NEGATIVE_INFINITY
46
+ private centerSum = 0
47
+ private rightPeak = Number.NEGATIVE_INFINITY
48
+ private rightSum = 0
49
+
42
50
  constructor() {
43
51
  super({
44
52
  filters: [{ name: "Motherboard" }],
@@ -281,28 +289,37 @@ export class Motherboard extends Device implements IMotherboard {
281
289
  center -= this.applyTare(center)
282
290
  right -= this.applyTare(right)
283
291
 
284
- this.massMax = Math.max(Number(this.massMax), Math.max(-1000, left + center + right)).toFixed(1)
292
+ const totalCurrent = Math.max(-1000, left + center + right)
293
+ const leftClamped = Math.max(-1000, left)
294
+ const centerClamped = Math.max(-1000, center)
295
+ const rightClamped = Math.max(-1000, right)
296
+
297
+ this.peak = Math.max(this.peak, totalCurrent)
285
298
 
286
- // Update running sum and count
287
- const currentMassTotal = Math.max(-1000, left + center + right)
288
- this.massTotalSum += currentMassTotal
299
+ // Update running sum and count (total)
300
+ this.sum += totalCurrent
289
301
  this.dataPointCount++
302
+ this.mean = this.sum / this.dataPointCount
290
303
 
291
- // Calculate the average dynamically
292
- this.massAverage = (this.massTotalSum / this.dataPointCount).toFixed(1)
304
+ // Per-zone peak and sum for distribution
305
+ this.leftPeak = Math.max(this.leftPeak, leftClamped)
306
+ this.leftSum += leftClamped
307
+ this.centerPeak = Math.max(this.centerPeak, centerClamped)
308
+ this.centerSum += centerClamped
309
+ this.rightPeak = Math.max(this.rightPeak, rightClamped)
310
+ this.rightSum += rightClamped
293
311
 
294
312
  // Check if device is being used
295
313
  this.activityCheck(center)
296
314
 
297
- // Notify with weight data
298
- this.notifyCallback({
299
- massTotal: Math.max(-1000, left + center + right).toFixed(1),
300
- massMax: this.massMax,
301
- massAverage: this.massAverage,
302
- massLeft: Math.max(-1000, packet.masses[0]).toFixed(1),
303
- massCenter: Math.max(-1000, packet.masses[1]).toFixed(1),
304
- massRight: Math.max(-1000, packet.masses[2]).toFixed(1),
305
- })
315
+ // Notify with weight data (distribution zones have proper peak/mean per zone)
316
+ this.notifyCallback(
317
+ this.buildForceMeasurement(totalCurrent, {
318
+ left: this.buildZoneMeasurement(leftClamped, this.leftPeak, this.leftSum / this.dataPointCount),
319
+ center: this.buildZoneMeasurement(centerClamped, this.centerPeak, this.centerSum / this.dataPointCount),
320
+ right: this.buildZoneMeasurement(rightClamped, this.rightPeak, this.rightSum / this.dataPointCount),
321
+ }),
322
+ )
306
323
  } else if (this.writeLast === this.commands.GET_CALIBRATION) {
307
324
  // check data integrity
308
325
  if ((receivedData.match(/,/g) || []).length === 3) {
@@ -0,0 +1,263 @@
1
+ import { Device } from "../device.model.js"
2
+
3
+ /**
4
+ * Represents a NSD PB-700BT device.
5
+ * {@link https://www.nsd.com.tw/}
6
+ */
7
+ export class PB700BT extends Device {
8
+ constructor() {
9
+ super({
10
+ filters: [{ name: "NSD Workout" }],
11
+ services: [
12
+ {
13
+ name: "Battery Service",
14
+ id: "battery",
15
+ uuid: "0000180f-0000-1000-8000-00805f9b34fb",
16
+ characteristics: [
17
+ {
18
+ name: "Battery Level",
19
+ id: "level",
20
+ uuid: "00002a19-0000-1000-8000-00805f9b34fb", // 100
21
+ },
22
+ ],
23
+ },
24
+ {
25
+ name: "Custom Service", // Unknown custom service
26
+ id: "custom",
27
+ uuid: "0000feba-0000-1000-8000-00805f9b34fb",
28
+ characteristics: [
29
+ {
30
+ name: "Custom Characteristic 1", // Unknown characteristic
31
+ id: "custom1",
32
+ uuid: "0000fa10-0000-1000-8000-00805f9b34fb",
33
+ },
34
+ {
35
+ name: "Custom Characteristic 2", // Unknown characteristic
36
+ id: "custom2",
37
+ uuid: "0000fa11-0000-1000-8000-00805f9b34fb",
38
+ },
39
+ {
40
+ name: "Custom Characteristic 3", // Unknown characteristic
41
+ id: "custom3",
42
+ uuid: "0000fa13-0000-1000-8000-00805f9b34fb",
43
+ },
44
+ ],
45
+ },
46
+ {
47
+ name: "UART ISSC Transparent Service",
48
+ id: "uart",
49
+ uuid: "0000fff0-0000-1000-8000-00805f9b34fb",
50
+ characteristics: [
51
+ {
52
+ name: "TX",
53
+ id: "tx",
54
+ uuid: "0000fff1-0000-1000-8000-00805f9b34fb",
55
+ },
56
+ {
57
+ name: "Unknown UART Characteristic", // Unknown UART characteristic
58
+ id: "unknown1",
59
+ uuid: "0000fff3-0000-1000-8000-00805f9b34fb",
60
+ },
61
+ {
62
+ name: "RX",
63
+ id: "rx",
64
+ uuid: "0000fff4-0000-1000-8000-00805f9b34fb",
65
+ },
66
+ {
67
+ name: "Unknown UART Characteristic 2", // Unknown UART characteristic
68
+ id: "unknown2",
69
+ uuid: "0000fff6-0000-1000-8000-00805f9b34fb",
70
+ },
71
+ {
72
+ name: "Unknown UART Characteristic 3", // Unknown UART characteristic
73
+ id: "unknown3",
74
+ uuid: "0000fff7-0000-1000-8000-00805f9b34fb",
75
+ },
76
+ {
77
+ name: "Unknown UART Characteristic 4", // Unknown UART characteristic
78
+ id: "unknown4",
79
+ uuid: "0000fff8-0000-1000-8000-00805f9b34fb",
80
+ },
81
+ ],
82
+ },
83
+ {
84
+ name: "Device Information",
85
+ id: "device",
86
+ uuid: "0000180a-0000-1000-8000-00805f9b34fb",
87
+ characteristics: [
88
+ {
89
+ name: "System ID",
90
+ id: "system",
91
+ uuid: "00002a23-0000-1000-8000-00805f9b34fb",
92
+ },
93
+ {
94
+ name: "Model Number String",
95
+ id: "model",
96
+ uuid: "00002a24-0000-1000-8000-00805f9b34fb", // MD8107
97
+ },
98
+ // {
99
+ // name: "Serial Number String (Blocked)",
100
+ // id: "serial",
101
+ // uuid: "00002a25-0000-1000-8000-00805f9b34fb",
102
+ // },
103
+ {
104
+ name: "Firmware Revision String",
105
+ id: "firmware",
106
+ uuid: "00002a26-0000-1000-8000-00805f9b34fb", // 0.7
107
+ },
108
+ {
109
+ name: "Hardware Revision String",
110
+ id: "hardware",
111
+ uuid: "00002a27-0000-1000-8000-00805f9b34fb",
112
+ },
113
+ {
114
+ name: "Software Revision String",
115
+ id: "software",
116
+ uuid: "00002a28-0000-1000-8000-00805f9b34fb",
117
+ },
118
+ {
119
+ name: "Manufacturer Name String",
120
+ id: "manufacturer",
121
+ uuid: "00002a29-0000-1000-8000-00805f9b34fb", // AMICCOM Elec.
122
+ },
123
+ {
124
+ name: "PnP ID",
125
+ id: "pnp",
126
+ uuid: "00002a50-0000-1000-8000-00805f9b34fb",
127
+ },
128
+ ],
129
+ },
130
+ ],
131
+ })
132
+ }
133
+
134
+ /**
135
+ * Retrieves battery or voltage information from the device.
136
+ * @returns {Promise<string | undefined>} A Promise that resolves with the battery or voltage information.
137
+ */
138
+ battery = async (): Promise<string | undefined> => {
139
+ return await this.read("battery", "level", 250)
140
+ }
141
+
142
+ /**
143
+ * Retrieves IEEE 11073-20601 Regulatory Certification from the device.
144
+ * @returns {Promise<string>} A Promise that resolves with the certification.
145
+ */
146
+ certification = async (): Promise<string | undefined> => {
147
+ return await this.read("device", "certification", 250)
148
+ }
149
+
150
+ /**
151
+ * Retrieves firmware version from the device.
152
+ * @returns {Promise<string>} A Promise that resolves with the firmware version.
153
+ */
154
+ firmware = async (): Promise<string | undefined> => {
155
+ return await this.read("device", "firmware", 250)
156
+ }
157
+
158
+ /**
159
+ * Handles data received from the device, processes weight measurements,
160
+ * and updates mass data including maximum and average values.
161
+ * It also handles command responses for retrieving device information.
162
+ *
163
+ * @param {DataView} value - The notification event.
164
+ */
165
+ override handleNotifications = (value: DataView): void => {
166
+ if (value) {
167
+ // Update timestamp
168
+ this.updateTimestamp()
169
+ if (value.buffer) {
170
+ const period = value.getUint32(0, false)
171
+
172
+ if (!Number.isFinite(period) || period === 0) return
173
+
174
+ const ts = value.getUint32(4, false)
175
+
176
+ const rpmFloat = 60 * (666666 / period)
177
+
178
+ // Accept only RPMs in plausible range (~800–15000)
179
+ if (!Number.isFinite(rpmFloat) || rpmFloat < 800 || rpmFloat > 15000) return
180
+
181
+ const receivedData = Math.round(rpmFloat)
182
+ const receivedTime = Date.now()
183
+
184
+ const numericData = receivedData - this.applyTare(receivedData)
185
+
186
+ // Add data to downloadable Array
187
+ this.downloadPackets.push({
188
+ received: receivedTime,
189
+ sampleNum: ts,
190
+ battRaw: 0,
191
+ samples: [numericData],
192
+ masses: [numericData],
193
+ })
194
+
195
+ // Update peak
196
+ this.peak = Math.max(this.peak, numericData)
197
+
198
+ // Update running sum and count
199
+ const currentMassTotal = Math.max(-1000, numericData)
200
+ this.sum += currentMassTotal
201
+ this.dataPointCount++
202
+
203
+ // Calculate the average dynamically
204
+ this.mean = this.sum / this.dataPointCount
205
+
206
+ // Check if device is being used
207
+ this.activityCheck(numericData)
208
+
209
+ // Notify with weight data
210
+ this.notifyCallback(this.buildForceMeasurement(Math.max(-1000, numericData)))
211
+ }
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Retrieves hardware version from the device.
217
+ * @returns {Promise<string>} A Promise that resolves with the hardware version.
218
+ */
219
+ hardware = async (): Promise<string | undefined> => {
220
+ return await this.read("device", "hardware", 250)
221
+ }
222
+
223
+ /**
224
+ * Retrieves manufacturer information from the device.
225
+ * @returns {Promise<string>} A Promise that resolves with the manufacturer information.
226
+ */
227
+ manufacturer = async (): Promise<string | undefined> => {
228
+ return await this.read("device", "manufacturer", 250)
229
+ }
230
+
231
+ /**
232
+ * Retrieves model number from the device.
233
+ * @returns {Promise<string>} A Promise that resolves with the model number.
234
+ */
235
+ model = async (): Promise<string | undefined> => {
236
+ return await this.read("device", "model", 250)
237
+ }
238
+
239
+ /**
240
+ * Retrieves PnP ID from the device, a set of values that used to create a device ID value that is unique for this device.
241
+ * Included in the characteristic is a Vendor ID Source field, a Vendor ID field, a Product ID field and a Product Version field
242
+ * @returns {Promise<string>} A Promise that resolves with the PnP ID.
243
+ */
244
+ pnp = async (): Promise<string | undefined> => {
245
+ return await this.read("device", "pnp", 250)
246
+ }
247
+
248
+ /**
249
+ * Retrieves software version from the device.
250
+ * @returns {Promise<string>} A Promise that resolves with the software version.
251
+ */
252
+ software = async (): Promise<string | undefined> => {
253
+ return await this.read("device", "software", 250)
254
+ }
255
+
256
+ /**
257
+ * Retrieves system id from the device.
258
+ * @returns {Promise<string>} A Promise that resolves with the system id.
259
+ */
260
+ system = async (): Promise<string | undefined> => {
261
+ return await this.read("device", "system", 250)
262
+ }
263
+ }
@@ -158,23 +158,19 @@ export class Progressor extends Device implements IProgressor {
158
158
  masses: [numericData],
159
159
  })
160
160
  // Check for max weight
161
- this.massMax = Math.max(Number(this.massMax), Number(numericData)).toFixed(1)
161
+ this.peak = Math.max(this.peak, Number(numericData))
162
162
  // Update running sum and count
163
163
  const currentMassTotal = Math.max(-1000, Number(numericData))
164
- this.massTotalSum += currentMassTotal
164
+ this.sum += currentMassTotal
165
165
  this.dataPointCount++
166
166
 
167
167
  // Calculate the average dynamically
168
- this.massAverage = (this.massTotalSum / this.dataPointCount).toFixed(1)
168
+ this.mean = this.sum / this.dataPointCount
169
169
 
170
170
  // Check if device is being used
171
171
  this.activityCheck(numericData)
172
172
 
173
- this.notifyCallback({
174
- massMax: this.massMax,
175
- massAverage: this.massAverage,
176
- massTotal: Math.max(-1000, numericData).toFixed(1),
177
- })
173
+ this.notifyCallback(this.buildForceMeasurement(Math.max(-1000, numericData)))
178
174
  }
179
175
  }
180
176
  } else if (kind === ProgressorResponses.COMMAND_RESPONSE) {
@@ -12,8 +12,8 @@ export class SmartBoardPro extends Device implements ISmartBoardPro {
12
12
  filters: [{ name: "SMARTBOARD" }],
13
13
  services: [
14
14
  {
15
- name: "Basic Audio Announcement",
16
- id: "audio",
15
+ name: "Weight Scale Service",
16
+ id: "weight",
17
17
  uuid: "00001851-0000-1000-8000-00805f9b34fb",
18
18
  characteristics: [
19
19
  {
@@ -24,13 +24,13 @@ export class SmartBoardPro extends Device implements ISmartBoardPro {
24
24
  ],
25
25
  },
26
26
  {
27
- name: "Read + Notify",
28
- id: "",
27
+ name: "Smartboard Service",
28
+ id: "smartboard",
29
29
  uuid: "0000403d-0000-1000-8000-00805f9b34fb",
30
30
  characteristics: [
31
31
  {
32
- name: "",
33
- id: "",
32
+ name: "SmartBoard Measurement",
33
+ id: "rx",
34
34
  uuid: "00001583-0000-1000-8000-00805f9b34fb",
35
35
  },
36
36
  ],
@@ -50,4 +50,71 @@ export class SmartBoardPro extends Device implements ISmartBoardPro {
50
50
  ],
51
51
  })
52
52
  }
53
+
54
+ /**
55
+ * Handles data received from the device, processes weight measurements,
56
+ * and updates mass data including maximum and average values.
57
+ * It also handles command responses for retrieving device information.
58
+ *
59
+ * @param {DataView} value - The notification event.
60
+ */
61
+ override handleNotifications = (value: DataView): void => {
62
+ if (value) {
63
+ // Update timestamp
64
+ this.updateTimestamp()
65
+ if (value.buffer) {
66
+ const length = value.byteLength / 2
67
+ const dataArray: number[] = []
68
+
69
+ for (let i = 0; i < length; i++) {
70
+ const offset = i * 2
71
+ if (offset + 1 < value.byteLength) {
72
+ const intValue = value.getInt16(offset, true)
73
+ // For debugging purposes
74
+ console.log(intValue)
75
+
76
+ dataArray.push(intValue)
77
+ }
78
+ }
79
+
80
+ if (dataArray.length === 0) return
81
+
82
+ const receivedTime = Date.now()
83
+
84
+ // Process each data point
85
+ for (const receivedData of dataArray) {
86
+ // Skip invalid values
87
+ if (!Number.isFinite(receivedData)) continue
88
+
89
+ const numericData = receivedData - this.applyTare(receivedData)
90
+
91
+ // Add data to downloadable Array
92
+ this.downloadPackets.push({
93
+ received: receivedTime,
94
+ sampleNum: this.dataPointCount,
95
+ battRaw: 0,
96
+ samples: [numericData],
97
+ masses: [numericData],
98
+ })
99
+
100
+ // Update peak
101
+ this.peak = Math.max(this.peak, numericData)
102
+
103
+ // Update running sum and count
104
+ const currentMassTotal = Math.max(-1000, numericData)
105
+ this.sum += currentMassTotal
106
+ this.dataPointCount++
107
+
108
+ // Calculate the average dynamically
109
+ this.mean = this.sum / this.dataPointCount
110
+
111
+ // Check if device is being used
112
+ this.activityCheck(numericData)
113
+
114
+ // Notify with weight data
115
+ this.notifyCallback(this.buildForceMeasurement(Math.max(-1000, numericData)))
116
+ }
117
+ }
118
+ }
119
+ }
53
120
  }
@@ -117,26 +117,22 @@ export class WHC06 extends Device implements IWHC06 {
117
117
  masses: [numericData],
118
118
  })
119
119
 
120
- // Update massMax
121
- this.massMax = Math.max(Number(this.massMax), numericData).toFixed(1)
120
+ // Update peak
121
+ this.peak = Math.max(this.peak, numericData)
122
122
 
123
123
  // Update running sum and count
124
124
  const currentMassTotal = Math.max(-1000, numericData)
125
- this.massTotalSum += currentMassTotal
125
+ this.sum += currentMassTotal
126
126
  this.dataPointCount++
127
127
 
128
128
  // Calculate the average dynamically
129
- this.massAverage = (this.massTotalSum / this.dataPointCount).toFixed(1)
129
+ this.mean = this.sum / this.dataPointCount
130
130
 
131
131
  // Check if device is being used
132
132
  this.activityCheck(numericData)
133
133
 
134
134
  // Notify with weight data
135
- this.notifyCallback({
136
- massMax: this.massMax,
137
- massAverage: this.massAverage,
138
- massTotal: Math.max(-1000, numericData).toFixed(1),
139
- })
135
+ this.notifyCallback(this.buildForceMeasurement(Math.max(-1000, numericData)))
140
136
  }
141
137
  // Reset "still advertising" counter
142
138
  this.resetAdvertisementTimeout()