@hangtime/grip-connect 0.13.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.
- package/README.md +9 -9
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interfaces/device/aurora.interface.d.ts +17 -0
- package/dist/cjs/interfaces/device/aurora.interface.d.ts.map +1 -0
- package/dist/cjs/interfaces/device/{kilterboard.interface.js → aurora.interface.js} +1 -1
- package/dist/cjs/interfaces/device/aurora.interface.js.map +1 -0
- package/dist/cjs/interfaces/device/motherboard.interface.d.ts +1 -1
- package/dist/cjs/interfaces/device/pb-700bt.interface.d.ts +53 -0
- package/dist/cjs/interfaces/device/pb-700bt.interface.d.ts.map +1 -0
- package/dist/cjs/interfaces/device/pb-700bt.interface.js +3 -0
- package/dist/cjs/interfaces/device/pb-700bt.interface.js.map +1 -0
- package/dist/cjs/interfaces/index.d.ts +2 -1
- package/dist/cjs/interfaces/index.d.ts.map +1 -1
- package/dist/cjs/models/device/{kilterboard.model.d.ts → aurora.model.d.ts} +82 -40
- package/dist/cjs/models/device/aurora.model.d.ts.map +1 -0
- package/dist/cjs/models/device/aurora.model.js +407 -0
- package/dist/cjs/models/device/aurora.model.js.map +1 -0
- package/dist/cjs/models/device/climbro.model.js +1 -1
- package/dist/cjs/models/device/climbro.model.js.map +1 -1
- package/dist/cjs/models/device/cts500.model.d.ts.map +1 -1
- package/dist/cjs/models/device/cts500.model.js +12 -4
- package/dist/cjs/models/device/cts500.model.js.map +1 -1
- package/dist/cjs/models/device/forceboard.model.d.ts.map +1 -1
- package/dist/cjs/models/device/forceboard.model.js +6 -2
- package/dist/cjs/models/device/forceboard.model.js.map +1 -1
- package/dist/cjs/models/device/motherboard.model.d.ts +4 -1
- package/dist/cjs/models/device/motherboard.model.d.ts.map +1 -1
- package/dist/cjs/models/device/motherboard.model.js +26 -10
- package/dist/cjs/models/device/motherboard.model.js.map +1 -1
- package/dist/cjs/models/device/pb-700bt.model.d.ts +2 -1
- package/dist/cjs/models/device/pb-700bt.model.d.ts.map +1 -1
- package/dist/cjs/models/device/pb-700bt.model.js.map +1 -1
- package/dist/cjs/models/device/progressor.model.d.ts.map +1 -1
- package/dist/cjs/models/device/progressor.model.js +1 -6
- package/dist/cjs/models/device/progressor.model.js.map +1 -1
- package/dist/cjs/models/device/wh-c06.model.d.ts +2 -0
- package/dist/cjs/models/device/wh-c06.model.d.ts.map +1 -1
- package/dist/cjs/models/device/wh-c06.model.js +45 -34
- package/dist/cjs/models/device/wh-c06.model.js.map +1 -1
- package/dist/cjs/models/device.model.d.ts +18 -5
- package/dist/cjs/models/device.model.d.ts.map +1 -1
- package/dist/cjs/models/device.model.js +71 -16
- package/dist/cjs/models/device.model.js.map +1 -1
- package/dist/cjs/models/index.d.ts +1 -1
- package/dist/cjs/models/index.d.ts.map +1 -1
- package/dist/cjs/models/index.js +3 -4
- package/dist/cjs/models/index.js.map +1 -1
- package/dist/cjs/package.json +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/device/aurora.interface.d.ts +17 -0
- package/dist/interfaces/device/aurora.interface.d.ts.map +1 -0
- package/dist/interfaces/device/aurora.interface.js +2 -0
- package/dist/interfaces/device/aurora.interface.js.map +1 -0
- package/dist/interfaces/device/motherboard.interface.d.ts +1 -1
- package/dist/interfaces/device/pb-700bt.interface.d.ts +53 -0
- package/dist/interfaces/device/pb-700bt.interface.d.ts.map +1 -0
- package/dist/interfaces/device/pb-700bt.interface.js +2 -0
- package/dist/interfaces/device/pb-700bt.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +2 -1
- package/dist/interfaces/index.d.ts.map +1 -1
- package/dist/models/device/{kilterboard.model.d.ts → aurora.model.d.ts} +82 -40
- package/dist/models/device/aurora.model.d.ts.map +1 -0
- package/dist/models/device/aurora.model.js +401 -0
- package/dist/models/device/aurora.model.js.map +1 -0
- package/dist/models/device/climbro.model.js +1 -1
- package/dist/models/device/climbro.model.js.map +1 -1
- package/dist/models/device/cts500.model.d.ts.map +1 -1
- package/dist/models/device/cts500.model.js +12 -4
- package/dist/models/device/cts500.model.js.map +1 -1
- package/dist/models/device/forceboard.model.d.ts.map +1 -1
- package/dist/models/device/forceboard.model.js +6 -2
- package/dist/models/device/forceboard.model.js.map +1 -1
- package/dist/models/device/motherboard.model.d.ts +4 -1
- package/dist/models/device/motherboard.model.d.ts.map +1 -1
- package/dist/models/device/motherboard.model.js +26 -10
- package/dist/models/device/motherboard.model.js.map +1 -1
- package/dist/models/device/pb-700bt.model.d.ts +2 -1
- package/dist/models/device/pb-700bt.model.d.ts.map +1 -1
- package/dist/models/device/pb-700bt.model.js.map +1 -1
- package/dist/models/device/progressor.model.d.ts.map +1 -1
- package/dist/models/device/progressor.model.js +1 -6
- package/dist/models/device/progressor.model.js.map +1 -1
- package/dist/models/device/wh-c06.model.d.ts +2 -0
- package/dist/models/device/wh-c06.model.d.ts.map +1 -1
- package/dist/models/device/wh-c06.model.js +44 -34
- package/dist/models/device/wh-c06.model.js.map +1 -1
- package/dist/models/device.model.d.ts +18 -5
- package/dist/models/device.model.d.ts.map +1 -1
- package/dist/models/device.model.js +71 -16
- package/dist/models/device.model.js.map +1 -1
- package/dist/models/index.d.ts +1 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +1 -1
- package/dist/models/index.js.map +1 -1
- package/package.json +45 -42
- package/src/index.ts +4 -3
- package/src/interfaces/device/aurora.interface.ts +18 -0
- package/src/interfaces/device/motherboard.interface.ts +1 -1
- package/src/interfaces/device/pb-700bt.interface.ts +61 -0
- package/src/interfaces/index.ts +4 -2
- package/src/models/device/aurora.model.ts +497 -0
- package/src/models/device/climbro.model.ts +1 -1
- package/src/models/device/cts500.model.ts +14 -7
- package/src/models/device/forceboard.model.ts +6 -2
- package/src/models/device/motherboard.model.ts +51 -9
- package/src/models/device/pb-700bt.model.ts +2 -1
- package/src/models/device/progressor.model.ts +1 -6
- package/src/models/device/wh-c06.model.ts +54 -42
- package/src/models/device.model.ts +82 -16
- package/src/models/index.ts +2 -2
- package/dist/cjs/interfaces/device/kilterboard.interface.d.ts +0 -17
- package/dist/cjs/interfaces/device/kilterboard.interface.d.ts.map +0 -1
- package/dist/cjs/interfaces/device/kilterboard.interface.js.map +0 -1
- package/dist/cjs/models/device/kilterboard.model.d.ts.map +0 -1
- package/dist/cjs/models/device/kilterboard.model.js +0 -327
- package/dist/cjs/models/device/kilterboard.model.js.map +0 -1
- package/dist/interfaces/device/kilterboard.interface.d.ts +0 -17
- package/dist/interfaces/device/kilterboard.interface.d.ts.map +0 -1
- package/dist/interfaces/device/kilterboard.interface.js +0 -2
- package/dist/interfaces/device/kilterboard.interface.js.map +0 -1
- package/dist/models/device/kilterboard.model.d.ts.map +0 -1
- package/dist/models/device/kilterboard.model.js +0 -323
- package/dist/models/device/kilterboard.model.js.map +0 -1
- package/dist/tsconfig.cjs.tsbuildinfo +0 -1
- package/src/interfaces/device/kilterboard.interface.ts +0 -12
- package/src/models/device/kilterboard.model.ts +0 -347
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { Device } from "../device.model.js"
|
|
2
|
+
import type { AuroraLedPlacement, IAurora } from "../../interfaces/device/aurora.interface.js"
|
|
3
|
+
|
|
4
|
+
type AuroraApiLevel = 2 | 3
|
|
5
|
+
|
|
6
|
+
interface AuroraPacketMarkers {
|
|
7
|
+
middle: AuroraPacket
|
|
8
|
+
first: AuroraPacket
|
|
9
|
+
last: AuroraPacket
|
|
10
|
+
only: AuroraPacket
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AuroraResolvedPlacement {
|
|
14
|
+
position: number
|
|
15
|
+
colorHex: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* For API level 2 and API level 3.
|
|
20
|
+
* The first byte in the data is dependent on where the packet is in the message as a whole.
|
|
21
|
+
*/
|
|
22
|
+
export enum AuroraPacket {
|
|
23
|
+
/** If this packet is in the middle, the byte gets set to 77 (M). */
|
|
24
|
+
V2_MIDDLE = 77,
|
|
25
|
+
/** If this packet is the first packet in the message, then this byte gets set to 78 (N). */
|
|
26
|
+
V2_FIRST,
|
|
27
|
+
/** If this is the last packet in the message, this byte gets set to 79 (O). */
|
|
28
|
+
V2_LAST,
|
|
29
|
+
/** 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. */
|
|
30
|
+
V2_ONLY,
|
|
31
|
+
/** If this packet is in the middle, the byte gets set to 81 (Q). */
|
|
32
|
+
V3_MIDDLE,
|
|
33
|
+
/** If this packet is the first packet in the message, then this byte gets set to 82 (R). */
|
|
34
|
+
V3_FIRST,
|
|
35
|
+
/** If this is the last packet in the message, this byte gets set to 83 (S). */
|
|
36
|
+
V3_LAST,
|
|
37
|
+
/** 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. */
|
|
38
|
+
V3_ONLY,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Represents a Aurora Climbing device.
|
|
43
|
+
* Aurora Board
|
|
44
|
+
* {@link https://auroraclimbing.com}
|
|
45
|
+
*/
|
|
46
|
+
export class Aurora extends Device implements IAurora {
|
|
47
|
+
/**
|
|
48
|
+
* UUID for the Aurora Climbing Advertising service.
|
|
49
|
+
* This constant is used to identify the specific Bluetooth service for Aurora LED boards.
|
|
50
|
+
* @type {string}
|
|
51
|
+
* @static
|
|
52
|
+
* @readonly
|
|
53
|
+
* @constant
|
|
54
|
+
*/
|
|
55
|
+
static readonly AuroraUUID: string = "4488b571-7806-4df6-bcff-a2897e4953ff"
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Maximum length of the message body for byte wrapping.
|
|
59
|
+
* This value defines the limit for the size of messages that can be sent or received
|
|
60
|
+
* to ensure proper byte wrapping in communication.
|
|
61
|
+
* @type {number}
|
|
62
|
+
* @private
|
|
63
|
+
* @readonly
|
|
64
|
+
* @constant
|
|
65
|
+
*/
|
|
66
|
+
private static readonly messageBodyMaxLength: number = 255
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Maximum length of the Bluetooth message chunk.
|
|
70
|
+
* This value sets the upper limit for the size of individual Bluetooth messages
|
|
71
|
+
* sent to and from the device to comply with Bluetooth protocol constraints.
|
|
72
|
+
* @type {number}
|
|
73
|
+
* @private
|
|
74
|
+
* @readonly
|
|
75
|
+
* @constant
|
|
76
|
+
*/
|
|
77
|
+
private static readonly maxBluetoothMessageSize: number = 20
|
|
78
|
+
|
|
79
|
+
private apiLevel: AuroraApiLevel
|
|
80
|
+
|
|
81
|
+
private writeQueue: Promise<void> = Promise.resolve()
|
|
82
|
+
|
|
83
|
+
constructor() {
|
|
84
|
+
super({
|
|
85
|
+
filters: [
|
|
86
|
+
{
|
|
87
|
+
services: [Aurora.AuroraUUID],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
services: [
|
|
91
|
+
{
|
|
92
|
+
name: "UART Nordic Service",
|
|
93
|
+
id: "uart",
|
|
94
|
+
uuid: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
|
|
95
|
+
characteristics: [
|
|
96
|
+
{
|
|
97
|
+
name: "TX",
|
|
98
|
+
id: "tx",
|
|
99
|
+
uuid: "6e400002-b5a3-f393-e0a9-e50e24dcca9e",
|
|
100
|
+
},
|
|
101
|
+
// {
|
|
102
|
+
// name: "RX",
|
|
103
|
+
// id: "rx",
|
|
104
|
+
// uuid: "6e400003-b5a3-f393-e0a9-e50e24dcca9e",
|
|
105
|
+
// },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
this.apiLevel = 2
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sets the API level from the Aurora board name format:
|
|
116
|
+
* display name, optional #serial, optional trailing @apiLevel. Missing @apiLevel defaults to API level 2.
|
|
117
|
+
* @param name - The name of the device.
|
|
118
|
+
*/
|
|
119
|
+
protected setApiLevelFromDeviceName(name?: string | null): void {
|
|
120
|
+
const detectedApiLevel = this.getApiLevelFromDeviceName(name)
|
|
121
|
+
|
|
122
|
+
this.apiLevel = detectedApiLevel
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected override onBluetoothDeviceSelected(device: BluetoothDevice): void {
|
|
126
|
+
this.setApiLevelFromDeviceName(device.name)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private getApiLevelFromDeviceName(name?: string | null): AuroraApiLevel {
|
|
130
|
+
const apiLevel = name?.match(/@(\d+)$/)?.[1]
|
|
131
|
+
|
|
132
|
+
if (apiLevel === undefined) {
|
|
133
|
+
return 2
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return this.normalizeApiLevel(Number(apiLevel))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private normalizeApiLevel(apiLevel: number): AuroraApiLevel {
|
|
140
|
+
if (apiLevel !== 2 && apiLevel !== 3) {
|
|
141
|
+
throw new Error(`Unsupported Aurora Board API level: ${apiLevel}`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return apiLevel
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Calculates the checksum for a byte array by summing up all packet-data bytes in a single-byte variable.
|
|
149
|
+
* @param data - The array of bytes to calculate the checksum for.
|
|
150
|
+
* @returns {number} The calculated checksum value.
|
|
151
|
+
*/
|
|
152
|
+
private checksum(data: number[]): number {
|
|
153
|
+
let i = 0
|
|
154
|
+
for (const value of data) {
|
|
155
|
+
i = (i + value) & 255
|
|
156
|
+
}
|
|
157
|
+
return ~i & 255
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Wraps a byte array with header and footer bytes for transmission.
|
|
162
|
+
* @param data - The array of bytes to wrap.
|
|
163
|
+
* @returns {number[]} The wrapped byte array.
|
|
164
|
+
*/
|
|
165
|
+
private wrapBytes(data: number[]): number[] {
|
|
166
|
+
if (data.length > Aurora.messageBodyMaxLength) {
|
|
167
|
+
return []
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
- 0x1
|
|
171
|
+
- len(packets)
|
|
172
|
+
- checksum(packets)
|
|
173
|
+
- 0x2
|
|
174
|
+
- *packets
|
|
175
|
+
- 0x3
|
|
176
|
+
|
|
177
|
+
First byte is always 1, the second is a number of packets, then checksum, then 2, packets themselves, and finally 3.
|
|
178
|
+
*/
|
|
179
|
+
return [1, data.length, this.checksum(data), 2, ...data, 3]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Encodes an API level 2 position into two bytes.
|
|
184
|
+
* The lowest 8 bits go in the first byte; the highest 2 bits are reserved for the second byte.
|
|
185
|
+
* @param position - The position to encode.
|
|
186
|
+
* @returns {number[]} The encoded byte array representing the position.
|
|
187
|
+
*/
|
|
188
|
+
private validatePosition(position: number, maxPosition: number, apiLevel: AuroraApiLevel): void {
|
|
189
|
+
if (!Number.isInteger(position) || position < 0 || position > maxPosition) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Aurora Board API level ${apiLevel} requires an integer LED position between 0 and ${maxPosition}`,
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private encodePositionV2(position: number): number[] {
|
|
197
|
+
this.validatePosition(position, 0x3ff, 2)
|
|
198
|
+
|
|
199
|
+
const position1 = position & 255
|
|
200
|
+
const position2 = (position & 0x300) >> 8
|
|
201
|
+
|
|
202
|
+
return [position1, position2]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Encodes an API level 3 position into two bytes.
|
|
207
|
+
* The lowest 8 bits go in the first byte; the highest 8 bits go in the second byte.
|
|
208
|
+
* @param position - The position to encode.
|
|
209
|
+
* @returns {number[]} The encoded byte array representing the position.
|
|
210
|
+
*/
|
|
211
|
+
private encodePositionV3(position: number): number[] {
|
|
212
|
+
this.validatePosition(position, 0xffff, 3)
|
|
213
|
+
|
|
214
|
+
const position1 = position & 255
|
|
215
|
+
const position2 = (position & 65280) >> 8
|
|
216
|
+
|
|
217
|
+
return [position1, position2]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Encodes a color string into a numeric representation.
|
|
222
|
+
* 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).
|
|
223
|
+
* Format: 0bRRRGGGBB where RRR is 3 bits for red, GGG is 3 bits for green, BB is 2 bits for blue.
|
|
224
|
+
* @param color - The color string in hexadecimal format (e.g., 'FFFFFF').
|
|
225
|
+
* @returns The encoded /compressed color value.
|
|
226
|
+
*/
|
|
227
|
+
private encodeColorV3(color: string): number {
|
|
228
|
+
const r = parseInt(color.substring(0, 2), 16)
|
|
229
|
+
const g = parseInt(color.substring(2, 4), 16)
|
|
230
|
+
const b = parseInt(color.substring(4, 6), 16)
|
|
231
|
+
|
|
232
|
+
// Integer division: R and G divided by 32, B divided by 64
|
|
233
|
+
// Then pack into 0bRRRGGGBB format
|
|
234
|
+
const rBits = Math.floor(r / 32) // 0-7 (3 bits)
|
|
235
|
+
const gBits = Math.floor(g / 32) // 0-7 (3 bits)
|
|
236
|
+
const bBits = Math.floor(b / 64) // 0-3 (2 bits)
|
|
237
|
+
|
|
238
|
+
// Pack: RRR in bits 7-5, GGG in bits 4-2, BB in bits 1-0
|
|
239
|
+
return (rBits << 5) | (gBits << 2) | bBits
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Encodes a color string using API level 2's 2-bit RGB format.
|
|
244
|
+
* Format: 0bRRGGBB00. The lowest two bits are reserved for the high bits of the LED position.
|
|
245
|
+
* @param color - The color string in hexadecimal format (e.g., 'FFFFFF').
|
|
246
|
+
* @returns The encoded /compressed color value.
|
|
247
|
+
*/
|
|
248
|
+
private encodeColorV2(color: string): number {
|
|
249
|
+
const r = parseInt(color.substring(0, 2), 16)
|
|
250
|
+
const g = parseInt(color.substring(2, 4), 16)
|
|
251
|
+
const b = parseInt(color.substring(4, 6), 16)
|
|
252
|
+
|
|
253
|
+
const rBits = Math.floor(r / 64) // 0-3 (2 bits)
|
|
254
|
+
const gBits = Math.floor(g / 64) // 0-3 (2 bits)
|
|
255
|
+
const bBits = Math.floor(b / 64) // 0-3 (2 bits)
|
|
256
|
+
|
|
257
|
+
return (rBits << 6) | (gBits << 4) | (bBits << 2)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private normalizeColor(color: string): string {
|
|
261
|
+
const colorHex = color.trim().replace(/^#/, "").toUpperCase()
|
|
262
|
+
|
|
263
|
+
if (!/^[0-9A-F]{6}$/.test(colorHex)) {
|
|
264
|
+
throw new Error(`Invalid Aurora Board LED color: ${color}`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return colorHex
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Encodes an API level 2 placement into two bytes.
|
|
272
|
+
* API level 2 stores a 10-bit position and a 2-bit-per-channel RGB color.
|
|
273
|
+
* @param position - The position to encode.
|
|
274
|
+
* @param ledColor - The color of the LED in hexadecimal format (e.g., 'FFFFFF').
|
|
275
|
+
* @returns The encoded byte array representing the placement.
|
|
276
|
+
*/
|
|
277
|
+
private encodePlacementV2(position: number, ledColor: string): number[] {
|
|
278
|
+
const [position1, position2] = this.encodePositionV2(position)
|
|
279
|
+
|
|
280
|
+
return [position1, this.encodeColorV2(ledColor) | position2]
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Encodes an API level 3 placement into three bytes.
|
|
285
|
+
* API level 3 stores a 16-bit position and a 3/3/2-bit RGB color.
|
|
286
|
+
* @param position - The position to encode.
|
|
287
|
+
* @param ledColor - The color of the LED in hexadecimal format (e.g., 'FFFFFF').
|
|
288
|
+
* @returns The encoded byte array representing the placement.
|
|
289
|
+
*/
|
|
290
|
+
private encodePlacementV3(position: number, ledColor: string): number[] {
|
|
291
|
+
return [...this.encodePositionV3(position), this.encodeColorV3(ledColor)]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Resolves placements into LED positions and concrete hex colors.
|
|
296
|
+
* @param climbPlacementList - The list of climb placements containing position and color.
|
|
297
|
+
* @returns The resolved placements ready for API-level encoding.
|
|
298
|
+
*/
|
|
299
|
+
private resolvePlacements(climbPlacementList: AuroraLedPlacement[]): AuroraResolvedPlacement[] {
|
|
300
|
+
return climbPlacementList.flatMap((climbPlacement) => {
|
|
301
|
+
const color = climbPlacement.color?.trim() ?? ""
|
|
302
|
+
|
|
303
|
+
if (color === "") {
|
|
304
|
+
return []
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
position: climbPlacement.position,
|
|
310
|
+
colorHex: this.normalizeColor(color),
|
|
311
|
+
},
|
|
312
|
+
]
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private buildPayload(
|
|
317
|
+
resolvedPlacements: AuroraResolvedPlacement[],
|
|
318
|
+
markers: AuroraPacketMarkers,
|
|
319
|
+
bytesPerPlacement: number,
|
|
320
|
+
encodePlacement: (position: number, ledColor: string) => number[],
|
|
321
|
+
): number[] {
|
|
322
|
+
const resultArray: number[][] = []
|
|
323
|
+
let tempArray: number[] = [markers.middle]
|
|
324
|
+
|
|
325
|
+
for (const climbPlacement of resolvedPlacements) {
|
|
326
|
+
if (tempArray.length + bytesPerPlacement > Aurora.messageBodyMaxLength) {
|
|
327
|
+
resultArray.push(tempArray)
|
|
328
|
+
tempArray = [markers.middle]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const encodedPlacement = encodePlacement(climbPlacement.position, climbPlacement.colorHex)
|
|
332
|
+
tempArray.push(...encodedPlacement)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
resultArray.push(tempArray)
|
|
336
|
+
|
|
337
|
+
if (resultArray.length === 1) {
|
|
338
|
+
resultArray[0][0] = markers.only
|
|
339
|
+
} else if (resultArray.length > 1) {
|
|
340
|
+
resultArray[0][0] = markers.first
|
|
341
|
+
resultArray[resultArray.length - 1][0] = markers.last
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const finalResultArray: number[] = []
|
|
345
|
+
for (const currentArray of resultArray) {
|
|
346
|
+
finalResultArray.push(...this.wrapBytes(currentArray))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return finalResultArray
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Prepares API level 2 byte arrays for transmission based on a list of climb placements.
|
|
354
|
+
* @param climbPlacementList - The list of climb placements containing position and color.
|
|
355
|
+
* @returns The final byte array ready for transmission.
|
|
356
|
+
*/
|
|
357
|
+
private prepBytesV2(climbPlacementList: AuroraLedPlacement[]): number[] {
|
|
358
|
+
return this.buildPayload(
|
|
359
|
+
this.resolvePlacements(climbPlacementList),
|
|
360
|
+
{
|
|
361
|
+
middle: AuroraPacket.V2_MIDDLE,
|
|
362
|
+
first: AuroraPacket.V2_FIRST,
|
|
363
|
+
last: AuroraPacket.V2_LAST,
|
|
364
|
+
only: AuroraPacket.V2_ONLY,
|
|
365
|
+
},
|
|
366
|
+
2,
|
|
367
|
+
(position, ledColor) => this.encodePlacementV2(position, ledColor),
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Prepares API level 3 byte arrays for transmission based on a list of climb placements.
|
|
373
|
+
* @param climbPlacementList - The list of climb placements containing position and color.
|
|
374
|
+
* @returns The final byte array ready for transmission.
|
|
375
|
+
*/
|
|
376
|
+
private prepBytesV3(climbPlacementList: AuroraLedPlacement[]): number[] {
|
|
377
|
+
return this.buildPayload(
|
|
378
|
+
this.resolvePlacements(climbPlacementList),
|
|
379
|
+
{
|
|
380
|
+
middle: AuroraPacket.V3_MIDDLE,
|
|
381
|
+
first: AuroraPacket.V3_FIRST,
|
|
382
|
+
last: AuroraPacket.V3_LAST,
|
|
383
|
+
only: AuroraPacket.V3_ONLY,
|
|
384
|
+
},
|
|
385
|
+
3,
|
|
386
|
+
(position, ledColor) => this.encodePlacementV3(position, ledColor),
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private prepBytes(climbPlacementList: AuroraLedPlacement[], apiLevel: AuroraApiLevel): number[] {
|
|
391
|
+
return apiLevel === 2 ? this.prepBytesV2(climbPlacementList) : this.prepBytesV3(climbPlacementList)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Splits a collection into slices of the specified length.
|
|
396
|
+
* https://github.com/ramda/ramda/blob/master/source/splitEvery
|
|
397
|
+
* @param {Number} n
|
|
398
|
+
* @param {Array} list
|
|
399
|
+
* @return {Array<number[]>}
|
|
400
|
+
*/
|
|
401
|
+
private splitEvery(n: number, list: number[]): number[][] {
|
|
402
|
+
if (n <= 0) {
|
|
403
|
+
throw new Error("First argument to splitEvery must be a positive integer")
|
|
404
|
+
}
|
|
405
|
+
const result = []
|
|
406
|
+
let idx = 0
|
|
407
|
+
while (idx < list.length) {
|
|
408
|
+
result.push(list.slice(idx, (idx += n)))
|
|
409
|
+
}
|
|
410
|
+
return result
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Aurora boards only support messages of 20 bytes
|
|
415
|
+
* at a time. This method splits a full message into parts
|
|
416
|
+
* of 20 bytes
|
|
417
|
+
*
|
|
418
|
+
* @param buffer
|
|
419
|
+
*/
|
|
420
|
+
private splitMessages = (buffer: number[]) =>
|
|
421
|
+
this.splitEvery(Aurora.maxBluetoothMessageSize, buffer).map((arr) => new Uint8Array(arr))
|
|
422
|
+
|
|
423
|
+
private getWriteCharacteristic(): BluetoothRemoteGATTCharacteristic | undefined {
|
|
424
|
+
return this.services.find((service) => service.id === "uart")?.characteristics.find((char) => char.id === "tx")
|
|
425
|
+
?.characteristic
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Sends a series of messages to a device.
|
|
430
|
+
*/
|
|
431
|
+
private async writeMessageSeries(characteristic: BluetoothRemoteGATTCharacteristic, messages: Uint8Array[]) {
|
|
432
|
+
for (const message of messages) {
|
|
433
|
+
if (!message) {
|
|
434
|
+
continue
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
await this.writeMessageChunk(characteristic, message)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private async queueWrite(operation: () => Promise<void>): Promise<void> {
|
|
442
|
+
const queuedOperation = this.writeQueue.catch(() => undefined).then(operation)
|
|
443
|
+
this.writeQueue = queuedOperation.catch(() => undefined)
|
|
444
|
+
|
|
445
|
+
await queuedOperation
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private async writeMessageChunk(
|
|
449
|
+
characteristic: BluetoothRemoteGATTCharacteristic,
|
|
450
|
+
message: Uint8Array,
|
|
451
|
+
): Promise<void> {
|
|
452
|
+
this.updateTimestamp()
|
|
453
|
+
const valueToWrite = new Uint8Array(message)
|
|
454
|
+
|
|
455
|
+
if (this.canWriteWithoutResponse(characteristic)) {
|
|
456
|
+
await characteristic.writeValueWithoutResponse(valueToWrite)
|
|
457
|
+
} else {
|
|
458
|
+
await characteristic.writeValue(valueToWrite)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
this.writeLast = message
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private canWriteWithoutResponse(characteristic: BluetoothRemoteGATTCharacteristic): boolean {
|
|
465
|
+
return (
|
|
466
|
+
characteristic.properties.writeWithoutResponse !== false &&
|
|
467
|
+
typeof characteristic.writeValueWithoutResponse === "function"
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Configures the LEDs based on an array of climb placements.
|
|
473
|
+
* @param config - Array of climb placements for the LEDs. Each placement must include a color hex string.
|
|
474
|
+
* @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for the Aurora board if LED settings were applied, or `undefined` if no action was taken.
|
|
475
|
+
*/
|
|
476
|
+
led = async (config: AuroraLedPlacement[] = []): Promise<number[] | undefined> => {
|
|
477
|
+
// Handle Aurora LED board logic: process placements and send payload if connected
|
|
478
|
+
if (Array.isArray(config)) {
|
|
479
|
+
// Prepares byte arrays for transmission based on a list of climb placements.
|
|
480
|
+
const payload = this.prepBytes(config, this.apiLevel)
|
|
481
|
+
if (this.isConnected()) {
|
|
482
|
+
const characteristic = this.getWriteCharacteristic()
|
|
483
|
+
if (characteristic) {
|
|
484
|
+
await this.queueWrite(() => this.writeMessageSeries(characteristic, this.splitMessages(payload)))
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return payload
|
|
488
|
+
}
|
|
489
|
+
return undefined
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Aurora Board
|
|
495
|
+
* {@link https://auroraboardapp.com}
|
|
496
|
+
*/
|
|
497
|
+
export class AuroraBoard extends Aurora implements IAurora {}
|
|
@@ -183,7 +183,7 @@ export class Climbro extends Device implements IClimbro {
|
|
|
183
183
|
if (value) {
|
|
184
184
|
this.updateTimestamp()
|
|
185
185
|
if (value.buffer) {
|
|
186
|
-
const buffer = new Uint8Array(value.buffer)
|
|
186
|
+
const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
187
187
|
const byteCount = buffer.length
|
|
188
188
|
|
|
189
189
|
let flagSynchro = this.flagSynchro
|
|
@@ -289,17 +289,24 @@ export class CTS500 extends Device implements ICTS500 {
|
|
|
289
289
|
*/
|
|
290
290
|
stream = async (duration = 0): Promise<void> => {
|
|
291
291
|
this.resetPacketTracking()
|
|
292
|
+
this.resetSessionData()
|
|
292
293
|
this.isStreaming = true
|
|
293
294
|
const command = this.commands.START_WEIGHT_MEAS as Uint8Array
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
}
|
|
300
306
|
|
|
301
307
|
if (duration > 0) {
|
|
302
308
|
await new Promise((resolve) => setTimeout(resolve, duration))
|
|
309
|
+
await this.stop()
|
|
303
310
|
}
|
|
304
311
|
}
|
|
305
312
|
|
|
@@ -640,7 +647,7 @@ export class CTS500 extends Device implements ICTS500 {
|
|
|
640
647
|
)
|
|
641
648
|
|
|
642
649
|
if (this.isStreaming) {
|
|
643
|
-
|
|
650
|
+
this.activityCheck(numericData)
|
|
644
651
|
}
|
|
645
652
|
|
|
646
653
|
this.notifyCallback(this.buildForceMeasurement(currentMassTotal))
|
|
@@ -72,7 +72,7 @@ export class ForceBoard extends NordicDfuDevice implements IForceBoard {
|
|
|
72
72
|
],
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
|
-
name: "Temperature
|
|
75
|
+
name: "Temperature Service",
|
|
76
76
|
id: "temperature",
|
|
77
77
|
uuid: "3a90328c-c266-4c76-b05a-6af6104a0b13",
|
|
78
78
|
characteristics: [
|
|
@@ -182,7 +182,7 @@ export class ForceBoard extends NordicDfuDevice implements IForceBoard {
|
|
|
182
182
|
this.updateTimestamp()
|
|
183
183
|
if (value.buffer) {
|
|
184
184
|
const receivedTime: number = Date.now()
|
|
185
|
-
const dataArray = new Uint8Array(value.buffer)
|
|
185
|
+
const dataArray = new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
186
186
|
|
|
187
187
|
const numSamples = (dataArray[0] << 8) | dataArray[1]
|
|
188
188
|
this.currentSamplesPerPacket = numSamples
|
|
@@ -254,7 +254,11 @@ export class ForceBoard extends NordicDfuDevice implements IForceBoard {
|
|
|
254
254
|
*/
|
|
255
255
|
stream = async (duration = 0): Promise<void> => {
|
|
256
256
|
this.resetPacketTracking()
|
|
257
|
+
this.resetSessionData()
|
|
257
258
|
await this.write("weight", "tx", this.commands.START_WEIGHT_MEAS, duration)
|
|
259
|
+
if (duration !== 0) {
|
|
260
|
+
await this.stop()
|
|
261
|
+
}
|
|
258
262
|
}
|
|
259
263
|
|
|
260
264
|
/**
|
|
@@ -40,10 +40,13 @@ export class Motherboard extends Device implements IMotherboard {
|
|
|
40
40
|
|
|
41
41
|
/** Per-zone peak and running sum for left/center/right (used for distribution stats). */
|
|
42
42
|
private leftPeak = Number.NEGATIVE_INFINITY
|
|
43
|
+
private leftMin = Number.POSITIVE_INFINITY
|
|
43
44
|
private leftSum = 0
|
|
44
45
|
private centerPeak = Number.NEGATIVE_INFINITY
|
|
46
|
+
private centerMin = Number.POSITIVE_INFINITY
|
|
45
47
|
private centerSum = 0
|
|
46
48
|
private rightPeak = Number.NEGATIVE_INFINITY
|
|
49
|
+
private rightMin = Number.POSITIVE_INFINITY
|
|
47
50
|
private rightSum = 0
|
|
48
51
|
|
|
49
52
|
constructor() {
|
|
@@ -295,12 +298,15 @@ export class Motherboard extends Device implements IMotherboard {
|
|
|
295
298
|
this.dataPointCount++
|
|
296
299
|
this.mean = this.sum / this.dataPointCount
|
|
297
300
|
|
|
298
|
-
// Per-zone peak and sum for distribution
|
|
301
|
+
// Per-zone peak, min and sum for distribution
|
|
299
302
|
this.leftPeak = Math.max(this.leftPeak, leftClamped)
|
|
303
|
+
this.leftMin = Math.min(this.leftMin, leftClamped)
|
|
300
304
|
this.leftSum += leftClamped
|
|
301
305
|
this.centerPeak = Math.max(this.centerPeak, centerClamped)
|
|
306
|
+
this.centerMin = Math.min(this.centerMin, centerClamped)
|
|
302
307
|
this.centerSum += centerClamped
|
|
303
308
|
this.rightPeak = Math.max(this.rightPeak, rightClamped)
|
|
309
|
+
this.rightMin = Math.min(this.rightMin, rightClamped)
|
|
304
310
|
this.rightSum += rightClamped
|
|
305
311
|
|
|
306
312
|
// Add data to downloadable Array (distribution = per-zone measurements)
|
|
@@ -310,13 +316,24 @@ export class Motherboard extends Device implements IMotherboard {
|
|
|
310
316
|
battRaw: packet.battRaw,
|
|
311
317
|
sampleIndex: packet.sampleIndex,
|
|
312
318
|
distribution: {
|
|
313
|
-
left: this.buildZoneMeasurement(
|
|
319
|
+
left: this.buildZoneMeasurement(
|
|
320
|
+
leftClamped,
|
|
321
|
+
this.leftPeak,
|
|
322
|
+
this.leftSum / this.dataPointCount,
|
|
323
|
+
this.leftMin,
|
|
324
|
+
),
|
|
314
325
|
center: this.buildZoneMeasurement(
|
|
315
326
|
centerClamped,
|
|
316
327
|
this.centerPeak,
|
|
317
328
|
this.centerSum / this.dataPointCount,
|
|
329
|
+
this.centerMin,
|
|
330
|
+
),
|
|
331
|
+
right: this.buildZoneMeasurement(
|
|
332
|
+
rightClamped,
|
|
333
|
+
this.rightPeak,
|
|
334
|
+
this.rightSum / this.dataPointCount,
|
|
335
|
+
this.rightMin,
|
|
318
336
|
),
|
|
319
|
-
right: this.buildZoneMeasurement(rightClamped, this.rightPeak, this.rightSum / this.dataPointCount),
|
|
320
337
|
},
|
|
321
338
|
}),
|
|
322
339
|
)
|
|
@@ -324,12 +341,27 @@ export class Motherboard extends Device implements IMotherboard {
|
|
|
324
341
|
// Check if device is being used
|
|
325
342
|
this.activityCheck(center)
|
|
326
343
|
|
|
327
|
-
// Notify with weight data (distribution zones have proper peak/mean per zone)
|
|
344
|
+
// Notify with weight data (distribution zones have proper peak/mean/min per zone)
|
|
328
345
|
this.notifyCallback(
|
|
329
346
|
this.buildForceMeasurement(totalCurrent, {
|
|
330
|
-
left: this.buildZoneMeasurement(
|
|
331
|
-
|
|
332
|
-
|
|
347
|
+
left: this.buildZoneMeasurement(
|
|
348
|
+
leftClamped,
|
|
349
|
+
this.leftPeak,
|
|
350
|
+
this.leftSum / this.dataPointCount,
|
|
351
|
+
this.leftMin,
|
|
352
|
+
),
|
|
353
|
+
center: this.buildZoneMeasurement(
|
|
354
|
+
centerClamped,
|
|
355
|
+
this.centerPeak,
|
|
356
|
+
this.centerSum / this.dataPointCount,
|
|
357
|
+
this.centerMin,
|
|
358
|
+
),
|
|
359
|
+
right: this.buildZoneMeasurement(
|
|
360
|
+
rightClamped,
|
|
361
|
+
this.rightPeak,
|
|
362
|
+
this.rightSum / this.dataPointCount,
|
|
363
|
+
this.rightMin,
|
|
364
|
+
),
|
|
333
365
|
}),
|
|
334
366
|
)
|
|
335
367
|
} else if (this.writeLast === this.commands.GET_CALIBRATION) {
|
|
@@ -359,7 +391,7 @@ export class Motherboard extends Device implements IMotherboard {
|
|
|
359
391
|
/**
|
|
360
392
|
* Sets the LED color based on a single color option. Defaults to turning the LEDs off if no configuration is provided.
|
|
361
393
|
* @param {"green" | "red" | "orange"} [config] - Optional color or array of climb placements for the LEDs. Ignored if placements are provided.
|
|
362
|
-
* @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for
|
|
394
|
+
* @returns {Promise<number[] | undefined>} A promise that resolves with the payload array for Aurora-compatible LED settings, or `undefined` if no action was taken or for the Motherboard.
|
|
363
395
|
*/
|
|
364
396
|
led = async (config?: "green" | "red" | "orange"): Promise<number[] | undefined> => {
|
|
365
397
|
if (this.isConnected()) {
|
|
@@ -413,7 +445,17 @@ export class Motherboard extends Device implements IMotherboard {
|
|
|
413
445
|
*/
|
|
414
446
|
stream = async (duration = 0): Promise<void> => {
|
|
415
447
|
this.resetPacketTracking()
|
|
416
|
-
this.
|
|
448
|
+
this.resetSessionData()
|
|
449
|
+
// Reset per-zone session stats for the distribution measurements
|
|
450
|
+
this.leftPeak = Number.NEGATIVE_INFINITY
|
|
451
|
+
this.leftMin = Number.POSITIVE_INFINITY
|
|
452
|
+
this.leftSum = 0
|
|
453
|
+
this.centerPeak = Number.NEGATIVE_INFINITY
|
|
454
|
+
this.centerMin = Number.POSITIVE_INFINITY
|
|
455
|
+
this.centerSum = 0
|
|
456
|
+
this.rightPeak = Number.NEGATIVE_INFINITY
|
|
457
|
+
this.rightMin = Number.POSITIVE_INFINITY
|
|
458
|
+
this.rightSum = 0
|
|
417
459
|
// Read calibration data if not already available
|
|
418
460
|
if (!this.calibrationData[0].length) {
|
|
419
461
|
await this.calibration()
|