@tiuxo/opencode-openrgb 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/LICENSE +11 -0
- package/README.md +287 -2
- package/index.ts +9 -0
- package/package.json +54 -4
- package/src/client.ts +464 -0
- package/src/controller.ts +360 -0
- package/src/index.ts +93 -0
- package/src/options.ts +229 -0
- package/src/palette.ts +86 -0
- package/src/protocol.ts +611 -0
- package/src/state.ts +215 -0
package/src/palette.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color parsing and brightness scaling.
|
|
3
|
+
*
|
|
4
|
+
* Brightness is applied in software rather than via the mode's brightness
|
|
5
|
+
* field: "Direct" modes commonly report `brightness_min == brightness_max == 0`
|
|
6
|
+
* (both devices on the reference machine do), meaning the hardware exposes no
|
|
7
|
+
* brightness control at all in that mode.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { rgb, unpackRgb } from "./protocol.js"
|
|
11
|
+
|
|
12
|
+
const NAMED_COLORS: Record<string, string> = {
|
|
13
|
+
black: "#000000",
|
|
14
|
+
white: "#ffffff",
|
|
15
|
+
red: "#ff0000",
|
|
16
|
+
green: "#00ff00",
|
|
17
|
+
blue: "#0000ff",
|
|
18
|
+
yellow: "#ffff00",
|
|
19
|
+
orange: "#ff8c00",
|
|
20
|
+
amber: "#ffbf00",
|
|
21
|
+
cyan: "#00ffff",
|
|
22
|
+
magenta: "#ff00ff",
|
|
23
|
+
violet: "#b400ff",
|
|
24
|
+
purple: "#8000ff",
|
|
25
|
+
pink: "#ff40a0",
|
|
26
|
+
teal: "#00c0a0",
|
|
27
|
+
lime: "#80ff00",
|
|
28
|
+
gold: "#ffd000",
|
|
29
|
+
off: "#000000",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse a color into an OpenRGB `RGBColor`.
|
|
34
|
+
*
|
|
35
|
+
* Accepts `#rgb`, `#rrggbb` (with or without the leading `#`), `rgb(r, g, b)`,
|
|
36
|
+
* and the named colors above. Returns null if the input is not understood, so
|
|
37
|
+
* callers can fall back to a default rather than silently lighting up black.
|
|
38
|
+
*/
|
|
39
|
+
export function parseColor(input: unknown): number | null {
|
|
40
|
+
if (typeof input === "number" && Number.isFinite(input)) {
|
|
41
|
+
return input >>> 0
|
|
42
|
+
}
|
|
43
|
+
if (typeof input !== "string") return null
|
|
44
|
+
|
|
45
|
+
const raw = input.trim().toLowerCase()
|
|
46
|
+
if (raw.length === 0) return null
|
|
47
|
+
|
|
48
|
+
const named = NAMED_COLORS[raw]
|
|
49
|
+
const value = named ?? raw
|
|
50
|
+
|
|
51
|
+
const fn = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/.exec(value)
|
|
52
|
+
if (fn) {
|
|
53
|
+
return rgb(Number(fn[1]), Number(fn[2]), Number(fn[3]))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hex = value.startsWith("#") ? value.slice(1) : value
|
|
57
|
+
if (/^[0-9a-f]{3}$/.test(hex)) {
|
|
58
|
+
const r = hex[0]!
|
|
59
|
+
const g = hex[1]!
|
|
60
|
+
const b = hex[2]!
|
|
61
|
+
return rgb(parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16))
|
|
62
|
+
}
|
|
63
|
+
if (/^[0-9a-f]{6}$/.test(hex)) {
|
|
64
|
+
return rgb(
|
|
65
|
+
parseInt(hex.slice(0, 2), 16),
|
|
66
|
+
parseInt(hex.slice(2, 4), 16),
|
|
67
|
+
parseInt(hex.slice(4, 6), 16),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Scale a color's intensity by `factor`, clamped to [0, 1]. */
|
|
75
|
+
export function scaleColor(color: number, factor: number): number {
|
|
76
|
+
const f = Math.max(0, Math.min(1, factor))
|
|
77
|
+
if (f === 1) return color >>> 0
|
|
78
|
+
const { r, g, b } = unpackRgb(color)
|
|
79
|
+
return rgb(r * f, g * f, b * f)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatColor(color: number): string {
|
|
83
|
+
const { r, g, b } = unpackRgb(color)
|
|
84
|
+
const hex = (v: number) => v.toString(16).padStart(2, "0")
|
|
85
|
+
return `#${hex(r)}${hex(g)}${hex(b)}`
|
|
86
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRGB SDK binary protocol codec.
|
|
3
|
+
*
|
|
4
|
+
* Wire format verified against OpenRGB 1.0rc3.1 (protocol 5) and the upstream
|
|
5
|
+
* sources `RGBController.cpp`, `RGBController_Network.cpp` and
|
|
6
|
+
* `RGBControllerInterface.h`.
|
|
7
|
+
*
|
|
8
|
+
* Every integer on the wire is little-endian. Every string is a `uint16`
|
|
9
|
+
* byte-length (which *includes* the NUL terminator) followed by that many
|
|
10
|
+
* bytes, the last of which is NUL.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const MAGIC = "ORGB"
|
|
14
|
+
export const HEADER_SIZE = 16
|
|
15
|
+
export const DEFAULT_HOST = "127.0.0.1"
|
|
16
|
+
export const DEFAULT_PORT = 6742
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Highest protocol version this client implements.
|
|
20
|
+
*
|
|
21
|
+
* Negotiation is `min(server, CLIENT_PROTOCOL_VERSION)`, so advertising 5 to a
|
|
22
|
+
* protocol-6 server is forward compatible: the server simply speaks 5 back.
|
|
23
|
+
* Protocol 6 changes device addressing (unique IDs instead of list indices),
|
|
24
|
+
* adds ACKs and drops `mode.value`; the decoders below are version-guarded so
|
|
25
|
+
* raising this constant is a contained change.
|
|
26
|
+
*/
|
|
27
|
+
export const CLIENT_PROTOCOL_VERSION = 5
|
|
28
|
+
|
|
29
|
+
export const PacketId = {
|
|
30
|
+
REQUEST_CONTROLLER_COUNT: 0,
|
|
31
|
+
REQUEST_CONTROLLER_DATA: 1,
|
|
32
|
+
REQUEST_PROTOCOL_VERSION: 40,
|
|
33
|
+
SET_CLIENT_NAME: 50,
|
|
34
|
+
DEVICE_LIST_UPDATED: 100,
|
|
35
|
+
RGBCONTROLLER_RESIZEZONE: 1000,
|
|
36
|
+
RGBCONTROLLER_UPDATELEDS: 1050,
|
|
37
|
+
RGBCONTROLLER_UPDATEZONELEDS: 1051,
|
|
38
|
+
RGBCONTROLLER_UPDATESINGLELED: 1052,
|
|
39
|
+
RGBCONTROLLER_SETCUSTOMMODE: 1100,
|
|
40
|
+
RGBCONTROLLER_UPDATEMODE: 1101,
|
|
41
|
+
RGBCONTROLLER_SAVEMODE: 1102,
|
|
42
|
+
} as const
|
|
43
|
+
|
|
44
|
+
/** Bit flags on `Mode.flags`. From `RGBControllerInterface.h`. */
|
|
45
|
+
export const ModeFlag = {
|
|
46
|
+
HAS_SPEED: 1 << 0,
|
|
47
|
+
HAS_DIRECTION_LR: 1 << 1,
|
|
48
|
+
HAS_DIRECTION_UD: 1 << 2,
|
|
49
|
+
HAS_DIRECTION_HV: 1 << 3,
|
|
50
|
+
HAS_BRIGHTNESS: 1 << 4,
|
|
51
|
+
HAS_PER_LED_COLOR: 1 << 5,
|
|
52
|
+
HAS_MODE_SPECIFIC_COLOR: 1 << 6,
|
|
53
|
+
HAS_RANDOM_COLOR: 1 << 7,
|
|
54
|
+
MANUAL_SAVE: 1 << 8,
|
|
55
|
+
AUTOMATIC_SAVE: 1 << 9,
|
|
56
|
+
REQUIRES_ENTIRE_DEVICE: 1 << 10,
|
|
57
|
+
HAS_DIRECTION_DIAG: 1 << 11,
|
|
58
|
+
} as const
|
|
59
|
+
|
|
60
|
+
/** Values for `Mode.colorMode`. */
|
|
61
|
+
export const ModeColors = {
|
|
62
|
+
NONE: 0,
|
|
63
|
+
PER_LED: 1,
|
|
64
|
+
MODE_SPECIFIC: 2,
|
|
65
|
+
RANDOM: 3,
|
|
66
|
+
} as const
|
|
67
|
+
|
|
68
|
+
/** Indexed by the `Device.type` integer. Order is significant. */
|
|
69
|
+
export const DEVICE_TYPES = [
|
|
70
|
+
"motherboard",
|
|
71
|
+
"dram",
|
|
72
|
+
"gpu",
|
|
73
|
+
"cooler",
|
|
74
|
+
"ledstrip",
|
|
75
|
+
"keyboard",
|
|
76
|
+
"mouse",
|
|
77
|
+
"mousemat",
|
|
78
|
+
"headset",
|
|
79
|
+
"headset_stand",
|
|
80
|
+
"gamepad",
|
|
81
|
+
"light",
|
|
82
|
+
"speaker",
|
|
83
|
+
"virtual",
|
|
84
|
+
"storage",
|
|
85
|
+
"case",
|
|
86
|
+
"microphone",
|
|
87
|
+
"accessory",
|
|
88
|
+
"keypad",
|
|
89
|
+
"laptop",
|
|
90
|
+
"monitor",
|
|
91
|
+
"unknown",
|
|
92
|
+
] as const
|
|
93
|
+
|
|
94
|
+
/** Indexed by the `Zone.type` integer. Order is significant. */
|
|
95
|
+
export const ZONE_TYPES = [
|
|
96
|
+
"single",
|
|
97
|
+
"linear",
|
|
98
|
+
"matrix",
|
|
99
|
+
"linear_loop",
|
|
100
|
+
"matrix_loop_x",
|
|
101
|
+
"matrix_loop_y",
|
|
102
|
+
"segmented",
|
|
103
|
+
] as const
|
|
104
|
+
|
|
105
|
+
export function deviceTypeName(type: number): string {
|
|
106
|
+
return DEVICE_TYPES[type] ?? "unknown"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function zoneTypeName(type: number): string {
|
|
110
|
+
return ZONE_TYPES[type] ?? "unknown"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/*---------------------------------------------------------------------------*\
|
|
114
|
+
| Colors |
|
|
115
|
+
\*---------------------------------------------------------------------------*/
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* An OpenRGB `RGBColor` is a `uint32` laid out as `0x00BBGGRR`, i.e. the
|
|
119
|
+
* little-endian byte order on the wire is red, green, blue, zero.
|
|
120
|
+
*
|
|
121
|
+
* Mirrors `ToRGBColor(r, g, b)` from `RGBControllerInterface.h`.
|
|
122
|
+
*/
|
|
123
|
+
export function rgb(r: number, g: number, b: number): number {
|
|
124
|
+
return ((clampByte(b) << 16) | (clampByte(g) << 8) | clampByte(r)) >>> 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function unpackRgb(color: number): { r: number; g: number; b: number } {
|
|
128
|
+
return {
|
|
129
|
+
r: color & 0xff,
|
|
130
|
+
g: (color >>> 8) & 0xff,
|
|
131
|
+
b: (color >>> 16) & 0xff,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function clampByte(v: number): number {
|
|
136
|
+
if (!Number.isFinite(v)) return 0
|
|
137
|
+
return Math.max(0, Math.min(255, Math.round(v)))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/*---------------------------------------------------------------------------*\
|
|
141
|
+
| Reader / Writer |
|
|
142
|
+
\*---------------------------------------------------------------------------*/
|
|
143
|
+
|
|
144
|
+
export class Reader {
|
|
145
|
+
constructor(
|
|
146
|
+
private readonly buf: Buffer,
|
|
147
|
+
private off = 0,
|
|
148
|
+
) {}
|
|
149
|
+
|
|
150
|
+
get offset(): number {
|
|
151
|
+
return this.off
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get remaining(): number {
|
|
155
|
+
return this.buf.length - this.off
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
u16(): number {
|
|
159
|
+
const v = this.buf.readUInt16LE(this.off)
|
|
160
|
+
this.off += 2
|
|
161
|
+
return v
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
u32(): number {
|
|
165
|
+
const v = this.buf.readUInt32LE(this.off)
|
|
166
|
+
this.off += 4
|
|
167
|
+
return v
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
i32(): number {
|
|
171
|
+
const v = this.buf.readInt32LE(this.off)
|
|
172
|
+
this.off += 4
|
|
173
|
+
return v
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Length-prefixed, NUL-terminated string. The prefix counts the NUL. */
|
|
177
|
+
str(): string {
|
|
178
|
+
const len = this.u16()
|
|
179
|
+
const raw = this.buf.toString("utf8", this.off, this.off + len)
|
|
180
|
+
this.off += len
|
|
181
|
+
// Strip the trailing NUL (and defensively any extra padding).
|
|
182
|
+
return raw.replace(/\0+$/, "")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
skip(n: number): void {
|
|
186
|
+
this.off += n
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
class Writer {
|
|
191
|
+
private buf = Buffer.alloc(256)
|
|
192
|
+
private off = 0
|
|
193
|
+
|
|
194
|
+
private ensure(n: number): void {
|
|
195
|
+
if (this.off + n <= this.buf.length) return
|
|
196
|
+
let size = this.buf.length
|
|
197
|
+
while (size < this.off + n) size *= 2
|
|
198
|
+
const next = Buffer.alloc(size)
|
|
199
|
+
this.buf.copy(next, 0, 0, this.off)
|
|
200
|
+
this.buf = next
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
u16(v: number): this {
|
|
204
|
+
this.ensure(2)
|
|
205
|
+
this.buf.writeUInt16LE(v >>> 0, this.off)
|
|
206
|
+
this.off += 2
|
|
207
|
+
return this
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
u32(v: number): this {
|
|
211
|
+
this.ensure(4)
|
|
212
|
+
this.buf.writeUInt32LE(v >>> 0, this.off)
|
|
213
|
+
this.off += 4
|
|
214
|
+
return this
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
i32(v: number): this {
|
|
218
|
+
this.ensure(4)
|
|
219
|
+
this.buf.writeInt32LE(v | 0, this.off)
|
|
220
|
+
this.off += 4
|
|
221
|
+
return this
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Length-prefixed, NUL-terminated string. The prefix counts the NUL. */
|
|
225
|
+
str(s: string): this {
|
|
226
|
+
const bytes = Buffer.from(s, "utf8")
|
|
227
|
+
this.u16(bytes.length + 1)
|
|
228
|
+
this.ensure(bytes.length + 1)
|
|
229
|
+
bytes.copy(this.buf, this.off)
|
|
230
|
+
this.off += bytes.length
|
|
231
|
+
this.buf.writeUInt8(0, this.off)
|
|
232
|
+
this.off += 1
|
|
233
|
+
return this
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
done(): Buffer {
|
|
237
|
+
return this.buf.subarray(0, this.off)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/*---------------------------------------------------------------------------*\
|
|
242
|
+
| Types |
|
|
243
|
+
\*---------------------------------------------------------------------------*/
|
|
244
|
+
|
|
245
|
+
export interface Mode {
|
|
246
|
+
name: string
|
|
247
|
+
/** Device-internal value. Must be round-tripped verbatim on protocol < 6. */
|
|
248
|
+
value: number
|
|
249
|
+
flags: number
|
|
250
|
+
speedMin: number
|
|
251
|
+
speedMax: number
|
|
252
|
+
brightnessMin: number
|
|
253
|
+
brightnessMax: number
|
|
254
|
+
colorsMin: number
|
|
255
|
+
colorsMax: number
|
|
256
|
+
speed: number
|
|
257
|
+
brightness: number
|
|
258
|
+
direction: number
|
|
259
|
+
colorMode: number
|
|
260
|
+
colors: number[]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface Segment {
|
|
264
|
+
name: string
|
|
265
|
+
type: number
|
|
266
|
+
startIdx: number
|
|
267
|
+
ledsCount: number
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface Zone {
|
|
271
|
+
name: string
|
|
272
|
+
type: number
|
|
273
|
+
typeName: string
|
|
274
|
+
ledsMin: number
|
|
275
|
+
ledsMax: number
|
|
276
|
+
ledsCount: number
|
|
277
|
+
matrix?: { height: number; width: number; data: number[] }
|
|
278
|
+
segments: Segment[]
|
|
279
|
+
flags: number
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface Led {
|
|
283
|
+
name: string
|
|
284
|
+
value: number
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface Device {
|
|
288
|
+
/** Index in the server's controller list; used as `pkt_dev_id`. */
|
|
289
|
+
index: number
|
|
290
|
+
type: number
|
|
291
|
+
typeName: string
|
|
292
|
+
name: string
|
|
293
|
+
vendor: string
|
|
294
|
+
description: string
|
|
295
|
+
version: string
|
|
296
|
+
serial: string
|
|
297
|
+
location: string
|
|
298
|
+
activeMode: number
|
|
299
|
+
modes: Mode[]
|
|
300
|
+
zones: Zone[]
|
|
301
|
+
leds: Led[]
|
|
302
|
+
colors: number[]
|
|
303
|
+
ledDisplayNames: string[]
|
|
304
|
+
flags: number
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/*---------------------------------------------------------------------------*\
|
|
308
|
+
| Packet framing |
|
|
309
|
+
\*---------------------------------------------------------------------------*/
|
|
310
|
+
|
|
311
|
+
export function encodePacket(devId: number, pktId: number, data?: Buffer): Buffer {
|
|
312
|
+
const body = data ?? Buffer.alloc(0)
|
|
313
|
+
const header = Buffer.alloc(HEADER_SIZE)
|
|
314
|
+
header.write(MAGIC, 0, "ascii")
|
|
315
|
+
header.writeUInt32LE(devId >>> 0, 4)
|
|
316
|
+
header.writeUInt32LE(pktId >>> 0, 8)
|
|
317
|
+
header.writeUInt32LE(body.length, 12)
|
|
318
|
+
return body.length === 0 ? header : Buffer.concat([header, body])
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface Frame {
|
|
322
|
+
devId: number
|
|
323
|
+
pktId: number
|
|
324
|
+
data: Buffer
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Pull as many complete frames as possible off the front of `buf`.
|
|
329
|
+
* Returns the frames plus whatever bytes remain unconsumed.
|
|
330
|
+
*/
|
|
331
|
+
export function decodeFrames(buf: Buffer): { frames: Frame[]; rest: Buffer } {
|
|
332
|
+
const frames: Frame[] = []
|
|
333
|
+
let cursor = buf
|
|
334
|
+
|
|
335
|
+
while (cursor.length >= HEADER_SIZE) {
|
|
336
|
+
if (cursor.toString("ascii", 0, 4) !== MAGIC) {
|
|
337
|
+
// Desynchronised. Resynchronise on the next magic value, or drop
|
|
338
|
+
// everything if none is present.
|
|
339
|
+
const next = cursor.indexOf(MAGIC, 1, "ascii")
|
|
340
|
+
if (next === -1) return { frames, rest: Buffer.alloc(0) }
|
|
341
|
+
cursor = cursor.subarray(next)
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const size = cursor.readUInt32LE(12)
|
|
346
|
+
if (cursor.length < HEADER_SIZE + size) break
|
|
347
|
+
|
|
348
|
+
frames.push({
|
|
349
|
+
devId: cursor.readUInt32LE(4),
|
|
350
|
+
pktId: cursor.readUInt32LE(8),
|
|
351
|
+
data: cursor.subarray(HEADER_SIZE, HEADER_SIZE + size),
|
|
352
|
+
})
|
|
353
|
+
cursor = cursor.subarray(HEADER_SIZE + size)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { frames, rest: cursor }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/*---------------------------------------------------------------------------*\
|
|
360
|
+
| Mode |
|
|
361
|
+
\*---------------------------------------------------------------------------*/
|
|
362
|
+
|
|
363
|
+
export function decodeMode(r: Reader, protocol: number): Mode {
|
|
364
|
+
const name = r.str()
|
|
365
|
+
// `mode.value` was removed in protocol 6.
|
|
366
|
+
const value = protocol < 6 ? r.i32() : 0
|
|
367
|
+
const flags = r.u32()
|
|
368
|
+
const speedMin = r.u32()
|
|
369
|
+
const speedMax = r.u32()
|
|
370
|
+
const brightnessMin = protocol >= 3 ? r.u32() : 0
|
|
371
|
+
const brightnessMax = protocol >= 3 ? r.u32() : 0
|
|
372
|
+
const colorsMin = r.u32()
|
|
373
|
+
const colorsMax = r.u32()
|
|
374
|
+
const speed = r.u32()
|
|
375
|
+
const brightness = protocol >= 3 ? r.u32() : 0
|
|
376
|
+
const direction = r.u32()
|
|
377
|
+
const colorMode = r.u32()
|
|
378
|
+
|
|
379
|
+
const numColors = r.u16()
|
|
380
|
+
const colors: number[] = []
|
|
381
|
+
for (let i = 0; i < numColors; i++) colors.push(r.u32())
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
name,
|
|
385
|
+
value,
|
|
386
|
+
flags,
|
|
387
|
+
speedMin,
|
|
388
|
+
speedMax,
|
|
389
|
+
brightnessMin,
|
|
390
|
+
brightnessMax,
|
|
391
|
+
colorsMin,
|
|
392
|
+
colorsMax,
|
|
393
|
+
speed,
|
|
394
|
+
brightness,
|
|
395
|
+
direction,
|
|
396
|
+
colorMode,
|
|
397
|
+
colors,
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Inverse of {@link decodeMode}. Field order mirrors
|
|
403
|
+
* `RGBController::GetModeDescriptionData` exactly.
|
|
404
|
+
*/
|
|
405
|
+
function encodeMode(w: Writer, mode: Mode, protocol: number): void {
|
|
406
|
+
w.str(mode.name)
|
|
407
|
+
if (protocol < 6) w.i32(mode.value)
|
|
408
|
+
w.u32(mode.flags)
|
|
409
|
+
w.u32(mode.speedMin)
|
|
410
|
+
w.u32(mode.speedMax)
|
|
411
|
+
if (protocol >= 3) {
|
|
412
|
+
w.u32(mode.brightnessMin)
|
|
413
|
+
w.u32(mode.brightnessMax)
|
|
414
|
+
}
|
|
415
|
+
w.u32(mode.colorsMin)
|
|
416
|
+
w.u32(mode.colorsMax)
|
|
417
|
+
w.u32(mode.speed)
|
|
418
|
+
if (protocol >= 3) w.u32(mode.brightness)
|
|
419
|
+
w.u32(mode.direction)
|
|
420
|
+
w.u32(mode.colorMode)
|
|
421
|
+
w.u16(mode.colors.length)
|
|
422
|
+
for (const c of mode.colors) w.u32(c)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/*---------------------------------------------------------------------------*\
|
|
426
|
+
| Zone / device |
|
|
427
|
+
\*---------------------------------------------------------------------------*/
|
|
428
|
+
|
|
429
|
+
function decodeZone(r: Reader, protocol: number): Zone {
|
|
430
|
+
const name = r.str()
|
|
431
|
+
const type = r.i32()
|
|
432
|
+
const ledsMin = r.u32()
|
|
433
|
+
const ledsMax = r.u32()
|
|
434
|
+
const ledsCount = r.u32()
|
|
435
|
+
|
|
436
|
+
let matrix: Zone["matrix"]
|
|
437
|
+
const matrixLen = r.u16()
|
|
438
|
+
if (matrixLen > 0) {
|
|
439
|
+
const height = r.u32()
|
|
440
|
+
const width = r.u32()
|
|
441
|
+
const cells = Math.max(0, (matrixLen - 8) >> 2)
|
|
442
|
+
const data: number[] = []
|
|
443
|
+
for (let i = 0; i < cells; i++) data.push(r.u32())
|
|
444
|
+
matrix = { height, width, data }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const segments: Segment[] = []
|
|
448
|
+
if (protocol >= 4) {
|
|
449
|
+
const numSegments = r.u16()
|
|
450
|
+
for (let i = 0; i < numSegments; i++) {
|
|
451
|
+
segments.push({
|
|
452
|
+
name: r.str(),
|
|
453
|
+
type: r.i32(),
|
|
454
|
+
startIdx: r.u32(),
|
|
455
|
+
ledsCount: r.u32(),
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const flags = protocol >= 5 ? r.u32() : 0
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
name,
|
|
464
|
+
type,
|
|
465
|
+
typeName: zoneTypeName(type),
|
|
466
|
+
ledsMin,
|
|
467
|
+
ledsMax,
|
|
468
|
+
ledsCount,
|
|
469
|
+
...(matrix ? { matrix } : {}),
|
|
470
|
+
segments,
|
|
471
|
+
flags,
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Decode a `REQUEST_CONTROLLER_DATA` response body.
|
|
477
|
+
*
|
|
478
|
+
* The body opens with a `uint32` total size (which includes itself) followed by
|
|
479
|
+
* the device description block.
|
|
480
|
+
*/
|
|
481
|
+
export function decodeDevice(data: Buffer, protocol: number, index: number): Device {
|
|
482
|
+
return decodeDeviceChecked(data, protocol, index).device
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* As {@link decodeDevice}, but also reports how many bytes were left over.
|
|
487
|
+
*
|
|
488
|
+
* A correct decode consumes the block exactly, so `trailingBytes` should always
|
|
489
|
+
* be 0. Any other value means the field layout has drifted from the server's —
|
|
490
|
+
* which is exactly what `scripts/probe.ts` asserts.
|
|
491
|
+
*/
|
|
492
|
+
export function decodeDeviceChecked(
|
|
493
|
+
data: Buffer,
|
|
494
|
+
protocol: number,
|
|
495
|
+
index: number,
|
|
496
|
+
): { device: Device; trailingBytes: number } {
|
|
497
|
+
const r = new Reader(data)
|
|
498
|
+
r.u32() // data_size, includes itself
|
|
499
|
+
|
|
500
|
+
const type = r.i32()
|
|
501
|
+
const name = r.str()
|
|
502
|
+
const vendor = protocol >= 1 ? r.str() : ""
|
|
503
|
+
const description = r.str()
|
|
504
|
+
const version = r.str()
|
|
505
|
+
const serial = r.str()
|
|
506
|
+
const location = r.str()
|
|
507
|
+
|
|
508
|
+
const numModes = r.u16()
|
|
509
|
+
const activeMode = r.i32()
|
|
510
|
+
const modes: Mode[] = []
|
|
511
|
+
for (let i = 0; i < numModes; i++) modes.push(decodeMode(r, protocol))
|
|
512
|
+
|
|
513
|
+
const numZones = r.u16()
|
|
514
|
+
const zones: Zone[] = []
|
|
515
|
+
for (let i = 0; i < numZones; i++) zones.push(decodeZone(r, protocol))
|
|
516
|
+
|
|
517
|
+
const numLeds = r.u16()
|
|
518
|
+
const leds: Led[] = []
|
|
519
|
+
for (let i = 0; i < numLeds; i++) {
|
|
520
|
+
const ledName = r.str()
|
|
521
|
+
// `led.value` was removed in protocol 6.
|
|
522
|
+
const ledValue = protocol < 6 ? r.u32() : 0
|
|
523
|
+
leds.push({ name: ledName, value: ledValue })
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const numColors = r.u16()
|
|
527
|
+
const colors: number[] = []
|
|
528
|
+
for (let i = 0; i < numColors; i++) colors.push(r.u32())
|
|
529
|
+
|
|
530
|
+
const ledDisplayNames: string[] = []
|
|
531
|
+
let flags = 0
|
|
532
|
+
if (protocol >= 5) {
|
|
533
|
+
const numDisplayNames = r.u16()
|
|
534
|
+
for (let i = 0; i < numDisplayNames; i++) ledDisplayNames.push(r.str())
|
|
535
|
+
flags = r.u32()
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
device: {
|
|
540
|
+
index,
|
|
541
|
+
type,
|
|
542
|
+
typeName: deviceTypeName(type),
|
|
543
|
+
name,
|
|
544
|
+
vendor,
|
|
545
|
+
description,
|
|
546
|
+
version,
|
|
547
|
+
serial,
|
|
548
|
+
location,
|
|
549
|
+
activeMode,
|
|
550
|
+
modes,
|
|
551
|
+
zones,
|
|
552
|
+
leds,
|
|
553
|
+
colors,
|
|
554
|
+
ledDisplayNames,
|
|
555
|
+
flags,
|
|
556
|
+
},
|
|
557
|
+
trailingBytes: r.remaining,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/*---------------------------------------------------------------------------*\
|
|
562
|
+
| Request bodies |
|
|
563
|
+
\*---------------------------------------------------------------------------*/
|
|
564
|
+
|
|
565
|
+
export function encodeProtocolVersion(version: number): Buffer {
|
|
566
|
+
const b = Buffer.alloc(4)
|
|
567
|
+
b.writeUInt32LE(version >>> 0, 0)
|
|
568
|
+
return b
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function encodeClientName(name: string): Buffer {
|
|
572
|
+
return Buffer.from(`${name}\0`, "utf8")
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* `UPDATELEDS` body: `data_size` (uint32, **including itself**), `num_colors`
|
|
577
|
+
* (uint16), then one `RGBColor` per LED.
|
|
578
|
+
*
|
|
579
|
+
* Confirmed against `RGBController_Network::CreateUpdateLEDsPacket`, which
|
|
580
|
+
* computes `data_size = sizeof(data_size) + GetColorDescriptionSize(...)` and
|
|
581
|
+
* passes that same value as the packet header size.
|
|
582
|
+
*/
|
|
583
|
+
export function encodeUpdateLeds(colors: readonly number[]): Buffer {
|
|
584
|
+
const size = 4 + 2 + colors.length * 4
|
|
585
|
+
const b = Buffer.alloc(size)
|
|
586
|
+
b.writeUInt32LE(size, 0)
|
|
587
|
+
b.writeUInt16LE(colors.length, 4)
|
|
588
|
+
for (let i = 0; i < colors.length; i++) {
|
|
589
|
+
b.writeUInt32LE((colors[i] ?? 0) >>> 0, 6 + i * 4)
|
|
590
|
+
}
|
|
591
|
+
return b
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* `UPDATEMODE` body: `data_size` (uint32, **including itself**), `mode_idx`
|
|
596
|
+
* (int32), then the mode description block.
|
|
597
|
+
*
|
|
598
|
+
* `SAVEMODE` uses an identical body.
|
|
599
|
+
*/
|
|
600
|
+
export function encodeUpdateMode(modeIdx: number, mode: Mode, protocol: number): Buffer {
|
|
601
|
+
const inner = new Writer()
|
|
602
|
+
encodeMode(inner, mode, protocol)
|
|
603
|
+
const modeBytes = inner.done()
|
|
604
|
+
|
|
605
|
+
const size = 4 + 4 + modeBytes.length
|
|
606
|
+
const b = Buffer.alloc(size)
|
|
607
|
+
b.writeUInt32LE(size, 0)
|
|
608
|
+
b.writeInt32LE(modeIdx | 0, 4)
|
|
609
|
+
modeBytes.copy(b, 8)
|
|
610
|
+
return b
|
|
611
|
+
}
|