@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
@@ -0,0 +1,709 @@
1
+ import { Device } from "../device.model.js"
2
+ import type { CTS500BaudRate, CTS500SamplingRate, ICTS500 } from "../../interfaces/device/cts500.interface.js"
3
+
4
+ const CTS500_HEADER = 0x05
5
+ const CTS500_RESPONSE_FLAG = 0x80
6
+ const CTS500_ACK_FRAME_LENGTH = 6
7
+ const CTS500_DATA_FRAME_LENGTH = 7
8
+ const CTS500_RESPONSE_TIMEOUT_MS = 2000
9
+
10
+ const CTS500_BAUD_RATE_PARAMS: Record<CTS500BaudRate, number> = {
11
+ 9600: 0x00,
12
+ 19200: 0x01,
13
+ 38400: 0x02,
14
+ 57600: 0x03,
15
+ 115200: 0x04,
16
+ }
17
+
18
+ const CTS500_SAMPLING_RATE_PARAMS: Record<CTS500SamplingRate, number> = {
19
+ 10: 0x00,
20
+ 20: 0x01,
21
+ 40: 0x02,
22
+ 80: 0x03,
23
+ 160: 0x04,
24
+ 320: 0x05,
25
+ }
26
+
27
+ function calculateChecksum(bytes: Uint8Array): number {
28
+ let checksum = 0
29
+
30
+ // CTS frames use a simple additive checksum over every byte before the checksum slot.
31
+ for (const byte of bytes) {
32
+ checksum = (checksum + byte) & 0xff
33
+ }
34
+
35
+ return checksum
36
+ }
37
+
38
+ function buildCommand(opcode: number, payload: readonly [number, number, number] = [0x00, 0x00, 0x00]): Uint8Array {
39
+ const frame = new Uint8Array(CTS500_ACK_FRAME_LENGTH)
40
+ frame[0] = CTS500_HEADER
41
+ frame[1] = opcode
42
+ frame[2] = payload[0]
43
+ frame[3] = payload[1]
44
+ frame[4] = payload[2]
45
+ frame[5] = calculateChecksum(frame.subarray(0, frame.length - 1))
46
+ return frame
47
+ }
48
+
49
+ interface PendingFrame {
50
+ match(frame: Uint8Array): boolean
51
+ reject(error: Error): void
52
+ resolve(frame: Uint8Array): void
53
+ timeout: ReturnType<typeof setTimeout>
54
+ }
55
+
56
+ /**
57
+ * Represents the CTS500 Climbing Training Scale, marketed as "Jlyscales CTS500".
58
+ * Supplier: Hunan Jinlian Cloud Information Technology Co., Ltd.
59
+ * {@link https://www.huaying-scales.com/}
60
+ * {@link https://www.alibaba.com/product-detail/Mini-Climbing-Training-Scale-CTS500-Aluminum_1601637814595.html}
61
+ */
62
+ export class CTS500 extends Device implements ICTS500 {
63
+ private bufferedFrames = new Uint8Array(0)
64
+ private pendingFrame: PendingFrame | undefined = undefined
65
+ private requestQueue: Promise<void> = Promise.resolve()
66
+ private isStreaming = false
67
+ private commandOpcodes = new Set<number>()
68
+
69
+ constructor() {
70
+ super({
71
+ filters: [{ name: "CTS-300" }, { name: "CTS500" }],
72
+ services: [
73
+ {
74
+ name: "Device Information",
75
+ id: "device",
76
+ uuid: "0000180a-0000-1000-8000-00805f9b34fb",
77
+ characteristics: [
78
+ {
79
+ name: "Model Number String",
80
+ id: "model",
81
+ uuid: "00002a24-0000-1000-8000-00805f9b34fb", // MY-BT102 https://www.muyusmart.cn/product/my-bt102/
82
+ },
83
+ // {
84
+ // name: "Serial Number String (Blocked)",
85
+ // id: "serial",
86
+ // uuid: "00002a25-0000-1000-8000-00805f9b34fb",
87
+ // },
88
+ {
89
+ name: "Firmware Revision String",
90
+ id: "firmware",
91
+ uuid: "00002a26-0000-1000-8000-00805f9b34fb", // 109a
92
+ },
93
+ {
94
+ name: "Hardware Revision String",
95
+ id: "hardware",
96
+ uuid: "00002a27-0000-1000-8000-00805f9b34fb", //1.0
97
+ },
98
+ {
99
+ name: "Software Revision String",
100
+ id: "software",
101
+ uuid: "00002a28-0000-1000-8000-00805f9b34fb", // 2.1.3
102
+ },
103
+ {
104
+ name: "Manufacturer Name String",
105
+ id: "manufacturer",
106
+ uuid: "00002a29-0000-1000-8000-00805f9b34fb", // DX
107
+ },
108
+ ],
109
+ },
110
+ {
111
+ name: "CTS500 Service",
112
+ id: "cts500",
113
+ uuid: "0000ffe0-0000-1000-8000-00805f9b34fb",
114
+ characteristics: [
115
+ {
116
+ name: "Notify",
117
+ id: "rx",
118
+ uuid: "0000ffe1-0000-1000-8000-00805f9b34fb",
119
+ },
120
+ {
121
+ name: "Write",
122
+ id: "tx",
123
+ uuid: "0000ffe2-0000-1000-8000-00805f9b34fb",
124
+ },
125
+ ],
126
+ },
127
+ ],
128
+ commands: {
129
+ SET_RANGE: 0x81, // set capacity/range; known presets include 100kg, 200kg, 300kg, 400kg, 500kg, 1T, and 3T
130
+ SET_DIVISION: 0x82, // set division; known presets include 10g, 20g, 50g, and 0.1kg
131
+ SET_FIRST_CALIBRATION_WEIGHT: 0x83, // set first calibration reference weight; known presets include 1kg, 5kg, 10kg, 20kg, 50kg, and 100kg
132
+ SET_SECOND_CALIBRATION_WEIGHT: 0x84, // set second calibration reference weight; known presets shown include 50kg, 100kg, and 200kg
133
+ POWER_ON_RESET: 0x85, // power-on reset mode; payload 00 disables automatic reset and 01 enables it
134
+ ZERO_SCALE: buildCommand(0x86), // update the hardware zero point
135
+ RUN_FIRST_CALIBRATION: buildCommand(0xa1), // run the first calibration step after placing the configured reference weight
136
+ RUN_SECOND_CALIBRATION: buildCommand(0xa2), // run the second calibration step after placing the configured reference weight
137
+ GET_FIRMWARE_VERSION: buildCommand(0xa4), // read firmware version over the transparent UART service
138
+ TARE_SCALE: buildCommand(0xa6), // tare the current load
139
+ NO_LOAD_CALIBRATION: buildCommand(0xa7), // run the no-load calibration routine with the scale unloaded
140
+ GET_WEIGHT: buildCommand(0xa9), // read the current weight immediately
141
+ START_WEIGHT_MEAS: buildCommand(0xaa), // turn on automatic weight uploading
142
+ STOP_WEIGHT_MEAS: buildCommand(0xab), // turn off automatic weight uploading
143
+ SET_BAUD_RATE: 0xc0, // set UART baud rate; payload presets are 00=9600, 01=19200, 02=38400, 03=57600, 04=115200
144
+ SET_SAMPLING_RATE: 0xc1, // set A/D sampling frequency; payload presets are 00=10Hz, 01=20Hz, 02=40Hz, 03=80Hz, 04=160Hz, 05=320Hz
145
+ SET_SHUTDOWN_TIME: 0xc3, // set auto-shutdown timer; the shown presets use payload 1E for 30 seconds and 00 to disable
146
+ GET_BATTERY_VOLTAGE: buildCommand(0xc4), // read battery voltage
147
+ GET_TEMPERATURE: buildCommand(0xc5), // read temperature
148
+ SET_UPPER_TEMPERATURE_LIMIT: 0xc6, // set upper temperature limit; the shown example uses payload 1D
149
+ SET_LOWER_TEMPERATURE_LIMIT: 0xc7, // set lower temperature limit; the shown example uses payload 2A and FF disables the lower limit
150
+ PEAK_MODE: 0xca, // peak mode; payload 00 turns it off and 01 turns it on
151
+ SET_MAX_WEIGHT_LIMIT: 0xd1, // set the upper/max weight limit threshold
152
+ SET_MIN_WEIGHT_LIMIT: 0xd2, // set the lower/min weight limit threshold
153
+ SET_WEIGHT_ALARM_MODE: 0xd3, // set weight alarm mode; payload 00 cancels, 01 alarms inside the range, and 02 alarms outside the range
154
+ SET_ALARM_OUTPUT: 0xd4, // enable or disable alarm-frame output; payload 00 turns it off and 01 turns it on
155
+ },
156
+ })
157
+
158
+ for (const command of Object.values(this.commands)) {
159
+ // Command echoes identify themselves by opcode byte 1 whether the command is stored as a raw opcode or a full frame.
160
+ if (typeof command === "number") {
161
+ this.commandOpcodes.add(command)
162
+ } else if (command instanceof Uint8Array && command.length >= 2) {
163
+ this.commandOpcodes.add(command[1])
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Retrieves battery voltage from the device.
170
+ * The returned string uses two decimal places, e.g. "3.55".
171
+ * @returns {Promise<string | undefined>} A Promise that resolves with the battery voltage.
172
+ */
173
+ battery = async (): Promise<string | undefined> => {
174
+ const command = this.commands.GET_BATTERY_VOLTAGE as Uint8Array
175
+ const frame = await this.queryFrame(command, (response) => this.isCommandResponse(response, command[1]))
176
+ if (!frame) {
177
+ return undefined
178
+ }
179
+
180
+ const rawVoltage = (frame[4] << 8) | frame[5]
181
+ return (rawVoltage / 100).toFixed(2)
182
+ }
183
+
184
+ /**
185
+ * Retrieves firmware version from the device.
186
+ * @returns {Promise<string | undefined>} A Promise that resolves with the firmware version.
187
+ */
188
+ firmware = async (): Promise<string | undefined> => {
189
+ return await this.read("device", "firmware", 250)
190
+ }
191
+
192
+ /**
193
+ * Retrieves hardware version from the device.
194
+ * @returns {Promise<string | undefined>} A Promise that resolves with the hardware version.
195
+ */
196
+ hardware = async (): Promise<string | undefined> => {
197
+ return await this.read("device", "hardware", 250)
198
+ }
199
+
200
+ /**
201
+ * Retrieves manufacturer information from the device.
202
+ * @returns {Promise<string | undefined>} A Promise that resolves with the manufacturer information.
203
+ */
204
+ manufacturer = async (): Promise<string | undefined> => {
205
+ return await this.read("device", "manufacturer", 250)
206
+ }
207
+
208
+ /**
209
+ * Retrieves model number from the device.
210
+ * @returns {Promise<string | undefined>} A Promise that resolves with the model number.
211
+ */
212
+ model = async (): Promise<string | undefined> => {
213
+ return await this.read("device", "model", 250)
214
+ }
215
+
216
+ /**
217
+ * Sets whether the device should reset to zero on power-up.
218
+ * @param {boolean} enabled - Whether power-on reset should be enabled.
219
+ * @returns {Promise<void>} A promise that resolves when the command is acknowledged.
220
+ */
221
+ powerOnReset = async (enabled: boolean): Promise<void> => {
222
+ await this.expectAck(this.commands.POWER_ON_RESET as number, [0x00, 0x00, enabled ? 0x01 : 0x00])
223
+ }
224
+
225
+ /**
226
+ * Enables or disables the device peak mode.
227
+ * @param {boolean} [enabled=true] - Whether peak mode should be enabled.
228
+ * @returns {Promise<void>} A promise that resolves when the command is acknowledged.
229
+ */
230
+ peakMode = async (enabled = true): Promise<void> => {
231
+ await this.expectAck(this.commands.PEAK_MODE as number, [0x00, 0x00, enabled ? 0x01 : 0x00])
232
+ }
233
+
234
+ /**
235
+ * Configures the device UART baud rate.
236
+ * @param {CTS500BaudRate} baudRate - Desired baud rate.
237
+ * @returns {Promise<void>} A promise that resolves when the command is acknowledged.
238
+ */
239
+ setBaudRate = async (baudRate: CTS500BaudRate): Promise<void> => {
240
+ await this.applyConfigCommand(this.commands.SET_BAUD_RATE as number, [
241
+ 0x00,
242
+ 0x00,
243
+ CTS500_BAUD_RATE_PARAMS[baudRate],
244
+ ])
245
+ }
246
+
247
+ /**
248
+ * Configures the device A/D sampling rate.
249
+ * @param {CTS500SamplingRate} samplingRate - Desired A/D sampling rate in Hz.
250
+ * @returns {Promise<void>} A promise that resolves when the command is acknowledged.
251
+ */
252
+ setSamplingRate = async (samplingRate: CTS500SamplingRate): Promise<void> => {
253
+ await this.applyConfigCommand(this.commands.SET_SAMPLING_RATE as number, [
254
+ 0x00,
255
+ 0x00,
256
+ CTS500_SAMPLING_RATE_PARAMS[samplingRate],
257
+ ])
258
+ }
259
+
260
+ /**
261
+ * Retrieves serial number from the device.
262
+ * @returns {Promise<string | undefined>} A Promise that resolves with the serial number.
263
+ */
264
+ serial = async (): Promise<string | undefined> => {
265
+ const hasSerial = this.services
266
+ .find((service) => service.id === "device")
267
+ ?.characteristics.some((characteristic) => characteristic.id === "serial")
268
+
269
+ // MY-BT102 variants can omit the serial characteristic entirely, so guard the read instead of letting it throw.
270
+ if (!hasSerial) {
271
+ return undefined
272
+ }
273
+
274
+ return await this.read("device", "serial", 250)
275
+ }
276
+
277
+ /**
278
+ * Retrieves software version from the device.
279
+ * @returns {Promise<string | undefined>} A Promise that resolves with the software version.
280
+ */
281
+ software = async (): Promise<string | undefined> => {
282
+ return await this.read("device", "software", 250)
283
+ }
284
+
285
+ /**
286
+ * Starts automatic weight uploads.
287
+ * @param {number} [duration=0] - Optional delay before the promise resolves.
288
+ * @returns {Promise<void>} A promise that resolves once upload mode has been enabled.
289
+ */
290
+ stream = async (duration = 0): Promise<void> => {
291
+ this.resetPacketTracking()
292
+ this.resetSessionData()
293
+ this.isStreaming = true
294
+ const command = this.commands.START_WEIGHT_MEAS as Uint8Array
295
+ try {
296
+ await this.queryFrame(
297
+ command,
298
+ (frame) =>
299
+ // The device can start auto-uploading before it echoes the start command, so the first weight frame also confirms success.
300
+ this.isAckFrame(frame, command[1], [command[2], command[3], command[4]]) || this.isWeightFrame(frame),
301
+ )
302
+ } catch (error) {
303
+ this.isStreaming = false
304
+ throw error
305
+ }
306
+
307
+ if (duration > 0) {
308
+ await new Promise((resolve) => setTimeout(resolve, duration))
309
+ await this.stop()
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Stops automatic weight uploads.
315
+ * @returns {Promise<void>} A promise that resolves once upload mode has been disabled.
316
+ */
317
+ stop = async (): Promise<void> => {
318
+ this.isStreaming = false
319
+ const command = this.commands.STOP_WEIGHT_MEAS as Uint8Array
320
+ await this.queryFrame(command, (frame) => this.isAckFrame(frame, command[1], [command[2], command[3], command[4]]))
321
+ }
322
+
323
+ /**
324
+ * Reads the current temperature from the device.
325
+ * @returns {Promise<string | undefined>} A Promise that resolves with the temperature in Celsius.
326
+ */
327
+ temperature = async (): Promise<string | undefined> => {
328
+ const command = this.commands.GET_TEMPERATURE as Uint8Array
329
+ const frame = await this.queryFrame(command, (response) => this.isCommandResponse(response, command[1]))
330
+ if (!frame) {
331
+ return undefined
332
+ }
333
+
334
+ const rawTemperature = frame[5]
335
+ // Negative temperatures are sent as 0x80 + abs(value) instead of two's complement.
336
+ const temperature = rawTemperature >= 0x80 ? -(rawTemperature - 0x80) : rawTemperature
337
+ return temperature.toString()
338
+ }
339
+
340
+ /**
341
+ * Uses the device's hardware tare command when connected and falls back to software tare otherwise.
342
+ * @param {number} [duration=5000] - Software tare duration when the device is not connected.
343
+ * @returns {boolean} `true` when the tare operation started successfully.
344
+ */
345
+ override tare = (duration = 5000): boolean => {
346
+ if (!this.isConnected()) {
347
+ return super.tare(duration)
348
+ }
349
+
350
+ this.updateTimestamp()
351
+ this.clearTareOffset()
352
+ const command = this.commands.TARE_SCALE as Uint8Array
353
+ void this.queryFrame(command, (frame) =>
354
+ this.isAckFrame(frame, command[1], [command[2], command[3], command[4]]),
355
+ ).catch((error: Error) => {
356
+ console.error(error)
357
+ })
358
+ return true
359
+ }
360
+
361
+ /**
362
+ * Reads the current weight from the device in kilograms.
363
+ * @returns {Promise<number | undefined>} A Promise that resolves with the current weight.
364
+ */
365
+ weight = async (): Promise<number | undefined> => {
366
+ const frame = await this.queryFrame(this.commands.GET_WEIGHT, (response) => this.isWeightFrame(response))
367
+ if (!frame) {
368
+ return undefined
369
+ }
370
+
371
+ return (frame[2] * 0x1000000 + frame[3] * 0x10000 + frame[4] * 0x100 + frame[5]) / 100
372
+ }
373
+
374
+ /**
375
+ * Updates the device hardware zero point.
376
+ * @returns {Promise<void>} A promise that resolves when the command is acknowledged.
377
+ */
378
+ zero = async (): Promise<void> => {
379
+ await this.expectAck(this.commands.ZERO_SCALE as number)
380
+ }
381
+
382
+ /**
383
+ * Parses UART frames received over the MY-BT102 notify characteristic.
384
+ * Supports fragmented BLE notifications by buffering until a complete CTS500 frame is available.
385
+ *
386
+ * @param {DataView} value - The notification payload from the device.
387
+ */
388
+ override handleNotifications = (value: DataView): void => {
389
+ this.updateTimestamp()
390
+
391
+ const bytes = new Uint8Array(value.byteLength)
392
+ for (let index = 0; index < value.byteLength; index++) {
393
+ bytes[index] = value.getUint8(index)
394
+ }
395
+
396
+ if (bytes.length === 0) {
397
+ return
398
+ }
399
+
400
+ // BLE notifications can split UART frames arbitrarily, so keep buffering until a full frame validates.
401
+ const combined = new Uint8Array(this.bufferedFrames.length + bytes.length)
402
+ combined.set(this.bufferedFrames)
403
+ combined.set(bytes, this.bufferedFrames.length)
404
+ this.bufferedFrames = combined
405
+
406
+ while (this.bufferedFrames.length >= CTS500_ACK_FRAME_LENGTH) {
407
+ const headerIndex = this.bufferedFrames.indexOf(CTS500_HEADER)
408
+
409
+ if (headerIndex === -1) {
410
+ this.bufferedFrames = new Uint8Array(0)
411
+ return
412
+ }
413
+
414
+ if (headerIndex > 0) {
415
+ this.bufferedFrames = this.bufferedFrames.slice(headerIndex)
416
+ }
417
+
418
+ const frame = this.extractNextFrame()
419
+ if (!frame) {
420
+ return
421
+ }
422
+
423
+ this.bufferedFrames = this.bufferedFrames.slice(frame.length)
424
+ this.handleFrame(frame)
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Waits for a specific frame pattern after sending a CTS500 command.
430
+ */
431
+ private queryFrame = async (
432
+ message: string | Uint8Array | undefined,
433
+ match: (frame: Uint8Array) => boolean,
434
+ ): Promise<Uint8Array | undefined> => {
435
+ return await this.enqueueRequest(async () => {
436
+ const waitForFrame = this.waitForFrame(match)
437
+
438
+ try {
439
+ await this.write("cts500", "tx", message, 0)
440
+ return await waitForFrame
441
+ } catch (error) {
442
+ this.clearPendingFrame(error instanceof Error ? error : new Error(String(error)))
443
+ throw error
444
+ }
445
+ })
446
+ }
447
+
448
+ /**
449
+ * Sends a command that should be acknowledged with a 6-byte echo frame.
450
+ */
451
+ private expectAck = async (
452
+ opcode: number,
453
+ payload: readonly [number, number, number] = [0x00, 0x00, 0x00],
454
+ ): Promise<void> => {
455
+ await this.queryFrame(buildCommand(opcode, payload), (frame) => this.isAckFrame(frame, opcode, payload))
456
+ }
457
+
458
+ /**
459
+ * Sends a configuration command that may reply with either a 6-byte echo, a typed response, or no reply after applying.
460
+ */
461
+ private applyConfigCommand = async (
462
+ opcode: number,
463
+ payload: readonly [number, number, number] = [0x00, 0x00, 0x00],
464
+ ): Promise<void> => {
465
+ try {
466
+ await this.queryFrame(
467
+ buildCommand(opcode, payload),
468
+ (frame) => this.isAckFrame(frame, opcode, payload) || this.isCommandResponse(frame, opcode),
469
+ )
470
+ } catch (error) {
471
+ // Some CTS firmwares apply UART/A-D rate changes immediately and do not echo a matching confirmation frame back over BLE.
472
+ if (error instanceof Error && error.message === "Timed out waiting for CTS500 response") {
473
+ return
474
+ }
475
+
476
+ throw error
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Resolves the currently pending frame promise if the incoming frame matches.
482
+ * @returns {boolean} Whether a pending request consumed the frame.
483
+ */
484
+ private consumePendingFrame = (frame: Uint8Array): boolean => {
485
+ if (!this.pendingFrame || !this.pendingFrame.match(frame)) {
486
+ return false
487
+ }
488
+
489
+ const { resolve, timeout } = this.pendingFrame
490
+ clearTimeout(timeout)
491
+ this.pendingFrame = undefined
492
+ resolve(frame)
493
+ return true
494
+ }
495
+
496
+ /**
497
+ * Clears the currently pending frame wait, if any.
498
+ */
499
+ private clearPendingFrame = (error?: Error): void => {
500
+ if (!this.pendingFrame) {
501
+ return
502
+ }
503
+
504
+ const { timeout, reject } = this.pendingFrame
505
+ clearTimeout(timeout)
506
+ this.pendingFrame = undefined
507
+
508
+ if (error) {
509
+ reject(error)
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Extracts the next valid CTS500 frame from the local notification buffer.
515
+ */
516
+ private extractNextFrame = (): Uint8Array | undefined => {
517
+ const secondByte = this.bufferedFrames[1]
518
+
519
+ // 6-byte command echoes and 7-byte data frames share the same header, so prefer command echoes when byte 1 is a known opcode.
520
+ if (this.commandOpcodes.has(secondByte) && this.bufferedFrames.length >= CTS500_ACK_FRAME_LENGTH) {
521
+ const commandCandidate = this.bufferedFrames.slice(0, CTS500_ACK_FRAME_LENGTH)
522
+ if (this.isValidFrame(commandCandidate)) {
523
+ return commandCandidate
524
+ }
525
+ }
526
+
527
+ if (this.bufferedFrames.length >= CTS500_DATA_FRAME_LENGTH) {
528
+ const dataCandidate = this.bufferedFrames.slice(0, CTS500_DATA_FRAME_LENGTH)
529
+ if (this.isValidFrame(dataCandidate)) {
530
+ return dataCandidate
531
+ }
532
+ }
533
+
534
+ if (this.bufferedFrames.length >= CTS500_DATA_FRAME_LENGTH) {
535
+ this.bufferedFrames = this.bufferedFrames.slice(1)
536
+ }
537
+
538
+ return undefined
539
+ }
540
+
541
+ /**
542
+ * Routes a validated CTS500 frame to pending requests, callbacks, and stream processing.
543
+ */
544
+ private handleFrame = (frame: Uint8Array): void => {
545
+ const matchedPendingRequest = this.consumePendingFrame(frame)
546
+
547
+ if (this.isWeightFrame(frame)) {
548
+ // Weight uploads carry a big-endian centi-unit value across bytes 2..5.
549
+ const weight = (frame[2] * 0x1000000 + frame[3] * 0x10000 + frame[4] * 0x100 + frame[5]) / 100
550
+ this.recordWeightMeasurement(weight)
551
+ this.writeCallback(weight.toFixed(2))
552
+ return
553
+ }
554
+
555
+ if (this.isCommandResponse(frame, (this.commands.GET_BATTERY_VOLTAGE as Uint8Array)[1])) {
556
+ const voltage = ((frame[4] << 8) | frame[5]) / 100
557
+ this.writeCallback(voltage.toFixed(2))
558
+ return
559
+ }
560
+
561
+ if (this.isCommandResponse(frame, (this.commands.GET_TEMPERATURE as Uint8Array)[1])) {
562
+ const rawTemperature = frame[5]
563
+ // Negative temperatures are sent as 0x80 + abs(value) instead of two's complement.
564
+ const temperature = rawTemperature >= 0x80 ? -(rawTemperature - 0x80) : rawTemperature
565
+ this.writeCallback(temperature.toString())
566
+ return
567
+ }
568
+
569
+ if (frame.length === CTS500_ACK_FRAME_LENGTH && !matchedPendingRequest) {
570
+ this.writeCallback("OK")
571
+ return
572
+ }
573
+
574
+ if (frame.length === CTS500_DATA_FRAME_LENGTH && !matchedPendingRequest) {
575
+ this.writeCallback(
576
+ Array.from(frame)
577
+ .map((byte) => byte.toString(16).padStart(2, "0").toUpperCase())
578
+ .join(" "),
579
+ )
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Returns whether a frame is a 6-byte command acknowledgment echo for the given opcode.
585
+ */
586
+ private isAckFrame = (frame: Uint8Array, opcode: number, payload: readonly [number, number, number]): boolean => {
587
+ return (
588
+ frame.length === CTS500_ACK_FRAME_LENGTH &&
589
+ frame[0] === CTS500_HEADER &&
590
+ frame[1] === opcode &&
591
+ frame[2] === payload[0] &&
592
+ frame[3] === payload[1] &&
593
+ frame[4] === payload[2] &&
594
+ this.isValidFrame(frame)
595
+ )
596
+ }
597
+
598
+ /**
599
+ * Returns whether a frame is a typed command response (`05 80 <opcode> ... checksum`).
600
+ */
601
+ private isCommandResponse = (frame: Uint8Array, opcode: number): boolean => {
602
+ return (
603
+ frame.length === CTS500_DATA_FRAME_LENGTH &&
604
+ frame[0] === CTS500_HEADER &&
605
+ frame[1] === CTS500_RESPONSE_FLAG &&
606
+ frame[2] === opcode &&
607
+ this.isValidFrame(frame)
608
+ )
609
+ }
610
+
611
+ /**
612
+ * Returns whether a frame contains a weight measurement payload.
613
+ */
614
+ private isWeightFrame = (frame: Uint8Array): boolean => {
615
+ return (
616
+ frame.length === CTS500_DATA_FRAME_LENGTH &&
617
+ frame[0] === CTS500_HEADER &&
618
+ frame[1] !== CTS500_RESPONSE_FLAG &&
619
+ !this.commandOpcodes.has(frame[1]) &&
620
+ this.isValidFrame(frame)
621
+ )
622
+ }
623
+
624
+ /**
625
+ * Updates rolling statistics and emits a force measurement from a CTS500 weight frame.
626
+ */
627
+ private recordWeightMeasurement = (receivedData: number): void => {
628
+ const receivedTime = Date.now()
629
+
630
+ this.currentSamplesPerPacket = 1
631
+ this.recordPacketReceived()
632
+
633
+ const numericData = receivedData - this.applyTare(receivedData)
634
+ const currentMassTotal = Math.max(-1000, numericData)
635
+
636
+ this.peak = Math.max(this.peak, numericData)
637
+ this.min = Math.min(this.min, Math.max(-1000, numericData))
638
+ this.sum += currentMassTotal
639
+ this.dataPointCount++
640
+ this.mean = this.sum / this.dataPointCount
641
+
642
+ this.downloadPackets.push(
643
+ this.buildDownloadPacket(currentMassTotal, [Math.round(receivedData * 100)], {
644
+ timestamp: receivedTime,
645
+ sampleIndex: this.dataPointCount,
646
+ }),
647
+ )
648
+
649
+ if (this.isStreaming) {
650
+ this.activityCheck(numericData)
651
+ }
652
+
653
+ this.notifyCallback(this.buildForceMeasurement(currentMassTotal))
654
+ }
655
+
656
+ /**
657
+ * Validates a CTS500 frame checksum.
658
+ */
659
+ private isValidFrame = (frame: Uint8Array): boolean => {
660
+ if (frame.length < CTS500_ACK_FRAME_LENGTH || frame[0] !== CTS500_HEADER) {
661
+ return false
662
+ }
663
+
664
+ return calculateChecksum(frame.subarray(0, frame.length - 1)) === frame[frame.length - 1]
665
+ }
666
+
667
+ /**
668
+ * Registers a pending frame matcher with a timeout.
669
+ */
670
+ private waitForFrame = (
671
+ match: (frame: Uint8Array) => boolean,
672
+ timeoutMs = CTS500_RESPONSE_TIMEOUT_MS,
673
+ ): Promise<Uint8Array> => {
674
+ // CTS uses one transparent UART channel for both commands and telemetry, so only one response wait can be active at a time.
675
+ if (this.pendingFrame) {
676
+ throw new Error("CTS500 already has a pending response request")
677
+ }
678
+
679
+ return new Promise<Uint8Array>((resolve, reject) => {
680
+ const timeout = setTimeout(() => {
681
+ if (!this.pendingFrame) {
682
+ return
683
+ }
684
+
685
+ this.pendingFrame = undefined
686
+ reject(new Error("Timed out waiting for CTS500 response"))
687
+ }, timeoutMs)
688
+
689
+ this.pendingFrame = {
690
+ match,
691
+ reject,
692
+ resolve,
693
+ timeout,
694
+ }
695
+ })
696
+ }
697
+
698
+ /**
699
+ * Serializes CTS500 command/response operations so query-style methods can be called in parallel by consumers.
700
+ */
701
+ private enqueueRequest = async <T>(request: () => Promise<T>): Promise<T> => {
702
+ const run = this.requestQueue.then(request, request)
703
+ this.requestQueue = run.then(
704
+ () => undefined,
705
+ () => undefined,
706
+ )
707
+ return await run
708
+ }
709
+ }