@hangtime/grip-connect 0.10.9 → 0.12.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 (50) hide show
  1. package/README.md +3 -3
  2. package/dist/cjs/interfaces/command.interface.d.ts +12 -1
  3. package/dist/cjs/interfaces/command.interface.d.ts.map +1 -1
  4. package/dist/cjs/interfaces/device/climbro.interface.d.ts +25 -0
  5. package/dist/cjs/interfaces/device/climbro.interface.d.ts.map +1 -1
  6. package/dist/cjs/interfaces/device/progressor.interface.d.ts +15 -5
  7. package/dist/cjs/interfaces/device/progressor.interface.d.ts.map +1 -1
  8. package/dist/cjs/models/device/climbro.model.d.ts +25 -0
  9. package/dist/cjs/models/device/climbro.model.d.ts.map +1 -1
  10. package/dist/cjs/models/device/climbro.model.js +93 -1
  11. package/dist/cjs/models/device/climbro.model.js.map +1 -1
  12. package/dist/cjs/models/device/entralpi.model.d.ts.map +1 -1
  13. package/dist/cjs/models/device/entralpi.model.js +3 -5
  14. package/dist/cjs/models/device/entralpi.model.js.map +1 -1
  15. package/dist/cjs/models/device/progressor.model.d.ts +23 -10
  16. package/dist/cjs/models/device/progressor.model.d.ts.map +1 -1
  17. package/dist/cjs/models/device/progressor.model.js +79 -17
  18. package/dist/cjs/models/device/progressor.model.js.map +1 -1
  19. package/dist/cjs/models/device.model.d.ts.map +1 -1
  20. package/dist/cjs/models/device.model.js +30 -25
  21. package/dist/cjs/models/device.model.js.map +1 -1
  22. package/dist/interfaces/command.interface.d.ts +12 -1
  23. package/dist/interfaces/command.interface.d.ts.map +1 -1
  24. package/dist/interfaces/device/climbro.interface.d.ts +25 -0
  25. package/dist/interfaces/device/climbro.interface.d.ts.map +1 -1
  26. package/dist/interfaces/device/progressor.interface.d.ts +15 -5
  27. package/dist/interfaces/device/progressor.interface.d.ts.map +1 -1
  28. package/dist/models/device/climbro.model.d.ts +25 -0
  29. package/dist/models/device/climbro.model.d.ts.map +1 -1
  30. package/dist/models/device/climbro.model.js +93 -1
  31. package/dist/models/device/climbro.model.js.map +1 -1
  32. package/dist/models/device/entralpi.model.d.ts.map +1 -1
  33. package/dist/models/device/entralpi.model.js +3 -5
  34. package/dist/models/device/entralpi.model.js.map +1 -1
  35. package/dist/models/device/progressor.model.d.ts +23 -10
  36. package/dist/models/device/progressor.model.d.ts.map +1 -1
  37. package/dist/models/device/progressor.model.js +79 -17
  38. package/dist/models/device/progressor.model.js.map +1 -1
  39. package/dist/models/device.model.d.ts.map +1 -1
  40. package/dist/models/device.model.js +30 -25
  41. package/dist/models/device.model.js.map +1 -1
  42. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/src/interfaces/command.interface.ts +14 -1
  45. package/src/interfaces/device/climbro.interface.ts +30 -0
  46. package/src/interfaces/device/progressor.interface.ts +17 -5
  47. package/src/models/device/climbro.model.ts +98 -1
  48. package/src/models/device/entralpi.model.ts +3 -5
  49. package/src/models/device/progressor.model.ts +83 -17
  50. package/src/models/device.model.ts +39 -25
@@ -66,9 +66,8 @@ function parseProgressorIdPayload(payload: Uint8Array): string {
66
66
  }
67
67
 
68
68
  /**
69
- * Parse calibration curve: 12 opaque bytes (CalibrationCurve = [u8; 12]).
70
- * Progressor two-point calibration stores two raw ADC readings (zero + reference)
71
- * and a third value (version/reserved). Exact layout is device-specific.
69
+ * Parse calibration block: float32 LE.
70
+ * value = raw * slope + intercept + trim.
72
71
  */
