Document Super X RGB OEM layer and add Linux bridge scaffold

This commit is contained in:
Kay Türtscher 2026-05-25 10:15:10 +02:00
commit 9ed0f7b71e
19 changed files with 1757 additions and 0 deletions

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
__pycache__/
*.pyc
*.pyo
*.swp
*.tmp
.env
.venv/
venv/
*.log
.cache/
*.exe
*.dll
*.pdb

30
README.md Normal file
View file

@ -0,0 +1,30 @@
# SuperX-goes-Arch
Arbeitsrepo für Linux-/Arch-/COSMIC-Support auf dem OneXplayer Super X.
Fokus:
- RGB-Stripes / interne LED-Steuerung
- Keyboard-/Controller-/HID-Pfade
- Frostbay-Wasserkühlung (BLE/HID)
- Reverse Engineering von OEM-Tools und Windows-Artefakten
- später: echte Linux-Tools / evtl. hhd-Plugin / udev+hwdb / Arch-Pakete
Aktueller Stand
- Für Super X gibt es schon brauchbare Linux-Basisarbeit im OXP/hhd-Umfeld.
- Fan-/EC-Themen sind teils schon im Kernel/hhd-Land sichtbar.
- Für Frostbay gibt es sehr frische Community-Arbeit.
- Für Super-X-RGB/Keyboard ist noch viel Nebel da. Da müssen wir ziemlich sicher selbst reverse engineeren.
Wichtige Hinweise
- Deine ersetzten RGB-Stripes sind WS2812B. Das ist hilfreich, aber nicht die eigentliche Linux-Baustelle.
- Entscheidend ist, welches Board/EC/MCU das Datensignal erzeugt und über welchen Transport das Gerät angesteuert wird.
- Frostbay wirkt deutlich greifbarer als die interne RGB-Leiste, weil es bereits konkrete BLE-/GATT-Hinweise gibt.
Ordner
- `docs/` Research, Protokollnotizen, Roadmap
- `tools/` kleine Hilfsskripte für Analyse/Probing
Nächste sinnvolle Stoßrichtung
1. Frostbay unter Linux/BlueZ auf Arch reproduzierbar sichtbar machen.
2. Parallel Super-X-HID/EC-/RGB-Pfad weiter eingrenzen.
3. Danach kleine Linux-Tools bauen statt blind Bytes zu würfeln.

View file

@ -0,0 +1,151 @@
# CompatLayerCT / OEM-Layer: neue Erkenntnisse
Stand: 2026-05-25
## Kurzfazit
CompatLayerCT ist nicht nur irgendein Beifang, sondern ziemlich klar der OEM-Control-Layer zwischen OneXConsole und den eigentlichen Geräte-Backends.
Für internes RGB heißt das:
- OneXConsole spricht semantische Routen
- CompatLayerCT exponiert diese Routen lokal
- darunter steckt sehr wahrscheinlich ein HID-naher Backend-Pfad
- damit ist unser Linux-Ziel nicht sofort "zufällige 64 Bytes raten", sondern eher:
1. OEM-Routen konservieren
2. Linux-Backend dahinter nachbauen
## Bestätigte Architektur-Hinweise
Aus `background.js` in `app.asar`:
- Standard-Layer-Port: `1013`
- Standard-Layer-URL: `http://localhost:1013`
- Pipe-Name: `\\.\\pipe\\CompatLayerCT`
- `isPipeMode: true`
Das ist wichtig, weil die App zwei Modi kennt:
- HTTP POST gegen `http://localhost:1013/...`
- JSON-Line-IPC über Named Pipe
## Bestätigtes Pipe-Protokoll auf App-Seite
OneXConsole baut pro Request JSON etwa in dieser Form:
```json
{
"id": "uuid",
"path": "/programhandle/rgb/setPreset/0",
"parameters": null
}
```
Wichtige Details:
- Zeilenende: `\n`
- Antworten werden zeilenweise verarbeitet
- Response-Shape:
- `id`
- `code`
- `result`
- Sonderfall Biz-Event:
- `id == "__SocketBizEvent"`
Praktische Konsequenz:
- die OEM-Semantik ist schon oberhalb des echten HID-Protokolls sichtbar
- wir können eine Linux-Bridge mit genau diesen Routen vorbereiten
## Aus CompatLayerCT.exe bestätigte Service-Routen
Per read-only-Strings bestätigt:
### ProgramHandle allgemein
- `programhandle/connect`
- `programhandle/disconnect`
- `programhandle/isOpen`
- `programhandle/resetAll`
### ProgramHandle RGB
- `programhandle/rgb/setPreset/{mode}`
- `programhandle/rgb/setColor/{r}/{g}/{b}`
- `programhandle/rgb/assignSetColor/{target}/{r}/{g}/{b}`
- `programhandle/rgb/setOpen2/{open}/{lightLevel}`
- `programhandle/rgb/assignSetOpen/{target}/{open}`
### Weitere RGB-Ebenen
- `rgb/setPreset/{mode}`
- `rgb/setColor/{r}/{g}/{b}`
- `rgb/setOpen2/{open}/{lightLevel}`
- `rgbPartition/setPreset/{pcode}/{mode}`
Das riecht nach mehreren Abstraktionsebenen:
- rohe RGB-Ebene
- Partition-/Zonen-Ebene
- ProgramHandle-spezifische Ebene
## Aus CompatLayerCT.exe bestätigte Backend-Hinweise
Read-only-Strings mit hoher Relevanz:
- `CompatLayerCT.programhandle.hid`
- `PortRgbHelper`
- `HidLibrary`
- `HidSharp`
- `NamedPipeServerStream`
- `SetFeature`
- `HidD_SetFeature`
- `SendFeatureReport`
- `OUT_reportByteLength`
- `InputReportByteLength`
Einordnung:
- sehr starkes Indiz für HID-Backend, nicht nur EC-Magie
- Feature-Reports bleiben für `hidraw1` weiter relevant
- Output-Report-Pfad bleibt für `hidraw2` weiter relevant
## Was das für Super X konkret bedeutet
Aktuell wahrscheinlichste Architektur:
```text
OneXConsole GUI
-> CompatLayerCT (HTTP/Pipe)
-> ProgramHandle HID helper
-> USB HID Interface(s)
-> interner RGB-/Keyboard-/Zone-Controller
```
Mit deiner aktuellen Linux-Lage passt das überraschend gut zu:
- `hidraw2` als kleiner Vendor-OUT-Kandidat
- `hidraw1` als möglicher Feature-/Companion-Kandidat
## Saubere nächste Proben
Noch keine wilden HID-Bytes.
Zuerst semantisch kleinste OEM-Level-Proben festhalten:
1. `/programhandle/connect`
2. `/programhandle/isOpen`
3. `/programhandle/rgb/setOpen2/false/0`
4. `/programhandle/rgb/setOpen2/true/0`
5. `/programhandle/rgb/setPreset/0`
6. `/programhandle/rgb/assignSetOpen/0/true`
7. erst danach `/programhandle/rgb/setColor/16/16/16`
Warum so herum:
- erst Pfad/Enable/Mode
- dann einzelne Zielprobe
- dunkle Farbe ganz am Ende
## Neue Repo-Hilfen
- `tools/compatlayerct_strings_probe.py`
- read-only Strings-Probe auf `CompatLayerCT.exe`
- `tools/superx_rgb_bridge.py`
- Linux-Bridge-Scaffold auf OEM-Routenebene
- aktuell Dry-Run / JSON-Line-Output
- später Hook für echten HID-Backend-Code
## Arbeitsentscheidung nach diesen Funden
Die sinnvollste Linux-Richtung ist gerade:
- nicht direkt „RGB-Treiber mit geratenen Payloads“
- sondern zuerst „CompatLayerCT-Semantik + Linux-Backend-Gerüst“
Das ist weniger sexy, aber viel weniger Quatsch.

View file

