Add Womier K87 SignalRGB plugin and LED mapping tools

Per-key RGB for the wired Womier K87 (EVision firmware, 320F:502A) using the
EVision protocol, with the 87-key LED map found by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-28 23:05:05 +01:00
commit ba56c6f8de
4 files changed
+254

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
# Sends an EVision per-key colour frame (same packets as SignalRGB's EVision plugin)
# Usage: evision_led.ps1 -Leds "0=ff0000,21=00ff00" [-Fill 000000] [-Count 128] [-Seconds 0] [-SoftwareMode]
param(
[string]$Leds = '',
[string]$Fill = '000000',
[int]$Count = 128,
[double]$Seconds = 0,
[switch]$SoftwareMode,
[string]$Match = 'VID_320F&PID_502A&MI_01&COL04'
)
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class EV {
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern IntPtr CreateFile(string n, uint a, uint s, IntPtr sa, uint c, uint f, IntPtr t);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool WriteFile(IntPtr h, byte[] b, int n, out int w, IntPtr o);
[DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h);
}
"@
$dev = Get-PnpDevice -PresentOnly -Class HIDClass | Where-Object { $_.InstanceId -match [regex]::Escape($Match) } | Select-Object -First 1
if (-not $dev) { throw "RGB endpoint $Match not found" }
$path = '\\?\' + ($dev.InstanceId -replace '\\','#') + '#{4d1e55b2-f16f-11cf-88cb-001111000030}'
$h = [EV]::CreateFile($path, [uint32]3221225472, 3, [IntPtr]::Zero, 3, 0, [IntPtr]::Zero)
if ($h -eq [IntPtr]-1) { throw "open failed, error $([Runtime.InteropServices.Marshal]::GetLastWin32Error())" }
function Send([byte[]]$pkt) {
$buf = New-Object byte[] 64; [Array]::Copy($pkt, $buf, [Math]::Min(64, $pkt.Length))
$w = 0
if (-not [EV]::WriteFile($h, $buf, 64, [ref]$w, [IntPtr]::Zero)) { throw "write failed, error $([Runtime.InteropServices.Marshal]::GetLastWin32Error())" }
}
function Rgb([string]$hex) { $hex = $hex.TrimStart('#'); ,([byte[]]@([Convert]::ToByte($hex.Substring(0,2),16), [Convert]::ToByte($hex.Substring(2,2),16), [Convert]::ToByte($hex.Substring(4,2),16))) }
# Build the colour buffer
$data = New-Object byte[] ($Count * 3)
$f = Rgb $Fill
for ($i = 0; $i -lt $Count; $i++) { $data[$i*3] = $f[0]; $data[$i*3+1] = $f[1]; $data[$i*3+2] = $f[2] }
foreach ($e in ($Leds -split ',' | Where-Object { $_ })) {
$idx, $col = $e.Split('=')
if ($idx -match '^(\d+)-(\d+)$') { $range = [int]$Matches[1]..[int]$Matches[2] } else { $range = @([int]$idx) }
$c = Rgb $col
foreach ($i in $range) { if ($i -lt $Count) { $data[$i*3] = $c[0]; $data[$i*3+1] = $c[1]; $data[$i*3+2] = $c[2] } }
}
function SendFrame {
$per = 56
for ($off = 0; $off -lt $data.Length; $off += $per) {
$chunk = New-Object byte[] $per
[Array]::Copy($data, $off, $chunk, 0, [Math]::Min($per, $data.Length - $off))
$pkt = [byte[]](@(0x04, 0, 0, 0x12, $per, ($off -band 0xFF), (($off -shr 8) -band 0xFF), 0) + $chunk)
# checksum = 16-bit sum of bytes 3..63 (matches the plugin's formula)
$sum = 0; for ($k = 3; $k -lt 64; $k++) { $sum += $pkt[$k] }
$pkt[1] = $sum -band 0xFF; $pkt[2] = ($sum -shr 8) -band 0xFF
Send $pkt
Start-Sleep -Milliseconds 1
}
}
try {
if ($SoftwareMode) { Send ([byte[]]@(0x04, 0x8c, 0x00, 0x0b, 0x30, 0x50, 0x01)); Start-Sleep -Milliseconds 20 }
$end = (Get-Date).AddSeconds($Seconds); $n = 0
do { SendFrame; $n++; if ($Seconds -gt 0) { Start-Sleep -Milliseconds 100 } } while ((Get-Date) -lt $end)
"sent $n frame(s), $([Math]::Ceiling($data.Length / 56)) packets each"
} finally { [void][EV]::CloseHandle($h) }
+33
View File
@@ -0,0 +1,33 @@
param([string]$Match = 'vid_320f&pid_502a')
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
public static class H {
[StructLayout(LayoutKind.Sequential)] public struct CAPS {
public ushort Usage, UsagePage, InLen, OutLen, FeatLen;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=17)] public ushort[] R;
public ushort a,b,c,d,e,f,g,h,i,j;
}
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern IntPtr CreateFile(string n, uint a, uint s, IntPtr sa, uint c, uint f, IntPtr t);
[DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h);
[DllImport("hid.dll")] public static extern bool HidD_GetPreparsedData(IntPtr h, out IntPtr p);
[DllImport("hid.dll")] public static extern bool HidD_FreePreparsedData(IntPtr p);
[DllImport("hid.dll")] public static extern int HidP_GetCaps(IntPtr p, out CAPS c);
[DllImport("hid.dll", CharSet=CharSet.Unicode)] public static extern bool HidD_GetProductString(IntPtr h, StringBuilder b, int l);
[DllImport("hid.dll", CharSet=CharSet.Unicode)] public static extern bool HidD_GetManufacturerString(IntPtr h, StringBuilder b, int l);
}
"@
$guid = '{4d1e55b2-f16f-11cf-88cb-001111000030}'
Get-PnpDevice -PresentOnly -Class HIDClass,Keyboard | Where-Object { $_.InstanceId -match [regex]::Escape($Match) } | ForEach-Object {
$path = '\\?\' + ($_.InstanceId -replace '\\','#') + '#' + $guid
$h = [H]::CreateFile($path, 0, 3, [IntPtr]::Zero, 3, 0, [IntPtr]::Zero)
if ($h -eq [IntPtr]-1) { "$($_.InstanceId): open failed"; return }
$p = [IntPtr]::Zero; [void][H]::HidD_GetPreparsedData($h, [ref]$p)
$c = New-Object H+CAPS; [void][H]::HidP_GetCaps($p, [ref]$c)
$ps = New-Object Text.StringBuilder 256; [void][H]::HidD_GetProductString($h, $ps, 512)
$ms = New-Object Text.StringBuilder 256; [void][H]::HidD_GetManufacturerString($h, $ms, 512)
'{0,-50} page=0x{1:X4} usage=0x{2:X4} in={3} out={4} feat={5} prod="{6}" mfr="{7}"' -f $_.InstanceId.Split('\')[1], $c.UsagePage, $c.Usage, $c.InLen, $c.OutLen, $c.FeatLen, $ps, $ms
[void][H]::HidD_FreePreparsedData($p); [void][H]::CloseHandle($h)
}