Files
Womier-K87-signal-rgb-plugin/Womier_K87_EVision.js
T
mangavalkandClaude Opus 5.5 822787ae0f Send 54-byte packets so no LED is split across packets
The keyboard applies each packet as it arrives. With 56 colour bytes per
packet, LEDs 18, 74 and 93 straddled two packets and briefly showed a mix of
old and new colour. 54 is a multiple of 3, and 7 x 54 covers exactly LEDs
0..125, so the frame no longer pads past the last LED either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 23:19:39 +01:00

152 lines
6.8 KiB
JavaScript

// 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
keyTest:readonly
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},
{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"},
{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);
}
let lastFrameTime = 0;
export function Render() {
// 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);
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);
} 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);
// 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)];
}