From 625a90b3e79237a9690bf58818c61420ac1fd8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kay=20T=C3=BCrtscher?= Date: Mon, 29 Jun 2026 21:22:18 +0200 Subject: [PATCH] feat: COSMIC Applet, Razer Naga swap, eGPU udev, Pfadfixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - COSMIC Applet: Pfad gefixt, Desktop-File korrigiert, systemd-User-Service - Razer Naga Left Handed: Button-Swap-Daemon (evdev/uinput) + udev + systemd - superx-ctrl.py: Pfade gefixt (FROSTBAY_BLE, tablet-rgb.py) - tablet-rgb.py: Dynamische HIDRAW-Erkennung statt hartkodiertem Pfad - README: Status aktualisiert (Tablet RGB, Keyboard RGB, Mainboard RGB) - udev: 99-razer-naga-swap.rules für Hotplug --- README.md | 5 +- configs/razer-naga-swap.service | 13 ++++ cosmic-applet-superx/src/app.rs | 2 +- tools/razer-naga-swap.py | 110 ++++++++++++++++++++++++++++ tools/superx-ctrl.py | 4 +- tools/tablet-rgb.py | 125 ++++++++++++++++++++------------ udev/99-razer-naga-swap.rules | 2 + 7 files changed, 210 insertions(+), 51 deletions(-) create mode 100644 configs/razer-naga-swap.service create mode 100644 tools/razer-naga-swap.py create mode 100644 udev/99-razer-naga-swap.rules diff --git a/README.md b/README.md index 6232840..7a623f6 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,10 @@ Betriebssystem: **Arch Linux** · Compositor: **Hyprland 0.55.4** · Shell: **Da | **DMS SuperXCtrl Widget** | ✅ Produktiv | GUI-Widget für Cooling/RGB-Steuerung im DMS-Panel | | **Turbo-Daemon** | ✅ Produktiv | Key-Combo-Listener für Turbo/Mode/KB-Tasten | | **HHD Patch** | ✅ Produktiv | Turbo-Bypass für Super X ohne oxp-platform | -| **Tablet RGB (HIDRAW)** | ✅ Produktiv | RGB-Steuerung des Tablet-Moduls via /dev/hidraw2 | +| **Tablet RGB (HIDRAW)** | ⚠️ Hardware fehlt | RGB-Steuerung des Tablet-Moduls via HIDRAW (1a86:1305 Interface 2) – Code fertig, aber Tablet-Modul noch nicht verbaut | +| **Keyboard RGB** | ❌ Nicht steuerbar | Keyboard-RGB kann nicht angesteuert werden | | **EC Turbo** | ✅ Produktiv | Turbo-Status lesen/setzen via EC (/sys/kernel/debug/ec/ec0/io) | -| **Mainboard RGB (WS2812B)** | 🔄 In Arbeit | Hardware-Reverse läuft — Logic Analyzer bestellt | +| **Mainboard RGB (WS2812B)** | 🔄 In Arbeit | Original-LEDs kommen morgen – dann Reverse-Engineering am Mainboard-RGB2-Anschluss | | **WMI/EC-Kernel-Module** | 🔄 In Arbeit | Erste Module kompilieren, aber noch nicht funktional | --- diff --git a/configs/razer-naga-swap.service b/configs/razer-naga-swap.service new file mode 100644 index 0000000..22217c3 --- /dev/null +++ b/configs/razer-naga-swap.service @@ -0,0 +1,13 @@ +[Unit] +Description=Razer Naga Left Handed - Button Swap Daemon +After=multi-user.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/razer-naga-swap.py +Restart=on-failure +RestartSec=5 +User=root + +[Install] +WantedBy=multi-user.target diff --git a/cosmic-applet-superx/src/app.rs b/cosmic-applet-superx/src/app.rs index 8cb0f6c..1d0b0b5 100644 --- a/cosmic-applet-superx/src/app.rs +++ b/cosmic-applet-superx/src/app.rs @@ -90,7 +90,7 @@ enum Message { fn run_ctrl(args: &[&str]) -> String { let home = env::var("HOME").unwrap_or_else(|_| "/home/nepharius".to_string()); - let script = PathBuf::from(&home).join("Projekte/superx-goes-arch/tools/superx-ctrl.py"); + let script = PathBuf::from(&home).join("git.nepharius.at/superx-goes-arch/tools/superx-ctrl.py"); let output = Command::new("python3") .arg(&script) .args(args) diff --git a/tools/razer-naga-swap.py b/tools/razer-naga-swap.py new file mode 100644 index 0000000..dafa641 --- /dev/null +++ b/tools/razer-naga-swap.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Razer Naga Left Handed Edition - Button Swapper +Debug version - shows which code fails. +""" +import evdev +from evdev import UInput, ecodes as e +import sys +import os + +VENDOR = 0x1532 +PRODUCT = 0x008d +UINPUT_NAME = "Razer Naga Left Handed (Swapped)" + + +def find_naga(): + for path in evdev.list_devices(): + try: + dev = evdev.InputDevice(path) + info = dev.info + if info.vendor == VENDOR and info.product == PRODUCT: + caps = dev.capabilities() + if e.EV_KEY in caps and e.BTN_LEFT in caps.get(e.EV_KEY, []): + dev.close() + return path + dev.close() + except: + continue + return None + + +def main(): + naga_path = find_naga() + if not naga_path: + print("Naga not found", file=sys.stderr) + sys.exit(1) + + naga = evdev.InputDevice(naga_path) + naga.grab() + + # Try each code individually to find the bad one + test_caps = { + e.EV_KEY: [e.BTN_LEFT, e.BTN_RIGHT], + e.EV_REL: [e.REL_X, e.REL_Y], + } + + for etype, codes in test_caps.items(): + for code in codes: + try: + ui = UInput( + name=UINPUT_NAME, + vendor=VENDOR, + product=PRODUCT, + version=1, + bustype=0x03, + events={etype: [code]}, + ) + ui.close() + print(f"OK: type={etype} code={code}", file=sys.stderr) + except Exception as ex: + print(f"FAIL: type={etype} code={code}: {ex}", file=sys.stderr) + + # Now create the real one with only working codes + caps = { + e.EV_KEY: [e.BTN_LEFT, e.BTN_RIGHT], + e.EV_REL: [e.REL_X, e.REL_Y], + } + + ui = UInput( + name=UINPUT_NAME, + vendor=VENDOR, + product=PRODUCT, + version=1, + bustype=0x03, + events=caps, + ) + + # Fork to background + if os.fork() > 0: + sys.exit(0) + os.setsid() + if os.fork() > 0: + sys.exit(0) + + try: + with open("/var/run/razer-naga-swap.pid", "w") as f: + f.write(str(os.getpid())) + + for event in naga.read_loop(): + if event.type == e.EV_KEY: + if event.code == e.BTN_LEFT: + ui.write(e.EV_KEY, e.BTN_RIGHT, event.value) + elif event.code == e.BTN_RIGHT: + ui.write(e.EV_KEY, e.BTN_LEFT, event.value) + elif event.type == e.EV_REL: + if event.code in (e.REL_X, e.REL_Y): + ui.write(e.EV_REL, event.code, event.value) + elif event.type == e.EV_SYN: + ui.write(e.EV_SYN, event.code, event.value) + ui.syn() + except: + pass + finally: + naga.ungrab() + naga.close() + ui.close() + + +if __name__ == "__main__": + main() diff --git a/tools/superx-ctrl.py b/tools/superx-ctrl.py index d999011..c7a2824 100644 --- a/tools/superx-ctrl.py +++ b/tools/superx-ctrl.py @@ -2,7 +2,7 @@ """superx-ctrl.py v4 — Frostbay cooling + RGB presets + Direct D-Bus writes.""" import json, sys, os, subprocess, time, dbus -FROSTBAY_BLE = os.path.expanduser("~/Projekte/superx-goes-arch/tools/frostbay_ble.py") +FROSTBAY_BLE = os.path.expanduser("~/git.nepharius.at/superx-goes-arch/tools/frostbay_ble.py") COOL_MODES = [ {"id": "off", "label": "Off", "icon": "power_settings_new"}, @@ -114,7 +114,7 @@ def cmd_rgb_off(): except Exception as e: return {"ok": False, "error": str(e)} -TABLET_CMD = os.path.expanduser("~/.local/bin/tablet-rgb.py") +TABLET_CMD = os.path.expanduser("~/git.nepharius.at/superx-goes-arch/tools/tablet-rgb.py") def cmd_tablet(*args): """Control tablet RGB via HIDRAW.""""" diff --git a/tools/tablet-rgb.py b/tools/tablet-rgb.py index 3a44da2..bdbc213 100644 --- a/tools/tablet-rgb.py +++ b/tools/tablet-rgb.py @@ -1,30 +1,61 @@ #!/usr/bin/env python3 -"""Test tablet RGB via HIDRAW — adapted from ps1x/onexplayer-superxcontrol for Super X.""" +"""Tablet RGB via HIDRAW — auto-detects the correct Super X HID node.""" import sys, os, json, time +from pathlib import Path -HIDRAW = "/dev/hidraw2" +VENDOR_ID = 0x1A86 +PRODUCT_ID = 0x1305 +INTERFACE = 2 # Interface 2 = RGB control PACKET_SIZE = 64 + +def read_attr(path: Path, name: str) -> str | None: + attr = path / name + if not attr.exists(): + return None + return attr.read_text(encoding="utf-8").strip() + + +def find_hidraw() -> str: + for hidraw in sorted(Path("/sys/class/hidraw").glob("hidraw*")): + device = (hidraw / "device").resolve() + usb_interface = device.parent + usb_device = usb_interface.parent + if read_attr(usb_device, "idVendor") != f"{VENDOR_ID:04x}": + continue + if read_attr(usb_device, "idProduct") != f"{PRODUCT_ID:04x}": + continue + if read_attr(usb_interface, "bInterfaceNumber") != f"{INTERFACE:02x}": + continue + return f"/dev/{hidraw.name}" + raise RuntimeError( + f"Super X HID not found (vendor=0x{VENDOR_ID:04x} " + f"product=0x{PRODUCT_ID:04x} interface={INTERFACE})" + ) + + def pad(prefix): return prefix + [0] * (PACKET_SIZE - len(prefix)) + def write_hidraw(data): - with open(HIDRAW, "r+b", buffering=0) as f: + path = find_hidraw() + with open(path, "r+b", buffering=0) as f: written = f.write(bytes(data)) return written + def cmd_static(r, g, b): - """Static color: 0x07 0xFF 0xFE + 20x RGB + 0x00""" payload = pad([0x07, 0xFF, 0xFE] + [r, g, b] * 20 + [0x00]) return write_hidraw(payload) + def cmd_preset(preset_id): - """Preset mode: 0x07 0xFF + preset_id + 0x00 0x00""" payload = pad([0x07, 0xFF, preset_id, 0x00, 0x00]) return write_hidraw(payload) + def cmd_brightness(mode): - """Brightness levels with RGB on/off""" levels = { "off": [0x07, 0xFF, 0xFD, 0x00, 0x05, 0x01], "low": [0x07, 0xFF, 0xFD, 0x01, 0x05, 0x01], @@ -34,51 +65,53 @@ def cmd_brightness(mode): payload = pad(levels.get(mode, levels["high"])) return write_hidraw(payload) + def cmd_on(): - """Enable RGB""" payload = pad([0x07, 0xFF, 0x05, 0x00]) return write_hidraw(payload) -if not os.access(HIDRAW, os.W_OK): - print(json.dumps({"ok": False, "error": f"no write access to {HIDRAW}"})) - sys.exit(1) -if len(sys.argv) >= 4: - # rgb R G B - r, g, b = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]) - # Step 1: brightness high first - cmd_brightness("high") - time.sleep(0.2) - # Step 2: static color - written = cmd_static(r, g, b) - print(json.dumps({"ok": True, "cmd": "static", "rgb": [r,g,b], "written": written})) -elif len(sys.argv) >= 2: - cmd = sys.argv[1] - if cmd == "on": +if __name__ == "__main__": + try: + # Quick accessibility check + find_hidraw() + except RuntimeError as e: + print(json.dumps({"ok": False, "error": str(e)})) + sys.exit(1) + + if len(sys.argv) >= 4: + r, g, b = int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]) cmd_brightness("high") time.sleep(0.2) - cmd_on() - print(json.dumps({"ok": True, "cmd": "on"})) - elif cmd == "off": - cmd_brightness("off") - print(json.dumps({"ok": True, "cmd": "off"})) - elif cmd == "rainbow": - cmd_brightness("high") - time.sleep(0.2) - cmd_preset(0x09) - print(json.dumps({"ok": True, "cmd": "rainbow", "preset": 0x09})) - elif cmd == "aurora": - cmd_brightness("high") - time.sleep(0.2) - cmd_preset(0x01) - print(json.dumps({"ok": True, "cmd": "aurora", "preset": 0x01})) - elif cmd == "preset" and len(sys.argv) >= 3: - pid = int(sys.argv[2], 0) - cmd_brightness("high") - time.sleep(0.2) - cmd_preset(pid) - print(json.dumps({"ok": True, "cmd": "preset", "preset": pid})) + written = cmd_static(r, g, b) + print(json.dumps({"ok": True, "cmd": "static", "rgb": [r, g, b], "written": written})) + elif len(sys.argv) >= 2: + cmd = sys.argv[1] + if cmd == "on": + cmd_brightness("high") + time.sleep(0.2) + cmd_on() + print(json.dumps({"ok": True, "cmd": "on"})) + elif cmd == "off": + cmd_brightness("off") + print(json.dumps({"ok": True, "cmd": "off"})) + elif cmd == "rainbow": + cmd_brightness("high") + time.sleep(0.2) + cmd_preset(0x09) + print(json.dumps({"ok": True, "cmd": "rainbow", "preset": 0x09})) + elif cmd == "aurora": + cmd_brightness("high") + time.sleep(0.2) + cmd_preset(0x01) + print(json.dumps({"ok": True, "cmd": "aurora", "preset": 0x01})) + elif cmd == "preset" and len(sys.argv) >= 3: + pid = int(sys.argv[2], 0) + cmd_brightness("high") + time.sleep(0.2) + cmd_preset(pid) + print(json.dumps({"ok": True, "cmd": "preset", "preset": pid})) + else: + print(json.dumps({"ok": False, "error": f"unknown: {cmd}"})) else: - print(json.dumps({"ok": False, "error": f"unknown: {cmd}"})) -else: - print(json.dumps({"ok": False, "error": "need args"})) \ No newline at end of file + print(json.dumps({"ok": False, "error": "need args"})) diff --git a/udev/99-razer-naga-swap.rules b/udev/99-razer-naga-swap.rules new file mode 100644 index 0000000..4ae970e --- /dev/null +++ b/udev/99-razer-naga-swap.rules @@ -0,0 +1,2 @@ +# Start Razer Naga button swapper when mouse is connected +ACTION=="add", SUBSYSTEM=="input", ATTRS{idVendor}=="1532", ATTRS{idProduct}=="008d", ENV{ID_INPUT_MOUSE}=="1", RUN+="/usr/local/bin/razer-naga-swap.py"