@hangtime/grip-connect 0.3.4 → 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 +12 -8
- package/package.json +1 -1
- package/src/commands/index.ts +1 -1
- package/src/commands/kilterboard.ts +23 -0
- package/src/commands/{musclemeter.ts → wh-c06.ts} +1 -1
- package/src/connect.ts +36 -8
- package/src/data/entralpi.ts +37 -0
- package/src/{data.ts → data/motherboard.ts} +14 -105
- package/src/data/progressor.ts +72 -0
- package/src/data/wh-c06.ts +43 -0
- package/src/devices/index.ts +1 -1
- package/src/devices/wh-c06.ts +19 -0
- package/src/index.ts +4 -1
- package/src/led.ts +187 -0
- package/src/notify.ts +1 -1
- package/src/read.ts +1 -6
- package/src/stream.ts +2 -2
- package/src/struct/index.ts +45 -21
- package/src/tare.ts +9 -9
- package/src/write.ts +16 -12
- package/src/devices/musclemeter.ts +0 -10
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
|
|
7
|
-
|
|
8
|
-
[
|
|
9
|
-
[
|
|
10
|
-
[
|
|
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),
|
|
@@ -91,10 +92,11 @@ available services with us.
|
|
|
91
92
|
- ✅ Griptonite Motherboard
|
|
92
93
|
- ✅ Tindeq Progressor
|
|
93
94
|
- ⏳ Entralpi (not verified)
|
|
94
|
-
- ⏳ Kilterboard (
|
|
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
|
|
|
@@ -133,7 +135,9 @@ A special thank you to:
|
|
|
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
137
|
- [@1-max-1](https://github.com/1-max-1) for the docs on his Kilter Board
|
|
136
|
-
[simulator](https://github.com/1-max-1/fake_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
|
+
"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": {
|
package/src/commands/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/connect.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Device } from "./types/devices"
|
|
2
|
-
import { handleEntralpiData
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 "
|
|
2
|
-
import { applyTare } from "
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
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
|
|
12
|
-
const NUM_SAMPLES
|
|
13
|
-
let MASS_MAX
|
|
14
|
-
let MASS_AVERAGE
|
|
15
|
-
let MASS_TOTAL_SUM
|
|
16
|
-
let DATAPOINT_COUNT
|
|
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
|
|
27
|
+
let sign = 1
|
|
30
28
|
// Initialize the final calibrated value
|
|
31
|
-
let final
|
|
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
|
+
}
|
package/src/devices/index.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
17
|
+
export const stream = async (board: Device, duration = 0): Promise<void> => {
|
|
18
18
|
if (isConnected(board)) {
|
|
19
19
|
// Reset download packets
|
|
20
20
|
emptyDownloadPackets()
|
package/src/struct/index.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
const rechk
|
|
2
|
-
const refmt
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
37
|
+
* @returns {void} A Promise that resolves when tare calibration is initiated.
|
|
38
38
|
*/
|
|
39
|
-
export const tare =
|
|
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
|
|
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() -
|
|
62
|
+
if (typeof runTare !== "boolean" && new Date().getTime() - runTare.getTime() > timeTare) {
|
|
63
63
|
// Calculate the sum of tare samples
|
|
64
|
-
for (
|
|
65
|
-
newTares +=
|
|
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
|
|
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
|
-
//
|
|
38
|
-
const
|
|
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(
|
|
41
|
+
.writeValue(valueToWrite)
|
|
41
42
|
.then(() => {
|
|
42
43
|
// Update the last written message
|
|
43
44
|
lastWrite = message
|
|
44
|
-
//
|
|
45
|
-
if (duration
|
|
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 {
|