@ -0,0 +1,100 @@
# Frostbay / CoolingSystem_ONEC1 Protokollnotizen
Basisquellen:
- lokales OEM-Archiv: `/home/nepharius/Downloads/frostbay firmware.zip`
- Community-Repo: https://github.com/tbitu/onexplayer-frostbay-bluetooth
- hhd PR: https://github.com/hhd-dev/hhd/pull/321
## Aus lokalem OEM-Paket bestätigt
### Archivinhalt
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/BLE-FW-TOOL.exe`
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/BLEDebug.EXE`
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/coolingsystem_debugger.html`
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/CoolingSystem_ONEC1_V08.bin`
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/FWConfig.ini`
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/HIDFirmwareUpgrad.exe`
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/CH375DLL64.dll`
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/WCHBLEDLL.dll`
- `GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/接口.C`
### GATT-Ziele aus `coolingsystem_debugger.html`
- Service: `0000ffe0-0000-1000-8000-00805f9b34fb`
- Characteristic: `0000ffe1-0000-1000-8000-00805f9b34fb`
- Device-Name-Filter: `CoolingSystem_ONEC1`
### Firmware-/Silizium-Hinweise
- Firmware-String: `CH32V20x_BLE_LIB_V1.4`
- Das riecht ziemlich stark nach WCH-CH32V20x-BLE-Firmware.
### Relevante Status-/Steuerfelder aus OEM-Kommentaren
- `state[4]`: Betriebsmodus
- `0x00` = aus
- `0xFE` = intelligent/smart
- `0xFF` = custom/fixed
- `state[5]`: Fan PWM / Fan-Byte
- `state[8]`: Pump PWM / Pump-Byte
- `state[6..7]`: Fan RPM readback
- `state[9..10]`: Pump RPM readback
- `state[11..12]`: Water flow readback
- `state[13]`: inlet temp
- `state[14]`: outlet temp
- `state[15]`: Alarmbits
- `state[16]`:
- `0xFD` = RGB switch/frequency/brightness mode
- `0xFE` = custom RGB mode
- `state[17..19]`:
- bei `0xFD`: switch / frequency / brightness
- bei `0xFE`: R / G / B
- `state[23..40]`: Kurven-/Presetbereich
- `state[57]`: Auto-RGB-Off-Zeit wenn Pumpe/Lüfter aus
### Frostbay-RGB-Hinweis
Im OEM-Code tauchen drei RGB-Strips/Kanäle auf.
Bitmaske:
- `0x01` = Strip 1
- `0x02` = Strip 2
- `0x04` = Strip 3
Das ist Frostbay-intern wichtig, nicht automatisch identisch mit dem Super-X-Mainboard-RGB-Pfad.
## Linux-/BlueZ-Notizen aus Community-Repo
### Wichtig
- Der Linux-Weg scheint über BlueZ + D-Bus machbar.
- Ein zweiter Userspace-GATT-Client muss wohl nicht künstlich "neu verbinden", wenn BlueZ die Session schon besitzt.
- Wichtige Zustände:
- `Connected=true`
- `ServicesResolved=true`
- `FFE0` in UUID-Liste
- GATT-Objekt für `FFE1` vorhanden
### Bekannter Stolperstein
- Auf dem Apex scheint der eingebaute BT-Adapter problematisch zu sein.
- Ein externer Bluetooth-Dongle lieferte laut Community die bessere/volle GATT-Sicht.
### Nerviger HID-Nebeneffekt
Beim Verbinden kann Frostbay zusätzlich ein HID-Device exposen, das Volume Up/Down-Müll erzeugt.
Community-Mitigation via hwdb:
```text
evdev:input:b0005v07D7p0000*
KEYBOARD_KEY_c00e9=reserved
KEYBOARD_KEY_c00ea=reserved
```
Danach typischerweise:
```bash
sudo systemd-hwdb update
sudo udevadm trigger /sys/class/input/eventX
```
## Arbeitsannahme für Arch / COSMIC
Realistischster kurzer Weg:
1. Frostbay per BlueZ sichtbar machen
2. Services/Characteristics prüfen
3. mit D-Bus lesen/schreiben
4. danach kleines Python-Tool bauen
5. optional später hhd-Plugin oder Standalone-Daemon

View file

@ -0,0 +1,35 @@
# OEM-Routen und Helper-Hinweise aus OneXConsole
Quelle:
- `/home/nepharius/Downloads/OneXConsole_0.9.6-fix15.exe`
- extrahiertes `app.asar` unter `.cache/onexconsole/`
## Bestätigte RGB-Routen
- `/programhandle/rgb/setPreset/{id}`
- `/programhandle/rgb/setColor/{r}/{g}/{b}`
- `/programhandle/rgb/assignSetColor/{zone}/{r}/{g}/{b}`
- `/programhandle/rgb/setOpen2/{bool}/{zone}`
- `/programhandle/rgb/assignSetOpen/{zone}/{bool}`
## Bestätigter Helper-/IPC-Hinweis
- Named Pipe: `\\.\\pipe\\CompatLayerCT`
- String: `CompatLayerCT`
## Bestätigte Zonennamen aus OneXConsole-Strings
- `rgbPartition_left_stick` -> `Left Joystick`
- `rgbPartition_right_stick` -> `Right Joystick`
- `rgbPartition_kb` -> `Keyboard`
- `rgbPartition_back` -> `Back`
- `rgbPartition_v` -> `V-Zone`
- `rgbPlace0304` -> `Middle Light`
- `rgbPlace0304Enable` -> `Middle Light Enable`
## Einordnung
Das sieht ziemlich klar nach einer logischen Zonenschicht über einem OEM-Helper aus.
Die GUI spricht also eher nicht direkt mit einem simplen LED-Device, sondern mit einer Abstraktion, die am Ende wahrscheinlich HID/EC/MCU anspricht.
## Nächste Folgefrage
Wir müssen herausfinden, ob `CompatLayerCT` am Ende:
- `hidraw2` benutzt,
- `hidraw1`-Feature-Reports benutzt,
- oder einen nicht-HID-Pfad nimmt.

95
docs/internal-rgb-plan.md Normal file
View file

@ -0,0 +1,95 @@
# Interne RGB-Rekonstruktion: Arbeitsplan
> Für Hermes: nicht blind fuzzing. Erst App/Helper, dann Linux-Korrelation, dann kleinste sichere Schreibtests.
**Goal:** Die echte Linux-Steuerroute für die internen Super-X-RGB-Zonen finden und die WS2812B-Ersatzstripes kontrollierbar machen.
**Architecture:** OEM-App und lokale HID-Interfaces werden parallel analysiert. Wir behandeln OneXConsole als Quelle für Begriffe, Zonen und API-Formen; Linux-HID/EC als Quelle für den echten Transport. Erst wenn beide Seiten zusammenpassen, werden kleine Writes getestet.
**Tech Stack:** Python 3, hidraw/sysfs, lsusb, udevadm, OneXConsole-asar-Mining.
---
## Phase A: OEM-App vollständig auswerten
### Task A1: OneXConsole-Routen und Zonen sichern
**Objective:** Alle bestätigten RGB-Routen und Zonennamen in ein maschinenlesbares Format bringen.
**Files:**
- Create: `docs/internal-rgb-oem-routes.md`
- Create: `tools/onexconsole_asar_probe.py`
**Verification:**
- Tool listet RGB-Routen und Pipe-Hinweise aus `OneXConsole_0.9.6-fix15.exe` / `app.asar`
- Ergebnis im Markdown dokumentiert
### Task A2: Helper-Schicht weiter eingrenzen
**Objective:** Hinweise auf `CompatLayerCT` und mögliche Transportformen sammeln.
**Files:**
- Modify: `docs/internal-rgb-oem-routes.md`
**Verification:**
- Dokument enthält alle gefundenen Pipe-/Helper-Strings
- klare Hypothese: Pipe -> helper -> HID/EC
## Phase B: Linux-Livepfad read-only mappen
### Task B1: HID-Inventur reproducible machen
**Objective:** Ein lokales Tool erzeugt einen sauberen Snapshot der HID-Kandidaten.
**Files:**
- Create: `tools/superx_rgb_inventory.py`
- Create: `docs/live-hid-inventory.md`
**Verification:**
- Script erzeugt Snapshot mit VID:PID, Interface, Permissions, Descriptor-Länge
- Snapshot bestätigt `hidraw2` als kleinsten Vendor-Kandidaten
### Task B2: Kandidaten sauber ranken
**Objective:** Dokumentieren, warum `hidraw2` vor `hidraw1` getestet wird.
**Files:**
- Modify: `docs/live-hid-inventory.md`
**Verification:**
- Ranking-Regeln sind klar dokumentiert
- keine Vermischung von Keyboard- und Vendor-Pfad
## Phase C: erste sichere Write-Phase
### Task C1: On/Off/Preset-Frames vorbereiten
**Objective:** Nur kleinste, reversible Testframes definieren.
**Files:**
- Create: `docs/safe-test-frames.md`
- Reuse/Modify: `tools/superx_hidraw_rgb.py`
**Verification:**
- Dokument enthält nur low-risk Frames
- kein wildes Farbzonen-Fuzzing
### Task C2: Vergleichstest `hidraw2` vs `hidraw1`
**Objective:** Nur wenn wir am Gerät sitzen: vergleichen, welcher Pfad reagiert.
**Files:**
- Modify: `docs/safe-test-frames.md`
**Verification:**
- Jede Probe mit exaktem Hexdump + sichtbarer Beobachtung protokolliert
## Phase D: danach echtes Tooling
### Task D1: kleines Linux-CLI bauen
**Objective:** Wenn die Route gefunden ist, ein minimales `superx-rgbctl` bauen.
**Files:**
- Future: `src/superx_rgbctl/...`
**Verification:**
- preset/solid/on/off reproduzierbar
## Aktueller Status
- Phase A teilweise erledigt
- Phase B teilweise erledigt
- Phase C noch absichtlich nicht gestartet

