From 0b2a90ba940cc435391581a6124a1c99e7e56bf2 Mon Sep 17 00:00:00 2001 From: MangaValk Date: Wed, 28 Oct 2026 23:24:57 +0100 Subject: [PATCH] Wait for packet echoes and send only changed packets The keyboard echoes each colour packet back; wait for that echo before sending the next one, like OpenRGB's EVision driver, instead of firing all packets back to back. Skip packets whose colours did not change and resend the whole frame every 500 ms so the keyboard stays under plugin control. Add a passive listener tool for the echoes. Co-Authored-By: Claude Opus 5.5 --- README.md | 7 +++++++ Womier_K87_EVision.js | 26 ++++++++++++++++++++++++++ tools/evision_listen.ps1 | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 tools/evision_listen.ps1 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))