"""Shared helpers for talking to the HyperX Eve 1800 lighting interface. Confirmed working (2026-10-25): write every colour-data packet `44 02 00 + 20 x RGB`, THEN commit with `44 01 `. 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])