-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWeikav_NUT65.js
More file actions
264 lines (224 loc) · 9.56 KB
/
Copy pathWeikav_NUT65.js
File metadata and controls
264 lines (224 loc) · 9.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
export function Name() { return "Weikav NUT65"; }
export function VendorId() { return 0x342D; }
export function ProductId() { return 0xE51A; }
export function Publisher() { return "Community"; }
export function Type() { return "Hid"; }
export function DeviceType() { return "keyboard"; }
export function Size() { return [15, 7]; }
export function DefaultPosition() { return [10, 100]; }
export function DefaultScale() { return Math.floor(85 / Size()[1]); }
export function ConflictingProcesses() { return ["via", "vial"]; }
/* global
shutdownColor:readonly
LightingMode:readonly
forcedColor:readonly
*/
export function ControllableParameters() {
return [
{"property":"shutdownColor", "group":"lighting", "label":"Shutdown Color", "description":"Color applied when SignalRGB shuts down", "min":"0", "max":"360", "type":"color", "default":"#000000"},
{"property":"LightingMode", "group":"lighting", "label":"Lighting Mode", "description":"Canvas pulls from active effect, Forced overrides to a specific color", "type":"combobox", "values":["Canvas", "Forced"], "default":"Canvas"},
{"property":"forcedColor", "group":"lighting", "label":"Forced Color", "description":"Color used when Forced mode is enabled", "min":"0", "max":"360", "type":"color", "default":"#009bde"},
];
}
// ── Constants ───────────────────────────────────────────────────────────────
const RS = 65; // Report size: 1 report ID + 64 data
const CMD_SET = 0x07;
const MODE_DIRECT = 45; // "Close All" = Direct Control mode
const MIN_INTERVAL_MS = 100; // Throttle: process at most 1 frame per 100ms (10 FPS)
const WRITES_PER_FRAME = 6; // Budget per frame (lower = less input lag)
const PAUSE_EVERY = 2; // Micro-pause every N writes (firmware breathing room)
const HYSTERESIS = 8; // Min H/S delta to trigger an update
// Throttle state — last processed frame timestamp
let lastRenderMs = 0;
// ── LED Mapping (82 LEDs) ───────────────────────────────────────────────────
const vKeyNames = [
// Row 0 (15 keys)
"Esc","1","2","3","4","5","6","7","8","9","0","-","=","Backspace","Delete",
// Row 1 (15 keys)
"Tab","Q","W","E","R","T","Y","U","I","O","P","[","]","\\","Page Up",
// Row 2 (14 keys — col 12 missing)
"CapsLock","A","S","D","F","G","H","J","K","L",";","'","Enter","Page Down",
// Row 3 (14 keys — col 1 missing)
"Left Shift","Z","X","C","V","B","N","M",",",".","/","Right Shift","Up","End",
// Row 4 (9 keys)
"Left Ctrl","Left Win","Left Alt","Space","Right Alt","Fn","Left Arrow","Down Arrow","Right Arrow",
// Light Bar (15 segments)
"Light Bar 1","Light Bar 2","Light Bar 3","Light Bar 4","Light Bar 5",
"Light Bar 6","Light Bar 7","Light Bar 8","Light Bar 9","Light Bar 10",
"Light Bar 11","Light Bar 12","Light Bar 13","Light Bar 14","Light Bar 15",
];
const vKeyPositions = [
[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[12,0],[13,0],[14,0],
[0,1],[1,1],[2,1],[3,1],[4,1],[5,1],[6,1],[7,1],[8,1],[9,1],[10,1],[11,1],[12,1],[13,1],[14,1],
[0,2],[1,2],[2,2],[3,2],[4,2],[5,2],[6,2],[7,2],[8,2],[9,2],[10,2],[11,2],[13,2],[14,2],
[0,3],[2,3],[3,3],[4,3],[5,3],[6,3],[7,3],[8,3],[9,3],[10,3],[11,3],[12,3],[13,3],[14,3],
[0,4],[1,4],[2,4],[5,4],[10,4],[11,4],[12,4],[13,4],[14,4],
[0,6],[1,6],[2,6],[3,6],[4,6],[5,6],[6,6],[7,6],[8,6],[9,6],[10,6],[11,6],[12,6],[13,6],[14,6],
];
const vKeyMatrix = [
[0,0],[0,1],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[0,8],[0,9],[0,10],[0,11],[0,12],[0,13],[0,14],
[1,0],[1,1],[1,2],[1,3],[1,4],[1,5],[1,6],[1,7],[1,8],[1,9],[1,10],[1,11],[1,12],[1,13],[1,14],
[2,0],[2,1],[2,2],[2,3],[2,4],[2,5],[2,6],[2,7],[2,8],[2,9],[2,10],[2,11],[2,13],[2,14],
[3,0],[3,2],[3,3],[3,4],[3,5],[3,6],[3,7],[3,8],[3,9],[3,10],[3,11],[3,12],[3,13],[3,14],
[4,0],[4,1],[4,2],[4,5],[4,10],[4,11],[4,12],[4,13],[4,14],
[5,0],[5,1],[5,2],[5,3],[5,4],[5,5],[5,6],[5,7],[5,8],[5,9],[5,10],[5,11],[5,12],[5,13],[5,14],
];
export function LedNames() { return vKeyNames; }
export function LedPositions() { return vKeyPositions; }
// ── State ───────────────────────────────────────────────────────────────────
const LED_COUNT = vKeyNames.length;
let lastH = new Array(LED_COUNT).fill(-100);
let lastS = new Array(LED_COUNT).fill(-100);
// Pre-allocated reusable packet buffer (avoids 600+ array allocations/sec)
const pkt = new Array(RS).fill(0);
// Scratch arrays for priority-based scheduling (zero allocation per frame)
const pendingIdx = new Uint8Array(LED_COUNT);
const pendingDelta = new Uint16Array(LED_COUNT);
let pendingCount = 0;
// Per-frame HSV cache (avoids re-reading device.color in the send pass)
const frameH = new Uint8Array(LED_COUNT);
const frameS = new Uint8Array(LED_COUNT);
// ── Lifecycle ───────────────────────────────────────────────────────────────
export function Initialize() {
// Set mode to Direct Control (45 = "Close All")
sendCmd([0x03, 0x02, MODE_DIRECT]);
device.pause(30);
// Set brightness to max
sendCmd([0x03, 0x01, 0xFF]);
device.pause(30);
// Set speed to 0
sendCmd([0x03, 0x03, 0x00]);
device.pause(30);
// Set lightstrip to static mode (Ch0 Cmd1 = 1)
sendCmd([0x00, 0x01, 0x01]);
device.pause(30);
// Force full refresh
lastH.fill(-100);
lastS.fill(-100);
}
export function Render() {
// ── Pass 0: temporal throttle — skip frames to give MCU matrix-scan headroom ──
// SignalRGB calls Render() ~60x/sec; at 100ms interval we process ~10x/sec.
// Skipped frames are "free" — delta grows between processed frames so the
// delta-sorted writes still prioritize the most visually important changes.
const nowMs = Date.now();
if (nowMs - lastRenderMs < MIN_INTERVAL_MS) return;
lastRenderMs = nowMs;
// ── Pass 1: scan ALL LEDs, collect changed ones with delta magnitude ──
pendingCount = 0;
for (let i = 0; i < LED_COUNT; i++) {
const pos = vKeyPositions[i];
let color;
if (LightingMode === "Forced") {
color = hexToRgb(forcedColor);
} else {
color = device.color(pos[0], pos[1]);
}
let h, s;
const brightness = Math.max(color[0], color[1], color[2]);
if (brightness < 15) {
h = 0;
s = 0;
} else {
const hsv = rgbToHsv(color[0], color[1], color[2]);
h = hsv[0];
s = hsv[1];
}
const dh = Math.abs(h - lastH[i]);
const ds = Math.abs(s - lastS[i]);
if (dh <= HYSTERESIS && ds <= HYSTERESIS) continue;
frameH[i] = h;
frameS[i] = s;
pendingIdx[pendingCount] = i;
pendingDelta[pendingCount] = dh + ds;
pendingCount++;
}
// ── Pass 2: sort by delta descending (biggest visual change first) ──
// Insertion sort — at most 82 elements, usually <10
for (let a = 1; a < pendingCount; a++) {
const keyA = pendingDelta[a];
const valA = pendingIdx[a];
let b = a - 1;
while (b >= 0 && pendingDelta[b] < keyA) {
pendingDelta[b + 1] = pendingDelta[b];
pendingIdx[b + 1] = pendingIdx[b];
b--;
}
pendingDelta[b + 1] = keyA;
pendingIdx[b + 1] = valA;
}
// ── Pass 3: send top WRITES_PER_FRAME with micro-pauses ──
let writes = 0;
for (let p = 0; p < pendingCount; p++) {
if (writes >= WRITES_PER_FRAME) break;
const i = pendingIdx[p];
const mat = vKeyMatrix[i];
sendPerKeyColorFast(mat[0], mat[1], frameH[i], frameS[i]);
lastH[i] = frameH[i];
lastS[i] = frameS[i];
writes++;
if (writes % PAUSE_EVERY === 0) device.pause(1);
}
if (writes > 0) {
applyColorsFast();
}
}
export function Shutdown(SystemSuspending) {
// Restore to Solid Color mode (1)
sendCmd([0x03, 0x02, 0x01]);
}
// ── Protocol ────────────────────────────────────────────────────────────────
function sendCmd(args) {
const packet = new Array(RS).fill(0);
packet[0] = 0x00;
packet[1] = CMD_SET;
for (let i = 0; i < args.length; i++) packet[2 + i] = args[i];
device.write(packet, RS);
}
// Reuses pre-allocated buffer — zero allocation per call
function sendPerKeyColorFast(row, col, hue, sat) {
pkt[0] = 0x00;
pkt[1] = CMD_SET;
pkt[2] = 0x00;
pkt[3] = 0x03;
pkt[4] = 0x00;
pkt[5] = row;
pkt[6] = col;
pkt[7] = sat;
pkt[8] = hue;
device.write(pkt, RS);
}
function applyColorsFast() {
pkt[0] = 0x00;
pkt[1] = CMD_SET;
pkt[2] = 0x00;
pkt[3] = 0x02;
pkt[4] = 0x00;
pkt[5] = 0x00;
pkt[6] = 0x00;
pkt[7] = 0x00;
pkt[8] = 0x00;
device.write(pkt, RS);
}
// ── Utilities ───────────────────────────────────────────────────────────────
function rgbToHsv(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
const d = max - min;
let h = 0;
if (d !== 0) {
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h /= 6;
}
const s = max === 0 ? 0 : d / max;
return [Math.round(h * 255), Math.round(s * 255)];
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
}
export function Validate(endpoint) {
return endpoint.usage === 0x61 && endpoint.usage_page === 0xFF60;
}