Files
signal-hyperx/HyperX_Eve_1800.js
steveandClaude Opus 5.5 5f36610a60 Initial HyperX Eve 1800 SignalRGB plugin and protocol tools
Working 10-zone plugin using the 0x44 live colour reports on the 0xFF90
vendor interface (data then commit). Includes Python tools used to
identify the device, confirm the protocol and map zones. Output is
dimmer than onboard effects; investigation pending.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EcbUcbNpyPn7cGpe4t6y5y
2026-10-25 16:45:27 -07:00

132 lines
4.2 KiB
JavaScript

export function Name() { return "HyperX Eve 1800"; }
export function VendorId() { return 0x03F0; }
export function ProductId() { return 0x08C2; }
export function Publisher() { return "Steve Ferguson"; }
export function Size() { return [21, 6]; }
export function DefaultPosition() { return [10, 100]; }
export function DefaultScale() { return 8.0; }
export function DeviceType() { return "keyboard"; }
export function ConflictingProcesses() { return ["NGenuity2.exe"]; }
/* global
shutdownColor:readonly
LightingMode:readonly
forcedColor:readonly
*/
export function ControllableParameters() {
return [
{"property":"shutdownColor", "group":"lighting", "label":"Shutdown Color", description: "This color is applied to the device when the System, or SignalRGB is shutting down", "min":"0", "max":"360", "type":"color", "default":"#000000"},
{"property":"LightingMode", "group":"lighting", "label":"Lighting Mode", description: "Determines where the device's RGB comes from. Canvas will pull from the active Effect, while Forced will override it to a specific color", "type":"combobox", "values":["Canvas", "Forced"], "default":"Canvas"},
{"property":"forcedColor", "group":"lighting", "label":"Forced Color", description: "The color used when 'Forced' Lighting Mode is enabled", "min":"0", "max":"360", "type":"color", "default":"#009bde"},
];
}
// The Eve 1800 has 10 lighting zones, each a vertical strip of the board,
// ordered left to right. X positions follow the physical key columns
// (roughly one unit per key) so effects sweep across the board evenly.
const vLedNames = [
"Zone 1 (Esc, F1-F4, Tab-Ctrl)",
"Zone 2 (2-4, E R, S D X)",
"Zone 3 (F5-F8, 5 6, T F G C V)",
"Zone 4 (7, Y H, B N, Space)",
"Zone 5 (8 9, U I J K M)",
"Zone 6 (0, O P L ;, , . /, Alt)",
"Zone 7 (- = Backspace, [ ] \\, Enter, Shift, Fn)",
"Zone 8 (F9-F12, Nav, Arrows)",
"Zone 9 (Numpad Left)",
"Zone 10 (Numpad Right)",
];
const vLedPositions = [
[1, 3], [3, 3], [5, 3], [7, 3], [9, 3],
[11, 3], [13, 3], [15, 3], [17, 3], [19, 3],
];
export function LedNames() { return vLedNames; }
export function LedPositions() { return vLedPositions; }
// Protocol (0xFF90 vendor interface, 64-byte reports, report ID 0x44):
// 44 02 <chunk> 00 + up to 20 x RGB colour data
// 44 01 <chunk count> apply ("commit") the frame
// The commit must come AFTER the data. These are live writes; nothing is
// saved to the keyboard. Without fresh frames the keyboard eventually falls
// back to its onboard effect, so we re-send periodically even when idle.
const KEEPALIVE_MS = 500;
let lastPacket = [];
let lastSendTime = 0;
export function Initialize() {
lastPacket = [];
lastSendTime = 0;
}
export function Render() {
sendColors();
}
export function Shutdown(SystemSuspending) {
const color = SystemSuspending ? "#000000" : shutdownColor;
sendColors(color, true);
}
function sendColors(overrideColor, force = false) {
const packet = [0x44, 0x02, 0x00, 0x00];
for (let iIdx = 0; iIdx < vLedPositions.length; iIdx++) {
let color;
if (overrideColor) {
color = hexToRgb(overrideColor);
} else if (LightingMode === "Forced") {
color = hexToRgb(forcedColor);
} else {
color = device.color(vLedPositions[iIdx][0], vLedPositions[iIdx][1]);
}
packet.push(color[0], color[1], color[2]);
}
const now = Date.now();
if (!force && arraysEqual(packet, lastPacket) && now - lastSendTime < KEEPALIVE_MS) {
return;
}
device.write(packet, 64);
device.write([0x44, 0x01, 0x01], 64);
lastPacket = packet;
lastSendTime = now;
}
function arraysEqual(a, b) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
const colors = [];
colors[0] = parseInt(result[1], 16);
colors[1] = parseInt(result[2], 16);
colors[2] = parseInt(result[3], 16);
return colors;
}
export function Validate(endpoint) {
return endpoint.interface === 2 && endpoint.usage === 0xFF00 && endpoint.usage_page === 0xFF90;
}
export function ImageUrl() {
return "https://assets.signalrgb.com/devices/default/keyboards/full-size-keyboard-render.png";
}