@hangtime/grip-connect 0.3.3 → 0.3.5

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.
package/README.md CHANGED
@@ -3,11 +3,12 @@
3
3
  **Force-Sensing Climbing Training**
4
4
 
5
5
  The objective of this project is to create a Web Bluetooth API client that can establish connections with various
6
- Force-Sensing Hangboards / Plates / LED system boards used by climbers for strength measurement. Examples of such
7
- hangboards include the [Griptonite Motherboard](https://griptonite.io/shop/motherboard/),
8
- [Climbro](https://climbro.com/), [mySmartBoard](https://www.smartboard-climbing.com/),
9
- [Entralpi](https://entralpi.com/), [Tindeq Progressor](https://tindeq.com/) or
10
- [MAT Muscle Meter](https://www.matassessment.com/musclemeter)
6
+ Force-Sensing Hangboards / Dynamometers / Plates / LED system boards used by climbers. Examples of such tools include
7
+ the [Griptonite Motherboard](https://griptonite.io/shop/motherboard/), [Climbro](https://climbro.com/),
8
+ [mySmartBoard](https://www.smartboard-climbing.com/), [Entralpi](https://entralpi.com/),
9
+ [Tindeq Progressor](https://tindeq.com/) or
10
+ [Weiheng WH-C06](https://weihengmanufacturer.com/products/wh-c06-bluetooth-300kg-hanging-scale/) also sold as
11
+ [MAT Muscle Meter](https://www.matassessment.com/musclemeter).
11
12
 
12
13
  And LED system boards from [Aurora Climbing](https://auroraclimbing.com/) like the
13
14
  [Kilter Board](https://settercloset.com/pages/the-kilter-board),
@@ -19,10 +20,10 @@ And LED system boards from [Aurora Climbing](https://auroraclimbing.com/) like t
19
20
  Learn more: [Docs](https://stevie-ray.github.io/hangtime-grip-connect/) -
20
21
  [Browser Support](https://caniuse.com/web-bluetooth)
21
22
 
22
- > [!CAUTION] This project is provided "as-is" without any express or implied warranties. By using this software, you
23
- > assume all risks associated with its use, including but not limited to hardware damage, data loss, or any other issues
24
- > that may arise. The developers and contributors are not responsible for any harm or loss incurred. Use this software
25
- > at your own discretion and responsibility.
23
+ > This project is provided "as-is" without any express or implied warranties. By using this software, you assume all
24
+ > risks associated with its use, including but not limited to hardware damage, data loss, or any other issues that may
25
+ > arise. The developers and contributors are not responsible for any harm or loss incurred. Use this software at your
26
+ > own discretion and responsibility.
26
27
 
27
28
  ## Try it out
28
29
 
@@ -91,10 +92,11 @@ available services with us.
91
92
  - ✅ Griptonite Motherboard
92
93
  - ✅ Tindeq Progressor
93
94
  - ⏳ Entralpi (not verified)
94
- - ⏳ Kilterboard (write only, see example)
95
+ - ⏳ Kilterboard (see example)
96
+ - ⏳ Weiheng WH-C06 / MAT Muscle Meter
97
+ - Enable: `chrome://flags#enable-experimental-web-platform-features`
95
98
  - ➡️ Climbro
96
99
  - ➡️ mySmartBoard
97
- - ➡️ MAT Muscle Meter
98
100
 
99
101
  ### Features
100
102
 
@@ -132,8 +134,10 @@ A special thank you to:
132
134
  [PyTindeq](https://github.com/StuartLittlefair/PyTindeq) implementation.
133
135
  - [@Phil9l](https://github.com/phil9l) for his research and providing a [blog](https://bazun.me/blog/kiterboard/) on how
134
136
  to connect with the Kilter Board.
135
- - [@1-max-1](https://github.com/1-max-1/fake_kilter_board) for the docs on his Kilter Board
136
- [https://github.com/1-max-1/fake_kilter_board](simulator).
137
+ - [@1-max-1](https://github.com/1-max-1) for the docs on his Kilter Board
138
+ [simulator](https://github.com/1-max-1/fake_kilter_board) that I coverted to
139
+ [hangtime-arduino-kilterboard](https://github.com/Stevie-Ray/hangtime-arduino-kilterboard).
140
+ - [sebws](https://github.com/sebw) for a [code sample](https://github.com/sebws/Crane) of the Weiheng WH-C06 App.
137
141
 
138
142
  ## Disclaimer
139
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hangtime/grip-connect",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "A client that can establish connections with various Force-Sensing Hangboards/Plates used by climbers for strength measurement. Examples of such hangboards include the Griptonite Motherboard, Climbro, SmartBoard, Entralpi or Tindeq Progressor",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ export { EntralpiCommands } from "./entralpi"
4
4
 
5
5
  export { MotherboardCommands } from "./motherboard"
6
6
 
7
- export { MuscleMeterCommands } from "./musclemeter"
7
+ export { WHC06Commands } from "./wh-c06"
8
8
 
9
9
  export { ProgressorCommands } from "./progressor"
10
10
 
@@ -0,0 +1,23 @@
1
+ /**
2
+ * For API level 2 and API level 3.
3
+ * The first byte in the data is dependent on where the packet is in the message as a whole.
4
+ * More details: https://github.com/1-max-1/fake_kilter_board
5
+ */
6
+ export enum KilterBoardPacket {
7
+ /** If this packet is in the middle, the byte gets set to 77 (M). */
8
+ V2_MIDDLE = 77,
9
+ /** If this packet is the first packet in the message, then this byte gets set to 78 (N). */
10
+ V2_FIRST,
11
+ /** If this is the last packet in the message, this byte gets set to 79 (0). */
12
+ V2_LAST,
13
+ /** If this packet is the only packet in the message, the byte gets set to 80 (P). Note that this takes priority over the other conditions. */
14
+ V2_ONLY,
15
+ /** If this packet is in the middle, the byte gets set to 81 (Q). */
16
+ V3_MIDDLE,
17
+ /** If this packet is the first packet in the message, then this byte gets set to 82 (R). */
18
+ V3_FIRST,
19
+ /** If this is the last packet in the message, this byte gets set to 83 (S). */
20
+ V3_LAST,
21
+ /** If this packet is the only packet in the message, the byte gets set to 84 (T). Note that this takes priority over the other conditions. */
22
+ V3_ONLY,
23
+ }
@@ -3,4 +3,4 @@ import type { Commands } from "../types/commands"
3
3
  * Warning:
4
4
  * Using other commands can seriously harm your device
5
5
  */
6
- export const MuscleMeterCommands: Commands = {}
6
+ export const WHC06Commands: Commands = {}
package/src/connect.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { Device } from "./types/devices"
2
- import { handleEntralpiData, handleMotherboardData, handleProgressorData } from "./data"
2
+ import { handleEntralpiData } from "./data/entralpi"
3
+ import { handleMotherboardData } from "./data/motherboard"
4
+ import { handleProgressorData } from "./data/progressor"
5
+ import { handleWHC06Data } from "./data/wh-c06"
3
6
 
4
7
  let server: BluetoothRemoteGATTServer
5
8
  const receiveBuffer: number[] = []
@@ -26,7 +29,7 @@ const handleNotifications = (event: Event, board: Device): void => {
26
29
  if (value) {
27
30
  // If the device is connected and it is a Motherboard device
28
31
  if (board.filters.some((filter) => filter.name === "Motherboard")) {
29
- for (let i: number = 0; i < value.byteLength; i++) {
32
+ for (let i = 0; i < value.byteLength; i++) {
30
33
  receiveBuffer.push(value.getUint8(i))
31
34
  }
32
35
 
@@ -64,7 +67,7 @@ const handleNotifications = (event: Event, board: Device): void => {
64
67
  const onConnected = async (board: Device, onSuccess: () => void): Promise<void> => {
65
68
  try {
66
69
  // Connect to GATT server and set up characteristics
67
- const services: BluetoothRemoteGATTService[] = await server?.getPrimaryServices()
70
+ const services: BluetoothRemoteGATTService[] = await server.getPrimaryServices()
68
71
 
69
72
  if (!services || services.length === 0) {
70
73
  console.error("No services found")
@@ -91,9 +94,9 @@ const onConnected = async (board: Device, onSuccess: () => void): Promise<void>
91
94
  // notify
92
95
  if (element.id === "rx") {
93
96
  matchingCharacteristic.startNotifications()
94
- matchingCharacteristic.addEventListener("characteristicvaluechanged", (event: Event) =>
95
- handleNotifications(event, board),
96
- )
97
+ matchingCharacteristic.addEventListener("characteristicvaluechanged", (event: Event) => {
98
+ handleNotifications(event, board)
99
+ })
97
100
  }
98
101
  }
99
102
  } else {
@@ -127,9 +130,15 @@ export const connect = async (board: Device, onSuccess: () => void): Promise<voi
127
130
  // Request device and set up connection
128
131
  const deviceServices = getAllServiceUUIDs(board)
129
132
 
133
+ // Only data matching the optionalManufacturerData parameter to requestDevice is included in the advertisement event: https://github.com/WebBluetoothCG/web-bluetooth/issues/598
134
+ const optionalManufacturerData = board.filters.flatMap(
135
+ (filter) => filter.manufacturerData?.map((data) => data.companyIdentifier) || [],
136
+ )
137
+
130
138
  const device = await navigator.bluetooth.requestDevice({
131
139
  filters: board.filters,
132
140
  optionalServices: deviceServices,
141
+ optionalManufacturerData,
133
142
  })
134
143
 
135
144
  board.device = device
@@ -139,9 +148,28 @@ export const connect = async (board: Device, onSuccess: () => void): Promise<voi
139
148
  return
140
149
  }
141
150
 
142
- server = await board.device?.gatt?.connect()
151
+ board.device.addEventListener("gattserverdisconnected", (event) => {
152
+ onDisconnected(event, board)
153
+ })
154
+
155
+ // WH-C06
156
+ const MANUFACTURER_ID = 256 // 0x0100
157
+
158
+ board.device.addEventListener("advertisementreceived", (event) => {
159
+ const manufacturerData = event.manufacturerData.get(MANUFACTURER_ID)
160
+ if (manufacturerData) {
161
+ // Device has no services / characteristics
162
+ onSuccess()
163
+ // Handle recieved data
164
+ handleWHC06Data(manufacturerData)
165
+ }
166
+ })
167
+
168
+ if (optionalManufacturerData.length) {
169
+ await board.device.watchAdvertisements()
170
+ }
143
171
 
144
- board.device.addEventListener("gattserverdisconnected", (event) => onDisconnected(event, board))
172
+ server = await board.device.gatt.connect()
145
173
 
146
174
  if (server.connected) {
147
175
  await onConnected(board, onSuccess)
@@ -0,0 +1,37 @@
1
+ import { notifyCallback } from "./../notify"
2
+ import { applyTare } from "./../tare"
3
+
4
+ // Constants
5
+ let MASS_MAX = "0"
6
+ let MASS_AVERAGE = "0"
7
+ let MASS_TOTAL_SUM = 0
8
+ let DATAPOINT_COUNT = 0
9
+
10
+ /**
11
+ * Handles data received from the Entralpi device.
12
+ * @param {string} receivedData - The received data string.
13
+ */
14
+ export const handleEntralpiData = (receivedData: string): void => {
15
+ let numericData = Number(receivedData)
16
+
17
+ // Tare correction
18
+ numericData -= applyTare(numericData)
19
+
20
+ // Update MASS_MAX
21
+ MASS_MAX = Math.max(Number(MASS_MAX), numericData).toFixed(1)
22
+
23
+ // Update running sum and count
24
+ const currentMassTotal = Math.max(-1000, numericData)
25
+ MASS_TOTAL_SUM += currentMassTotal
26
+ DATAPOINT_COUNT++
27
+
28
+ // Calculate the average dynamically
29
+ MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
30
+
31
+ // Notify with weight data
32
+ notifyCallback({
33
+ massMax: MASS_MAX,
34
+ massAverage: MASS_AVERAGE,
35
+ massTotal: Math.max(-1000, numericData).toFixed(1),
36
+ })
37
+ }
@@ -1,19 +1,17 @@
1
- import { notifyCallback } from "./notify"
2
- import { applyTare } from "./tare"
3
- import { ProgressorCommands, ProgressorResponses } from "./commands/progressor"
4
- import { MotherboardCommands } from "./commands"
5
- import { lastWrite } from "./write"
6
- import struct from "./struct"
7
- import { DownloadPackets } from "./download"
8
- import type { DownloadPacket } from "./types/download"
1
+ import { notifyCallback } from "./../notify"
2
+ import { applyTare } from "./../tare"
3
+ import { MotherboardCommands } from "./../commands"
4
+ import { lastWrite } from "./../write"
5
+ import { DownloadPackets } from "./../download"
6
+ import type { DownloadPacket } from "./../types/download"
9
7
 
10
8
  // Constants
11
- const PACKET_LENGTH: number = 32
12
- const NUM_SAMPLES: number = 3
13
- let MASS_MAX: string = "0"
14
- let MASS_AVERAGE: string = "0"
15
- let MASS_TOTAL_SUM: number = 0
16
- let DATAPOINT_COUNT: number = 0
9
+ const PACKET_LENGTH = 32
10
+ const NUM_SAMPLES = 3
11
+ let MASS_MAX = "0"
12
+ let MASS_AVERAGE = "0"
13
+ let MASS_TOTAL_SUM = 0
14
+ let DATAPOINT_COUNT = 0
17
15
  export const CALIBRATION = [[], [], [], []]
18
16
 
19
17
  /**
@@ -26,9 +24,9 @@ const applyCalibration = (sample: number, calibration: number[][]): number => {
26
24
  // Extract the calibrated value for the zero point
27
25
  const zeroCalibration: number = calibration[0][2]
28
26
  // Initialize sign as positive
29
- let sign: number = 1
27
+ let sign = 1
30
28
  // Initialize the final calibrated value
31
- let final: number = 0
29
+ let final = 0
32
30
 
33
31
  // If the sample value is less than the zero calibration point
34
32
  if (sample < zeroCalibration) {
@@ -154,92 +152,3 @@ export const handleMotherboardData = (receivedData: string): void => {
154
152
  console.log(receivedData)
155
153
  }
156
154
  }
157
-
158
- /**
159
- * Handles data received from the Progressor device.
160
- * @param {DataView} data - The received data.
161
- */
162
- export const handleProgressorData = (data: DataView): void => {
163
- const receivedTime: number = Date.now()
164
- const [kind] = struct("<bb").unpack(data.buffer.slice(0, 2))
165
- if (kind === ProgressorResponses.WEIGHT_MEASURE) {
166
- const iterable: IterableIterator<unknown[]> = struct("<fi").iter_unpack(data.buffer.slice(2))
167
- console.log(iterable)
168
- // eslint-disable-next-line prefer-const
169
- for (let [weight, seconds] of iterable) {
170
- if (typeof weight === "number" && !isNaN(weight) && typeof seconds === "number" && !isNaN(seconds)) {
171
- // Add data to downloadable Array: sample and mass are the same
172
- DownloadPackets.push({
173
- received: receivedTime,
174
- sampleNum: seconds,
175
- battRaw: 0,
176
- samples: [weight],
177
- masses: [weight],
178
- })
179
- // Tare correction
180
- weight -= applyTare(weight)
181
- // Check for max weight
182
- MASS_MAX = Math.max(Number(MASS_MAX), Number(weight)).toFixed(1)
183
- // Update running sum and count
184
- const currentMassTotal = Math.max(-1000, Number(weight))
185
- MASS_TOTAL_SUM += currentMassTotal
186
- DATAPOINT_COUNT++
187
-
188
- // Calculate the average dynamically
189
- MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
190
-
191
- notifyCallback({
192
- massMax: MASS_MAX,
193
- massAverage: MASS_AVERAGE,
194
- massTotal: Math.max(-1000, weight).toFixed(1),
195
- })
196
- }
197
- }
198
- } else if (kind === ProgressorResponses.COMMAND_RESPONSE) {
199
- if (!lastWrite) return
200
-
201
- let value: string = ""
202
-
203
- if (lastWrite === ProgressorCommands.GET_BATT_VLTG) {
204
- const vdd = new DataView(data.buffer, 2).getUint32(0, true)
205
- value = `ℹ️ Battery level: ${vdd} mV`
206
- } else if (lastWrite === ProgressorCommands.GET_FW_VERSION) {
207
- value = new TextDecoder().decode(data.buffer.slice(2))
208
- } else if (lastWrite === ProgressorCommands.GET_ERR_INFO) {
209
- value = new TextDecoder().decode(data.buffer.slice(2))
210
- }
211
- console.log(value)
212
- } else if (kind === ProgressorResponses.LOW_BATTERY_WARNING) {
213
- console.warn("⚠️ Low power detected. Please consider connecting to a power source.")
214
- } else {
215
- console.error(`❌ Error: Unknown message kind detected: ${kind}`)
216
- }
217
- }
218
- /**
219
- * Handles data received from the Entralpi device.
220
- * @param {string} receivedData - The received data string.
221
- */
222
- export const handleEntralpiData = (receivedData: string): void => {
223
- let numericData = Number(receivedData)
224
-
225
- // Tare correction
226
- numericData -= applyTare(numericData)
227
-
228
- // Update MASS_MAX
229
- MASS_MAX = Math.max(Number(MASS_MAX), numericData).toFixed(1)
230
-
231
- // Update running sum and count
232
- const currentMassTotal = Math.max(-1000, numericData)
233
- MASS_TOTAL_SUM += currentMassTotal
234
- DATAPOINT_COUNT++
235
-
236
- // Calculate the average dynamically
237
- MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
238
-
239
- // Notify with weight data
240
- notifyCallback({
241
- massMax: MASS_MAX,
242
- massAverage: MASS_AVERAGE,
243
- massTotal: Math.max(-1000, numericData).toFixed(1),
244
- })
245
- }
@@ -0,0 +1,72 @@
1
+ import { notifyCallback } from "./../notify"
2
+ import { applyTare } from "./../tare"
3
+ import { ProgressorCommands, ProgressorResponses } from "./../commands/progressor"
4
+ import { lastWrite } from "./../write"
5
+ import struct from "./../struct"
6
+ import { DownloadPackets } from "./../download"
7
+
8
+ // Constants
9
+ let MASS_MAX = "0"
10
+ let MASS_AVERAGE = "0"
11
+ let MASS_TOTAL_SUM = 0
12
+ let DATAPOINT_COUNT = 0
13
+
14
+ /**
15
+ * Handles data received from the Progressor device.
16
+ * @param {DataView} data - The received data.
17
+ */
18
+ export const handleProgressorData = (data: DataView): void => {
19
+ const receivedTime: number = Date.now()
20
+ const [kind] = struct("<bb").unpack(data.buffer.slice(0, 2))
21
+ if (kind === ProgressorResponses.WEIGHT_MEASURE) {
22
+ const iterable: IterableIterator<unknown[]> = struct("<fi").iter_unpack(data.buffer.slice(2))
23
+ // eslint-disable-next-line prefer-const
24
+ for (let [weight, seconds] of iterable) {
25
+ if (typeof weight === "number" && !isNaN(weight) && typeof seconds === "number" && !isNaN(seconds)) {
26
+ // Add data to downloadable Array: sample and mass are the same
27
+ DownloadPackets.push({
28
+ received: receivedTime,
29
+ sampleNum: seconds,
30
+ battRaw: 0,
31
+ samples: [weight],
32
+ masses: [weight],
33
+ })
34
+ // Tare correction
35
+ weight -= applyTare(weight)
36
+ // Check for max weight
37
+ MASS_MAX = Math.max(Number(MASS_MAX), Number(weight)).toFixed(1)
38
+ // Update running sum and count
39
+ const currentMassTotal = Math.max(-1000, Number(weight))
40
+ MASS_TOTAL_SUM += currentMassTotal
41
+ DATAPOINT_COUNT++
42
+
43
+ // Calculate the average dynamically
44
+ MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
45
+
46
+ notifyCallback({
47
+ massMax: MASS_MAX,
48
+ massAverage: MASS_AVERAGE,
49
+ massTotal: Math.max(-1000, weight).toFixed(1),
50
+ })
51
+ }
52
+ }
53
+ } else if (kind === ProgressorResponses.COMMAND_RESPONSE) {
54
+ if (!lastWrite) return
55
+
56
+ let value = ""
57
+
58
+ if (lastWrite === ProgressorCommands.GET_BATT_VLTG) {
59
+ const vdd = new DataView(data.buffer, 2).getUint32(0, true)
60
+ value = `ℹ️ Battery level: ${vdd} mV`
61
+ } else if (lastWrite === ProgressorCommands.GET_FW_VERSION) {
62
+ value = new TextDecoder().decode(data.buffer.slice(2))
63
+ } else if (lastWrite === ProgressorCommands.GET_ERR_INFO) {
64
+ value = new TextDecoder().decode(data.buffer.slice(2))
65
+ }
66
+ console.log(value)
67
+ } else if (kind === ProgressorResponses.LOW_BATTERY_WARNING) {
68
+ console.warn("⚠️ Low power detected. Please consider connecting to a power source.")
69
+ } else {
70
+ console.error(`❌ Error: Unknown message kind detected: ${kind}`)
71
+ }
72
+ }
@@ -0,0 +1,43 @@
1
+ import { notifyCallback } from "./../notify"
2
+ import { applyTare } from "./../tare"
3
+
4
+ // Constants
5
+ let MASS_MAX = "0"
6
+ let MASS_AVERAGE = "0"
7
+ let MASS_TOTAL_SUM = 0
8
+ let DATAPOINT_COUNT = 0
9
+ const WEIGHT_OFFSET = 10
10
+ // const STABLE_OFFSET = 14
11
+
12
+ /**
13
+ * Handles data received from the WH-C06 device.
14
+ * @param {DataView} data - The received data.
15
+ */
16
+ export const handleWHC06Data = (data: DataView): void => {
17
+ const weight = (data.getUint8(WEIGHT_OFFSET) << 8) | data.getUint8(WEIGHT_OFFSET + 1)
18
+ // const stable = (data.getUint8(STABLE_OFFSET) & 0xf0) >> 4
19
+ // const unit = data.getUint8(STABLE_OFFSET) & 0x0f
20
+
21
+ let numericData = weight / 100
22
+
23
+ // Tare correction
24
+ numericData -= applyTare(numericData)
25
+
26
+ // Update MASS_MAX
27
+ MASS_MAX = Math.max(Number(MASS_MAX), numericData).toFixed(1)
28
+
29
+ // Update running sum and count
30
+ const currentMassTotal = Math.max(-1000, numericData)
31
+ MASS_TOTAL_SUM += currentMassTotal
32
+ DATAPOINT_COUNT++
33
+
34
+ // Calculate the average dynamically
35
+ MASS_AVERAGE = (MASS_TOTAL_SUM / DATAPOINT_COUNT).toFixed(1)
36
+
37
+ // Notify with weight data
38
+ notifyCallback({
39
+ massMax: MASS_MAX,
40
+ massAverage: MASS_AVERAGE,
41
+ massTotal: Math.max(-1000, numericData).toFixed(1),
42
+ })
43
+ }
@@ -8,6 +8,6 @@ export { Motherboard } from "./motherboard"
8
8
 
9
9
  export { mySmartBoard } from "./mysmartboard"
10
10
 
11
- export { MuscleMeter } from "./musclemeter"
11
+ export { WHC06 } from "./wh-c06"
12
12
 
13
13
  export { Progressor } from "./progressor"
@@ -0,0 +1,19 @@
1
+ import type { Device } from "../types/devices"
2
+
3
+ /**
4
+ * Represents a Weiheng - WH-C06 (or MAT Muscle Meter) device
5
+ * Enable 'Experimental Web Platform features' Chrome Flags.
6
+ */
7
+ export const WHC06: Device = {
8
+ filters: [
9
+ {
10
+ // namePrefix: "IF_B7",
11
+ manufacturerData: [
12
+ {
13
+ companyIdentifier: 0x0100, // 256
14
+ },
15
+ ],
16
+ },
17
+ ],
18
+ services: [],
19
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Export device types
2
- export { Climbro, Entralpi, KilterBoard, Motherboard, mySmartBoard, MuscleMeter, Progressor } from "./devices/index"
2
+ export { Climbro, Entralpi, KilterBoard, Motherboard, mySmartBoard, WHC06, Progressor } from "./devices/index"
3
3
 
4
4
  // Export battery related functions
5
5
  export { battery } from "./battery"
@@ -18,6 +18,9 @@ export { isConnected } from "./is-connected"
18
18
  // Export information retrieval function
19
19
  export { info } from "./info"
20
20
 
21
+ // Export led retrieval function
22
+ export { led } from "./led"
23
+
21
24
  // Export notification related function
22
25
  export { notify } from "./notify"
23
26
 
package/src/led.ts ADDED
@@ -0,0 +1,187 @@
1
+ import type { Device } from "./types/devices"
2
+ import { write } from "./write"
3
+ import { isConnected } from "./is-connected"
4
+ import { KilterBoard } from "./devices"
5
+ import { KilterBoardPacket } from "./commands/kilterboard"
6
+ /**
7
+ * Maximum length of the message body for byte wrapping.
8
+ */
9
+ const MESSAGE_BODY_MAX_LENGTH = 255
10
+ /**
11
+ * Maximum length of the the bluetooth chunk.
12
+ */
13
+ const MAX_BLUETOOTH_MESSAGE_SIZE = 20
14
+ /**
15
+ * Calculates the checksum for a byte array by summing up all bytes ot hre packet in a single-byte variable.
16
+ * @param data - The array of bytes to calculate the checksum for.
17
+ * @returns The calculated checksum value.
18
+ */
19
+ function checksum(data: number[]) {
20
+ let i = 0
21
+ for (const value of data) {
22
+ i = (i + value) & 255
23
+ }
24
+ return ~i & 255
25
+ }
26
+ /**
27
+ * Wraps a byte array with header and footer bytes for transmission.
28
+ * @param data - The array of bytes to wrap.
29
+ * @returns The wrapped byte array.
30
+ */
31
+ function wrapBytes(data: number[]) {
32
+ if (data.length > MESSAGE_BODY_MAX_LENGTH) {
33
+ return []
34
+ }
35
+ /**
36
+ - 0x1
37
+ - len(packets)
38
+ - checksum(packets)
39
+ - 0x2
40
+ - *packets
41
+ - 0x3
42
+
43
+ First byte is always 1, the second is a number of packets, then checksum, then 2, packets themselves, and finally 3.
44
+ */
45
+ return [1, data.length, checksum(data), 2, ...data, 3]
46
+ }
47
+ class ClimbPlacement {
48
+ position: number
49
+ role_id: string
50
+
51
+ constructor(position: number, role_id: string) {
52
+ this.position = position
53
+ this.role_id = role_id
54
+ }
55
+ }
56
+ /**
57
+ * Encodes a position into a byte array.
58
+ * The lowest 8 bits of the position get put in the first byte of the group.
59
+ * The highest 8 bits of the position get put in the second byte of the group.
60
+ * @param position - The position to encode.
61
+ * @returns The encoded byte array representing the position.
62
+ */
63
+ function encodePosition(position: number) {
64
+ const position1 = position & 255
65
+ const position2 = (position & 65280) >> 8
66
+
67
+ return [position1, position2]
68
+ }
69
+ /**
70
+ * Encodes a color string into a numeric representation.
71
+ * 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).
72
+ * @param color - The color string in hexadecimal format (e.g., 'FFFFFF').
73
+ * @returns The encoded /compressed color value.
74
+ */
75
+ function encodeColor(color: string) {
76
+ const substring = color.substring(0, 2)
77
+ const substring2 = color.substring(2, 4)
78
+
79
+ const parsedSubstring = parseInt(substring, 16) / 32
80
+ const parsedSubstring2 = parseInt(substring2, 16) / 32
81
+ const parsedResult = (parsedSubstring << 5) | (parsedSubstring2 << 2)
82
+
83
+ const substring3 = color.substring(4, 6)
84
+ const parsedSubstring3 = parseInt(substring3, 16) / 64
85
+ const finalParsedResult = parsedResult | parsedSubstring3
86
+
87
+ return finalParsedResult
88
+ }
89
+ /**
90
+ * Encodes a placement (requires a 16-bit position and a 24-bit rgb color. ) into a byte array.
91
+ * @param position - The position to encode.
92
+ * @param ledColor - The color of the LED in hexadecimal format (e.g., 'FFFFFF').
93
+ * @returns The encoded byte array representing the placement.
94
+ */
95
+ function encodePlacement(position: number, ledColor: string) {
96
+ return [...encodePosition(position), encodeColor(ledColor)]
97
+ }
98
+ /**
99
+ * Prepares byte arrays for transmission based on a list of climb placements.
100
+ * @param climbPlacementList - The list of climb placements containing position and role ID.
101
+ * @returns The final byte array ready for transmission.
102
+ */
103
+ export function prepBytesV3(climbPlacementList: ClimbPlacement[]) {
104
+ const resultArray: number[][] = []
105
+ let tempArray: number[] = [KilterBoardPacket.V3_MIDDLE]
106
+
107
+ for (const climbPlacement of climbPlacementList) {
108
+ if (tempArray.length + 3 > MESSAGE_BODY_MAX_LENGTH) {
109
+ resultArray.push(tempArray)
110
+ tempArray = [KilterBoardPacket.V3_MIDDLE]
111
+ }
112
+
113
+ const ledColor = climbPlacement.role_id
114
+
115
+ const encodedPlacement = encodePlacement(climbPlacement.position, ledColor)
116
+ tempArray.push(...encodedPlacement)
117
+ }
118
+
119
+ resultArray.push(tempArray)
120
+
121
+ if (resultArray.length === 1) {
122
+ resultArray[0][0] = KilterBoardPacket.V3_ONLY
123
+ } else if (resultArray.length > 1) {
124
+ resultArray[0][0] = KilterBoardPacket.V3_FIRST
125
+ resultArray[resultArray.length - 1][0] = KilterBoardPacket.V3_LAST
126
+ }
127
+
128
+ const finalResultArray: number[] = []
129
+ for (const currentArray of resultArray) {
130
+ finalResultArray.push(...wrapBytes(currentArray))
131
+ }
132
+
133
+ return finalResultArray
134
+ }
135
+ /**
136
+ * Splits a collection into slices of the specified length.
137
+ * https://github.com/ramda/ramda/blob/master/source/splitEvery.js
138
+ * @param {Number} n
139
+ * @param {Array} list
140
+ * @return {Array}
141
+ */
142
+ function splitEvery(n: number, list: number[]) {
143
+ if (n <= 0) {
144
+ throw new Error("First argument to splitEvery must be a positive integer")
145
+ }
146
+ const result = []
147
+ let idx = 0
148
+ while (idx < list.length) {
149
+ result.push(list.slice(idx, (idx += n)))
150
+ }
151
+ return result
152
+ }
153
+ /**
154
+ * The kilter board only supports messages of 20 bytes
155
+ * at a time. This method splits a full message into parts
156
+ * of 20 bytes
157
+ *
158
+ * @param buffer
159
+ */
160
+ const splitMessages = (buffer: number[]) =>
161
+ splitEvery(MAX_BLUETOOTH_MESSAGE_SIZE, buffer).map((arr) => new Uint8Array(arr))
162
+ /**
163
+ * Sends a series of messages to a device.
164
+ */
165
+ async function writeMessageSeries(messages: Uint8Array[]) {
166
+ for (const message of messages) {
167
+ await write(KilterBoard, "uart", "tx", message)
168
+ }
169
+ }
170
+ /**
171
+ * Set device leds.
172
+ * @param {Device} board - The device to retrieve information from.
173
+ * @returns {Promise<void>} A promise that resolves when the information retrieval is completed.
174
+ */
175
+ export const led = async (board: Device, placement: ClimbPlacement[]): Promise<number[] | undefined> => {
176
+ // Check if the filter contains the Aurora Climbing Advertising service
177
+ const AuroraUUID = "4488b571-7806-4df6-bcff-a2897e4953ff"
178
+ if (board.filters.some((filter) => filter.services?.includes(AuroraUUID))) {
179
+ // Prepares byte arrays for transmission based on a list of climb placements.
180
+ const payload = prepBytesV3(placement)
181
+ // Sends the payload to the device by splitting it into messages and writing each message.
182
+ if (isConnected(board)) {
183
+ writeMessageSeries(splitMessages(payload))
184
+ }
185
+ return payload
186
+ }
187
+ }
package/src/notify.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { massObject } from "./types/notify"
2
2
  /** Define the type for the callback function */
3
- export type NotifyCallback = (data: massObject) => void
3
+ type NotifyCallback = (data: massObject) => void
4
4
  /**
5
5
  * Defines the type for the callback function.
6
6
  * @callback NotifyCallback
package/src/read.ts CHANGED
@@ -10,12 +10,7 @@ import { isConnected } from "./is-connected"
10
10
  * @param {number} [duration=0] - The duration to wait before resolving the promise, in milliseconds.
11
11
  * @returns {Promise<void>} A promise that resolves when the read operation is completed.
12
12
  */
13
- export const read = (
14
- board: Device,
15
- serviceId: string,
16
- characteristicId: string,
17
- duration: number = 0,
18
- ): Promise<void> => {
13
+ export const read = (board: Device, serviceId: string, characteristicId: string, duration = 0): Promise<void> => {
19
14
  return new Promise((resolve, reject) => {
20
15
  if (isConnected(board)) {
21
16
  const characteristic = getCharacteristic(board, serviceId, characteristicId)
package/src/stream.ts CHANGED
@@ -5,7 +5,7 @@ import { stop } from "./stop"
5
5
  import { Motherboard, Progressor } from "./devices"
6
6
  import { MotherboardCommands, ProgressorCommands } from "./commands"
7
7
  import { emptyDownloadPackets } from "./download"
8
- import { CALIBRATION } from "./data"
8
+ import { CALIBRATION } from "./data/motherboard"
9
9
  import { calibration } from "./calibration"
10
10
 
11
11
  /**
@@ -14,7 +14,7 @@ import { calibration } from "./calibration"
14
14
  * @param {number} [duration=0] - The duration of the stream in milliseconds. If set to 0, stream will continue indefinitely.
15
15
  * @returns {Promise<void>} A promise that resolves when the streaming operation is completed.
16
16
  */
17
- export const stream = async (board: Device, duration: number = 0): Promise<void> => {
17
+ export const stream = async (board: Device, duration = 0): Promise<void> => {
18
18
  if (isConnected(board)) {
19
19
  // Reset download packets
20
20
  emptyDownloadPackets()
@@ -1,11 +1,12 @@
1
- const rechk: RegExp = /^([<>])?(([1-9]\d*)?([xcbB?hHiIfdsp]))*$/
2
- const refmt: RegExp = /([1-9]\d*)?([xcbB?hHiIfdsp])/g
1
+ const rechk = /^([<>])?(([1-9]\d*)?([xcbB?hHiIfdsp]))*$/
2
+ const refmt = /([1-9]\d*)?([xcbB?hHiIfdsp])/g
3
3
 
4
4
  const str = (v: DataView, o: number, c: number): string =>
5
5
  String.fromCharCode(...Array.from(new Uint8Array(v.buffer, v.byteOffset + o, c)))
6
6
 
7
- const rts = (v: DataView, o: number, c: number, s: string): void =>
7
+ const rts = (v: DataView, o: number, c: number, s: string): void => {
8
8
  new Uint8Array(v.buffer, v.byteOffset + o, c).set(s.split("").map((str) => str.charCodeAt(0)))
9
+ }
9
10
 
10
11
  const pst = (v: DataView, o: number, c: number): string => str(v, o + 1, Math.min(v.getUint8(o), c - 1))
11
12
 
@@ -19,9 +20,7 @@ interface FormatFn {
19
20
  p: (v: DataView, value: unknown) => void
20
21
  }
21
22
 
22
- interface LUT {
23
- [key: string]: (c: number) => [number, number, (o: number) => FormatFn]
24
- }
23
+ type LUT = Record<string, (c: number) => [number, number, (o: number) => FormatFn]>
25
24
 
26
25
  const lut = (le: boolean): LUT => ({
27
26
  x: (c: number) =>
@@ -39,7 +38,9 @@ const lut = (le: boolean): LUT => ({
39
38
  1,
40
39
  (o: number) => ({
41
40
  u: (v: DataView) => str(v, o, 1),
42
- p: (v: DataView, s: string) => rts(v, o, 1, s),
41
+ p: (v: DataView, s: string) => {
42
+ rts(v, o, 1, s)
43
+ },
43
44
  }),
44
45
  ] as [number, number, (o: number) => FormatFn],
45
46
  "?": (c: number) =>
@@ -48,7 +49,9 @@ const lut = (le: boolean): LUT => ({
48
49
  1,
49
50
  (o: number) => ({
50
51
  u: (v: DataView) => Boolean(v.getUint8(o)),
51
- p: (v: DataView, B: boolean) => v.setUint8(o, B ? 1 : 0),
52
+ p: (v: DataView, B: boolean) => {
53
+ v.setUint8(o, B ? 1 : 0)
54
+ },
52
55
  }),
53
56
  ] as [number, number, (o: number) => FormatFn],
54
57
  b: (c: number) =>
@@ -57,7 +60,9 @@ const lut = (le: boolean): LUT => ({
57
60
  1,
58
61
  (o: number) => ({
59
62
  u: (v: DataView) => v.getInt8(o),
60
- p: (v: DataView, b: number) => v.setInt8(o, b),
63
+ p: (v: DataView, b: number) => {
64
+ v.setInt8(o, b)
65
+ },
61
66
  }),
62
67
  ] as [number, number, (o: number) => FormatFn],
63
68
  B: (c: number) =>
@@ -66,7 +71,9 @@ const lut = (le: boolean): LUT => ({
66
71
  1,
67
72
  (o: number) => ({
68
73
  u: (v: DataView) => v.getUint8(o),
69
- p: (v: DataView, B: number) => v.setUint8(o, B),
74
+ p: (v: DataView, B: number) => {
75
+ v.setUint8(o, B)
76
+ },
70
77
  }),
71
78
  ] as [number, number, (o: number) => FormatFn],
72
79
  h: (c: number) =>
@@ -75,7 +82,9 @@ const lut = (le: boolean): LUT => ({
75
82
  2,
76
83
  (o: number) => ({
77
84
  u: (v: DataView) => v.getInt16(o, le),
78
- p: (v: DataView, h: number) => v.setInt16(o, h, le),
85
+ p: (v: DataView, h: number) => {
86
+ v.setInt16(o, h, le)
87
+ },
79
88
  }),
80
89
  ] as [number, number, (o: number) => FormatFn],
81
90
  H: (c: number) =>
@@ -84,7 +93,9 @@ const lut = (le: boolean): LUT => ({
84
93
  2,
85
94
  (o: number) => ({
86
95
  u: (v: DataView) => v.getUint16(o, le),
87
- p: (v: DataView, H: number) => v.setUint16(o, H, le),
96
+ p: (v: DataView, H: number) => {
97
+ v.setUint16(o, H, le)
98
+ },
88
99
  }),
89
100
  ] as [number, number, (o: number) => FormatFn],
90
101
  i: (c: number) =>
@@ -93,7 +104,9 @@ const lut = (le: boolean): LUT => ({
93
104
  4,
94
105
  (o: number) => ({
95
106
  u: (v: DataView) => v.getInt32(o, le),
96
- p: (v: DataView, i: number) => v.setInt32(o, i, le),
107
+ p: (v: DataView, i: number) => {
108
+ v.setInt32(o, i, le)
109
+ },
97
110
  }),
98
111
  ] as [number, number, (o: number) => FormatFn],
99
112
  I: (c: number) =>
@@ -102,7 +115,9 @@ const lut = (le: boolean): LUT => ({
102
115
  4,
103
116
  (o: number) => ({
104
117
  u: (v: DataView) => v.getUint32(o, le),
105
- p: (v: DataView, I: number) => v.setUint32(o, I, le),
118
+ p: (v: DataView, I: number) => {
119
+ v.setUint32(o, I, le)
120
+ },
106
121
  }),
107
122
  ] as [number, number, (o: number) => FormatFn],
108
123
  f: (c: number) =>
@@ -111,7 +126,9 @@ const lut = (le: boolean): LUT => ({
111
126
  4,
112
127
  (o: number) => ({
113
128
  u: (v: DataView) => v.getFloat32(o, le),
114
- p: (v: DataView, f: number) => v.setFloat32(o, f, le),
129
+ p: (v: DataView, f: number) => {
130
+ v.setFloat32(o, f, le)
131
+ },
115
132
  }),
116
133
  ] as [number, number, (o: number) => FormatFn],
117
134
  d: (c: number) =>
@@ -120,7 +137,9 @@ const lut = (le: boolean): LUT => ({
120
137
  8,
121
138
  (o: number) => ({
122
139
  u: (v: DataView) => v.getFloat64(o, le),
123
- p: (v: DataView, d: number) => v.setFloat64(o, d, le),
140
+ p: (v: DataView, d: number) => {
141
+ v.setFloat64(o, d, le)
142
+ },
124
143
  }),
125
144
  ] as [number, number, (o: number) => FormatFn],
126
145
  s: (c: number) =>
@@ -129,7 +148,9 @@ const lut = (le: boolean): LUT => ({
129
148
  c,
130
149
  (o: number) => ({
131
150
  u: (v: DataView) => str(v, o, c),
132
- p: (v: DataView, s: string) => rts(v, o, c, s.slice(0, c)),
151
+ p: (v: DataView, s: string) => {
152
+ rts(v, o, c, s.slice(0, c))
153
+ },
133
154
  }),
134
155
  ] as [number, number, (o: number) => FormatFn],
135
156
  p: (c: number) =>
@@ -138,7 +159,9 @@ const lut = (le: boolean): LUT => ({
138
159
  c,
139
160
  (o: number) => ({
140
161
  u: (v: DataView) => pst(v, o, c),
141
- p: (v: DataView, s: string) => tsp(v, o, c, s.slice(0, c - 1)),
162
+ p: (v: DataView, s: string) => {
163
+ tsp(v, o, c, s.slice(0, c - 1))
164
+ },
142
165
  }),
143
166
  ] as [number, number, (o: number) => FormatFn],
144
167
  })
@@ -148,7 +171,7 @@ const errval: RangeError = new RangeError("Not enough values for structure")
148
171
 
149
172
  export default function struct(format: string) {
150
173
  const fns: FormatFn[] = []
151
- let size: number = 0
174
+ let size = 0
152
175
  let m: RegExpExecArray | null = rechk.exec(format)
153
176
 
154
177
  if (!m) {
@@ -159,7 +182,6 @@ export default function struct(format: string) {
159
182
  const lu = (n: string, c: string): [number, number, (o: number) => FormatFn] => t[c](n ? parseInt(n, 10) : 1)
160
183
 
161
184
  while ((m = refmt.exec(format))) {
162
- // eslint-disable-next-line no-extra-semi
163
185
  ;((r: number, s: number, f: (o: number) => FormatFn) => {
164
186
  for (let i = 0; i < r; ++i, size += s) {
165
187
  if (f) {
@@ -186,7 +208,9 @@ export default function struct(format: string) {
186
208
  }
187
209
  const v = new DataView(arrb, offs)
188
210
  new Uint8Array(arrb, offs, size).fill(0)
189
- fns.forEach((f, i) => f.p(v, values[i]))
211
+ fns.forEach((f, i) => {
212
+ f.p(v, values[i])
213
+ })
190
214
  }
191
215
 
192
216
  const pack = (...values: unknown[]): ArrayBuffer => {
package/src/tare.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Represents the current tare value for calibration.
3
3
  * @type {number}
4
4
  */
5
- let currentTare: number = 0
5
+ let currentTare = 0
6
6
 
7
7
  /**
8
8
  * Represents the state of tare calibration.
@@ -23,20 +23,20 @@ let tareSamples: number[] = []
23
23
  * Array holding the sum of samples collected during tare calibration.
24
24
  * @type {number}
25
25
  */
26
- let newTares: number = 0
26
+ let newTares = 0
27
27
 
28
28
  /**
29
29
  * Duration time for tare calibration process.
30
30
  * @type {number}
31
31
  */
32
- let timeTare: number = 5000
32
+ let timeTare = 5000
33
33
 
34
34
  /**
35
35
  * Initiates the tare calibration process.
36
36
  * @param {number} time - The duration time for tare calibration process.
37
- * @returns {Promise<void>} A Promise that resolves when tare calibration is initiated.
37
+ * @returns {void} A Promise that resolves when tare calibration is initiated.
38
38
  */
39
- export const tare = async (time: number = 5000): Promise<void> => {
39
+ export const tare = (time = 5000): void => {
40
40
  runTare = true
41
41
  timeTare = time
42
42
  tareSamples = []
@@ -50,7 +50,7 @@ export const tare = async (time: number = 5000): Promise<void> => {
50
50
  export const applyTare = (sample: number): number => {
51
51
  if (runTare) {
52
52
  // If taring process is initiated
53
- if (typeof runTare === "boolean" && runTare === true) {
53
+ if (typeof runTare === "boolean" && runTare) {
54
54
  // If tare flag is true (first time), set it to the current date
55
55
  runTare = new Date()
56
56
  // Initialize the sum of new tare values
@@ -59,10 +59,10 @@ export const applyTare = (sample: number): number => {
59
59
  // Push current sample to tareSamples array
60
60
  tareSamples.push(sample)
61
61
  // Check if taring process duration has passed (defaults to 5 seconds)
62
- if (typeof runTare !== "boolean" && new Date().getTime() - (runTare as Date).getTime() > timeTare) {
62
+ if (typeof runTare !== "boolean" && new Date().getTime() - runTare.getTime() > timeTare) {
63
63
  // Calculate the sum of tare samples
64
- for (let i = 0; i < tareSamples.length; ++i) {
65
- newTares += tareSamples[i]
64
+ for (const tareSample of tareSamples) {
65
+ newTares += tareSample
66
66
  }
67
67
  // Calculate the average by dividing the sum by the number of samples and update the tare value with the calculated average
68
68
  currentTare = newTares / tareSamples.length
package/src/write.ts CHANGED
@@ -4,16 +4,16 @@ import { getCharacteristic } from "./characteristic"
4
4
 
5
5
  /**
6
6
  * The last message written to the device.
7
- * @type {string | null}
7
+ * @type {string | Uint8Array | null}
8
8
  */
9
- export let lastWrite: string | null = null
9
+ export let lastWrite: string | Uint8Array | null = null
10
10
 
11
11
  /**
12
12
  * Writes a message to the specified characteristic of the device.
13
13
  * @param {Device} board - The device board to write to.
14
14
  * @param {string} serviceId - The service ID where the characteristic belongs.
15
15
  * @param {string} characteristicId - The characteristic ID to write to.
16
- * @param {string | undefined} message - The message to write.
16
+ * @param {string | Uint8Array | undefined} message - The message to write.
17
17
  * @param {number} [duration=0] - The duration to wait before resolving the promise, in milliseconds.
18
18
  * @returns {Promise<void>} A promise that resolves when the write operation is completed.
19
19
  */
@@ -21,8 +21,8 @@ export const write = (
21
21
  board: Device,
22
22
  serviceId: string,
23
23
  characteristicId: string,
24
- message: string | undefined,
25
- duration: number = 0,
24
+ message: string | Uint8Array | undefined,
25
+ duration = 0,
26
26
  ): Promise<void> => {
27
27
  return new Promise((resolve, reject) => {
28
28
  if (isConnected(board)) {
@@ -31,24 +31,28 @@ export const write = (
31
31
  // If not provided, return without performing write operation
32
32
  return
33
33
  }
34
- // Get the characteristic
34
+ // Get the characteristic from the device using serviceId and characteristicId
35
35
  const characteristic = getCharacteristic(board, serviceId, characteristicId)
36
36
  if (characteristic) {
37
- // Encode the message
38
- const encoder = new TextEncoder()
37
+ // Convert the message to Uint8Array if it's a string
38
+ const valueToWrite: Uint8Array = typeof message === "string" ? new TextEncoder().encode(message) : message
39
+ // Write the value to the characteristic
39
40
  characteristic
40
- .writeValue(encoder.encode(message))
41
+ .writeValue(valueToWrite)
41
42
  .then(() => {
42
43
  // Update the last written message
43
44
  lastWrite = message
44
- // Handle timeout
45
- if (duration !== 0) {
45
+ // If a duration is specified, resolve the promise after the duration
46
+ if (duration > 0) {
46
47
  setTimeout(() => {
47
48
  resolve()
48
49
  }, duration)
50
+ } else {
51
+ // Otherwise, resolve the promise immediately
52
+ resolve()
49
53
  }
50
54
  })
51
- .catch((error) => {
55
+ .catch((error: unknown) => {
52
56
  reject(error)
53
57
  })
54
58
  } else {
@@ -1,10 +0,0 @@
1
- import type { Device } from "../types/devices"
2
-
3
- /**
4
- * Represents a MAT Muscle Meter device
5
- * TODO: Add services, do you own a MAT Muscle Meter? Help us!
6
- */
7
- export const MuscleMeter: Device = {
8
- filters: [{ name: "MAT" }],
9
- services: [],
10
- }