From d477b99c735b91758e917e0af0cfda3d387424e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kay=20T=C3=BCrtscher?= Date: Fri, 19 Jun 2026 17:28:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20vollst=C3=A4ndige=20SuperX-Steuerung=20?= =?UTF-8?q?dokumentiert=20&=20abgelegt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neu hinzugefügt: - tools/superx-ctrl.py — zentrale CLI-Steuerung (Frostbay + Tablet RGB + EC Turbo) - tools/tablet-rgb.py — Tablet RGB via HIDRAW - tools/ec-read.py — EC Turbo Status Reader - tools/turbo-*.py — 7 Turbo-Debug/Listener-Scripts - tools/patch-hhd-superx.py — HHD Patch für Super X Turbo-Bypass - udev/99-superx-hid.rules — HIDRAW-Zugriffsregel (0x1A86:0x1305) - dms/plugins/SuperXCtrl/ — DMS Widget (plugin.json + QML + Settings) - README.md — vollständig überarbeitet mit Architektur, Nutzung, Verzeichnisstruktur Geändert: - tools/frostbay_ble.py — rgb-brightness Subcommand hinzugefügt (0xFD Mode) --- README.md | 365 +++++++++++++++++--- dms/plugins/SuperXCtrl/SuperXWidget.qml | 275 +++++++++++++++ dms/plugins/SuperXCtrl/plugin.json | 12 + dms/plugins/SuperXCtrl/plugin_settings.json | 5 + tools/ec-read.py | 42 +++ tools/frostbay_ble.py | 15 +- tools/patch-hhd-superx.py | 58 ++++ tools/superx-ctrl.py | 170 +++++++++ tools/tablet-rgb.py | 84 +++++ tools/turbo-all.py | 56 +++ tools/turbo-atkbd.py | 24 ++ tools/turbo-daemon.py | 69 ++++ tools/turbo-deep.py | 55 +++ tools/turbo-hid.py | 21 ++ tools/turbo-monitor.py | 21 ++ tools/turbo-scanner.py | 25 ++ udev/99-superx-hid.rules | 1 + 17 files changed, 1254 insertions(+), 44 deletions(-) create mode 100644 dms/plugins/SuperXCtrl/SuperXWidget.qml create mode 100644 dms/plugins/SuperXCtrl/plugin.json create mode 100644 dms/plugins/SuperXCtrl/plugin_settings.json create mode 100755 tools/ec-read.py create mode 100644 tools/patch-hhd-superx.py create mode 100644 tools/superx-ctrl.py create mode 100644 tools/tablet-rgb.py create mode 100644 tools/turbo-all.py create mode 100644 tools/turbo-atkbd.py create mode 100644 tools/turbo-daemon.py create mode 100644 tools/turbo-deep.py create mode 100644 tools/turbo-hid.py create mode 100644 tools/turbo-monitor.py create mode 100644 tools/turbo-scanner.py create mode 100644 udev/99-superx-hid.rules diff --git a/README.md b/README.md index 42c7d78..6232840 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,340 @@ # SuperX-goes-Arch -Arbeitsrepo für Linux-/Arch-Support auf dem OneXplayer Super X. +Arbeitsrepo für Linux-/Arch-Support auf dem **OneXPlayer Super X** (Ryzen AI Max+ 395, Radeon 8060S, 64GB). -## Aktueller Stand (2026-06-05) +Betriebssystem: **Arch Linux** · Compositor: **Hyprland 0.55.4** · Shell: **DankMaterialShell (DMS)** -### Fertig +--- -| Bereich | Status | +## Übersicht + +| Bereich | Status | Beschreibung | +|---------|--------|-------------| +| **Frostbay BLE (Kühlung)** | ✅ Produktiv | Fan/Pumpe via BlueZ/D-Bus steuern | +| **Frostbay RGB** | ⚠️ Funktional | BLE-Writes funktionieren, aber LED-Strips physisch nicht angeschlossen | +| **superx-ctrl.py** | ✅ Produktiv | Zentrale CLI-Steuerung (Frostbay + Tablet RGB + EC Turbo) | +| **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 | +| **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 | +| **WMI/EC-Kernel-Module** | 🔄 In Arbeit | Erste Module kompilieren, aber noch nicht funktional | + +--- + +## Verzeichnisstruktur + +``` +superx-goes-arch/ +├── README.md # Diese Datei +├── docs/ +│ ├── frostbay-ble-protocol.md # BLE GATT Protokoll (vollständig dokumentiert) +│ ├── frostbay-onec1-initial-findings.md +│ ├── frostbay-onec1-protocol.md +│ ├── frostbay-protocol-notes.md +│ ├── internal-rgb-*.md # Mainboard RGB Research +│ ├── egpu-setup.md # eGPU Setup (RX 9060 XT) +│ ├── live-hid-inventory.md # HID Device Inventory +│ ├── hidraw1-feature-probe.md +│ ├── research-summary.md +│ ├── sources.md +│ └── ... +├── tools/ +│ ├── frostbay_ble.py # Frostbay BLE GATT Reader/Controller +│ ├── superx-ctrl.py # Zentrale CLI-Steuerung (v4) +│ ├── tablet-rgb.py # Tablet RGB via HIDRAW +│ ├── ec-read.py # EC Turbo Status Reader +│ ├── turbo-daemon.py # Turbo/FN Key Combo Daemon +│ ├── turbo-monitor.py # evdev Monitor für Turbo-Tasten +│ ├── turbo-hid.py # HIDRAW Turbo-Listener +│ ├── turbo-all.py # Multi-Source Turbo-Scanner (ACPI + evdev + HID) +│ ├── turbo-deep.py # Deep Turbo-Scanner (CoolingSystem + HID + Keyboard) +│ ├── turbo-atkbd.py # AT Keyboard Turbo-Listener +│ ├── turbo-scanner.py # Alle evdev-Geräte scannen +│ ├── patch-hhd-superx.py # HHD Patch: Turbo-Bypass für Super X +│ ├── superx_hid_*.py # HID Reverse-Engineering Tools +│ ├── superx_rgb_*.py # RGB Reverse-Engineering Tools +│ ├── superx_wmi_*.py/sh # WMI/EC Tools +│ ├── superx-egpu-perf.sh # eGPU Performance Tuning +│ ├── superx-game-wrapper.sh # Game Wrapper mit GPU-Selection +│ ├── steam-egpu* # Steam eGPU Integration +│ └── wmi/ # WMI Kernel Module Source +├── udev/ +│ ├── 99-superx-hid.rules # HIDRAW-Zugriff für Super X Vendor HID +│ └── 99-superx-rgb-hidraw.rules # Alternative HIDRAW-Regeln (älter) +├── configs/ +│ ├── hyprland.conf # Hyprland Konfiguration +│ ├── amdgpu-egpu.conf # AMDGPU eGPU Tuning +│ └── steam-egpu.desktop # Steam Desktop Entry mit eGPU +└── dms/ + └── plugins/ + └── SuperXCtrl/ + ├── plugin.json # DMS Plugin-Metadaten + ├── plugin_settings.json # Plugin-Settings (enabled) + └── SuperXWidget.qml # QML Widget (Cooling + RGB Tabs) +``` + +--- + +## Frostbay BLE (Kühlung) + +Die Frostbay ONEC1 Wasserkühlung kommuniziert via **BLE GATT** (nicht HID!). + +### Verbindung + +| Eigenschaft | Wert | |---|---| -| **Frostbay BLE (Drosselung)** | Produktiv — `tools/frostbay_ble.py` steuert Fan/Pumpe via BlueZ/D-Bus | -| **Frostbay Status** | Live-Read von Temp, RPM, Flow, Alerts via BLE GATT | -| **Niri + DMS** | Installiert & konfiguriert — niri 26.04 + DankMaterialShell 1.4.6 | -| **Protokoll-Doku** | `docs/frostbay-ble-protocol.md` — vollständig dokumentiert | +| Gerätename | `CoolingSystem_ONEC1` | +| BT-Adresse | variabel (z.B. `C8:17:17:F5:C8:62`) | +| Service | `0000ffe0-0000-1000-8000-00805f9b34fb` | +| Char (R/W) | `0000ffe1-0000-1000-8000-00805f9b34fb` (64 Byte Blocks) | -### In Arbeit / Blockiert - -| Bereich | Status | -|---|---| -| Frostbay RGB | Funktional via BLE, aber LED-Strips physisch nicht angeschlossen | -| Mainboard RGB (WS2812B) | Hardware-Reverse läuft — Logic Analyzer bestellt, `tools/superx_signal_decode.py` vorbereitet | -| WMI/EC-Kernel-Module | Erste Module kompilieren, aber noch nicht funktional | - -## Ordner - -- `docs/` — Protokolldokumentation, Research-Notizen -- `tools/` — Python-Skripte und WMI-Kernel-Module -- `config/` — Niri/Wayland-Konfigurationen - -## Frostbay-Steuerung +### Nutzung ```bash -# Status check -sudo python3 tools/frostbay_ble.py status +# Status abfragen +python3 tools/frostbay_ble.py status -# Drosseln (custom mode) -sudo python3 tools/frostbay_ble.py mode custom --fan 25 --pump 25 +# Modus setzen +python3 tools/frostbay_ble.py mode silent +python3 tools/frostbay_ble.py mode custom --fan 25 --pump 25 # Ausschalten -sudo python3 tools/frostbay_ble.py off +python3 tools/frostbay_ble.py off + +# Raw-Dump +python3 tools/frostbay_ble.py raw ``` +**Wichtig:** D-Bus (BlueZ) muss für den User zugänglich sein. Standardmässig via `TAG+="uaccess"` in der udev-Regel. Kein sudo nötig. + Details: `docs/frostbay-ble-protocol.md` -## Niri + DankMaterialShell +--- -Niri ist ein scrollend-kachelnder Wayland-Compositor, DMS liefert -Material-3-Shell mit Panel, Launcher, Notification-Center und mehr. -Konfiguration liegt unter `config/niri/`. +## superx-ctrl.py (Zentrale Steuerung) + +`tools/superx-ctrl.py` ist die zentrale CLI-Schnittstelle, die alle Subsysteme zusammenfasst. + +### Installation ```bash -# Installation (bereits erfolgt) -sudo pacman -S dms-shell dms-shell-niri matugen +# Kopieren nach ~/.local/bin/ für PATH-Zugriff +cp tools/superx-ctrl.py ~/.local/bin/superx-ctrl.py +chmod +x ~/.local/bin/superx-ctrl.py ``` +### Nutzung + +```bash +# Status (Frostbay + verfügbare Modi) +superx-ctrl.py status + +# Frostbay Cooling Mode +superx-ctrl.py mode off|silent|gentle|strong|extreme|smart + +# Custom Fan/Pump +superx-ctrl.py custom 30 30 + +# Frostbay RGB Preset (1-10) +superx-ctrl.py rgb-preset 9 + +# Frostbay RGB Helligkeit +superx-ctrl.py rgb-brightness +superx-ctrl.py rgb-on +superx-ctrl.py rgb-off + +# Tablet RGB (HIDRAW) +superx-ctrl.py tablet on|off|rainbow|aurora +superx-ctrl.py tablet # Static Color + +# EC Turbo +superx-ctrl.py ec-status +superx-ctrl.py ec-turbo-on +superx-ctrl.py ec-turbo-off +``` + +### Architektur + +``` +superx-ctrl.py +├── Frostbay BLE (D-Bus/BlueZ) → tools/frostbay_ble.py +│ ├── Cooling Modes (off/silent/gentle/strong/extreme/smart/custom) +│ ├── RGB Presets (1-10) via 2-Step BLE Write +│ └── RGB Brightness/Switch/Speed +├── Tablet RGB (HIDRAW) → tools/tablet-rgb.py +│ ├── Static Color (R G B) +│ ├── Presets (rainbow, aurora) +│ └── On/Off +└── EC Turbo (debugfs) → tools/ec-read.py + ├── Turbo Status lesen + └── Turbo setzen (0x30 = 0x06/0x00) +``` + +--- + +## DMS SuperXCtrl Widget + +Das DMS-Widget bietet eine GUI für Cooling- und RGB-Steuerung im DMS-Panel. + +### Installation + +```bash +# Plugin-Verzeichnis anlegen +mkdir -p ~/.config/DankMaterialShell/plugins/SuperXCtrl/ + +# Dateien kopieren +cp dms/plugins/SuperXCtrl/* ~/.config/DankMaterialShell/plugins/SuperXCtrl/ + +# Plugin aktivieren (plugin_settings.json) +# → {"superXCtrl": {"enabled": true}} +``` + +### Features + +- **Cooling Tab:** 6 Modes (Off/Silent/Gentle/Strong/Extreme/Smart) + Custom Fan/Pump Slider +- **RGB Tab:** 10 Frostbay RGB Presets + On/Off + Tablet RGB (4 Presets + 6 Farben) +- **Status-Pill** in der DMS-Bar: zeigt an ob Cooling aktiv ist + +### QML-Architektur + +Das Widget (`SuperXWidget.qml`) ruft `superx-ctrl.py` via `Process`-Komponente auf: + +```qml +Process { + id: cmdProcess + command: ["sh", "-c", "python3 /home/nepharius/.local/bin/superx-ctrl.py " + args + " 2>/dev/null"] + running: false +} +``` + +--- + +## HHD Patch (Turbo-Bypass) + +`tools/patch-hhd-superx.py` patcht HHD, damit der Turbo-Button auf dem Super X funktioniert — auch ohne `oxp-platform` Kernel-Modul. + +### Was der Patch macht + +Ersetzt in `/usr/lib/python3.14/site-packages/hhd/device/oxp/__init__.py` die Turbo-Erkennung: + +- **Vorher:** Turbo nur aktiv wenn `/sys/devices/platform/oxp-platform/tt_toggle` existiert +- **Nachher:** Zusätzlich: Wenn Super X Vendor HID (0x1A86:0x1305) erkannt wird → Turbo aktivieren + +### Anwendung + +```bash +# Nach HHD-Update erneut ausführen +sudo python3 tools/patch-hhd-superx.py +``` + +--- + +## Turbo-Daemon + +`tools/turbo-daemon.py` lauscht auf Key Combos und führt Aktionen aus. + +### Key Combos + +| Combo | Aktion | +|---|---| +| `LEFTCTRL + LEFTALT + LEFTMETA` | Turbo → Cooling Mode "smart" | +| `LEFTMETA + D` | Mode → RGB Preset 9 (Aurora) | +| `LEFTMETA + O + RIGHTCTRL` | KB → Cooling Mode "gentle" | + +### Start + +```bash +# Manuell +python3 tools/turbo-daemon.py + +# Als systemd-user Service (empfohlen) +# → ~/.config/systemd/user/turbo-daemon.service +``` + +--- + +## udev-Regeln + +### 99-superx-hid.rules (aktiv) + +```udev +KERNEL=="hidraw*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="1305", MODE="0666", TAG+="uaccess" +``` + +Gibt Lese-/Schreibzugriff auf die Super X Vendor HID-Schnittstelle (0x1A86:0x1305). + +### Installation + +```bash +sudo cp udev/99-superx-hid.rules /etc/udev/rules.d/ +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +--- + +## EC Turbo + +Der Embedded Controller des Super X speichert den Turbo-Status in EC-RAM. + +### Register + +| Offset | Grösse | Beschreibung | +|---|---|---| +| 0x30 | 1 Byte | Turbo Status (0x00=off, 0x06=turbo) | +| 0x31 | 1 Byte | Fan1 PWM | +| 0x32 | 1 Byte | Fan2 PWM | +| 0x33 | 1 Byte | Pump PWM | + +### Nutzung + +```bash +# Status lesen +sudo python3 tools/ec-read.py + +# Watch-Mode (alle 500ms) +sudo python3 tools/ec-read.py watch + +# Turbo setzen +sudo sh -c 'printf "\\x06" | dd of=/sys/kernel/debug/ec/ec0/io bs=1 seek=$((0x30)) count=1 conv=notrunc 2>/dev/null' +``` + +--- + +## Tablet RGB (HIDRAW) + +`tools/tablet-rgb.py` steuert die RGB-Beleuchtung des Tablet-Moduls via `/dev/hidraw2`. + +### Nutzung + +```bash +# An/Aus +python3 tools/tablet-rgb.py on +python3 tools/tablet-rgb.py off + +# Presets +python3 tools/tablet-rgb.py rainbow +python3 tools/tablet-rgb.py aurora +python3 tools/tablet-rgb.py preset 0x09 + +# Static Color +python3 tools/tablet-rgb.py 255 0 0 # Rot +python3 tools/tablet-rgb.py 0 255 0 # Grün +python3 tools/tablet-rgb.py 0 0 255 # Blau +``` + +--- + ## Hardware -- OneXplayer Super X (Intel Meteor Lake) -- Eingelötete WS2812B-Stripes am Mainboard-Anschluss RGB2 -- SYSTEK ZT5571VG-Platine (GA25-RGB_FPC_VA_5522) als RGB-Controller -- Frostbay ONEC1 Wasserkühlung (BLE GATT) +- **OneXPlayer Super X** (Ryzen AI Max+ 395, Radeon 8060S, 64GB) +- **Frostbay ONEC1** Wasserkühlung (BLE GATT) +- **Tablet-Modul** mit RGB via HIDRAW (0x1A86:0x1305) +- **Eingelötete WS2812B-Stripes** am Mainboard-Anschluss RGB2 (aktuell nicht angeschlossen) +- **SYSTEK ZT5571VG-Platine** (GA25-RGB_FPC_VA_5522) als RGB-Controller +- **eGPU:** RX 9060 XT via OCuLink (all-ways-egpu) ## Git Remote @@ -68,8 +344,11 @@ git remote add origin ssh://git@git.nepharius.at:2222/nepharius/superx-goes-arch ## Roadmap -1. ~~Frostbay unter Linux/BlueZ auf Arch reproduzierbar steuern~~ -2. Logic Analyzer an Mainboard-RGB2-Pin → OEM-Protokoll decodieren -3. Entweder kompatible Ersatz-LEDs oder Software-Übersetzer (MCU→WS2812B) -4. Niri/DMS-Integration mit Frostbay-Daemon (Fan-Kurven + Quick-Toggles) -5. Optional: hhd-Plugin oder Standalone-Daemon für vollständige Systemsteuerung +1. ✅ ~~Frostbay BLE unter Linux/BlueZ steuern~~ +2. ✅ ~~superx-ctrl.py als zentrale CLI~~ +3. ✅ ~~DMS Widget für GUI-Steuerung~~ +4. ✅ ~~HHD Patch für Turbo-Button~~ +5. ✅ ~~Tablet RGB via HIDRAW~~ +6. 🔄 Logic Analyzer an Mainboard-RGB2-Pin → OEM-Protokoll decodieren +7. 🔄 Kompatible Ersatz-LEDs oder Software-Übersetzer (MCU→WS2812B) +8. 🔄 WMI/EC-Kernel-Module für vollständige Systemsteuerung diff --git a/dms/plugins/SuperXCtrl/SuperXWidget.qml b/dms/plugins/SuperXCtrl/SuperXWidget.qml new file mode 100644 index 0000000..96d539c --- /dev/null +++ b/dms/plugins/SuperXCtrl/SuperXWidget.qml @@ -0,0 +1,275 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Plugins + +PluginComponent { + id: root + pluginId: "superXCtrl" + + property string currentMode: pluginData.currentMode || "smart" + property bool coolingEnabled: pluginData.coolingEnabled !== false + property int customFan: pluginData.customFan || 30 + property int customPump: pluginData.customPump || 30 + property int currentPreset: pluginData.currentPreset || 9 + property int currentTab: 0 // 0=Cooling, 1=RGB + + popoutWidth: 360 + popoutHeight: 680 + + popoutContent: Component { + Rectangle { + implicitWidth: 360 + implicitHeight: Math.max(col.implicitHeight + Theme.spacingL * 2, 620) + radius: Theme.cornerRadius + color: Theme.surfaceContainer + border.color: Theme.outlineMedium + border.width: Theme.layerOutlineWidth + + Column { + id: col + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + // --- Header --- + Row { + width: parent.width; spacing: Theme.spacingM + DankIcon { name: "settings_input_component"; size: 28; color: Theme.primary; anchors.verticalCenter: parent.verticalCenter } + StyledText { text: "Super X Control"; font.pixelSize: Theme.fontSizeLarge; color: Theme.surfaceText; anchors.verticalCenter: parent.verticalCenter } + Item { width: 10; height: 1 } + Rectangle { + width: 60; height: 32; radius: Theme.cornerRadius + color: root.coolingEnabled ? Theme.primary : Theme.surfaceVariant + anchors.verticalCenter: parent.verticalCenter + MouseArea { + anchors.fill: parent; cursorShape: Qt.PointingHandCursor + onClicked: { + root.coolingEnabled = !root.coolingEnabled + root.runCmd(root.coolingEnabled ? "mode " + root.currentMode : "mode off") + } + } + StyledText { text: root.coolingEnabled ? "ON" : "OFF"; font.pixelSize: Theme.fontSizeSmall; color: root.coolingEnabled ? Theme.primaryText : Theme.surfaceVariantText; anchors.centerIn: parent } + } + } + + // --- Tab Bar --- + Rectangle { + width: parent.width; height: 36; radius: Theme.cornerRadius + color: Theme.surfaceContainerHigh; border.width: 0 + + Row { + anchors.centerIn: parent; spacing: Theme.spacingXS + Rectangle { + width: (parent.parent.width - Theme.spacingXS) / 2; height: 30; radius: Theme.cornerRadius + color: root.currentTab === 0 ? Theme.primary : "transparent" + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: root.currentTab = 0 } + StyledText { text: "❄️ Cooling"; font.pixelSize: Theme.fontSizeSmall; color: root.currentTab === 0 ? Theme.primaryText : Theme.surfaceVariantText; anchors.centerIn: parent } + } + Rectangle { + width: (parent.parent.width - Theme.spacingXS) / 2; height: 30; radius: Theme.cornerRadius + color: root.currentTab === 1 ? Theme.primary : "transparent" + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: root.currentTab = 1 } + StyledText { text: "🌈 RGB"; font.pixelSize: Theme.fontSizeSmall; color: root.currentTab === 1 ? Theme.primaryText : Theme.surfaceVariantText; anchors.centerIn: parent } + } + } + } + + // --- Cooling Tab Content --- + Column { + width: parent.width + spacing: Theme.spacingM + visible: root.currentTab === 0 + + StyledText { text: "Cooling Mode"; font.pixelSize: Theme.fontSizeMedium; color: Theme.surfaceText } + + Grid { + width: parent.width; columns: 3; rowSpacing: Theme.spacingXS; columnSpacing: Theme.spacingXS + Repeater { + model: [ + {id: "off", label: "Off", icon: "power_settings_new"}, + {id: "silent", label: "Silent", icon: "ac_unit"}, + {id: "gentle", label: "Gentle", icon: "air"}, + {id: "strong", label: "Strong", icon: "speed"}, + {id: "extreme", label: "Extreme", icon: "rocket_launch"}, + {id: "smart", label: "Smart", icon: "auto_mode"}, + ] + Rectangle { + width: (col.width - Theme.spacingXS * 2) / 3; height: 48; radius: Theme.cornerRadius + color: mam.containsMouse ? Theme.surfaceContainerHighest : "transparent" + border.color: root.currentMode === modelData.id ? Theme.primary : Theme.outlineMedium + border.width: root.currentMode === modelData.id ? 2 : 1 + MouseArea { + id: mam; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor + onClicked: root.setMode(modelData.id) + } + Column { + anchors.centerIn: parent; spacing: 2 + DankIcon { name: modelData.icon; color: root.currentMode === modelData.id ? Theme.primary : Theme.surfaceVariantText; size: Theme.iconSize; anchors.horizontalCenter: parent.horizontalCenter } + StyledText { text: modelData.label; font.pixelSize: Theme.fontSizeSmall; color: root.currentMode === modelData.id ? Theme.primary : Theme.surfaceText; horizontalAlignment: Text.AlignHCenter; anchors.horizontalCenter: parent.horizontalCenter } + } + } + } + } + + StyledText { text: "Custom Fan / Pump"; font.pixelSize: Theme.fontSizeMedium; color: Theme.surfaceText } + + Rectangle { + width: parent.width; height: 100; radius: Theme.cornerRadius; color: Theme.surfaceContainerHigh; border.width: 0 + Column { + anchors.fill: parent; anchors.margins: Theme.spacingM; spacing: Theme.spacingXS + Row { + width: parent.width; spacing: Theme.spacingS + StyledText { text: "Fan: " + root.customFan + "%"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; width: 70; anchors.verticalCenter: parent.verticalCenter } + Slider { width: parent.width - 120; from: 0; to: 100; value: root.customFan; stepSize: 5; anchors.verticalCenter: parent.verticalCenter; onValueChanged: root.customFan = value } + } + Row { + width: parent.width; spacing: Theme.spacingS + StyledText { text: "Pump: " + root.customPump + "%"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; width: 70; anchors.verticalCenter: parent.verticalCenter } + Slider { width: parent.width - 120; from: 0; to: 100; value: root.customPump; stepSize: 5; anchors.verticalCenter: parent.verticalCenter; onValueChanged: root.customPump = value } + } + Rectangle { + width: 80; height: 28; radius: Theme.cornerRadius; color: Theme.primary; anchors.horizontalCenter: parent.horizontalCenter + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: root.setCustom() } + StyledText { text: "Apply"; font.pixelSize: Theme.fontSizeSmall; color: Theme.primaryText; anchors.centerIn: parent } + } + } + } + } + + // --- RGB Tab Content --- + Column { + width: parent.width + spacing: Theme.spacingM + visible: root.currentTab === 1 + + StyledText { text: "Frostbay RGB"; font.pixelSize: Theme.fontSizeMedium; color: Theme.surfaceText } + + Grid { + width: parent.width; columns: 4; rowSpacing: Theme.spacingXS; columnSpacing: Theme.spacingXS + Repeater { + model: [ + {id: 1, label: "Static", icon: "circle"}, + {id: 2, label: "Breath", icon: "blur_on"}, + {id: 3, label: "Wave", icon: "waves"}, + {id: 4, label: "Strobe", icon: "flash_on"}, + {id: 5, label: "Rainbow", icon: "gradient"}, + {id: 6, label: "Marquee", icon: "arrow_forward"}, + {id: 7, label: "Meteor", icon: "star"}, + {id: 8, label: "Fire", icon: "local_fire_department"}, + {id: 9, label: "Aurora", icon: "auto_awesome"}, + {id: 10, label: "Pulse", icon: "favorite"}, + ] + Rectangle { + width: (col.width - Theme.spacingXS * 3) / 4; height: 40; radius: Theme.cornerRadius + color: rgbma.containsMouse ? Theme.surfaceContainerHighest : "transparent" + border.color: root.currentPreset === modelData.id ? Theme.primary : Theme.outlineMedium + border.width: root.currentPreset === modelData.id ? 2 : 1 + MouseArea { + id: rgbma; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor + onClicked: root.runCmd("rgb-preset " + modelData.id) + } + StyledText { text: modelData.label; font.pixelSize: Theme.fontSizeSmall; color: root.currentPreset === modelData.id ? Theme.primary : Theme.surfaceText; anchors.centerIn: parent } + } + } + } + + Row { + width: parent.width; spacing: Theme.spacingM + Rectangle { + width: (parent.width - Theme.spacingM) / 2; height: 32; radius: Theme.cornerRadius + color: Theme.primary + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: root.runCmd("rgb-on") } + StyledText { text: "RGB ON"; font.pixelSize: Theme.fontSizeSmall; color: Theme.primaryText; anchors.centerIn: parent } + } + Rectangle { + width: (parent.width - Theme.spacingM) / 2; height: 32; radius: Theme.cornerRadius + color: Theme.surfaceVariant + MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: root.runCmd("rgb-off") } + StyledText { text: "RGB OFF"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceVariantText; anchors.centerIn: parent } + } + } + + Item { width: 1; height: Theme.spacingXS } + + // --- Tablet RGB (HIDRAW) --- + StyledText { text: "Tablet RGB"; font.pixelSize: Theme.fontSizeMedium; color: Theme.surfaceText } + + Grid { + width: parent.width; columns: 4; rowSpacing: Theme.spacingXS; columnSpacing: Theme.spacingXS + Repeater { + model: [ + {cmd: "on", label: "An", icon: "lightbulb"}, + {cmd: "off", label: "Aus", icon: "light_off"}, + {cmd: "rainbow", label: "Regenbogen", icon: "gradient"}, + {cmd: "aurora", label: "Aurora", icon: "auto_awesome"}, + ] + Rectangle { + width: (col.width - Theme.spacingXS * 3) / 4; height: 40; radius: Theme.cornerRadius + color: tma.containsMouse ? Theme.surfaceContainerHighest : "transparent" + border.color: Theme.outlineMedium; border.width: 1 + MouseArea { id: tma; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: root.runCmd("tablet " + modelData.cmd) } + Column { anchors.centerIn: parent; spacing: 2 + DankIcon { name: modelData.icon; color: Theme.primary; size: Theme.iconSize - 4; anchors.horizontalCenter: parent.horizontalCenter } + StyledText { text: modelData.label; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; horizontalAlignment: Text.AlignHCenter } + } + } + } + } + + StyledText { text: "Farbe wählen"; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceVariantText } + + Grid { + width: parent.width; columns: 6; rowSpacing: Theme.spacingXS; columnSpacing: Theme.spacingXS + Repeater { + model: [ + {cmd: "255 0 0", label: "Rot", color: "#ff0000"}, + {cmd: "0 255 0", label: "Grün", color: "#00ff00"}, + {cmd: "0 0 255", label: "Blau", color: "#0000ff"}, + {cmd: "255 255 0", label: "Gelb", color: "#ffff00"}, + {cmd: "255 0 255", label: "Lila", color: "#ff00ff"}, + {cmd: "255 255 255", label: "Weiß", color: "#ffffff"}, + ] + Rectangle { + width: (col.width - Theme.spacingXS * 5) / 6; height: 36; radius: Theme.cornerRadius + color: tma2.containsMouse ? Theme.surfaceContainerHighest : "transparent" + border.color: modelData.color; border.width: 2 + MouseArea { id: tma2; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: root.runCmd("tablet " + modelData.cmd) } + StyledText { text: modelData.label; font.pixelSize: Theme.fontSizeSmall; color: Theme.surfaceText; anchors.centerIn: parent } + } + } + } + } + } + } + } + + horizontalBarPill: Component { + Row { spacing: 4 + DankIcon { name: root.coolingEnabled ? "settings_input_component" : "ac_unit"; color: root.coolingEnabled ? Theme.primary : Theme.surfaceVariantText; size: Theme.iconSize - 4; anchors.verticalCenter: parent.verticalCenter } + StyledText { text: "SX"; font.pixelSize: Theme.fontSizeSmall; color: root.coolingEnabled ? Theme.primary : Theme.surfaceVariantText; anchors.verticalCenter: parent.verticalCenter } + } + } + + function runCmd(args) { + cmdProcess.command = ["sh", "-c", "python3 /home/nepharius/.local/bin/superx-ctrl.py " + args + " 2>/dev/null"] + cmdProcess.running = true + } + + Process { id: cmdProcess; running: false } + + function setMode(modeId) { + currentMode = modeId + if (!coolingEnabled) coolingEnabled = true + runCmd("mode " + modeId) + } + + function setCustom() { + if (!coolingEnabled) return + runCmd("custom " + customFan + " " + customPump) + } +} \ No newline at end of file diff --git a/dms/plugins/SuperXCtrl/plugin.json b/dms/plugins/SuperXCtrl/plugin.json new file mode 100644 index 0000000..f95ea29 --- /dev/null +++ b/dms/plugins/SuperXCtrl/plugin.json @@ -0,0 +1,12 @@ +{ + "id": "superXCtrl", + "name": "Super X Control", + "description": "Frostbay cooling modes, fan curves, and RGB control for OneXPlayer Super X", + "version": "1.0.0", + "author": "Kay & Hoerman", + "type": "widget", + "capabilities": ["control-center"], + "component": "./SuperXWidget.qml", + "icon": "settings_input_component", + "permissions": ["settings_read", "settings_write"] +} \ No newline at end of file diff --git a/dms/plugins/SuperXCtrl/plugin_settings.json b/dms/plugins/SuperXCtrl/plugin_settings.json new file mode 100644 index 0000000..ea6a6b2 --- /dev/null +++ b/dms/plugins/SuperXCtrl/plugin_settings.json @@ -0,0 +1,5 @@ +{ + "superXCtrl": { + "enabled": true + } +} \ No newline at end of file diff --git a/tools/ec-read.py b/tools/ec-read.py new file mode 100755 index 0000000..ebfb935 --- /dev/null +++ b/tools/ec-read.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""EC Turbo Reader — Liest EC RAM. Wird via sudo ausgeführt.""" +import os, struct, json, sys, time + +EC_DEV = "/sys/kernel/debug/ec/ec0/io" + +def read_ec(): + with open(EC_DEV, "rb") as f: + data = f.read() + return data + +def get_turbo(data): + b30 = data[0x30] # Turbo Status + fan1 = data[0x31] + fan2 = data[0x32] + pump = data[0x33] + + turbo_map = { + 0: "off", + 6: "turbo", + } + + return { + "turbo": b30 > 0, + "turbo_status": b30, + "turbo_label": turbo_map.get(b30, f"mode_{b30}"), + "fan1_pwm": fan1, + "fan2_pwm": fan2, + "pump_pwm": pump, + } + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "watch": + prev = None + while True: + status = get_turbo(read_ec()) + if status != prev: + print(json.dumps(status), flush=True) + prev = status + time.sleep(0.5) + else: + print(json.dumps(get_turbo(read_ec()))) \ No newline at end of file diff --git a/tools/frostbay_ble.py b/tools/frostbay_ble.py index 7f29cfe..6b9b7dd 100644 --- a/tools/frostbay_ble.py +++ b/tools/frostbay_ble.py @@ -133,7 +133,7 @@ def write_state(mode_code: int, fan_pwm: int = None, pump_pwm: int = None, if rgb_mode is not None: buff[16] = rgb_mode & 0xFF - if rgb_mode == 0xFE or rgb_mode == 0xFF: + if rgb_mode == 0xFE or rgb_mode == 0xFF or rgb_mode == 0xFD: # RGB values go to write_buff[17-19] (→ Ram_op_params_buff[20-22]) if rgb_r is not None: buff[17] = rgb_r & 0xFF @@ -174,6 +174,11 @@ def main(): p_rgb.add_argument("g", type=int) p_rgb.add_argument("b", type=int) + p_rgb_brightness = sub.add_parser("rgb-brightness", help="Set RGB brightness/switch/speed (0xFD)") + p_rgb_brightness.add_argument("switch", type=int) + p_rgb_brightness.add_argument("speed", type=int) + p_rgb_brightness.add_argument("brightness", type=int) + sub.add_parser("off", help="Turn Frostbay off") sub.add_parser("raw", help="Raw hex dump of ffe1") @@ -216,6 +221,14 @@ def main(): rgb_r=args.r, rgb_g=args.g, rgb_b=args.b) print_state(s) + elif args.cmd == "rgb-brightness": + cur = parse_state(read_state()) + s = write_state(0xFF, rgb_mode=0xFD, + fan_pwm=cur["fan_pwm"], + pump_pwm=cur["pump_pwm"], + rgb_r=args.switch, rgb_g=args.speed, rgb_b=args.brightness) + print_state(s) + elif args.cmd == "off": s = write_state(0x00) print_state(s) diff --git a/tools/patch-hhd-superx.py b/tools/patch-hhd-superx.py new file mode 100644 index 0000000..c73d685 --- /dev/null +++ b/tools/patch-hhd-superx.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Patch HHD __init__.py: enable turbo for Super X even without oxp-platform.""" +filepath = "/usr/lib/python3.14/site-packages/hhd/device/oxp/__init__.py" + +with open(filepath) as f: + c = f.read() + +old = """ turbo = False + if self.dconf.get("turbo", True) and os.path.exists( + "/sys/devices/platform/oxp-platform/tt_toggle" + ): + try: + with open("/sys/devices/platform/oxp-platform/tt_toggle", "w") as f: + f.write("1") + logger.info(f"Turbo button takeover enabled") + turbo = True + + if os.path.exists("/sys/devices/platform/oxp-platform/tt_led"): + with open("/sys/devices/platform/oxp-platform/tt_led", "w") as f: + f.write("0") + except Exception: + logger.warning( + f"Turbo takeover failed. Ensure you have the latest oxp-sensors driver installed." + ) + self.turbo = turbo""" + +new = """ turbo = False + from hhd.controller.lib.hid import enumerate_unique + has_superx = bool(enumerate_unique(vid=0x1A86, pid=0x1305, usage_page=0xFF00, usage=0xFF00)) + if self.dconf.get("turbo", True): + if os.path.exists( + "/sys/devices/platform/oxp-platform/tt_toggle" + ): + try: + with open("/sys/devices/platform/oxp-platform/tt_toggle", "w") as f: + f.write("1") + logger.info(f"Turbo button takeover enabled") + turbo = True + + if os.path.exists("/sys/devices/platform/oxp-platform/tt_led"): + with open("/sys/devices/platform/oxp-platform/tt_led", "w") as f: + f.write("0") + except Exception: + logger.warning( + f"Turbo takeover failed. Ensure you have the latest oxp-sensors driver installed." + ) + elif has_superx: + logger.info(f"Super X turbo button enabled via HID vendor device.") + turbo = True + self.turbo = turbo""" + +assert old in c, "Patch target not found" +c = c.replace(old, new) + +with open(filepath, "w") as f: + f.write(c) + +print("✅ PATCHED: __init__.py — Super X turbo bypasses oxp-platform check") \ No newline at end of file diff --git a/tools/superx-ctrl.py b/tools/superx-ctrl.py new file mode 100644 index 0000000..f2bc36b --- /dev/null +++ b/tools/superx-ctrl.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""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") + +COOL_MODES = [ + {"id": "off", "label": "Off", "icon": "power_settings_new"}, + {"id": "silent", "label": "Silent", "icon": "ac_unit"}, + {"id": "gentle", "label": "Gentle", "icon": "air"}, + {"id": "strong", "label": "Strong", "icon": "speed"}, + {"id": "extreme", "label": "Extreme", "icon": "rocket_launch"}, + {"id": "smart", "label": "Smart", "icon": "auto_mode"}, +] + +RGB_PRESETS = [ + {"id": 1, "label": "Mode 1"}, + {"id": 2, "label": "Mode 2"}, + {"id": 3, "label": "Mode 3"}, + {"id": 4, "label": "Mode 4"}, + {"id": 5, "label": "Mode 5"}, + {"id": 6, "label": "Mode 6"}, + {"id": 7, "label": "Mode 7"}, + {"id": 8, "label": "Mode 8"}, + {"id": 9, "label": "Aurora"}, + {"id": 10, "label": "Mode 10"}, +] + +# Direct BLE access +BUS = dbus.SystemBus() +BASE = "/org/bluez/hci0/dev_C8_17_17_F5_C8_62" +CHAR = f"{BASE}/service003f/char0040" + +def ble_read(): + c = dbus.Interface(BUS.get_object("org.bluez", CHAR), "org.bluez.GattCharacteristic1") + return bytes(c.ReadValue({})) + +def ble_write(data): + c = dbus.Interface(BUS.get_object("org.bluez", CHAR), "org.bluez.GattCharacteristic1") + c.WriteValue(dbus.Array(list(data), signature="y"), {}) + +def make_buff(): + """Build a fresh 64-byte buffer based on current state.""" + cur = ble_read() + b = bytearray(64) + b[:] = cur[:64] + b[0] = 0xC1 + b[1] = 0x02 # write flag + b[3] = 0x01 # follow mode + b[4] = 0xFF # custom mode (keep fans/pump in current state) + return b + +def run_frostbay(*args): + try: + r = subprocess.run([sys.executable, FROSTBAY_BLE] + list(args), + capture_output=True, text=True, timeout=10) + return {"ok": True, "stdout": r.stdout} + except Exception as e: + return {"ok": False, "error": str(e)} + +def cmd_status(): + r = run_frostbay("raw") + return {"ok": r["ok"], "modes": COOL_MODES, "rgb_presets": RGB_PRESETS} + +def cmd_mode(mid): + r = run_frostbay("mode", mid) + return {"ok": r["ok"], "mode": mid} + +def cmd_custom(fan, pump): + r = run_frostbay("mode", "custom", "--fan", str(fan), "--pump", str(pump)) + return {"ok": r["ok"], "fan": fan, "pump": pump} + +def cmd_rgb_preset(num): + """Two-step direct BLE write: brightness first, then preset.""" + try: + # Step 1: Brightness/switch AE + b = make_buff() + b[16] = 0xFD + b[17] = 0x07 # all strips + b[18] = 0x05 # speed + b[19] = 0x04 # brightness max + ble_write(bytes(b)) + time.sleep(0.3) + + # Step 2: Preset number + b2 = make_buff() + b2[16] = num & 0xFF + ble_write(bytes(b2)) + time.sleep(0.3) + + return {"ok": True, "preset": num} + except Exception as e: + return {"ok": False, "error": str(e)} + +def cmd_rgb_brightness(switch, speed, brightness): + try: + b = make_buff() + b[16] = 0xFD + b[17] = switch & 0xFF + b[18] = speed & 0xFF + b[19] = brightness & 0xFF + ble_write(bytes(b)) + return {"ok": True} + except Exception as e: + return {"ok": False, "error": str(e)} + +def cmd_rgb_off(): + try: + b = make_buff() + b[16] = 0xFD + b[17] = 0x00 # switch=0 = all off + ble_write(bytes(b)) + return {"ok": True} + except Exception as e: + return {"ok": False, "error": str(e)} + +TABLET_CMD = os.path.expanduser("~/.local/bin/tablet-rgb.py") + +def cmd_tablet(*args): + """Control tablet RGB via HIDRAW.""""" + try: + r = subprocess.run([sys.executable, TABLET_CMD] + list(args), + capture_output=True, text=True, timeout=10) + return json.loads(r.stdout) + except Exception as e: + return {"ok": False, "error": str(e)} + +def cmd_ec_status(): + """Read EC turbo status via sudo.""" + try: + r = subprocess.run(["sudo", "/home/nepharius/.local/bin/ec-read.py"], + capture_output=True, text=True, timeout=5) + if r.returncode == 0: + return json.loads(r.stdout) + return {"ok": False, "error": r.stderr} + except Exception as e: + return {"ok": False, "error": str(e)} + +def cmd_ec_turbo(on=True): + """Set turbo via EC (writes to offset 0x30).""" + try: + val = "06" if on else "00" + r = subprocess.run( + ["sh", "-c", f"""printf '\\x{val}' | sudo dd of=/sys/kernel/debug/ec/ec0/io bs=1 seek=$((0x30)) count=1 conv=notrunc 2>/dev/null"""], + capture_output=True, text=True, timeout=5 + ) + return {"ok": True, "turbo": on} + except Exception as e: + return {"ok": False, "error": str(e)} + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(json.dumps({"ok": False, "error": "Need command"})) + sys.exit(1) + + cmd = sys.argv[1] + if cmd == "status": result = cmd_status() + elif cmd == "mode" and len(sys.argv) >= 3: result = cmd_mode(sys.argv[2]) + elif cmd == "custom" and len(sys.argv) >= 4: result = cmd_custom(int(sys.argv[2]), int(sys.argv[3])) + elif cmd == "rgb-preset" and len(sys.argv) >= 3: result = cmd_rgb_preset(int(sys.argv[2])) + elif cmd == "rgb-brightness" and len(sys.argv) >= 5: result = cmd_rgb_brightness(int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])) + elif cmd == "rgb-on": result = cmd_rgb_brightness(7, 5, 4) + elif cmd == "rgb-off": result = cmd_rgb_off() + elif cmd == "tablet" and len(sys.argv) >= 3: result = cmd_tablet(*sys.argv[2:]) + elif cmd == "ec-status": result = cmd_ec_status() + elif cmd == "ec-turbo-on": result = cmd_ec_turbo(True) + elif cmd == "ec-turbo-off": result = cmd_ec_turbo(False) + else: result = {"ok": False, "error": f"Unknown: {cmd}"} + + print(json.dumps(result)) \ No newline at end of file diff --git a/tools/tablet-rgb.py b/tools/tablet-rgb.py new file mode 100644 index 0000000..3a44da2 --- /dev/null +++ b/tools/tablet-rgb.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Test tablet RGB via HIDRAW — adapted from ps1x/onexplayer-superxcontrol for Super X.""" +import sys, os, json, time + +HIDRAW = "/dev/hidraw2" +PACKET_SIZE = 64 + +def pad(prefix): + return prefix + [0] * (PACKET_SIZE - len(prefix)) + +def write_hidraw(data): + with open(HIDRAW, "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], + "mid": [0x07, 0xFF, 0xFD, 0x01, 0x05, 0x03], + "high": [0x07, 0xFF, 0xFD, 0x01, 0x05, 0x04], + } + 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": + 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": "need args"})) \ No newline at end of file diff --git a/tools/turbo-all.py b/tools/turbo-all.py new file mode 100644 index 0000000..a1b7091 --- /dev/null +++ b/tools/turbo-all.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Listen for ACPI events + HID + evdev — catch Turbo from anywhere.""" +import evdev, select, os, subprocess, threading, time + +# Start acpi_listen in background +acpi_out = [] + +def acpi_thread(): + try: + proc = subprocess.Popen(['acpi_listen'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) + while True: + line = proc.stdout.readline() + if line: + print(f'[ACPI] {line.strip()}', flush=True) + except: + pass + +t = threading.Thread(target=acpi_thread, daemon=True) +t.start() + +# Also listen on hidraw2 +try: + fd = os.open('/dev/hidraw2', os.O_RDONLY | os.O_NONBLOCK) + hid_fds = [fd] +except: + hid_fds = [] + +# And all evdev +evdevs = [] +for i in range(30): + try: + d = evdev.InputDevice(f'/dev/input/event{i}') + evdevs.append(d) + except: + pass + +print(f'📟 Hörman lauscht auf:') +print(f' - ACPI events') +print(f' - {len(evdevs)} evdev devices') +print(f' - hidraw2 (vendor HID)') +print(f'Drück den TURBO-Knopf am Gehäuse!') +print('=' * 50) + +while True: + r, _, _ = select.select(evdevs + hid_fds, [], [], 1.0) + for fd in r: + if isinstance(fd, int): # hidraw + data = os.read(fd, 64) + if data: + print(f'[HIDRAW] {data[:16].hex(" ")}', flush=True) + else: # evdev + for e in fd.read(): + if e.type != 0: + name = evdev.ecodes.KEY.get(e.code, f'0x{e.code:04x}') + val = {1: '⬇️', 0: '⬆️', 2: '🔄'}.get(e.value, str(e.value)) + print(f'[{fd.name[:30]:30s}] {val} {name:20s} 0x{e.code:04x}', flush=True) \ No newline at end of file diff --git a/tools/turbo-atkbd.py b/tools/turbo-atkbd.py new file mode 100644 index 0000000..66717c6 --- /dev/null +++ b/tools/turbo-atkbd.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Listen on AT keyboard (event2) for Turbo combo - no grab.""" +import evdev, select + +d = evdev.InputDevice('/dev/input/event2') +print(f'📟 Hörman lauscht auf: {d.name}') +print('Drück den TURBO-Knopf am Gehäuse!') +print('=' * 50) + +pressed = set() +TURBO = {29, 56, 125} + +for e in d.read_loop(): + if e.type == 1: + name = evdev.ecodes.KEY.get(e.code, f'0x{e.code:04x}') + if e.value == 1: + pressed.add(e.code) + if TURBO.issubset(pressed): + print(f'🔥🔥🔥 TURBO PRESSED!', flush=True) + elif e.value == 0: + pressed.discard(e.code) + if e.value != 2: + val = {1: '⬇️', 0: '⬆️'}.get(e.value, str(e.value)) + print(f' {val} {name:25s} 0x{e.code:04x}', flush=True) \ No newline at end of file diff --git a/tools/turbo-daemon.py b/tools/turbo-daemon.py new file mode 100644 index 0000000..98da9d4 --- /dev/null +++ b/tools/turbo-daemon.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Turbo/FN Button Daemon für Super X — direct key combo listening.""" +import evdev, select, subprocess, os, signal + +# HHD Key combos from const.py: +# Turbo = LEFTCTRL(29) + LEFTALT(56) + LEFTMETA(125) +# Mode = KEY_D(32) + LEFTMETA(125) +# KB = KEY_O(24) + RIGHTCTRL(97) + LEFTMETA(125) + +TURBO_COMBO = {29, 56, 125} +MODE_COMBO = {32, 125} +KB_COMBO = {24, 97, 125} + +# Super X Vendor HID (for RGB + Turntable) +VENDOR_VID = 0x1A86 +VENDOR_PID = 0x1305 + +pressed = set() + +# Auto-start HHD +try: + subprocess.run(["systemctl", "--user", "is-active", "hhd"], capture_output=True, timeout=2) +except: + pass + +def run_cmd(cmd): + subprocess.run(cmd, capture_output=True, timeout=5) + +def handle_combo(keys, label): + print(f"🔥 {label} combo triggered! active keys: {keys}", flush=True) + + if TURBO_COMBO.issubset(keys): + # Turbo: toggle TDP profile + run_cmd(["python3", "/home/nepharius/.local/bin/superx-ctrl.py", "mode", "smart"]) + print(" -> TDP balanced", flush=True) + + elif MODE_COMBO.issubset(keys): + # Mode: toggle RGB preset + run_cmd(["python3", "/home/nepharius/.local/bin/superx-ctrl.py", "rgb-preset", "9"]) + print(" -> RGB Aurora", flush=True) + + elif KB_COMBO.issubset(keys): + # KB: toggle cooling mode + run_cmd(["python3", "/home/nepharius/.local/bin/superx-ctrl.py", "mode", "gentle"]) + print(" -> Cooling gentle", flush=True) + +d = evdev.InputDevice('/dev/input/event4') +print(f'Hörman Turbo Daemon läuft auf: {d.name}', flush=True) +print(f'Active combos:', flush=True) +print(f' LEFTCTRL+LEFTALT+LEFTMETA = Turbo (TDP toggle)', flush=True) +print(f' LEFTSUPER+D = Mode (RGB Aurora)', flush=True) +print(f' LEFTSUPER+O+RIGHTCTRL = Keyboard (Cooling)', flush=True) + +for e in d.read_loop(): + if e.type == 1: # KEY event + ec = evdev.ecodes.KEY + name = ec.get(e.code, hex(e.code)) + + if e.value == 1: # press + pressed.add(e.code) + # Check combos instantly + if TURBO_COMBO.issubset(pressed): + print(f'🔥 TURBO PRESSED!', flush=True) + try: + subprocess.run(["python3", "/home/nepharius/.local/bin/superx-ctrl.py", "mode", "smart"], + capture_output=True, timeout=3) + except: pass + elif e.value == 0: # release + pressed.discard(e.code) \ No newline at end of file diff --git a/tools/turbo-deep.py b/tools/turbo-deep.py new file mode 100644 index 0000000..05199d1 --- /dev/null +++ b/tools/turbo-deep.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Listen on ACPI + CoolingSystem + all for Turbo.""" +import evdev, select, os, subprocess, threading + +# ACPI listener +def acpi_loop(): + try: + proc = subprocess.Popen(['acpi_listen'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) + for line in iter(proc.stdout.readline, ''): + if line: + print(f'[ACPI] {line.strip()}', flush=True) + except: pass + +threading.Thread(target=acpi_loop, daemon=True).start() + +# CoolingSystem (event20) - Frostbay's own HID +try: + d_cool = evdev.InputDevice('/dev/input/event20') + print(f'📟 CoolingSystem: {d_cool.name}', flush=True) +except: d_cool = None + +# Also listen on hidraw2 (vendor HID) +try: + fd_hid = os.open('/dev/hidraw2', os.O_RDONLY | os.O_NONBLOCK) + print(f'📟 hidraw2 (vendor HID)', flush=True) +except: fd_hid = None + +# And event4 (Juer Xin keyboard) for MSC scan codes +try: + d_kbd = evdev.InputDevice('/dev/input/event4') + print(f'📟 Juer Xin Keyboard (event4)', flush=True) +except: d_kbd = None + +devices = [] +if d_cool: devices.append(d_cool) +if d_kbd: devices.append(d_kbd) +hid_fds = [] +if fd_hid: hid_fds.append(fd_hid) + +print(f'\nDrück den TURBO-Knopf am Gehäuse!') +print('=' * 50) + +while True: + r, _, _ = select.select(devices + hid_fds, [], [], 1.0) + for fd in r: + if isinstance(fd, int): # hidraw + data = os.read(fd, 64) + if data: + print(f'[HIDRAW] {data[:16].hex(" ")}', flush=True) + else: # evdev + for e in fd.read(): + if e.type != 0: + name = evdev.ecodes.KEY.get(e.code, f'0x{e.code:04x}') + val = {1: '⬇️', 0: '⬆️', 2: '🔄'}.get(e.value, str(e.value)) + print(f'[{fd.name[:30]:30s}] {val} {name:20s} 0x{e.code:04x}', flush=True) \ No newline at end of file diff --git a/tools/turbo-hid.py b/tools/turbo-hid.py new file mode 100644 index 0000000..6e6b769 --- /dev/null +++ b/tools/turbo-hid.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Read raw HID reports from Super X vendor interface.""" +import select, os, struct + +fd = os.open('/dev/hidraw2', os.O_RDONLY | os.O_NONBLOCK) +print('📟 Hörman lauscht auf /dev/hidraw2 (Super X Vendor HID)') +print('Drück den TURBO-Knopf am Gehäuse!') +print('=' * 50) + +while True: + r, _, _ = select.select([fd], [], [], 60) + if r: + data = os.read(fd, 64) + if data and len(data) >= 8: + hex_str = ' '.join(f'{b:02x}' for b in data[:20]) + vals = list(data[:20]) + cid = data[0] + btn = data[6] if len(data) > 6 else 0 + pressed = data[12] if len(data) > 12 else 0 + print(f'REPORT: {hex_str}') + print(f' cid=0x{cid:02x} btn=0x{btn:02x} pressed={pressed}', flush=True) \ No newline at end of file diff --git a/tools/turbo-monitor.py b/tools/turbo-monitor.py new file mode 100644 index 0000000..036196a --- /dev/null +++ b/tools/turbo-monitor.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +"""Turbo/fn monitor — listen on keyboard for scan codes + key events.""" +import evdev, select + +d = evdev.InputDevice('/dev/input/event4') +d.grab() +print(f'📟 Hörman lauscht auf: {d.name}', flush=True) +print('Drück Turbo / FN / Spezialtasten!', flush=True) + +KEY_EVENT = 1 +MSC_EVENT = 4 + +for e in d.read_loop(): + if e.type == MSC_EVENT: + print(f' MSC code=0x{e.code:04x}', flush=True) + elif e.type == KEY_EVENT: + name = evdev.ecodes.KEY.get(e.code, f'0x{e.code:04x}') + val = {1: '⬇️', 0: '⬆️', 2: '🔄'}.get(e.value, str(e.value)) + print(f' KEY {val} {name:30s} 0x{e.code:04x}', flush=True) + elif e.type == 0: # SYN + pass \ No newline at end of file diff --git a/tools/turbo-scanner.py b/tools/turbo-scanner.py new file mode 100644 index 0000000..f107d32 --- /dev/null +++ b/tools/turbo-scanner.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Scan ALL input devices for Turbo button activity.""" +import evdev, select, os + +# List all event devices +devices = [] +for path in [f'/dev/input/event{i}' for i in range(30)]: + try: + d = evdev.InputDevice(path) + devices.append(d) + except: + pass + +print(f'Hörman scannt {len(devices)} Devices...') +print('Drück den TURBO-Knopf (am Gehäuse, unterhalb Volume)!') +print('=' * 50) + +while True: + r, _, _ = select.select(devices, [], [], 1.0) + for d in r: + for e in d.read(): + if e.type != 0: # Skip SYN + name = evdev.ecodes.KEY.get(e.code, f'0x{e.code:04x}') + val = {1: '⬇️', 0: '⬆️', 2: '🔄'}.get(e.value, str(e.value)) + print(f'[{d.name[:35]:35s}] {val} {name:25s} 0x{e.code:04x}', flush=True) \ No newline at end of file diff --git a/udev/99-superx-hid.rules b/udev/99-superx-hid.rules new file mode 100644 index 0000000..4f9dc01 --- /dev/null +++ b/udev/99-superx-hid.rules @@ -0,0 +1 @@ +KERNEL=="hidraw*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="1305", MODE="0666", TAG+="uaccess"