Initial HyperX Eve 1800 SignalRGB plugin and protocol tools
Working 10-zone plugin using the 0x44 live colour reports on the 0xFF90 vendor interface (data then commit). Includes Python tools used to identify the device, confirm the protocol and map zones. Output is dimmer than onboard effects; investigation pending. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EcbUcbNpyPn7cGpe4t6y5y
This commit is contained in:
commit
5f36610a60
11 files changed
+491
No files matched your search
@@ -0,0 +1,89 @@
|
||||
"""Shared helpers for talking to the HyperX Eve 1800 lighting interface.
|
||||
|
||||
Confirmed working (2026-10-25): write every colour-data packet
|
||||
`44 02 <chunk> 00 + 20 x RGB`, THEN commit with `44 01 <chunk count>`.
|
||||
No init packet needed. Only report 0x44 is used; undo by replugging.
|
||||
"""
|
||||
import time
|
||||
|
||||
import hid
|
||||
|
||||
VID, PID, USAGE_PAGE = 0x03F0, 0x08C2, 0xFF90
|
||||
REPORT_LEN = 64 # 1 report-ID byte + 63 data bytes (from descriptor)
|
||||
LEDS_PER_PACKET = 20 # 60 RGB bytes after the 4-byte header
|
||||
|
||||
|
||||
def pad(pkt):
|
||||
return bytes(pkt + [0] * (REPORT_LEN - len(pkt)))
|
||||
|
||||
|
||||
def build_packets(colors, order="commit"):
|
||||
"""colors: list of (r, g, b), one per LED slot."""
|
||||
data = [c for rgb in colors for c in rgb]
|
||||
chunks = [data[i:i + LEDS_PER_PACKET * 3] for i in range(0, len(data), LEDS_PER_PACKET * 3)]
|
||||
frames = [pad([0x44, 0x02, idx, 0x00] + chunk) for idx, chunk in enumerate(chunks)]
|
||||
header = pad([0x44, 0x01, len(frames)])
|
||||
return frames + [header] if order == "commit" else [header] + frames
|
||||
|
||||
|
||||
def open_device():
|
||||
info = next((d for d in hid.enumerate(VID, PID) if d["usage_page"] == USAGE_PAGE), None)
|
||||
if info is None:
|
||||
raise SystemExit("HyperX Eve 1800 lighting interface not found - is it plugged in?")
|
||||
dev = hid.device()
|
||||
dev.open_path(info["path"])
|
||||
return dev
|
||||
|
||||
|
||||
def read_replies(dev, timeout_ms=150):
|
||||
replies = []
|
||||
deadline = time.monotonic() + timeout_ms / 1000
|
||||
while time.monotonic() < deadline:
|
||||
r = dev.read(REPORT_LEN, 20)
|
||||
if r:
|
||||
replies.append(bytes(r))
|
||||
return replies
|
||||
|
||||
|
||||
def send(colors, order="commit", verbose=True):
|
||||
packets = build_packets(colors, order)
|
||||
dev = open_device()
|
||||
try:
|
||||
for p in packets:
|
||||
if verbose:
|
||||
print("TX", p[:16].hex(" "), "...")
|
||||
if dev.write(p) <= 0:
|
||||
print("WRITE FAILED:", dev.error())
|
||||
for r in read_replies(dev):
|
||||
if verbose:
|
||||
print("RX", r[:16].hex(" "), "...")
|
||||
finally:
|
||||
dev.close()
|
||||
|
||||
|
||||
def stream(colors, seconds, fps=10, order="commit"):
|
||||
"""Re-send the same frame repeatedly, like SignalRGB's Render() loop."""
|
||||
packets = build_packets(colors, order)
|
||||
dev = open_device()
|
||||
dev.set_nonblocking(True)
|
||||
frames = errors = 0
|
||||
end = time.monotonic() + seconds
|
||||
try:
|
||||
while time.monotonic() < end:
|
||||
for p in packets:
|
||||
if dev.write(p) <= 0:
|
||||
errors += 1
|
||||
while dev.read(REPORT_LEN): # drain replies so the input queue never fills
|
||||
pass
|
||||
frames += 1
|
||||
time.sleep(1 / fps)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
dev.close()
|
||||
print(f"sent {frames} frames, {errors} write errors")
|
||||
|
||||
|
||||
def hex_rgb(s):
|
||||
b = bytes.fromhex(s.lstrip("#"))
|
||||
return (b[0], b[1], b[2])
|
||||
Reference in new issue
Block a user