diff --git a/README.md b/README.md index 8867480..d6fe3b5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ The device settings have a "Key test" option (column or row sweep) to check the - The keyboard shows each packet as it arrives, so `len` must be a multiple of 3. With the stock plugin's 56, LEDs 18, 74 and 93 (W, O, Menu) are split across packets and flash random colours. - The keyboard falls back to its own effect when frames stop, so colours must be streamed. + The plugin only sends packets whose colours changed, plus a full frame every 500 ms. +- The keyboard echoes every colour packet's header back as an input report. The plugin waits + for that echo (up to 10 ms) before the next packet, as OpenRGB's EVision driver does. +- OpenRGB supports this keyboard (`EVision Keyboard 320F:502A`) but only through the saved + custom-colour command (`0x11` between begin `0x01` / end `0x02`), not a live mode. ## LED map @@ -32,3 +37,5 @@ Right Ctrl 101, arrows 109 / 117 / 125. - `tools/evision_led.ps1` sends test frames straight to the keyboard, e.g. `evision_led.ps1 -Leds "0=ff0000,21=00ff00" -Seconds 60` lights LED 0 red and 21 green for a minute. Used to map the LEDs; quit SignalRGB first so the two don't fight over the device. +- `tools/evision_listen.ps1` passively reads the RGB endpoint's input reports (the packet echoes) + for a few seconds, grouped by header. Safe to run while SignalRGB is streaming. diff --git a/Womier_K87_EVision.js b/Womier_K87_EVision.js index 1133784..2262379 100644 --- a/Womier_K87_EVision.js +++ b/Womier_K87_EVision.js @@ -85,6 +85,7 @@ export function Render() { } export function Shutdown(SystemSuspending) { + lastFullRefresh = 0; // always send the whole frame sendColors(SystemSuspending ? "#000000" : shutdownColor); } @@ -117,16 +118,37 @@ function sendColors(overrideColor) { writeRGBPackage(RGBData); } +// Last packet sent per offset, so unchanged packets can be skipped +let sentPackets = {}; +let lastFullRefresh = 0; + 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; + // Resend everything twice a second so the keyboard never falls back to its own effect + const now = Date.now(); + const fullRefresh = now - lastFullRefresh >= 500; + + if (fullRefresh) { + lastFullRefresh = now; + sentPackets = {}; + } + for (let offset = 0; offset < RGBData.length; offset += bytesToSend) { const data = RGBData.slice(offset, offset + bytesToSend); while (data.length < bytesToSend) { data.push(0); } + const key = `${bytesToSend}:${data.join(",")}`; + + if (sentPackets[offset] === key) { + continue; + } + + sentPackets[offset] = key; + 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) @@ -139,6 +161,10 @@ function writeRGBPackage(RGBData) { device.write(packet, 64); + // The keyboard echoes each packet back; wait for it (as OpenRGB does) so the next + // packet doesn't arrive while it's still processing this one + device.read([0x04], 64, 10); + // device.pause() rounds up to the Windows timer tick (up to ~15 ms), so only pause when asked if (lowFPS) { device.pause(3); } } diff --git a/tools/evision_listen.ps1 b/tools/evision_listen.ps1 new file mode 100644 index 0000000..ee43c99 --- /dev/null +++ b/tools/evision_listen.ps1 @@ -0,0 +1,35 @@ +# Passively reads input reports from the EVision RGB endpoint for a few seconds +param([double]$Seconds = 2, [string]$Match = 'VID_320F&PID_502A&MI_01&COL04') +Add-Type @" +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +public static class EVL { + [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] + public static extern SafeFileHandle CreateFile(string n, uint a, uint s, IntPtr sa, uint c, uint f, IntPtr t); + public static string Listen(string path, int ms) { + var h = CreateFile(path, 0x80000000, 3, IntPtr.Zero, 3, 0x40000000, IntPtr.Zero); + if (h.IsInvalid) return "open failed " + Marshal.GetLastWin32Error(); + var sb = new System.Text.StringBuilder(); + var counts = new System.Collections.Generic.Dictionary(); + using (var fs = new FileStream(h, FileAccess.Read, 64, true)) { + var end = DateTime.Now.AddMilliseconds(ms); int n = 0; + while (DateTime.Now < end) { + var buf = new byte[64]; + var t = fs.ReadAsync(buf, 0, 64); + if (!t.Wait(end - DateTime.Now > TimeSpan.Zero ? end - DateTime.Now : TimeSpan.Zero)) break; + n++; + var key = BitConverter.ToString(buf, 0, 8); + counts[key] = counts.ContainsKey(key) ? counts[key] + 1 : 1; + } + sb.AppendLine("reports: " + n); + foreach (var kv in counts) sb.AppendLine(kv.Value + "x " + kv.Key); + } + return sb.ToString(); + } +} +"@ +$dev = Get-PnpDevice -PresentOnly -Class HIDClass | Where-Object { $_.InstanceId -match [regex]::Escape($Match) } | Select-Object -First 1 +$path = '\\?\' + ($dev.InstanceId -replace '\\','#') + '#{4d1e55b2-f16f-11cf-88cb-001111000030}' +[EVL]::Listen($path, [int]($Seconds * 1000))