73
72
  function parseCalibrationCurvePayload(payload: Uint8Array): string {
74
73
  const hex = toHex(payload)
@@ -76,14 +75,49 @@ function parseCalibrationCurvePayload(payload: Uint8Array): string {
76
75
  if (payload.length !== 12) return hex
77
76
 
78
77
  const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)
79
- const [v0, v1, v2] = [0, 4, 8].map((o) => view.getUint32(o, true))
78
+ const slope = view.getFloat32(0, true)
79
+ const intercept = view.getFloat32(4, true)
80
+ const trim = view.getFloat32(8, true)
81
+ const effectiveOffset = intercept + trim
82
+
83
+ const formatSignedFloat = (value: number): string => {
84
+ const formatted = formatCalibrationFloat(Math.abs(value))
85
+ return value < 0 ? ` - ${formatted}` : ` + ${formatted}`
86
+ }
87
+
88
+ return `${hex} — slope: ${formatCalibrationFloat(slope)} | intercept: ${formatCalibrationFloat(intercept)} | trim: ${formatCalibrationFloat(trim)} | effective offset: ${formatCalibrationFloat(effectiveOffset)} | formula: raw * ${formatCalibrationFloat(slope)}${formatSignedFloat(intercept)}${formatSignedFloat(trim)}`
89
+ }
90
+
91
+ /**
92
+ * Format floating-point values for calibration-table display.
93
+ */
94
+ function formatCalibrationFloat(value: number): string {
95
+ if (!Number.isFinite(value)) return String(value)
96
+ const abs = Math.abs(value)
97
+ return abs !== 0 && (abs >= 1_000_000 || abs < 0.0001) ? value.toExponential(6) : value.toFixed(6)
98
+ }
99
+
100
+ /**
101
+ * Parse one calibration table record: [u32 lower, u32 upper, f32 slope, f32 intercept].
102
+ */
103
+ function parseCalibrationTableRecordPayload(payload: Uint8Array, index: number): string {
104
+ if (payload.length !== 16) return `${String(index).padStart(2, "0")}: ${toHex(payload)}`
105
+
106
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)
107
+ const lowerRaw = view.getUint32(0, true)
108
+ const upperRaw = view.getUint32(4, true)
109
+ const slope = view.getFloat32(8, true)
110
+ const intercept = view.getFloat32(12, true)
111
+ const hex = toHex(payload)
80
112
 
81
- return `${hex} 1: ${v1.toLocaleString()} | 2: ${v0.toLocaleString()} | 3: ${v2}`
113
+ return `${String(index).padStart(2, "0")}: ${hex} | raw ${lowerRaw.toLocaleString()}..${upperRaw.toLocaleString()} | slope ${formatCalibrationFloat(slope)} | intercept ${formatCalibrationFloat(intercept)}`
82
114
  }
83
115
 
84
116
  export class Progressor extends Device implements IProgressor {
85
117
  /** Device timestamps (µs) of recent samples (samples in last 1s device time). */
86
118
  private recentSampleTimestamps: number[] = []
119
+ /** 1-based index for multi-packet calibration-table export responses. */
120
+ private calibrationTableRecordIndex = 0
87
121
 
88
122
  constructor() {
89
123
  super({
@@ -119,7 +153,7 @@ export class Progressor extends Device implements IProgressor {
119
153
  ],
120
154
  },
121
155
  ],
122
- // Tindeq API: opcode = single byte (ASCII char code = decimal 100–114;)
156
+ // Tindeq API: opcode = single byte (ASCII char code = decimal 100–114 v2 firmware: 115-118)
123
157
  commands: {
124
158
  TARE_SCALE: "d", // 100 (0x64)
125
159
  START_WEIGHT_MEAS: "e", // 101 (0x65)
@@ -136,6 +170,11 @@ export class Progressor extends Device implements IProgressor {
136
170
  GET_PROGRESSOR_ID: "p", // 112 (0x70)
137
171
  SET_CALIBRATION: "q", // 113 (0x71)
138
172
  GET_CALIBRATION: "r", // 114 (0x72)
173
+ // V2 FIRMWARE ONLY COMMANDS
174
+ // ADD_CALIBRATION_TABLE_POINT: "s", // 115 (0x73)
175
+ GET_CALIBRATION_TABLE: "t", // 116 (0x74)
176
+ REBOOT: "u", // 117 (0x75)
177
+ // CLR_CALIBRATION_TABLE: "v", // 118 (0x76)
139
178
  },
140
179
  })
141
180
  }
@@ -153,7 +192,7 @@ export class Progressor extends Device implements IProgressor {
153
192
  }
154
193
 
155
194
  /**
156
- * Retrieves firmware version from the device (GetAppVersion, opcode 0x6B).
195
+ * Retrieves firmware version from the device.
157
196
  * @returns {Promise<string>} A Promise that resolves with the firmware version,
158
197
  */