125
docs/internal-rgb-status.md Normal file
View file

@ -0,0 +1,125 @@
# Interne RGB-Stripes / Keyboard-RGB: aktueller Stand
Stand: 2026-05-25
## Kurzfazit
Die interne Super-X-RGB-Steuerung lebt sehr wahrscheinlich nicht in einem separaten Windows-Treiber, sondern in der OEM-App-/Helper-Schicht.
Der aktuell beste Linux-Kandidat für direkte Vendor-Steuerung ist weiter:
- `/dev/hidraw2`
- USB VID:PID `1a86:1305`
- kleines Vendor-Descriptor-Layout
- 64-Byte Output-Report
- eigener Interrupt-OUT-Endpunkt
Aber:
- bekannte OXP/hhd-Gen1- und Gen2-RGB-Frames wurden in früheren Tests schon akzeptiert, ohne sichtbare LED-Reaktion
- daher ist noch unklar, ob
1. ein Init-/Unlock-Handshake fehlt,
2. die falsche HID-Schnittstelle gewählt wurde,
3. der echte Pfad über EC/ACPI/MCU läuft,
4. oder die Ersatz-WS2812B-Stripes zwar elektrisch passen, aber logisch anders angesteuert werden
## Bestätigte Live-Fakten auf diesem Gerät
### USB-Gerät
`lsusb -d 1a86:1305 -v` zeigt:
- Hersteller: `Juer Xin`
- Produkt: `Keyboard K2445`
- 3 HID-Interfaces
### Interface 0 / hidraw0
- Boot-Keyboard-artig
- nur Interrupt IN
- nicht der spannende RGB-Kandidat
### Interface 1 / hidraw1
- `hid-multitouch`
- großer Report Descriptor: 508 Byte
- eingebettete Vendor-Reports auf Usage Page `0xff00`
- eher komplizierter Misch-Interface-Kandidat
### Interface 2 / hidraw2
- kleiner Report Descriptor: 35 Byte
- Usage Page `0xff00`
- 8 Byte IN + 64 Byte OUT
- Interrupt OUT Endpoint `0x03`
- stärkster Vendor-/RGB-Kandidat
## OneXConsole-Funde
Lokales Artefakt:
- `/home/nepharius/Downloads/OneXConsole_0.9.6-fix15.exe`
Aus dem App-Bundle sind bestätigt:
- RGB-API-Routen:
- `/programhandle/rgb/setPreset/{id}`
- `/programhandle/rgb/setColor/{r}/{g}/{b}`
- `/programhandle/rgb/assignSetColor/{zone}/{r}/{g}/{b}`
- `/programhandle/rgb/setOpen2/{bool}/{zone}`
- `/programhandle/rgb/assignSetOpen/{zone}/{bool}`
- Helper/IPC:
- Named Pipe `\\.\\pipe\\CompatLayerCT`
- damit sehr wahrscheinlich GUI -> CompatLayerCT -> echter Control Path
## In OneXConsole sichtbare RGB-Zonen
- Left Joystick
- Right Joystick
- Keyboard
- Back
- V-Zone
- zusätzlich Strings wie `Middle Light` / `Middle Light Enable`
Das ist wichtig, weil es zeigt:
- Super X hat logisch mehr Zonen als nur die Stick-Ringe
- Keyboard und Back/V-Zone hängen wohl im selben OEM-Control-Universum
- die interne Strip-Reparatur mit WS2812B ändert nicht automatisch die logische Zonenstruktur
## Einordnung der Ersatzstripes
Bekannt:
- Ersatzstripes sind WS2812B
- laut dir vermutlich Anschluss `RGB2`
Bedeutung:
- sehr wahrscheinlich digital/addressable, nicht analog RGB
- das passt grundsätzlich besser zu einem datengetriebenen OEM-MCU-Pfad als zu einfacher 12V-RGB-Logik
- trotzdem ist der LED-Typ allein kein Protokollbeweis
## Was wir ohne Frostbay schon sinnvoll machen können
1. OEM-App weiter auswerten
- Routen
- Pipe-/Helper-Schicht
- mögliche Zonen-/Opcode-Mappings
2. Linux-seitig sichere Inventur / Read-only-Probing
- HID-Deskriptoren
- udev/Permissions
- Monitoren auf Input-Reports
3. Erst danach kleinste Schreibtests
- on/off
- preset
- nicht gleich RGB-Feuerwerk
## Was gerade noch NICHT bewiesen ist
- dass `hidraw2` wirklich der finale RGB-Schreibpfad ist
- dass Super X exakt das gleiche Protokoll wie andere OXP-Geräte fährt
- dass die ersetzten WS2812B-Stripes exakt so auf OEM-Daten reagieren wie die Originalteile
- dass Keyboard-RGB und Strip-RGB denselben Transport teilen
## Nächster engster sicherer Schritt
Nicht blind schreiben.
Sondern jetzt in dieser Reihenfolge:
- CompatLayerCT-/OEM-Layer semantisch konservieren
- Linux-Bridge-/Route-Gerüst vorbereiten
- dann nur kleinste HID-Proben gezielt gegen `hidraw2` und eventuell `hidraw1`
Neue belastbare Funde dafür:
- Layer läuft logisch über `http://localhost:1013` und/oder `\\.\\pipe\\CompatLayerCT`
- Pipe-Requests sind JSON-Lines mit `id`, `path`, `parameters`
- `CompatLayerCT.exe` enthält sowohl `programhandle/rgb/...`-Routen als auch starke HID-Hinweise (`CompatLayerCT.programhandle.hid`, `SetFeature`, `SendFeatureReport`)

View file

@ -0,0 +1,48 @@
# Live HID Inventory: Super X RGB-Kandidaten
Stand vom lokalen Gerät heute.
## USB-Gerät
`lsusb -d 1a86:1305 -v`:
- `ID 1a86:1305`
- `Juer Xin / Keyboard K2445`
- 3 HID-Interfaces
## Kandidaten
### `/dev/hidraw0`
- Interface 0
- Boot-Keyboard-artig
- Descriptor-Länge: 63
- nur IN-Endpunkt
- Rechte: root-only
- Bewertung: nicht erster RGB-Kandidat
### `/dev/hidraw1`
- Interface 1
- Treiber: `hid-multitouch`
- Descriptor-Länge: 508
- enthält Vendor-Pages/Reports auf `0xff00`
- Rechte: les-/schreibbar
- Bewertung: interessant, aber komplizierter Mischkandidat
### `/dev/hidraw2`
- Interface 2
- Treiber: `hid-generic`
- Descriptor-Länge: 35
- Descriptor: `0600ff0a00ffa101150026ff0075089508090181020901150026ff00750895409102c0`
- 8 Byte IN, 64 Byte OUT
- eigener Interrupt-OUT-Endpunkt
- Rechte: les-/schreibbar
- Bewertung: bester erster RGB-Kandidat
## Passive Beobachtung
`tools/superx_hidraw_rgb.py --device /dev/hidraw2 monitor --duration-ms 400`
- keine spontanen Events gesehen
- heißt nur: kein ungefragter Traffic in diesem kurzen Fenster
- heißt NICHT: falsches Device
## Wichtigste Schlussfolgerung
Wenn wir die internen RGB-Stripes ohne Frostbay weiter verfolgen, ist der sinnvollste erste Write-Zielpfad weiterhin `hidraw2`.
`hidraw1` bleibt der zweite Kandidat, falls `hidraw2` zwar sauber schreibt, aber tot bleibt.

