Add Lian Li SL-INF wireless tools and SignalRGB plugin

Proof-of-concept tools for the UNI FAN SL-INF Wireless kit (SLV3 TX/RX
dongles 0416:8040/8041): a Python scan/colour script, tinyuz helpers, a
JavaScript tinyuz encoder, the SignalRGB plugin and tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-26 00:12:23 +01:00
commit 35240e5dfc
13 files changed
+971

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
# Send colours compressed by the JS encoder (tuz.js) and check the fan confirms them.
import os, subprocess, sys, time
TOOLS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
sys.path.insert(0, TOOLS)
import lianli_poc as p
def js_tinyuz(raw):
js = "const {tuzCompress}=require(process.argv[1]);process.stdout.write(Buffer.from(tuzCompress([...Buffer.from(process.argv[2],'hex')])))"
return subprocess.run(["node", "-e", js, TOOLS + "/tuz.js", raw.hex()], capture_output=True, check=True).stdout
p.tinyuz = js_tinyuz
tx, rx = p.Dongle(p.TX_ID, "TX"), p.Dongle(p.RX_ID, "RX")
try:
master, ch, _ = p.find_master(tx)
dev = [d for d in p.get_devices(rx) if d["master"] == master][0]
# gradient frame exercises dict matches and literals, then back to purple
grad = [(i * 5, 0, 255 - i * 5) for i in range(44)]
for name, colors in [("gradient", grad), ("purple", [(128, 0, 255)] * 44)]:
up = p.build_upload(colors)
# reuse set_color's send path with a custom frame
n = (len(up["compressed"]) + p.CHUNK_BYTES - 1) // p.CHUNK_BYTES + 1
for i in range(n):
pkt = p.rgb_packet(up, dev["mac"], master, i)
for _ in range(2 if i == 0 else 1):
p.send_rf(tx, ch, dev["rx"], pkt); time.sleep(0.002)
ok = p.wait_for_effect(rx, dev["mac"], up["effect"])
print(name, len(up["compressed"]), "bytes", "confirmed" if ok else "NOT confirmed")
time.sleep(3)
finally:
tx.close(); rx.close()
+25
View File
@@ -0,0 +1,25 @@
const { tuzCompress } = require("../tuz.js");
const { execFileSync } = require("child_process");
let rnd = 12345;
const rand = () => (rnd = (rnd * 1103515245 + 12345) & 0x7fffffff) & 0xff;
const cases = [];
for (const leds of [44, 88, 176]) {
cases.push(Array.from({ length: leds * 3 }, (_, i) => [255, 0, 128][i % 3]));
cases.push(Array.from({ length: leds * 3 }, (_, i) => (i * 7) & 0xff));
cases.push(Array.from({ length: leds * 3 }, rand));
cases.push(Array.from({ length: leds * 3 }, (_, i) => (Math.floor(i / 30) * 40) & 0xff));
}
for (let t = 0; t < 200; t++) {
const n = 1 + (rand() * 7) % 700;
const alph = 1 + rand() % 6;
cases.push(Array.from({ length: n }, () => rand() % alph * 50));
}
let ok = 0;
for (const c of cases) {
const code = Buffer.from(tuzCompress(c));
const out = execFileSync(require("path").join(__dirname, "..", "tuzd.exe"), [String(c.length)], { input: code });
if (Buffer.compare(out, Buffer.from(c)) !== 0) { console.log("MISMATCH len", c.length); process.exit(1); }
ok++;
}
const solid = tuzCompress(Array.from({ length: 132 }, (_, i) => [128, 0, 255][i % 3]));
console.log(`${ok}/${cases.length} round-trips ok; solid 44 LEDs -> ${solid.length} bytes`);
+19
View File
@@ -0,0 +1,19 @@
# Watch the fan's effect id via the RX dongle only (TX stays free for SignalRGB).
import os, sys, time
TOOLS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
sys.path.insert(0, TOOLS)
import lianli_poc as p
rx = p.Dongle(p.RX_ID, "RX")
try:
seen = []
end = time.time() + (float(sys.argv[1]) if len(sys.argv) > 1 else 10)
while time.time() < end:
for d in p.get_devices(rx):
e = d["effect"].hex()
if not seen or seen[-1] != e:
seen.append(e)
print(f"{time.strftime('%H:%M:%S')} effect {e}")
time.sleep(0.2)
print(f"{len(seen)} distinct effect ids")
finally:
rx.close()