feat: COSMIC Applet, Razer Naga swap, eGPU udev, Pfadfixes

- 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
This commit is contained in:
Kay Türtscher 2026-06-29 21:22:18 +02:00
parent dc9f7a5948
commit 625a90b3e7
7 changed files with 210 additions and 51 deletions

View file

@ -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 |
---

View file

@ -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

View file

@ -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)

110
tools/razer-naga-swap.py Normal file
View file

@ -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()

View file

@ -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."""""

View file

@ -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"}))
print(json.dumps({"ok": False, "error": "need args"}))

View file

@ -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"