50
docs/next-steps.md Normal file
View file

@ -0,0 +1,50 @@
# Nächste Schritte
## Phase 1: Frostbay zuerst
1. BlueZ-Discovery auf dem echten Gerät prüfen
- erscheint `CoolingSystem_ONEC1`?
- welcher Adapter funktioniert wirklich?
- wird `FFE1` sauber sichtbar?
2. Linux-Probe bauen
- read current state
- set OFF
- set smart preset
- set manual fan/pump
3. HID-Nebenwirkungen dokumentieren
- welches `/dev/input/eventX` entsteht?
- wie sieht passende hwdb-Regel auf deinem Gerät genau aus?
## Phase 2: Super-X-RGB / Keyboard
1. Live-Geräteinventur
- `lsusb`
- `usb-devices`
- `hid-recorder` / `usbhid-dump`
- `dmesg | grep -Ei 'hid|oxp|onex|platform|wmi|rgb|led'`
- `/sys/devices/platform/` und `/sys/class/leds/`
2. Kandidaten sortieren
- separater vendor HID für RGB?
- EC-/ACPI-Pfad?
- hhd-kompatibler hid_v1/hid_v2-Pfad?
3. Erst danach sichere Schreibtests
- keine blind random Bytes
- erst Report-Längen / Feature-vs-Output sauber klären
## Phase 3: Nutzbares Tooling
- Standalone `superx-frostbayctl`
- evtl. `superx-rgb-probe`
- udev/hwdb snippets
- Arch-PKG oder Repo-Howto
## Architekturidee fürs Repo
- `docs/` = Wissen
- `tools/` = kleine Diagnose-/Analysehelfer
- später `src/` = echte Linux-Tools
- später `packaging/` = Arch-/PKGBUILD-Kram

144
docs/research-summary.md Normal file
View file

@ -0,0 +1,144 @@
# Research Summary: OneXplayer Super X / RGB / Frostbay
Stand: 2026-05-25
## Kurzfazit
Das brauchbarste Einfallstor ist gerade nicht die interne RGB-Leiste, sondern Frostbay.
Dafür gibt es inzwischen:
- ein OEM-Firmware-/Debug-Paket in deinem Downloads-Ordner
- konkrete BLE-UUIDs
- ein frisches Community-Repo mit Linux-/BlueZ-Erkenntnissen
- eine offene hhd-PR für ein Frostbay-Plugin
Für die interne Super-X-RGB-/Keyboard-Steuerung gibt es dagegen noch kein klar bestätigtes fertiges Linux-Tooling. Es gibt aber verwandte OXP-/hhd-/Kernel-Arbeit, die uns beim Reverse Engineering hilft.
## Web-Funde mit hoher Relevanz
### hhd / laufende Entwicklung
- hhd Issue: Fan Controls for OneXPlayer Super X
- https://github.com/hhd-dev/hhd/issues/303
- Aussage: "Everything in HHD works quite well for the new Super X device. However, there are currently no fan controls."
- Kommentar verweist direkt auf Linux-Kernel-Datei `oxpec.c` und EC-Registerabgleich.
- hhd PR: Add Frostbay cooling plugin
- https://github.com/hhd-dev/hhd/pull/321
- Beschreibung nennt explizit Apex und Super-X/Super-V.
- Wichtige Probleme laut PR:
1. eingebauter Bluetooth-Adapter des Apex arbeitet mit BlueZ/Frostbay derzeit unzuverlässig
2. Frostbay erzeugt unter Linux ein HID-Keyboard/Consumer-Control-artiges Gerät mit nervigem Volume-Down-Fehler
- hhd PR: oxp: enable secondary RGB LED support for hid_v1 protocol
- https://github.com/hhd-dev/hhd/pull/239
- zeigt, dass in hhd bereits sekundäre RGB-LEDs bei OXP-nahen Geräten aktiv Thema sind
- macht aber NICHT automatisch Super-X-internes RGB gelöst
### Community-Repo Frostbay
- https://github.com/tbitu/onexplayer-frostbay-bluetooth
- README beschreibt den Frostbay-BLE-Stack als Entwicklerreferenz
- dort festgehalten:
- FFE0/FFE1-GATT-Pfad
- BlueZ-/D-Bus-Nutzung unter Linux
- Problem mit eingebautem BT-Adapter vs. externem Adapter
- HID-/Volume-Up/Volume-Down-Nebenwirkung und hwdb-Mitigation
### Linux-Kernel / OXP-EC-Pfad
- https://github.com/torvalds/linux/blob/master/drivers/platform/x86/oxpec.c
- Datei beschreibt OneXPlayer-/AOKZOE-Platform-Treiber
- nennt u.a.:
- Fan/PWM-Skalierung für verschiedene Generationen
- X1 Turbo LED Register
- DMI-Quirks / EC-Zugriff
- Relevanz: Super-X-Fan-/EC-Themen sind im OXP-Kosmos nicht komplett exotisch
### OpenRGB
- Keine belastbaren Treffer für fertigen OpenRGB-Support speziell für OneXplayer Super X.
- Für unser Gerät also aktuell eher nein/unklar als "läuft schon".
## Lokale OEM-Artefakte: wichtigste Erkenntnisse
### Frostbay-ZIP
Datei:
- `/home/nepharius/Downloads/frostbay firmware.zip`
Wichtige Inhalte im Archiv:
- `BLE-FW-TOOL.exe`
- `BLEDebug.EXE`
- `coolingsystem_debugger.html`
- `CoolingSystem_ONEC1_V08.bin`
- `FWConfig.ini`
- `HIDFirmwareUpgrad.exe`
- `CH375DLL64.dll`
- `WCHBLEDLL.dll`
- `接口.C`
Wichtige Befunde:
- Produktname: `CoolingSystem_ONEC1`
- Firmware enthält String `CH32V20x_BLE_LIB_V1.4`
- wirkt sehr nach WCH/CH32V20x-BLE-MCU
- HTML-Debugger nennt:
- Service UUID `0000ffe0-0000-1000-8000-00805f9b34fb`
- Characteristic UUID `0000ffe1-0000-1000-8000-00805f9b34fb`
- Bluetooth-Gerätename `CoolingSystem_ONEC1`
- Protokoll ist 64-Byte-orientiert
- RGB-relevante Bytes aus OEM-Code:
- `state[16] = 0xFD` für RGB-Schalter/Frequenz/Helligkeit
- `state[16] = 0xFE` für Custom RGB
- `state[17..19]` für Schalter/Frequenz/Helligkeit oder RGB-Werte je nach Modus
- drei Lichtkanäle/Stripes im Frostbay-Code sichtbar:
- Bit 0 = Strip 1
- Bit 1 = Strip 2
- Bit 2 = Strip 3
### Super-X-Driverbundle-ZIP
Datei:
- `/home/nepharius/Downloads/HH-GA25-SUPERX-Devices_V1.0.zip`
Eindruck:
- normales Windows-Treiberbundle
- kein klarer Frostbay-/ONEC1- oder Keyboard-RGB-Treiber drin gefunden
- WCH dort nur als USB-NIC-Kontext auffällig, nicht als offensichtlicher RGB-Treiber
## Einschätzung je Baustelle
### 1) Frostbay unter Arch/COSMIC
Status: am greifbarsten
Warum:
- OEM-Tool vorhanden
- Community-Protokoll vorhanden
- hhd-Plugin in Arbeit
- Linux-Pfad via BlueZ/D-Bus scheint realistisch
Wahrscheinlichster Linux-Ansatz:
1. BlueZ-Device für `CoolingSystem_ONEC1` sauber sichtbar machen
2. `ServicesResolved=true` und `FFE1` finden
3. per D-Bus `ReadValue` / `WriteValue` nutzen
4. ggf. eingebauten BT-Adapter meiden und Test mit externem Dongle machen
5. nerviges HID-Volume-Device per hwdb neutralisieren
### 2) Interne RGB-Stripes / Keyboard-RGB
Status: noch Nebel
Was wir wissen:
- deine Ersatzstripes sind WS2812B
- laut dir vermutlich Board-Anschluss `RGB2`
- hhd kennt sekundäre RGB-LEDs bei verwandten OXP-Protokollen
- aber: das beweist nichts für Super X
Was noch fehlt:
- welcher Transport ist echt?
- vendor HID?
- EC?
- ACPI/WMI?
- Hilfs-MCU?
- welche USB-/HID-IDs und Report-Formate nutzt Super X live?
- ob interne RGB und Keyboard über denselben oder getrennte Pfade laufen
## Meine ehrliche Priorisierung
1. Frostbay zuerst linuxfähig machen
2. danach Super-X-HID/EC sauber mappen
3. erst dann interne RGB aktiv anfassen
Das ist der Weg mit dem besten Signal-Rausch-Verhältnis. Alles andere wäre eher Voodoo mit LEDs.

