Files
Womier-K87-signal-rgb-plugin/Womier_K87_EVision.js
T

151 lines
6.8 KiB
JavaScript
Raw Normal View History

// Womier K87 (wired, EVision firmware, USB 320F:502A).
// Same per-key protocol as SignalRGB's built-in EVision plugin; LED numbering mapped
// by hand on 2026-10-26 (column * 8 + row, like the Redragon K557 without numpad).
export function Name() { return "Womier K87"; }
export function VendorId() { return 0x320F; }
export function ProductId() { return 0x502A; }
export function Publisher() { return "Custom (based on WhirlwindFx & Nuonuo EVision plugin)"; }
export function Size() { return [17, 6]; }
export function DefaultPosition() { return [10, 100]; }
export function DefaultScale() { return 8.0; }
export function DeviceType() { return "keyboard"; }
export function Validate(endpoint) { return endpoint.interface === 1; }
export function ImageUrl() { return "https://assets.signalrgb.com/devices/default/misc/usb-drive-render.png"; }
/* global
shutdownColor:readonly
LightingMode:readonly
forcedColor:readonly
lowFPS:readonly
2026-10-28 23:09:48 +01:00
keyTest:readonly
2026-10-28 23:14:21 +01:00
frameLimit: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"},
{property:"lowFPS", group:"lighting", label:"Low frame-rate mode", description: "Sends smaller packets with pauses in between; normally not needed", type:"boolean", default:false},
2026-10-28 23:14:21 +01:00
{property:"frameLimit", group:"lighting", label:"Frame rate limit", description: "Maximum frames per second sent to the keyboard", type:"combobox", values:["Unlimited", "30", "15", "10"], default:"Unlimited"},
2026-10-28 23:09:48 +01:00
{property:"keyTest", group:"lighting", label:"Key test", description: "Sweeps a white line across the keys to check the LED map", type:"combobox", values:["Off", "Column sweep", "Row sweep"], default:"Off"},
];
}
const vLedNames = [
"Esc", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "Print Screen", "Scroll Lock", "Pause Break",
"`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-_", "=+", "Backspace", "Insert", "Home", "Page Up",
"Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]", "\\", "Del", "End", "Page Down",
"CapsLock", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "'", "Enter",
"Left Shift", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "Right Shift", "Up Arrow",
"Left Ctrl", "Left Win", "Left Alt", "Space", "Right Alt", "Fn", "Menu", "Right Ctrl", "Left Arrow", "Down Arrow", "Right Arrow",
];
const vLeds = [
0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120,
1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, 97, 105, 113, 121, 115,
2, 10, 18, 26, 34, 42, 50, 58, 66, 74, 82, 90, 98, 106, 114, 122, 123,
3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 107,
4, 20, 28, 36, 44, 52, 60, 68, 76, 84, 92, 108, 116,
5, 13, 21, 45, 77, 85, 93, 101, 109, 117, 125,
];
const vLedPositions = [
[0, 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], [15, 0], [16, 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], [15, 1], [16, 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], [12, 2], [13, 2], [14, 2], [15, 2], [16, 2],
[0, 3], [1, 3], [2, 3], [3, 3], [4, 3], [5, 3], [6, 3], [7, 3], [8, 3], [9, 3], [10, 3], [11, 3], [13, 3],
[0, 4], [2, 4], [3, 4], [4, 4], [5, 4], [6, 4], [7, 4], [8, 4], [9, 4], [10, 4], [11, 4], [13, 4], [15, 4],
[0, 5], [1, 5], [2, 5], [6, 5], [10, 5], [11, 5], [12, 5], [13, 5], [14, 5], [15, 5], [16, 5],
];
// Highest LED index is 125; 126 slots = 378 bytes = exactly 7 packets of 54
const LED_SLOTS = 126;
export function LedNames() { return vLedNames; }
export function LedPositions() { return vLedPositions; }
export function Initialize() {
device.set_endpoint(1, 0x0092, 0xFF1C, 0x0004);
device.setName("Womier K87");
device.setControllableLeds(vLedNames, vLedPositions);
}
2026-10-28 23:14:21 +01:00
let lastFrameTime = 0;
export function Render() {
2026-10-28 23:14:21 +01:00
// The keyboard falls back to its own effect after about a second without frames,
// so the lowest limit (10 fps) still keeps it under plugin control
const now = Date.now();
if (frameLimit !== "Unlimited" && now - lastFrameTime < 1000 / parseInt(frameLimit)) {
return;
}
lastFrameTime = now;
sendColors();
}
export function Shutdown(SystemSuspending) {
sendColors(SystemSuspending ? "#000000" : shutdownColor);
}
function sendColors(overrideColor) {
const RGBData = new Array(LED_SLOTS * 3).fill(0);
2026-10-28 23:09:48 +01:00
const [width, height] = Size();
const step = Math.floor(Date.now() / 500);
for (let iIdx = 0; iIdx < vLeds.length; iIdx++) {
const [iPxX, iPxY] = vLedPositions[iIdx];
let color;
if (overrideColor) {
color = hexToRgb(overrideColor);
2026-10-28 23:09:48 +01:00
} else if (keyTest === "Column sweep") {
color = iPxX === step % width ? [255, 255, 255] : [0, 0, 0];
} else if (keyTest === "Row sweep") {
color = iPxY === step % height ? [255, 255, 255] : [0, 0, 0];
} else if (LightingMode === "Forced") {
color = hexToRgb(forcedColor);
} else {
color = device.color(iPxX, iPxY);
}
RGBData[vLeds[iIdx] * 3] = color[0];
RGBData[vLeds[iIdx] * 3 + 1] = color[1];
RGBData[vLeds[iIdx] * 3 + 2] = color[2];
}
writeRGBPackage(RGBData);
}
function writeRGBPackage(RGBData) {
// Must be a multiple of 3: the keyboard shows each packet as it arrives, so an LED split
// across two packets briefly flashes a mix of its old and new colour
const bytesToSend = lowFPS ? 48 : 54;
for (let offset = 0; offset < RGBData.length; offset += bytesToSend) {
const data = RGBData.slice(offset, offset + bytesToSend);
while (data.length < bytesToSend) { data.push(0); }
const packet = [0x04, 0x00, 0x00, 0x12, bytesToSend, offset & 0xFF, (offset >> 8) & 0xFF, 0x00].concat(data);
// Checksum: 16-bit sum of bytes 3..end (same result as the EVision plugin's formula)
let sum = 0;
for (let i = 3; i < packet.length; i++) { sum += packet[i]; }
packet[1] = sum & 0xFF;
packet[2] = (sum >> 8) & 0xFF;
device.write(packet, 64);
2026-10-28 23:09:48 +01:00
// device.pause() rounds up to the Windows timer tick (up to ~15 ms), so only pause when asked
if (lowFPS) { device.pause(3); }
}
}
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)];
}