feat: add ONEXPLAYER SUPER X support to oxp-sensors + Super X Control tools

- Patched oxpec.c with DMI match for 'ONEXPLAYER SUPER X' (oxp_x1)
- Added DKMS build files (dkms.conf, Makefile)
- Added superx-ctrl-menu.py (yad-based GUI menu)
- Added superx-tray.py (Qt6 system tray, optional)
- Fixed frostbay_ble.py path in superx-ctrl.py
This commit is contained in:
Kay Türtscher 2026-06-20 06:33:39 +02:00
parent cc2d513554
commit 1738820df5
7 changed files with 1439 additions and 1 deletions

7
patches/oxpec-Makefile Normal file
View file

@ -0,0 +1,7 @@
obj-m += oxpec.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

5
patches/oxpec-dkms.conf Normal file
View file

@ -0,0 +1,5 @@
PACKAGE_NAME=oxpec-superx
PACKAGE_VERSION=1.0
BUILT_MODULE_NAME[0]=oxpec
DEST_MODULE_LOCATION[0]=/kernel/drivers/platform/x86
AUTOINSTALL=yes

1008
patches/oxpec-superx.c Normal file

File diff suppressed because it is too large Load diff

183
tools/superx-ctrl-menu.py Normal file
View file

@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""
Super X Control - CLI Menu (zenity)
Einfach, robust, funktioniert immer.
Starte mit: python3 superx-ctrl-menu.py
"""
import sys
import os
import subprocess
import json
CTRL = os.path.expanduser("~/Projekte/superx-goes-arch/tools/superx-ctrl.py")
def run_cmd(*args):
try:
r = subprocess.run([sys.executable, CTRL] + list(args),
capture_output=True, text=True, timeout=10)
if r.returncode == 0 and r.stdout.strip():
return json.loads(r.stdout)
return {"ok": False, "error": r.stderr}
except Exception as e:
return {"ok": False, "error": str(e)}
def zenity_list(title, text, columns, items):
"""Zeigt eine zenity-Liste und gibt die Auswahl zurück."""
cols = "--column=" + " --column=".join(columns)
args = ["zenity", "--list", "--title=" + title, "--text=" + text,
"--width=400", "--height=400", cols]
for item in items:
args.append(str(item))
try:
r = subprocess.run(args, capture_output=True, text=True, timeout=30)
if r.returncode == 0:
return r.stdout.strip()
return None
except:
return None
def zenity_scale(title, text, value=50):
"""Zeigt einen Schieberegler."""
try:
r = subprocess.run(
["zenity", "--scale", "--title=" + title, "--text=" + text,
"--value=" + str(value), "--min-value=0", "--max-value=100",
"--step=5", "--width=400"],
capture_output=True, text=True, timeout=30)
if r.returncode == 0 and r.stdout.strip():
return int(r.stdout.strip())
return None
except:
return None
def zenity_question(text):
"""Ja/Nein Frage."""
try:
r = subprocess.run(
["zenity", "--question", "--text=" + text, "--width=300"],
capture_output=True, timeout=30)
return r.returncode == 0
except:
return False
def main_menu():
while True:
choice = zenity_list(
"Super X Control",
"Wähle eine Aktion:",
["Option"],
[
"❄️ Cooling Mode",
"🎮 Custom Fan/Pump",
"🌈 Frostbay RGB",
"💡 Tablet RGB",
"📊 Status",
"❌ Beenden"
]
)
if not choice or "Beenden" in choice:
break
elif "Cooling" in choice:
mode = zenity_list(
"Cooling Mode",
"Wähle Kühlmodus:",
["Modus"],
["⏹️ Off", "🔇 Silent", "🌬️ Gentle", "💨 Strong", "🚀 Extreme", "🤖 Smart"]
)
if mode:
mid = mode.split()[-1].lower()
if mid == "off": mid = "off"
elif mid == "silent": mid = "silent"
elif mid == "gentle": mid = "gentle"
elif mid == "strong": mid = "strong"
elif mid == "extreme": mid = "extreme"
elif mid == "smart": mid = "smart"
result = run_cmd("mode", mid)
if result.get("ok"):
subprocess.run(["notify-send", "Super X", f"Mode: {mid}"])
else:
subprocess.run(["notify-send", "-u", "critical", "Super X", f"Fehler: {result.get('error', '?')}"])
elif "Custom" in choice:
# Beide Regler in einem Fenster
try:
r = subprocess.run(
["yad", "--form", "--title=Lüfter & Pumpe",
"--text=Stelle Lüfter und Pumpe ein:",
"--field=Lüfter:SCL", "0:100:30",
"--field=Pumpe:SCL", "0:100:30",
"--width=400", "--button=Abbrechen:1", "--button=Anwenden:0"],
capture_output=True, text=True, timeout=60
)
if r.returncode == 0 and r.stdout.strip():
parts = r.stdout.strip().split("|")
if len(parts) >= 2:
fan = int(float(parts[0].strip()))
pump = int(float(parts[1].strip()))
result = run_cmd("custom", str(fan), str(pump))
if result.get("ok"):
subprocess.run(["notify-send", "Super X", f"Fan: {fan}% | Pump: {pump}%"])
else:
subprocess.run(["notify-send", "-u", "critical", "Super X", f"Fehler: {result.get('error', '?')}"])
except Exception as e:
subprocess.run(["notify-send", "-u", "critical", "Super X", f"Fehler: {e}"])
elif "Frostbay" in choice:
preset = zenity_list(
"Frostbay RGB",
"Wähle RGB-Preset:",
["Preset"],
["1 Static", "2 Breath", "3 Wave", "4 Strobe",
"5 Rainbow", "6 Marquee", "7 Meteor", "8 Fire",
"9 Aurora", "10 Pulse", "---", "RGB ON", "RGB OFF"]
)
if preset:
if "ON" in preset:
run_cmd("rgb-on")
subprocess.run(["notify-send", "Super X", "RGB ON"])
elif "OFF" in preset:
run_cmd("rgb-off")
subprocess.run(["notify-send", "Super X", "RGB OFF"])
elif preset.strip():
pid = preset.split()[0]
result = run_cmd("rgb-preset", pid)
if result.get("ok"):
subprocess.run(["notify-send", "Super X", f"RGB Preset: {pid}"])
elif "Tablet" in choice:
action = zenity_list(
"Tablet RGB",
"Wähle Aktion:",
["Aktion"],
["An", "Aus", "Regenbogen", "Aurora", "Farbe wählen"]
)
if action:
if action == "Farbe wählen":
color = zenity_list(
"Farbe",
"Wähle Farbe:",
["Farbe"],
["Rot", "Grün", "Blau", "Gelb", "Lila", "Weiß"]
)
if color:
colors = {"Rot": "255 0 0", "Grün": "0 255 0", "Blau": "0 0 255",
"Gelb": "255 255 0", "Lila": "255 0 255", "Weiß": "255 255 255"}
run_cmd("tablet", colors.get(color, "255 255 255"))
subprocess.run(["notify-send", "Super X", f"Tablet: {color}"])
else:
cmd = action.lower()
run_cmd("tablet", cmd)
subprocess.run(["notify-send", "Super X", f"Tablet: {action}"])
elif "Status" in choice:
result = run_cmd("status")
if result.get("ok"):
subprocess.run(["notify-send", "Super X", "Status OK ✓"])
else:
subprocess.run(["notify-send", "-u", "critical", "Super X", f"Fehler: {result.get('error', '?')}"])
if __name__ == "__main__":
main_menu()

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("~/git.nepharius.at/superx-goes-arch/tools/frostbay_ble.py")
FROSTBAY_BLE = os.path.expanduser("~/Projekte/superx-goes-arch/tools/frostbay_ble.py")
COOL_MODES = [
{"id": "off", "label": "Off", "icon": "power_settings_new"},

187
tools/superx-tray.py Normal file
View file

@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""
Super X Control - COSMIC System Tray (Qt6)
Steuert Frostbay Kühlung, RGB und Tablet-RGB
"""
import sys
import os
import subprocess
import signal
import functools
from PyQt6.QtCore import QProcess, QTimer
from PyQt6.QtGui import QIcon, QAction, QPixmap, QPainter, QColor, QFont
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSlider, QPushButton
CTRL = os.path.expanduser("~/Projekte/superx-goes-arch/tools/superx-ctrl.py")
def run_cmd(*args):
proc = QProcess()
proc.start(sys.executable, [CTRL] + list(args))
proc.waitForFinished(10000)
return proc.readAllStandardOutput().data().decode()
def create_icon():
pixmap = QPixmap(32, 32)
pixmap.fill(QColor(0, 0, 0, 0))
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setBrush(QColor(0, 150, 255))
painter.setPen(QColor(0, 100, 200))
painter.drawEllipse(2, 2, 28, 28)
painter.setPen(QColor(255, 255, 255))
font = QFont("sans-serif", 12, QFont.Weight.Bold)
painter.setFont(font)
painter.drawText(4, 4, 24, 24, 0x0084, "SX")
painter.end()
return QIcon(pixmap)
class CustomDialog(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("Lüfter & Pumpe")
self.setFixedSize(350, 200)
layout = QVBoxLayout()
# Fan
fan_row = QHBoxLayout()
fan_row.addWidget(QLabel("Lüfter:"))
self.fan_slider = QSlider()
self.fan_slider.setRange(0, 100)
self.fan_slider.setValue(30)
self.fan_label = QLabel("30%")
self.fan_slider.valueChanged.connect(lambda v: self.fan_label.setText(f"{v}%"))
fan_row.addWidget(self.fan_slider)
fan_row.addWidget(self.fan_label)
layout.addLayout(fan_row)
# Pump
pump_row = QHBoxLayout()
pump_row.addWidget(QLabel("Pumpe:"))
self.pump_slider = QSlider()
self.pump_slider.setRange(0, 100)
self.pump_slider.setValue(30)
self.pump_label = QLabel("30%")
self.pump_slider.valueChanged.connect(lambda v: self.pump_label.setText(f"{v}%"))
pump_row.addWidget(self.pump_slider)
pump_row.addWidget(self.pump_label)
layout.addLayout(pump_row)
# Buttons
btn_row = QHBoxLayout()
cancel_btn = QPushButton("Abbrechen")
cancel_btn.clicked.connect(self.reject)
apply_btn = QPushButton("Anwenden")
apply_btn.clicked.connect(self.accept)
btn_row.addWidget(cancel_btn)
btn_row.addWidget(apply_btn)
layout.addLayout(btn_row)
self.setLayout(layout)
def get_values(self):
return self.fan_slider.value(), self.pump_slider.value()
class SuperXTray:
def __init__(self):
self.app = QApplication(sys.argv)
self.app.setApplicationName("Super X Control")
self.app.setQuitOnLastWindowClosed(False)
self.tray = QSystemTrayIcon()
self.tray.setIcon(create_icon())
self.tray.setToolTip("Super X Control")
self._build_menu()
self.tray.setContextMenu(self.menu)
self.tray.activated.connect(self._on_activated)
self.tray.show()
def _make_cmd(self, *args):
"""Factory-Funktion: erzeugt eine Callback-Funktion mit eingefrorenen args"""
def cmd():
run_cmd(*args)
return cmd
def _show_custom_dialog(self):
dialog = CustomDialog()
if dialog.exec() == QDialog.DialogCode.Accepted:
fan, pump = dialog.get_values()
run_cmd("custom", str(fan), str(pump))
def _build_menu(self):
self.menu = QMenu()
title = self.menu.addAction("❄️ Cooling Modes")
title.setEnabled(False)
modes = [
("off", "⏹️ Off"),
("silent", "🔇 Silent"),
("gentle", "🌬️ Gentle"),
("strong", "💨 Strong"),
("extreme", "🚀 Extreme"),
("smart", "🤖 Smart"),
]
for mid, label in modes:
action = QAction(label, self.menu)
action.triggered.connect(self._make_cmd("mode", mid))
self.menu.addAction(action)
self.menu.addSeparator()
# Custom Fan/Pump
action = QAction("🎮 Custom Fan/Pump...", self.menu)
action.triggered.connect(self._show_custom_dialog)
self.menu.addAction(action)
self.menu.addSeparator()
title = self.menu.addAction("🌈 Frostbay RGB")
title.setEnabled(False)
rgb_presets = [
("1", "Static"), ("2", "Breath"), ("3", "Wave"), ("4", "Strobe"),
("5", "Rainbow"), ("6", "Marquee"), ("7", "Meteor"), ("8", "Fire"),
("9", "Aurora"), ("10", "Pulse"),
]
for pid, label in rgb_presets:
action = QAction(label, self.menu)
action.triggered.connect(self._make_cmd("rgb-preset", pid))
self.menu.addAction(action)
self.menu.addAction("RGB ON", self._make_cmd("rgb-on"))
self.menu.addAction("RGB OFF", self._make_cmd("rgb-off"))
self.menu.addSeparator()
title = self.menu.addAction("💡 Tablet RGB")
title.setEnabled(False)
self.menu.addAction("An", self._make_cmd("tablet", "on"))
self.menu.addAction("Aus", self._make_cmd("tablet", "off"))
self.menu.addAction("Regenbogen", self._make_cmd("tablet", "rainbow"))
self.menu.addAction("Aurora", self._make_cmd("tablet", "aurora"))
self.menu.addSeparator()
self.menu.addAction("❌ Beenden", self.app.quit)
def _on_activated(self, reason):
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
run_cmd("status")
def run(self):
sys.exit(self.app.exec())
if __name__ == "__main__":
while True:
try:
tray = SuperXTray()
tray.run()
except Exception as e:
print(f"Crash: {e}, restarting...", file=sys.stderr)
QTimer.singleShot(2000, lambda: None)
continue
break

48
tools/superx-tray.sh Normal file
View file

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Super X Control - COSMIC System Tray (yad)
set -e
CTRL="$HOME/Projekte/superx-goes-arch/tools/superx-ctrl.py"
# yad --notification mit flachem Menu (keine Submenüs möglich)
yad --notification \
--command="bash -c 'python3 $CTRL status && notify-send \"Super X\" \"Status OK\"'" \
--menu="\
⏹️ Off!bash -c 'python3 $CTRL mode off'|\
🔇 Silent!bash -c 'python3 $CTRL mode silent'|\
🌬️ Gentle!bash -c 'python3 $CTRL mode gentle'|\
💨 Strong!bash -c 'python3 $CTRL mode strong'|\
🚀 Extreme!bash -c 'python3 $CTRL mode extreme'|\
🤖 Smart!bash -c 'python3 $CTRL mode smart'|\
---!true|\
🎮 Custom Fan/Pump!bash -c '
FAN=\$(yad --scale --title=\"Lüfter\" --text=\"Lüftergeschwindigkeit:\" --value=30 --min-value=0 --max-value=100 --step=5 --width=400 --button=Abbrechen:1 --button=OK:0)
if [ \$? -eq 0 ] && [ -n \"\$FAN\" ]; then
PUMP=\$(yad --scale --title=\"Pumpe\" --text=\"Pumpengeschwindigkeit:\" --value=30 --min-value=0 --max-value=100 --step=5 --width=400 --button=Abbrechen:1 --button=OK:0)
if [ \$? -eq 0 ] && [ -n \"\$PUMP\" ]; then
python3 \"$CTRL\" custom \"\$FAN\" \"\$PUMP\"
notify-send \"Super X\" \"Fan: \$FAN% | Pump: \$PUMP%\"
fi
fi'|\
---!true|\
Static!bash -c 'python3 $CTRL rgb-preset 1'|\
Breath!bash -c 'python3 $CTRL rgb-preset 2'|\
Wave!bash -c 'python3 $CTRL rgb-preset 3'|\
Strobe!bash -c 'python3 $CTRL rgb-preset 4'|\
Rainbow!bash -c 'python3 $CTRL rgb-preset 5'|\
Marquee!bash -c 'python3 $CTRL rgb-preset 6'|\
Meteor!bash -c 'python3 $CTRL rgb-preset 7'|\
Fire!bash -c 'python3 $CTRL rgb-preset 8'|\
Aurora!bash -c 'python3 $CTRL rgb-preset 9'|\
Pulse!bash -c 'python3 $CTRL rgb-preset 10'|\
RGB ON!bash -c 'python3 $CTRL rgb-on'|\
RGB OFF!bash -c 'python3 $CTRL rgb-off'|\
---!true|\
Tablet An!bash -c 'python3 $CTRL tablet on'|\
Tablet Aus!bash -c 'python3 $CTRL tablet off'|\
Tablet Regenbogen!bash -c 'python3 $CTRL tablet rainbow'|\
Tablet Aurora!bash -c 'python3 $CTRL tablet aurora'|\
---!true|\
❌ Beenden!quit" \
--image="preferences-system" \
--text="Super X Control"