45
docs/safe-test-frames.md Normal file
View file

@ -0,0 +1,45 @@
# Safe Test Frames / Regeln
Noch kein finales Protokoll. Deshalb erstmal Regeln statt Heldentum.
## Do
- nur einzelne kurze Testframes
- jede Probe mit Device, Hex, sichtbarer Wirkung notieren
- zuerst `hidraw2`, dann erst `hidraw1`
- erst Preset/Enable/Disable, dann Farbe
## Don't
- kein blindes Fuzzing
- keine Schleifen mit hunderten Writes
- keine zufälligen Feature-Reports in alle Interfaces ballern
## Aktuell plausible Klassen von Testframes
### Klasse 1: OEM-Level Path-Proben
Nicht direkt HID-Bytes, sondern kleinste semantische CompatLayerCT-Routen:
- `/programhandle/connect`
- `/programhandle/isOpen`
### Klasse 2: globale Enable-/Disable-Proben
Passend zu bestätigten Routen:
- `/programhandle/rgb/setOpen2/false/0`
- `/programhandle/rgb/setOpen2/true/0`
### Klasse 3: kleine Preset-/Mode-Proben
Passend zu:
- `/programhandle/rgb/setPreset/0`
- `/programhandle/rgb/assignSetOpen/0/true`
### Klasse 4: erst ganz zuletzt dunkle Color-Proben
Passend zu:
- `/programhandle/rgb/setColor/16/16/16`
- später erst `assignSetColor`
## Aktueller Arbeitsgrundsatz
CompatLayerCT ist jetzt klar genug, dass wir die OEM-Semantik zuerst festhalten.
Dann machen wir lieber 3 gute Proben als 300 dumme.
Praktisch heißt das:
- erst OEM-Route-Level katalogisieren
- dann Linux-Bridge-/Backend-Gerüst vorbereiten
- dann erst minimale HID-Writes gegen `hidraw2` und ggf. `hidraw1`

12
docs/sources.md Normal file
View file

@ -0,0 +1,12 @@
# Quellen
## Web / Upstream
- hhd Issue 303: https://github.com/hhd-dev/hhd/issues/303
- hhd PR 321: https://github.com/hhd-dev/hhd/pull/321
- hhd PR 239: https://github.com/hhd-dev/hhd/pull/239
- Frostbay community repo: https://github.com/tbitu/onexplayer-frostbay-bluetooth
- Linux kernel `oxpec.c`: https://github.com/torvalds/linux/blob/master/drivers/platform/x86/oxpec.c
## Lokal
- `/home/nepharius/Downloads/frostbay firmware.zip`
- `/home/nepharius/Downloads/HH-GA25-SUPERX-Devices_V1.0.zip`

