Document Super X RGB OEM layer and add Linux bridge scaffold
This commit is contained in:
commit
9ed0f7b71e
19 changed files with 1757 additions and 0 deletions
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.swp
|
||||
*.tmp
|
||||
.env
|
||||
.venv/
|
||||
venv/
|
||||
*.log
|
||||
.cache/
|
||||
*.exe
|
||||
*.dll
|
||||
*.pdb
|
||||
30
README.md
Normal file
30
README.md
Normal 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.
|
||||
151
docs/compatlayerct-findings.md
Normal file
151
docs/compatlayerct-findings.md
Normal 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.
|
||||
100
docs/frostbay-protocol-notes.md
Normal file
100
docs/frostbay-protocol-notes.md
Normal 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
|
||||
35
docs/internal-rgb-oem-routes.md
Normal file
35
docs/internal-rgb-oem-routes.md
Normal 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
95
docs/internal-rgb-plan.md
Normal 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
125
docs/internal-rgb-status.md
Normal 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`)
|
||||
48
docs/live-hid-inventory.md
Normal file
48
docs/live-hid-inventory.md
Normal 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
50
docs/next-steps.md
Normal 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
144
docs/research-summary.md
Normal 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
45
docs/safe-test-frames.md
Normal 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
12
docs/sources.md
Normal 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`
|
||||
119
tools/compatlayerct_strings_probe.py
Normal file
119
tools/compatlayerct_strings_probe.py
Normal 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())
|
||||
89
tools/frostbay_zip_probe.py
Normal file
89
tools/frostbay_zip_probe.py
Normal 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())
|
||||
109
tools/onexconsole_asar_probe.py
Normal file
109
tools/onexconsole_asar_probe.py
Normal 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
318
tools/superx_hidraw_rgb.py
Executable 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
179
tools/superx_rgb_bridge.py
Normal 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())
|
||||
91
tools/superx_rgb_inventory.py
Normal file
91
tools/superx_rgb_inventory.py
Normal 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())
|
||||
4
udev/99-superx-rgb-hidraw.rules
Normal file
4
udev/99-superx-rgb-hidraw.rules
Normal 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"
|
||||
Loading…
Add table
Reference in a new issue