From 9ed0f7b71e1587cd68e14330ea575be269f6c668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kay=20T=C3=BCrtscher?= Date: Mon, 25 May 2026 10:15:10 +0200 Subject: [PATCH] Document Super X RGB OEM layer and add Linux bridge scaffold --- .gitignore | 13 ++ README.md | 30 +++ docs/compatlayerct-findings.md | 151 +++++++++++++ docs/frostbay-protocol-notes.md | 100 +++++++++ docs/internal-rgb-oem-routes.md | 35 +++ docs/internal-rgb-plan.md | 95 ++++++++ docs/internal-rgb-status.md | 125 +++++++++++ docs/live-hid-inventory.md | 48 ++++ docs/next-steps.md | 50 +++++ docs/research-summary.md | 144 ++++++++++++ docs/safe-test-frames.md | 45 ++++ docs/sources.md | 12 + tools/compatlayerct_strings_probe.py | 119 ++++++++++ tools/frostbay_zip_probe.py | 89 ++++++++ tools/onexconsole_asar_probe.py | 109 +++++++++ tools/superx_hidraw_rgb.py | 318 +++++++++++++++++++++++++++ tools/superx_rgb_bridge.py | 179 +++++++++++++++ tools/superx_rgb_inventory.py | 91 ++++++++ udev/99-superx-rgb-hidraw.rules | 4 + 19 files changed, 1757 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/compatlayerct-findings.md create mode 100644 docs/frostbay-protocol-notes.md create mode 100644 docs/internal-rgb-oem-routes.md create mode 100644 docs/internal-rgb-plan.md create mode 100644 docs/internal-rgb-status.md create mode 100644 docs/live-hid-inventory.md create mode 100644 docs/next-steps.md create mode 100644 docs/research-summary.md create mode 100644 docs/safe-test-frames.md create mode 100644 docs/sources.md create mode 100644 tools/compatlayerct_strings_probe.py create mode 100644 tools/frostbay_zip_probe.py create mode 100644 tools/onexconsole_asar_probe.py create mode 100755 tools/superx_hidraw_rgb.py create mode 100644 tools/superx_rgb_bridge.py create mode 100644 tools/superx_rgb_inventory.py create mode 100644 udev/99-superx-rgb-hidraw.rules diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0eee555 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +*.pyc +*.pyo +*.swp +*.tmp +.env +.venv/ +venv/ +*.log +.cache/ +*.exe +*.dll +*.pdb diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8a88ed --- /dev/null +++ b/README.md @@ -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. diff --git a/docs/compatlayerct-findings.md b/docs/compatlayerct-findings.md new file mode 100644 index 0000000..525647c --- /dev/null +++ b/docs/compatlayerct-findings.md @@ -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. diff --git a/docs/frostbay-protocol-notes.md b/docs/frostbay-protocol-notes.md new file mode 100644 index 0000000..45ab66d --- /dev/null +++ b/docs/frostbay-protocol-notes.md @@ -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 diff --git a/docs/internal-rgb-oem-routes.md b/docs/internal-rgb-oem-routes.md new file mode 100644 index 0000000..faa311d --- /dev/null +++ b/docs/internal-rgb-oem-routes.md @@ -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. diff --git a/docs/internal-rgb-plan.md b/docs/internal-rgb-plan.md new file mode 100644 index 0000000..a125dbb --- /dev/null +++ b/docs/internal-rgb-plan.md @@ -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 diff --git a/docs/internal-rgb-status.md b/docs/internal-rgb-status.md new file mode 100644 index 0000000..04023ca --- /dev/null +++ b/docs/internal-rgb-status.md @@ -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`) diff --git a/docs/live-hid-inventory.md b/docs/live-hid-inventory.md new file mode 100644 index 0000000..0b67306 --- /dev/null +++ b/docs/live-hid-inventory.md @@ -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. diff --git a/docs/next-steps.md b/docs/next-steps.md new file mode 100644 index 0000000..bbeadca --- /dev/null +++ b/docs/next-steps.md @@ -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 diff --git a/docs/research-summary.md b/docs/research-summary.md new file mode 100644 index 0000000..dd974a7 --- /dev/null +++ b/docs/research-summary.md @@ -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. diff --git a/docs/safe-test-frames.md b/docs/safe-test-frames.md new file mode 100644 index 0000000..e7e9cac --- /dev/null +++ b/docs/safe-test-frames.md @@ -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` diff --git a/docs/sources.md b/docs/sources.md new file mode 100644 index 0000000..9c220b0 --- /dev/null +++ b/docs/sources.md @@ -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` diff --git a/tools/compatlayerct_strings_probe.py b/tools/compatlayerct_strings_probe.py new file mode 100644 index 0000000..c611386 --- /dev/null +++ b/tools/compatlayerct_strings_probe.py @@ -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()) diff --git a/tools/frostbay_zip_probe.py b/tools/frostbay_zip_probe.py new file mode 100644 index 0000000..ab9cd51 --- /dev/null +++ b/tools/frostbay_zip_probe.py @@ -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()) diff --git a/tools/onexconsole_asar_probe.py b/tools/onexconsole_asar_probe.py new file mode 100644 index 0000000..c9f4d7d --- /dev/null +++ b/tools/onexconsole_asar_probe.py @@ -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()) diff --git a/tools/superx_hidraw_rgb.py b/tools/superx_hidraw_rgb.py new file mode 100755 index 0000000..52ee78a --- /dev/null +++ b/tools/superx_hidraw_rgb.py @@ -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()) diff --git a/tools/superx_rgb_bridge.py b/tools/superx_rgb_bridge.py new file mode 100644 index 0000000..f3c030f --- /dev/null +++ b/tools/superx_rgb_bridge.py @@ -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()) diff --git a/tools/superx_rgb_inventory.py b/tools/superx_rgb_inventory.py new file mode 100644 index 0000000..d07c021 --- /dev/null +++ b/tools/superx_rgb_inventory.py @@ -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()) diff --git a/udev/99-superx-rgb-hidraw.rules b/udev/99-superx-rgb-hidraw.rules new file mode 100644 index 0000000..b448297 --- /dev/null +++ b/udev/99-superx-rgb-hidraw.rules @@ -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"