View file

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
Read-only Probe für CompatLayerCT.exe.
- extrahiert per `strings` sichtbare RGB-/ProgramHandle-Routen
- zeigt Pipe-/Port-/HID-Hinweise
- schreibt nichts an Hardware
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
DEFAULT_EXE = Path.home() / "SuperX-goes-Arch/.cache/compatlayerct/CompatLayerCT.exe"
TERMS = {
"routes": [
"programhandle/connect",
"programhandle/disconnect",
"programhandle/isOpen",
"programhandle/resetAll",
"programhandle/rgb/setPreset",
"programhandle/rgb/setColor",
"programhandle/rgb/assignSetColor",
"programhandle/rgb/setOpen2",
"programhandle/rgb/assignSetOpen",
"rgb/setPreset",
"rgb/setColor",
"rgb/setOpen2",
"rgbPartition/setPreset",
"rgbPartition/setColor",
],
"hints": [
"CompatLayerCT.programhandle.hid",
"PortRgbHelper",
"HidLibrary",
"HidSharp",
"NamedPipeServerStream",
"pipeServer",
"StartPipeServer",
"SetFeature",
"HidD_SetFeature",
"SendFeatureReport",
"OUT_reportByteLength",
"InputReportByteLength",
],
}
def collect_strings(exe: Path) -> list[str]:
raw = subprocess.check_output(["strings", "-a", str(exe)], text=True, errors="replace")
return raw.splitlines()
def match_terms(lines: list[str]) -> dict[str, list[str]]:
result: dict[str, list[str]] = {}
for section, terms in TERMS.items():
hits: list[str] = []
seen: set[str] = set()
for line in lines:
lower = line.lower()
if any(term.lower() in lower for term in terms):
if line not in seen:
seen.add(line)
hits.append(line)
result[section] = hits
return result
def main() -> int:
parser = argparse.ArgumentParser(description="Read-only strings probe for CompatLayerCT.exe")
parser.add_argument("--exe", default=str(DEFAULT_EXE), help=f"path to CompatLayerCT.exe (default: {DEFAULT_EXE})")
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
args = parser.parse_args()
exe = Path(args.exe)
if not exe.exists():
print(f"missing exe: {exe}", file=sys.stderr)
return 1
lines = collect_strings(exe)
matches = match_terms(lines)
payload = {
"exe": str(exe),
"total_strings": len(lines),
"matches": matches,
"summary": {
"has_programhandle_rgb": any("programhandle/rgb/" in s for s in matches["routes"]),
"has_raw_rgb": any(s.startswith("rgb/") or "rgb/" in s for s in matches["routes"]),
"has_rgb_partition": any("rgbPartition/" in s for s in matches["routes"]),
"suggests_hid_backend": any("hid" in s.lower() for s in matches["hints"]),
"suggests_pipe_server": any("pipe" in s.lower() for s in matches["hints"]),
},
}
if args.json:
print(json.dumps(payload, indent=2, ensure_ascii=False))
return 0
print(f"exe: {exe}")
print(f"total_strings: {len(lines)}")
for section, hits in matches.items():
print(f"\n## {section}")
if not hits:
print("(none)")
continue
for hit in hits:
print(hit)
print("\n## summary")
for key, value in payload["summary"].items():
print(f"{key}: {value}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
Liest das lokale Frostbay-OEM-ZIP rein lesend und zieht die wichtigsten
UUID-/Protokoll-/String-Hinweise raus. Kein Entpacken, keine Writes.
"""
from __future__ import annotations
import re
import sys
import zipfile
from pathlib import Path
ZIP_PATH = Path.home() / "Downloads" / "frostbay firmware.zip"
FILES = [
"GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/FWConfig.ini",
"GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/coolingsystem_debugger.html",
"GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/接口.C",
"GA25水冷固件_V08/CoolingSystem_ONEC1-FW-TOOL-V1.8/CoolingSystem_ONEC1_V08.bin",
]
PATTERNS = [
r"0000ffe[0-9a-f]-0000-1000-8000-00805f9b34fb",
r"CoolingSystem_ONEC1",
r"CH32V20x_BLE_LIB_V[0-9.]+",
r"0xFD",
r"0xFE",
r"灯带[123]",
r"风扇",
r"水泵",
]
def decode_blob(data: bytes) -> str:
for enc in ("utf-8", "utf-16le", "gb18030", "latin1"):
try:
return data.decode(enc)
except Exception:
pass
return data.decode("utf-8", "replace")
def strings_from_binary(data: bytes, min_len: int = 4) -> list[str]:
out: list[str] = []
buf: list[str] = []
for b in data:
ch = chr(b)
if 32 <= b < 127:
buf.append(ch)
else:
if len(buf) >= min_len:
out.append("".join(buf))
buf = []
if len(buf) >= min_len:
out.append("".join(buf))
return out
def main() -> int:
if not ZIP_PATH.exists():
print(f"ZIP nicht gefunden: {ZIP_PATH}")
return 1
with zipfile.ZipFile(ZIP_PATH) as zf:
names = set(zf.namelist())
for member in FILES:
if member not in names:
print(f"Fehlt im Archiv: {member}")
continue
print(f"\n=== {member} ===")
data = zf.read(member)
if member.endswith(".bin"):
hits = [s for s in strings_from_binary(data) if any(re.search(p, s, re.I) for p in PATTERNS)]
for line in hits[:60]:
print(line)
continue
text = decode_blob(data)
for i, line in enumerate(text.splitlines(), 1):
if any(re.search(p, line, re.I) for p in PATTERNS):
print(f"{i:04d}: {line[:240]}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
Kleine Read-only-Hilfe zum Mining von OneXConsole app.asar.
Zieht gezielt RGB-Routen, Pipe-/Helper-Hinweise und Zonennamen raus.
"""
from __future__ import annotations
import json
import re
import struct
from pathlib import Path
DEFAULT_ASAR = Path.home() / "SuperX-goes-Arch/.cache/onexconsole/resources/app.asar"
PATTERNS = {
"route_prefixes": [
"/programhandle/rgb/setPreset/",
"/programhandle/rgb/setColor/",
"/programhandle/rgb/assignSetColor/",
"/programhandle/rgb/setOpen2/",
"/programhandle/rgb/assignSetOpen/",
],
"helpers": [
r"CompatLayerCT",
r"\\\\\.\\pipe\\CompatLayerCT",
],
"zones": [
r'rgbPartition_left_stick:\"[^\"]+\"',
r'rgbPartition_right_stick:\"[^\"]+\"',
r'rgbPartition_kb:\"[^\"]+\"',
r'rgbPartition_back:\"[^\"]+\"',
r'rgbPartition_v:\"[^\"]+\"',
r'rgbPlace0304:\"[^\"]+\"',
r'rgbPlace0304Enable:\"[^\"]+\"',
],
}
def load_asar_text(path: Path) -> dict[str, str]:
with path.open("rb") as f:
_, _, _, header_len = struct.unpack("<4I", f.read(16))
header = json.loads(f.read(header_len))
payload = f.read()
files: dict[str, str] = {}
def walk(prefix: str, node: dict) -> None:
if "files" in node:
for name, child in node["files"].items():
walk(f"{prefix}/{name}" if prefix else name, child)
return
if "offset" not in node or "size" not in node:
return
off = int(node["offset"])
size = int(node["size"])
if prefix.endswith((".js", ".json", ".html", ".txt")):
files[prefix] = payload[off : off + size].decode("utf-8", "replace")
walk("", header)
return files
def main() -> int:
if not DEFAULT_ASAR.exists():
print(f"app.asar nicht gefunden: {DEFAULT_ASAR}")
return 1
texts = load_asar_text(DEFAULT_ASAR)
interesting = [
p for p in texts if p == "background.js" or p.startswith("js/") or p == "package.json"
]
print(f"asar: {DEFAULT_ASAR}")
print(f"dateien gescannt: {len(interesting)}")
for section, pats in PATTERNS.items():
print(f"\n## {section}")
seen: set[str] = set()
for path in interesting:
text = texts[path]
if section == "route_prefixes":
for prefix in pats:
start = 0
while True:
idx = text.find(prefix, start)
if idx == -1:
break
snippet = text[max(0, idx - 80) : min(len(text), idx + 220)]
snippet = snippet.replace("\n", " ")
if snippet not in seen:
seen.add(snippet)
print(f"[{path}] {snippet}")
start = idx + len(prefix)
continue
for pat in pats:
for m in re.finditer(pat, text, re.I):
s = m.group(0)
if s in seen:
continue
seen.add(s)
print(f"[{path}] {s}")
if not seen:
print("(nichts gefunden)")
return 0
if __name__ == "__main__":
raise SystemExit(main())

318
tools/superx_hidraw_rgb.py Executable file
View file

@ -0,0 +1,318 @@
#!/usr/bin/env python3
import argparse
import binascii
import fcntl
import os
import select
import stat
import sys
import textwrap
from pathlib import Path
DEFAULT_DEVICE = "/dev/hidraw2"
DEFAULT_REPORT_LEN = 64
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def sysfs_info(device: str):
name = os.path.basename(device)
base = Path("/sys/class/hidraw") / name / "device"
info = {"sysfs_base": str(base)}
uevent = base / "uevent"
rep = base / "report_descriptor"
if uevent.exists():
parsed = {}
for line in uevent.read_text(errors="ignore").splitlines():
if "=" in line:
k, v = line.split("=", 1)
parsed[k] = v
info["uevent"] = parsed
if rep.exists():
data = rep.read_bytes()
info["report_descriptor_hex"] = data.hex()
info["report_descriptor_len"] = len(data)
return info
def file_info(device: str):
st = os.stat(device)
return {
"mode": stat.filemode(st.st_mode),
"uid": st.st_uid,
"gid": st.st_gid,
"major": os.major(st.st_rdev),
"minor": os.minor(st.st_rdev),
}
def can_open(device: str, flags: int):
try:
fd = os.open(device, flags | os.O_NONBLOCK)
except OSError as exc:
return False, f"{exc.__class__.__name__}: {exc}"
else:
os.close(fd)
return True, "ok"
def parse_hex(s: str) -> bytes:
cleaned = s.replace("0x", "").replace(" ", "").replace(":", "").replace("-", "")
if len(cleaned) % 2:
raise ValueError("Hex payload needs an even number of nybbles")
return binascii.unhexlify(cleaned)
def pad_payload(data: bytes, length: int, exact: bool) -> bytes:
if exact:
if len(data) != length:
raise ValueError(f"Exact mode requires {length} bytes, got {len(data)}")
return data
if len(data) > length:
raise ValueError(f"Payload too long: {len(data)} > {length}")
return data + bytes(length - len(data))
def open_for_write(device: str):
return os.open(device, os.O_RDWR | os.O_NONBLOCK)
def _IOC(direction: int, ioc_type: int, nr: int, size: int) -> int:
IOC_NRBITS = 8
IOC_TYPEBITS = 8
IOC_SIZEBITS = 14
IOC_NRSHIFT = 0
IOC_TYPESHIFT = IOC_NRSHIFT + IOC_NRBITS
IOC_SIZESHIFT = IOC_TYPESHIFT + IOC_TYPEBITS
IOC_DIRSHIFT = IOC_SIZESHIFT + IOC_SIZEBITS
return (direction << IOC_DIRSHIFT) | (ioc_type << IOC_TYPESHIFT) | (nr << IOC_NRSHIFT) | (size << IOC_SIZESHIFT)
IOC_WRITE = 1
IOC_READ = 2
def HIDIOCSFEATURE(length: int) -> int:
return _IOC(IOC_WRITE | IOC_READ, ord('H'), 0x06, length)
def send_feature_report(fd: int, data: bytes) -> int:
buf = bytearray(data)
return fcntl.ioctl(fd, HIDIOCSFEATURE(len(buf)), buf)
def cmd_info(args):
print(f"device: {args.device}")
try:
finfo = file_info(args.device)
print(f"perms: {finfo['mode']} uid={finfo['uid']} gid={finfo['gid']} major={finfo['major']} minor={finfo['minor']}")
except OSError as exc:
print(f"stat: ERROR {exc}")
return 1
rd_ok, rd_msg = can_open(args.device, os.O_RDONLY)
rw_ok, rw_msg = can_open(args.device, os.O_RDWR)
print(f"open r: {rd_ok} ({rd_msg})")
print(f"open rw:{rw_ok} ({rw_msg})")
try:
info = sysfs_info(args.device)
except Exception as exc:
print(f"sysfs: ERROR {exc}")
return 0
uevent = info.get("uevent", {})
for key in ["HID_NAME", "HID_ID", "HID_PHYS", "DRIVER", "MODALIAS"]:
if key in uevent:
print(f"{key.lower():8} {uevent[key]}")
rep_hex = info.get("report_descriptor_hex")
rep_len = info.get("report_descriptor_len")
if rep_hex is not None:
print(f"report_descriptor_len: {rep_len}")
print(f"report_descriptor_hex: {rep_hex}")
return 0
def cmd_write_hex(args):
payload = parse_hex(args.hex_payload)
final = pad_payload(payload, args.length, args.exact_length)
print(f"target: {args.device}")
print(f"input_len: {len(payload)}")
print(f"write_len: {len(final)}")
print(f"write_hex: {final.hex()}")
if args.dry_run:
print("dry-run: no bytes written")
return 0
try:
fd = open_for_write(args.device)
except OSError as exc:
eprint(f"open failed: {exc}")
return 2
try:
written = os.write(fd, final)
print(f"written: {written}")
if args.read_reply_ms > 0:
wait_for_reply(fd, args.read_reply_ms)
finally:
os.close(fd)
return 0
def cmd_feature_hex(args):
payload = parse_hex(args.hex_payload)
final = pad_payload(payload, args.length, args.exact_length)
print(f"target: {args.device}")
print(f"input_len: {len(payload)}")
print(f"feature_len: {len(final)}")
print(f"feature_hex: {final.hex()}")
if args.dry_run:
print("dry-run: no feature report sent")
return 0
try:
fd = open_for_write(args.device)
except OSError as exc:
eprint(f"open failed: {exc}")
return 2
try:
rc = send_feature_report(fd, final)
print(f"ioctl_rc: {rc}")
if args.read_reply_ms > 0:
wait_for_reply(fd, args.read_reply_ms)
finally:
os.close(fd)
return 0
def wait_for_reply(fd: int, timeout_ms: int):
timeout = timeout_ms / 1000.0
r, _, _ = select.select([fd], [], [], timeout)
if not r:
print(f"reply: none within {timeout_ms} ms")
return
try:
data = os.read(fd, 4096)
except OSError as exc:
print(f"reply_read_error: {exc}")
return
print(f"reply_len: {len(data)}")
print(f"reply_hex: {data.hex()}")
def cmd_read(args):
print(f"target: {args.device}")
try:
fd = os.open(args.device, os.O_RDONLY | os.O_NONBLOCK)
except OSError as exc:
eprint(f"open failed: {exc}")
return 2
try:
timeout = args.timeout_ms / 1000.0
r, _, _ = select.select([fd], [], [], timeout)
if not r:
print(f"no data within {args.timeout_ms} ms")
return 0
data = os.read(fd, args.max_bytes)
print(f"read_len: {len(data)}")
print(f"read_hex: {data.hex()}")
return 0
finally:
os.close(fd)
def cmd_monitor(args):
print(f"target: {args.device}")
print(f"monitoring for {args.duration_ms} ms... press buttons / change RGB now")
try:
fd = os.open(args.device, os.O_RDONLY | os.O_NONBLOCK)
except OSError as exc:
eprint(f"open failed: {exc}")
return 2
try:
deadline = __import__('time').monotonic() + (args.duration_ms / 1000.0)
seen = 0
while True:
remaining = deadline - __import__('time').monotonic()
if remaining <= 0:
break
r, _, _ = select.select([fd], [], [], remaining)
if not r:
break
data = os.read(fd, args.max_bytes)
seen += 1
print(f"event[{seen}] len={len(data)} hex={data.hex()}")
if seen == 0:
print("monitor: no events captured")
return 0
finally:
os.close(fd)
def build_parser():
p = argparse.ArgumentParser(
description="Tiny hidraw poking tool for Super X RGB reverse-engineering",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent(
"""
Examples:
superx_hidraw_rgb.py info
superx_hidraw_rgb.py write-hex 00 --dry-run
superx_hidraw_rgb.py write-hex '00 01 02 03' --length 64
superx_hidraw_rgb.py read --timeout-ms 500
superx_hidraw_rgb.py monitor --duration-ms 3000
Notes:
- Default target is /dev/hidraw2.
- Writes are padded to 64 bytes unless --exact-length is used.
- You usually need root or a udev rule for real writes.
"""
),
)
p.add_argument("--device", default=DEFAULT_DEVICE, help=f"hidraw node (default: {DEFAULT_DEVICE})")
sub = p.add_subparsers(dest="cmd", required=True)
info = sub.add_parser("info", help="show access + descriptor info")
info.set_defaults(func=cmd_info)
wh = sub.add_parser("write-hex", help="write a raw output report")
wh.add_argument("hex_payload", help="hex bytes, e.g. '00 ff 12 34'")
wh.add_argument("--length", type=int, default=DEFAULT_REPORT_LEN, help="pad target length (default: 64)")
wh.add_argument("--exact-length", action="store_true", help="require exact payload length instead of padding")
wh.add_argument("--dry-run", action="store_true", help="print final frame only")
wh.add_argument("--read-reply-ms", type=int, default=0, help="optionally wait for one reply after write")
wh.set_defaults(func=cmd_write_hex)
fh = sub.add_parser("feature-hex", help="send a raw HID feature report via ioctl")
fh.add_argument("hex_payload", help="hex bytes including report ID, e.g. '1d 01'")
fh.add_argument("--length", type=int, default=DEFAULT_REPORT_LEN, help="pad target length (default: 64)")
fh.add_argument("--exact-length", action="store_true", help="require exact payload length instead of padding")
fh.add_argument("--dry-run", action="store_true", help="print final frame only")
fh.add_argument("--read-reply-ms", type=int, default=0, help="optionally wait for one reply after ioctl")
fh.set_defaults(func=cmd_feature_hex)
rd = sub.add_parser("read", help="read one report if available")
rd.add_argument("--timeout-ms", type=int, default=500, help="wait time for one read")
rd.add_argument("--max-bytes", type=int, default=4096, help="max bytes to read")
rd.set_defaults(func=cmd_read)
mon = sub.add_parser("monitor", help="monitor input reports for a short window")
mon.add_argument("--duration-ms", type=int, default=3000, help="monitor duration in milliseconds")
mon.add_argument("--max-bytes", type=int, default=4096, help="max bytes per read")
mon.set_defaults(func=cmd_monitor)
return p
def main():
parser = build_parser()
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())