159
198
  firmware = async (): Promise<string | undefined> => {
@@ -165,7 +204,7 @@ export class Progressor extends Device implements IProgressor {
165
204
  }
166
205
 
167
206
  /**
168
- * Retrieves the Progressor ID from the device (opcode 0x70).
207
+ * Retrieves the Progressor ID from the device.
169
208
  * @returns {Promise<string>} A Promise that resolves with the raw response (hex of payload).
170
209
  */
171
210
  progressorId = async (): Promise<string | undefined> => {
@@ -177,8 +216,8 @@ export class Progressor extends Device implements IProgressor {
177
216
  }
178
217
 
179
218
  /**
180
- * Retrieves calibration values from the device.
181
- * @returns {Promise<string>} A Promise that resolves with the raw response (hex of payload).
219
+ * Retrieves the linear calibration block from the device.
220
+ * Returns raw hex plus decoded slope/intercept/trim coefficients.
182
221
  */
183
222
  calibration = async (): Promise<string | undefined> => {
184
223
  let response: string | undefined = undefined
@@ -189,7 +228,21 @@ export class Progressor extends Device implements IProgressor {
189
228
  }
190
229
 
191
230
  /**
192
- * Computes calibration curve from stored points and saves to flash (opcode 0x6A).
231
+ * Retrieves the hidden 15-entry piecewise calibration table.
232
+ * Each response packet contains one 16-byte record.
233
+ * @returns {Promise<string | undefined>} Newline-separated decoded records.
234
+ */
235
+ calibrationTable = async (): Promise<string | undefined> => {
236
+ const responses: string[] = []
237
+ this.calibrationTableRecordIndex = 0
238
+ await this.write("progressor", "tx", this.commands.GET_CALIBRATION_TABLE, 1000, (data) => {
239
+ responses.push(data)
240
+ })
241
+ return responses.length > 0 ? responses.join("\n") : undefined
242
+ }
243
+
244
+ /**
245
+ * Computes calibration curve from stored points and saves to flash.
193
246
  * Requires addCalibrationPoint() for zero and reference. Normal flow: i → i → j.
194
247
  * @returns {Promise<void>} A Promise that resolves when the command is sent.
195
248
  */
@@ -198,24 +251,24 @@ export class Progressor extends Device implements IProgressor {
198
251
  }
199
252
 
200
253
  /**
201
- * Opcode 0x71 ('q'): write calibration curve directly (raw overwrite).
254
+ * Write calibration block directly (raw overwrite).
202
255
  *
203
256
  * Payload layout (14 bytes):
204
- * - [0] opcode ('q' = 0x71)
257
+ * - [0] opcode ('q')
205
258
  * - [1] reserved (ignored by firmware)
206
- * - [2..13] 12-byte calibration curve (3× u32 LE read at offsets +2, +6, +10)
259
+ * - [2..13] 12-byte calibration block (3× float32 LE: slope, intercept, trim)
207
260
  *
208
261
  * Notes:
209
262
  * - This command does not compute anything; it overwrites stored calibration data.
210
- * - Sending only the opcode (no 12-byte curve) is not a supported "reset" mode.
263
+ * - Sending only the opcode (no 12-byte calibration block) is not a supported "reset" mode.
211
264
  *
212
- * @param curve - Raw 12-byte calibration curve (required).
265
+ * @param curve - Raw 12-byte calibration block (3× float32 LE: slope, intercept, trim) (required).
213
266
  * @returns Promise that resolves when the command is sent.
214
267
  */
215
268
  setCalibration = async (curve: Uint8Array): Promise<void> => {
216
269
  if (curve.length !== 12) throw new Error("Curve must be 12 bytes")
217
270
 
218
- const opcode = (this.commands.SET_CALIBRATION as string).charCodeAt(0) // 0x71
271
+ const opcode = (this.commands.SET_CALIBRATION as string).charCodeAt(0)
219
272
  const payload = new Uint8Array(14)
220
273
 
221
274
  payload[0] = opcode
@@ -269,6 +322,16 @@ export class Progressor extends Device implements IProgressor {
269
322
  await this.write("progressor", "tx", typeof cmd === "string" ? cmd : String(cmd), 0)
270
323
  }
271
324
 
325
+ /**
326
+ * Reboots the device immediately.
327
+ * @returns {Promise<void>} A Promise that resolves when the command is sent.
328
+ */
329
+ reboot = async (): Promise<void> => {
330
+ const opcode = (this.commands.REBOOT as string).charCodeAt(0)
331
+ // Send byte 1 to trigger the reboot.
332
+ await this.write("progressor", "tx", new Uint8Array([opcode, 0, 1]), 0)
333
+ }
334
+
272
335
  /**
273
336
  * Retrieves error information from the device.
274
337
  * @returns {Promise<string | undefined>} A Promise that resolves with the error info text.
@@ -364,6 +427,9 @@ export class Progressor extends Device implements IProgressor {
364
427
  output = parseProgressorIdPayload(payload)
365
428
  } else if (this.writeLast === this.commands.GET_CALIBRATION) {
366
429
  output = parseCalibrationCurvePayload(payload)
430
+ } else if (this.writeLast === this.commands.GET_CALIBRATION_TABLE) {
431
+ this.calibrationTableRecordIndex += 1
432
+ output = parseCalibrationTableRecordPayload(payload, this.calibrationTableRecordIndex)
367
433
  } else {
368
434
  // Unknown command response: return raw hex
369
435
  output = toHex(payload)
@@ -596,31 +596,39 @@ export abstract class Device extends BaseModel implements IDevice {
596
596
  * device.disconnect();
597
597
  */
598
598
  disconnect = (): void => {
599
- if (this.isConnected()) {
599
+ const isConnected = this.isConnected()
600
+ if (isConnected) {
600
601
  this.updateTimestamp()
601
- // Remove all notification listeners
602
- this.services.forEach((service) => {
603
- service.characteristics.forEach((char) => {
604
- // Look for the "rx" characteristic that accepts notifications
605
- if (char.characteristic && char.id === "rx") {
606
- char.characteristic.stopNotifications()
607
- const listener = this.notificationListeners.get(char.uuid)
608
- if (listener) {
609
- char.characteristic.removeEventListener("characteristicvaluechanged", listener)
610
- this.notificationListeners.delete(char.uuid)
611
- }
612
- }
613
- })
602
+ }
603
+
604
+ // Remove all notification listeners and stop notifications if possible.
605
+ this.services.forEach((service) => {
606
+ service.characteristics.forEach((char) => {
607
+ if (!char.characteristic || char.id !== "rx") return
608
+
609
+ if (isConnected) {
610
+ // Best effort only: avoid unhandled rejections when the device already disconnected.
611
+ void char.characteristic.stopNotifications().catch(() => undefined)
612
+ }
613
+
614
+ const listener = this.notificationListeners.get(char.uuid)
615
+ if (listener) {
616
+ char.characteristic.removeEventListener("characteristicvaluechanged", listener)
617
+ this.notificationListeners.delete(char.uuid)
618
+ }
614
619
  })
615
- // Remove disconnect listener
616
- this.bluetooth?.removeEventListener("gattserverdisconnected", this.onDisconnectedListener)
617
- // Safely attempt to disconnect the device's GATT server, if available
618
- this.bluetooth?.gatt?.disconnect()
619
- // Reset properties
620
- this.server = undefined
621
- this.writeLast = null
622
- this.isActive = false
620
+ })
621
+
622
+ // Remove disconnect listener
623
+ this.bluetooth?.removeEventListener("gattserverdisconnected", this.onDisconnectedListener)
624
+ // Safely attempt to disconnect the device's GATT server, if available
625
+ if (this.bluetooth?.gatt?.connected) {
626
+ this.bluetooth.gatt.disconnect()
623
627
  }
628
+ // Reset properties
629
+ this.server = undefined
630
+ this.writeLast = null
631
+ this.isActive = false
624
632
  }
625
633
 
626
634
  /**
@@ -890,7 +898,9 @@ export abstract class Device extends BaseModel implements IDevice {
890
898
  }
891
899
 
892
900
  for (const service of services) {
893
- const matchingService = this.services.find((boardService) => boardService.uuid === service.uuid)
901
+ const matchingService = this.services.find(
902
+ (boardService) => boardService.uuid.toLowerCase() === service.uuid.toLowerCase(),
903
+ )
894
904
 
895
905
  if (matchingService) {
896
906
  // Android bug: Add a small delay before getting characteristics
@@ -899,11 +909,15 @@ export abstract class Device extends BaseModel implements IDevice {
899
909
  const characteristics = await service.getCharacteristics()
900
910
 
901
911
  for (const characteristic of matchingService.characteristics) {
902
- const matchingCharacteristic = characteristics.find((char) => char.uuid === characteristic.uuid)
912
+ const matchingCharacteristic = characteristics.find(
913
+ (char) => char.uuid.toLowerCase() === characteristic.uuid.toLowerCase(),
914
+ )
903
915
 
904
916
  if (matchingCharacteristic) {
905
917
  // Find the corresponding characteristic descriptor in the service's characteristics array
906
- const descriptor = matchingService.characteristics.find((char) => char.uuid === matchingCharacteristic.uuid)
918
+ const descriptor = matchingService.characteristics.find(
919
+ (char) => char.uuid.toLowerCase() === matchingCharacteristic.uuid.toLowerCase(),
920
+ )
907
921
  if (descriptor) {
908
922
  // Assign the actual Bluetooth characteristic object to the descriptor so it can be used later
909
923
  descriptor.characteristic = matchingCharacteristic