@tiuxo/opencode-openrgb 0.0.0-stage → 0.1.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/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
+ }
@@ -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
+ }