Drop the per-packet device.pause(1), which rounds up to the Windows timer tick and capped the keyboard at roughly 10 fps; keep a pause in low frame-rate mode. Add a "Key test" option with column and row sweeps to verify the LED map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
6.1 KiB
JavaScript
137 lines
6.1 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
|
|
*/
|
|
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, can improve stability", type:"boolean", default:false},
|
|
{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; 128 slots = 384 bytes = 7 full packets of 56
|
|
const LED_SLOTS = 128;
|
|
|
|
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);
|
|
}
|
|
|
|
export function Render() {
|
|
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) {
|
|
const bytesToSend = lowFPS ? 48 : 56;
|
|
|
|
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)];
|
|
}
|