179
tools/superx_rgb_bridge.py Normal file
View file

@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
Kleine Linux-seitige Bridge-Vorstufe für SuperX-RGB.
Noch kein echter HID-Treiber erstmal saubere OEM-Routen + Dry-Run.
Idee:
- OEM-Semantik aus OneXConsole/CompatLayerCT bewahren
- später Backend für hidraw2/hidraw1 oder EC/MCU einklinken
- aktuell standardmäßig nur JSON-Line-Requests ausgeben
"""
from __future__ import annotations
import argparse
import json
import sys
import uuid
from dataclasses import dataclass
@dataclass
class Request:
path: str
params: dict | None = None
def as_pipe_message(self) -> dict:
return {
"id": str(uuid.uuid4()),
"path": self.path,
"parameters": self.params,
}
def emit(req: Request, dry_run: bool, pretty: bool) -> int:
msg = req.as_pipe_message()
if pretty:
print(json.dumps(msg, indent=2, ensure_ascii=False))
else:
print(json.dumps(msg, ensure_ascii=False))
if dry_run:
print("# dry-run: keine Hardware-Writes")
return 0
print("# no-op backend: echte HID/EC-Implementierung noch offen", file=sys.stderr)
return 2
def add_bool_arg(parser: argparse.ArgumentParser, name: str, help_text: str) -> None:
parser.add_argument(name, choices=["true", "false"], help=help_text)
def parse_bool(value: str) -> bool:
return value.lower() == "true"
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description="SuperX RGB bridge scaffold (OEM-route level, dry-run first)")
p.add_argument("--dry-run", action="store_true", default=True, help="standardmäßig nur JSON-Line-Requests ausgeben")
p.add_argument("--pretty", action="store_true", help="JSON hübsch formatieren")
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("connect", help="programhandle/connect")
sub.add_parser("disconnect", help="programhandle/disconnect")
sub.add_parser("is-open", help="programhandle/isOpen")
sub.add_parser("reset-all", help="programhandle/resetAll")
sp = sub.add_parser("set-preset", help="programhandle/rgb/setPreset/{mode}")
sp.add_argument("mode", type=int)
sc = sub.add_parser("set-color", help="programhandle/rgb/setColor/{r}/{g}/{b}")
sc.add_argument("r", type=int)
sc.add_argument("g", type=int)
sc.add_argument("b", type=int)
asc = sub.add_parser("assign-set-color", help="programhandle/rgb/assignSetColor/{target}/{r}/{g}/{b}")
asc.add_argument("target", type=int)
asc.add_argument("r", type=int)
asc.add_argument("g", type=int)
asc.add_argument("b", type=int)
so = sub.add_parser("set-open2", help="programhandle/rgb/setOpen2/{open}/{lightLevel}")
add_bool_arg(so, "open", "true|false")
so.add_argument("light_level", type=int)
aso = sub.add_parser("assign-set-open", help="programhandle/rgb/assignSetOpen/{target}/{open}")
aso.add_argument("target", type=int)
add_bool_arg(aso, "open", "true|false")
cat = sub.add_parser("catalog", help="zeige die kleinsten sinnvollen OEM-Level-Testproben")
cat.add_argument("--json", action="store_true")
return p
def catalog() -> list[dict]:
return [
{
"id": "P0",
"path": "/programhandle/connect",
"risk": "sehr niedrig",
"why": "öffnet nur den ProgramHandle-Pfad, noch keine RGB-Farbmutation",
},
{
"id": "P1",
"path": "/programhandle/isOpen",
"risk": "sehr niedrig",
"why": "Statusprobe, semantisch read-mostly auf OEM-Level",
},
{
"id": "P2",
"path": "/programhandle/rgb/setOpen2/false/0",
"risk": "niedrig",
"why": "globale Abschalt-/LightLevel-Probe, reversibel",
},
{
"id": "P3",
"path": "/programhandle/rgb/setOpen2/true/0",
"risk": "niedrig",
"why": "kleinster globaler Enable-Gegentest",
},
{
"id": "P4",
"path": "/programhandle/rgb/setPreset/0",
"risk": "niedrig bis mittel",
"why": "Mode-/Preset-Probe vor zonierten Farben",
},
{
"id": "P5",
"path": "/programhandle/rgb/assignSetOpen/0/true",
"risk": "niedrig bis mittel",
"why": "einzelne Ziel-/Zonenprobe vor Farbdaten",
},
{
"id": "P6",
"path": "/programhandle/rgb/setColor/16/16/16",
"risk": "mittel",
"why": "sehr dunkle globale Farbausgabe als erste Farbprobe",
},
]
def main() -> int:
parser = build_parser()
args = parser.parse_args()
if args.cmd == "catalog":
items = catalog()
if args.json:
print(json.dumps(items, indent=2, ensure_ascii=False))
else:
for item in items:
print(f"{item['id']}: {item['path']} | risk={item['risk']} | {item['why']}")
return 0
if args.cmd == "connect":
return emit(Request("/programhandle/connect"), args.dry_run, args.pretty)
if args.cmd == "disconnect":
return emit(Request("/programhandle/disconnect"), args.dry_run, args.pretty)
if args.cmd == "is-open":
return emit(Request("/programhandle/isOpen"), args.dry_run, args.pretty)
if args.cmd == "reset-all":
return emit(Request("/programhandle/resetAll"), args.dry_run, args.pretty)
if args.cmd == "set-preset":
return emit(Request(f"/programhandle/rgb/setPreset/{args.mode}"), args.dry_run, args.pretty)
if args.cmd == "set-color":
return emit(Request(f"/programhandle/rgb/setColor/{args.r}/{args.g}/{args.b}"), args.dry_run, args.pretty)
if args.cmd == "assign-set-color":
return emit(Request(f"/programhandle/rgb/assignSetColor/{args.target}/{args.r}/{args.g}/{args.b}"), args.dry_run, args.pretty)
if args.cmd == "set-open2":
return emit(Request(f"/programhandle/rgb/setOpen2/{str(parse_bool(args.open)).lower()}/{args.light_level}"), args.dry_run, args.pretty)
if args.cmd == "assign-set-open":
return emit(Request(f"/programhandle/rgb/assignSetOpen/{args.target}/{str(parse_bool(args.open)).lower()}"), args.dry_run, args.pretty)
parser.error(f"unknown cmd: {args.cmd}")
return 2
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""
Read-only Inventur der Super-X-HID-Kandidaten.
Kein Schreiben, kein ioctl, nur Snapshot aus /dev und /sys.
"""
from __future__ import annotations
import argparse
import os
import stat
from pathlib import Path
DEFAULT_GLOB = "/dev/hidraw*"
def stat_line(dev: str) -> str:
st = os.stat(dev)
return (
f"{stat.filemode(st.st_mode)} uid={st.st_uid} gid={st.st_gid} "
f"major={os.major(st.st_rdev)} minor={os.minor(st.st_rdev)}"
)
def openable(dev: str, flags: int) -> str:
try:
fd = os.open(dev, flags | os.O_NONBLOCK)
except OSError as exc:
return f"no ({exc.__class__.__name__}: {exc})"
else:
os.close(fd)
return "yes"
def read_text(path: Path) -> str:
try:
return path.read_text(errors="replace").strip()
except Exception:
return ""
def read_bytes_hex(path: Path) -> tuple[int, str]:
try:
data = path.read_bytes()
except Exception:
return 0, ""
return len(data), data.hex()
def inspect(dev: str) -> None:
name = os.path.basename(dev)
base = Path("/sys/class/hidraw") / name / "device"
uevent = {}
for line in read_text(base / "uevent").splitlines():
if "=" in line:
k, v = line.split("=", 1)
uevent[k] = v
rep_len, rep_hex = read_bytes_hex(base / "report_descriptor")
print(f"device: {dev}")
print(f" perms: {stat_line(dev)}")
print(f" open r : {openable(dev, os.O_RDONLY)}")
print(f" open rw: {openable(dev, os.O_RDWR)}")
print(f" hid_name: {uevent.get('HID_NAME','')}")
print(f" hid_id: {uevent.get('HID_ID','')}")
print(f" hid_phys: {uevent.get('HID_PHYS','')}")
print(f" driver: {uevent.get('DRIVER','')}")
print(f" modalias: {uevent.get('MODALIAS','')}")
print(f" report_descriptor_len: {rep_len}")
if rep_hex:
print(f" report_descriptor_hex: {rep_hex}")
print()
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("devices", nargs="*", help="explizite hidraw devices, z.B. /dev/hidraw1")
args = ap.parse_args()
devices = args.devices or sorted(str(p) for p in Path("/dev").glob("hidraw*"))
if not devices:
print("Keine hidraw devices gefunden.")
return 1
for dev in devices:
inspect(dev)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,4 @@
# SuperX RGB candidate hidraw access
# Important: use ENV properties from the same resolved device context.
SUBSYSTEM=="hidraw", ENV{ID_VENDOR_ID}=="1a86", ENV{ID_MODEL_ID}=="1305", ENV{ID_USB_INTERFACE_NUM}=="01", MODE="0660", GROUP="wheel", TAG+="uaccess"
SUBSYSTEM=="hidraw", ENV{ID_VENDOR_ID}=="1a86", ENV{ID_MODEL_ID}=="1305", ENV{ID_USB_INTERFACE_NUM}=="02", MODE="0660", GROUP="wheel", TAG+="uaccess"