Compare commits

..

15 Commits

Author SHA1 Message Date
50419fb487 release: v0.9.6 2026-05-02 20:58:40 +02:00
f196b8d29a release: v0.9.5 2026-05-01 18:09:24 +02:00
1d3c5a7e1b release: v0.9.4 2026-05-01 11:24:08 +02:00
c22296d880 chore: sync v0.9.3 – alle Fixes, CHANGELOG, README, VERSION 2026-05-01 10:36:09 +02:00
d9d3581e22 fix: VERSION ins Dockerfile, STABLE_RELEASE_API fix, Version im Header (#14) 2026-05-01 10:11:54 +02:00
966d421016 fix: Dockerfile für flache Release-Struktur angepasst, config.ini.example hinzugefügt 2026-04-30 09:51:17 +02:00
2a12ecca51 chore: sync v0.9.2 – README/CHANGELOG DE+EN, config_loader, aktuelle Bridge-Quelldateien
- README.md (EN), README.de.md (DE) – README.en.md entfernt
- CHANGELOG.md (EN), CHANGELOG.de.md (DE)
- config_loader.py neu (config.ini statt .env)
- kobrax_moonraker_bridge.py, kobrax_client.py, env_loader.py aktualisiert
- Dockerfile, docker-compose.yml, VERSION auf 0.9.2

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 14:53:23 +02:00
ae4777187f docs: Netzwerkhinweis auf Host-IP statt 0.0.0.0 verbessert 2026-04-26 20:30:55 +02:00
8ccafb96c4 docs: Logo in englische README eingefügt 2026-04-26 20:25:37 +02:00
21f340271b docs: Projektname auf 'Klipper Bridge', Version auf 0.9.1-beta15 2026-04-26 20:24:17 +02:00
2f56a1f056 feat: Logo ins README hinzugefügt 2026-04-26 20:20:44 +02:00
c3a62a13c5 release: v0.9.1-beta15 2026-04-26 15:52:27 +02:00
4f1eaf7e93 fix: apt ffmpeg entfernt, imageio-ffmpeg übernimmt 2026-04-26 15:18:38 +02:00
fc681316fc release: v0.9.1-beta14 2026-04-26 14:58:20 +02:00
23756b82a9 release: v0.9.1-beta12 2026-04-25 22:21:22 +02:00
18 changed files with 1909 additions and 820 deletions

View File

@@ -21,3 +21,4 @@ DEVICE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Modell-ID (Kobra X Standard: 20030) # Modell-ID (Kobra X Standard: 20030)
MODE_ID=20030 MODE_ID=20030

258
CHANGELOG.de.md Normal file
View File

@@ -0,0 +1,258 @@
# Changelog
## [0.9.6] 2026-05-02
### Neu
- **Fortschritts-Karte:** Verstrichen / Slicer-Schätzung / Restzeit als Mini-Cards (gleicher Stil wie Temperaturkarten)
- **Layer-Mini-Card:** Layerzahl als Mini-Card neben der Fortschrittsleiste
### Fixes
- **Slicer-Schätzzeit:** OrcaSlicer schreibt die geschätzte Zeit ans Ende der GCode-Datei — Bridge liest jetzt auch die letzten 64 KB (vorher nur die ersten 16 KB)
- **start.sh:** `config/`-Verzeichnis wird jetzt automatisch erstellt und `config.ini.example` wird beim ersten Start hineinkopiert (Issue #15)
---
## [0.9.5] 2026-05-01
### Neu
- **Upload-Banner:** Nach „Nur hochladen" erscheint ein grüner Banner mit Dateiname — „▶ Druck starten" startet den Druck direkt, „✕ Abbrechen" schließt den Banner
### Fixes
- **Auto-Print:** `auto_print` wurde nach dem Multipart-Loop immer auf `False` zurückgesetzt — OrcaSlicer „Hochladen und drucken" startete den Druck nie automatisch
- **Thumbnail:** Vorschaubild wird jetzt auch bei „Nur hochladen" angezeigt — Bridge fragt `fileDetails` direkt nach dem Upload an
- **Log Auto-Scroll:** Scroll-Position bleibt erhalten wenn Auto-Scroll deaktiviert ist — kein ungewollter Sprung nach oben mehr
---
## [0.9.4] 2026-05-01
### Neu
- **AMS-Slot-Editor:** Slot im AMS-Panel anklicken → Dialog mit Farbpicker und Material-Auswahl (Schnellbuttons: PLA/PETG/ABS/ASA/TPU/PA/PC/HIPS oder Freitext) direkt im Browser
- **Verbessertes Log-Panel:** Vollständige MQTT-Payloads (keine Kürzung mehr), Richtungsfilter (Alle/RX/TX) und Topic-Schnellfilter (AMS / print / info / status)
### Fixes
- **i18n:** Kamera-Placeholder-Text und Log-Richtungs-Button „Alle" werden jetzt korrekt beim Sprachwechsel übersetzt
---
## [0.9.3] 2026-05-01
### Fixes
- **Update-Check:** Stable-User erhalten keine Dev-Pre-Releases mehr — `STABLE_RELEASE_API` hatte `pre-release=true`, wodurch stabile Installationen Dev-Builds statt stabiler Releases fanden (Issue #14)
- **Version nach Update:** `VERSION`-Datei wird jetzt im Docker-Image mitgeliefert (`COPY VERSION .`) — `_write_version()` benötigt eine vorhandene Datei, ohne die wurde die Version nach dem Self-Update nie aktualisiert (Issue #14)
### Neu
- **Version im Header:** Laufende Version wird im Web-UI-Header neben dem Druckernamen angezeigt — kein Öffnen der Einstellungen nötig (Issue #14)
---
## [0.9.2] 2026-04-29
### ⚠️ Breaking Change: Konfiguration wechselt von `.env` zu `config/config.ini`
**Migration erfolgt automatisch** beim ersten Start — keine manuelle Aktion nötig.
- Einstellungen werden ab sofort aus `config/config.ini` gelesen statt aus `.env`
- Beim ersten Start ohne `config.ini` wird die Datei automatisch aus `.env` erstellt
- **Docker:** Volume `./config:/app/config` in `docker-compose.yml` ist der persistente Speicherort — Einstellungen überleben `docker-compose restart` und Updates
- **Standalone:** `config/config.ini` liegt neben der Binary und wird bei Updates nicht überschrieben
- `.env` bleibt als read-only Fallback gemountet — kann liegen bleiben
- Zum manuellen Anlegen einer `config.ini`: Vorlage unter `config/config.ini.example`
### Neu
- **Persistente Einstellungen:** `config/config.ini` ersetzt `.env` — Einstellungen gehen nach `docker-compose restart` nicht mehr verloren (Issue #9)
- **Verbindungsfehler-Banner:** Roter Banner oben in der Web-UI wenn MQTT-Verbindung fehlschlägt (z.B. falsches Passwort, Drucker nicht erreichbar) (Issue #11)
- **Slicer-Schätzzeit:** Geschätzte Gesamtdruckzeit aus dem GCode-Header wird im Fortschritts-Panel angezeigt
### Fixes
- README: OrcaSlicer-Verbindung explizit mit `http://` und Port `:7125` dokumentiert (Issue #12)
- README: Direkter Download-Link für `extract_credentials` auf Gitea-Releases (Issue #13)
---
## [0.9.1-dev] laufend (dev-Branch)
### Neu
- **Dev-Branch-Infrastruktur:** Versionsschema `0.9.1-dev+<hash>` — jeder Build eindeutig identifizierbar
- **Separater Update-Kanal:** Dev-Versionen prüfen auf Gitea Pre-Releases mit `-dev+` im Tag
- **AMS-Slot-Auswahl:** Einstellung „Standard-Slot (Einfarbdruck)" im Settings-Modal — fixiert einen bestimmten AMS-Kanal oder Auto (alle belegten Slots)
- **Auto-Leveling:** Checkbox im Settings-Modal — steuert `task_settings.auto_leveling` beim Druckstart
- **MQTT-Logging:** Strukturiertes TX/RX-Log mit Duplikat-Filter (`kobrax.mqtt` Logger)
- **Server-Log im Browser:** Live-Stream via SSE (`/api/log/stream`) — alle Server-Logs erscheinen im Log-Tab der UI
- **Log-Tab Verbesserungen:**
- Auto-Scroll ein/aus — deaktiviert sich beim manuellen Hochscrollen, Button zum Reaktivieren
- Textfilter — Live-Filterung der Log-Einträge
- Error-Badge — roter Zähler im Tab-Button bei Fehlern/Warnungen
- Clear-Button — Buffer leeren
- Download-Button — letzte 500 Einträge als `kx-bridge.log`
- Log-Fenster füllt den gesamten verfügbaren Platz (statt fester Höhe 160px)
- **Log-Buffer:** 500 Einträge (Server + Browser vereinheitlicht)
- **Changelog im Update-Dialog:** Release-Notes aus Gitea werden direkt im Update-Dialog angezeigt
- **Slicer-Schätzzeit:** Geschätzte Gesamtdruckzeit aus dem GCode-Header im Fortschritts-Panel
---
## [0.9.1-beta15] 2026-04-26
### Fixes
- AMS: Leere Slots werden beim Druckstart übersprungen — kein `filament runout` mehr bei unbelegten Kanälen (Issue #5)
- AMS: Material-Typ wird jetzt korrekt aus dem Drucker-Protokoll gelesen (Feld `type` statt `material_type`)
- AMS UI: Leere Slots werden grau/transparent mit „Leer"-Label dargestellt
---
## [0.9.1-beta14] 2026-04-26
### Fixes
- Z-Achse: ▲ fährt jetzt aufwärts (Z+), ▼ abwärts (Z) — Pfeile waren vertauscht (Issue #4)
- Home All: korrekter Achsen-Code 5 — homed alle Achsen XYZ (Issue #4)
- Neuer Button „Home XY" (axis=4) in der UI
- Neuer Button „Motoren aus" (axis turnOff) in der UI
---
## [0.9.1-beta13] 2026-04-26
### Fixes (Windows)
- Self-Update / Settings-Neustart: `os.execv` funktioniert jetzt korrekt in der PyInstaller-Binary
- Kamera: `ffmpeg nicht gefunden` crasht nicht mehr — saubere 503-Antwort wenn ffmpeg fehlt
- Reconnect-Loop: Kurze leere TCP-Reads unter Windows lösen keine sofortigen Reconnects mehr aus
### Struktur
- `bridge/`: Bridge-Dateien aus `05_scripts/` herausgelöst
- `tools/`: `extract_credentials.py` als eigenständiges Tool mit eigenem README
- `_archive/`: RE-Forschungsordner, Analyse-Tools und alte Release-Checksums archiviert
- README komplett neu: klarer 3-Schritte-Schnellstart
---
## [0.9.1-beta12] 2026-04-25
### Fixes
- Falsche MQTT-Zugangsdaten zeigen jetzt eine verständliche Fehlermeldung statt des kryptischen `CONNACK failed: 20020005`
---
## [0.9.1-beta11] 2026-04-25
### Fixes
- Drucker-IP wird automatisch bereinigt wenn der Nutzer versehentlich den Port miteingibt (z.B. `192.168.1.102:9883``192.168.1.102`)
- Settings-Modal: Hinweis erscheint wenn ein `:` in der IP erkannt wird
- `docker-compose.yml`: `.env` als Volume gemountet — Einstellungen bleiben nach `docker-compose restart` erhalten
---
## [0.9.1-beta10] 2026-04-25
### Neu
- `start.sh` — startet die Bridge per Docker, baut das Image automatisch beim ersten Aufruf
- Tests: pytest-Suite (19 Tests) für API-State, Moonraker-Endpunkte, Settings; Installations-Smoke-Test (`test_install.sh`)
- Settings-Modal öffnet sich beim ersten Start automatisch wenn keine Zugangsdaten hinterlegt sind
### Geändert
- README: Schnellstart zeigt jetzt `./start.sh` statt manuellem `docker build`
- README: LAN-Modus korrekt als Drucker-Menüoption beschrieben
- README: Versionsnummer wird ab jetzt automatisch bei jedem Release aktualisiert
- `extract_credentials`: `--write-env` nicht mehr empfohlen — Werte im ⚙-Menü eintragen
- Dockerfile im Release-Repo: Pfade ohne `05_scripts/`-Präfix
- `release.sh`: Dockerfile für Release-Repo automatisch per `sed` angepasst
### Fixes
- Restdruckzeit (`remain_time`) wird jetzt korrekt aus `print/report` übernommen und in der UI angezeigt
- Übersetzungen: „Schrittweite" und „Ziel"-Placeholder in Temperatureingaben korrekt übersetzt
---
## [0.9.1-beta9] 2026-04-25
### Neu
- OrcaSlicer-Profil (`kobra_x_orcaslicer_preset.zip`) als Release-Asset
- `release.sh`: OrcaSlicer-Profil wird automatisch ins Release-Repo kopiert und hochgeladen
### Geändert
- README: `extract_credentials` ohne `--write-env`, Werte manuell im ⚙-Menü eintragen
- README: Docker-Schnellstart vereinfacht
---
## [0.9.1-beta8] 2026-04-25
### Neu
- Restdruckzeit-Anzeige in der UI (≈ Xh Ym verbleibend) aus `remain_time`-Feld
- Settings-Modal: Verbindungseinstellungen und Self-Update direkt im Browser
- Self-Update: Bridge prüft Gitea-Release-API auf neue Versionen und aktualisiert sich selbst
### Geändert
- Bridge startet im Offline-Modus wenn Drucker nicht erreichbar (kein Absturz)
- Verbinden/Trennen-Button im Header
---
## [0.9.1-beta7] 2026-04-22
### Neu
- Offline-Start: Bridge läuft auch ohne MQTT-Verbindung, verbindet automatisch sobald Drucker erreichbar
- Verbinden/Trennen-Button im Header
---
## [0.9.1-beta6] 2026-04-20
### Neu
- Release-ZIPs: `kx-bridge-linux.zip`, `kx-bridge-windows.zip`, `anycubic-certs.zip` mit Zertifikaten
### Fixes
- PyInstaller frozen-Binary: `__file__` durch `sys.executable`-Pfad ersetzt (Cert-Pfad-Fix)
---
## [0.9.1-beta5] 2026-04-19
### Neu
- `kx-bridge.exe` (Windows) wird automatisch via GitHub Actions gebaut
---
## [0.9.1-beta4] 2026-04-18
### Neu
- `release.sh`: baut Linux-Binary und Windows-EXE, lädt alle Assets auf Gitea hoch
- Englische README (`README.en.md`)
### Fixes
- `progress` und `filename` werden bei `stoped`/`canceled` korrekt auf 0 zurückgesetzt
---
## [0.9.1-beta3] 2026-04-17
### Neu
- Druckgeschwindigkeit-Karte (Leise / Normal / Sport)
- Übersetzungen (DE/EN) vervollständigt
---
## [0.9.1-beta2] 2026-04-17
### Fixes
- Temperatursteuerung während eines laufenden Drucks
---
## [0.9.1-beta1] 2026-04-17
### Neu
- UI-Komplettüberarbeitung: Settings-Modal, Self-Update, Dashboard, Responsive Design
- Neue Drucker-Zustände: `pausing`, `paused`, `resuming`, `resumed`, `stopping`
- `release.sh`: Version-Bump und Release-Sync Skript
---
## [0.9.0-beta1] 2026-04-10
### Neu
- Erster öffentlicher Release
- Docker-Deployment, Linux-Binary, `extract_credentials`-Tool
- Moonraker-kompatible HTTP/WebSocket-Bridge für den Anycubic Kobra X
- AMS Einziehen/Ausziehen, Licht- und Lüftersteuerung
- Web-UI mit Dashboard, Temperaturkarten, Achsensteuerung

View File

@@ -1,125 +1,258 @@
# Changelog # Changelog
## [0.9.6] 2026-05-02
### New
- **Progress card:** Elapsed / Slicer estimate / Remaining time shown as mini-cards (same style as temperature cards)
- **Layer mini-card:** Layer count displayed as mini-card next to the progress bar
### Fixes
- **Slicer estimate time:** OrcaSlicer writes the estimated time at the end of the GCode file — bridge now also scans the last 64 KB (previously only the first 16 KB were checked)
- **start.sh:** `config/` directory is now created automatically and `config.ini.example` is copied into it on first run (Issue #15)
---
## [0.9.5] 2026-05-01
### New
- **Upload banner:** After "Upload only", a green banner appears with the filename — "▶ Start Print" starts the print directly, "✕ Cancel" dismisses the banner
### Fixes
- **Auto-print:** `auto_print` was always reset to `False` after the multipart loop — OrcaSlicer "Upload and print" never started the print automatically
- **Thumbnail:** Preview image is now shown after "Upload only" — bridge requests `fileDetails` immediately after upload
- **Log auto-scroll:** Scroll position is preserved when auto-scroll is disabled — no more unwanted jump to top
---
## [0.9.4] 2026-05-01
### New
- **AMS slot editor:** Click any slot in the AMS panel to open an edit dialog — set color (color picker) and material (preset buttons: PLA/PETG/ABS/ASA/TPU/PA/PC/HIPS or free text) directly from the browser
- **Improved log panel:** Full MQTT payloads (no truncation), direction filter (All/RX/TX) and topic quick-filter buttons (AMS / print / info / status)
### Fixes
- **i18n:** Camera placeholder text and log direction "All" button now correctly translated on language switch
---
## [0.9.3] 2026-05-01
### Fixes
- **Update check:** Stable users no longer receive dev pre-releases — `STABLE_RELEASE_API` had `pre-release=true` which caused stable installs to find dev builds instead of stable releases (Issue #14)
- **Version after update:** `VERSION` file is now included in the Docker image (`COPY VERSION .`) — `_write_version()` requires the file to exist, without it the version was never updated after self-update (Issue #14)
### New
- **Version in header:** Running version shown in the Web-UI header next to the printer name — no need to open Settings to check (Issue #14)
---
## [0.9.2] 2026-04-29
### ⚠️ Breaking Change: Configuration moves from `.env` to `config/config.ini`
**Migration is automatic** on first start — no manual action required.
- Settings are now read from `config/config.ini` instead of `.env`
- On first start without `config.ini`, the file is created automatically from `.env`
- **Docker:** Volume `./config:/app/config` in `docker-compose.yml` is the persistent storage — settings survive `docker-compose restart` and updates
- **Standalone:** `config/config.ini` sits next to the binary and is not overwritten on update
- `.env` stays mounted read-only as a migration source — you can leave it in place
- To create a `config.ini` manually: copy `config/config.ini.example`
### New
- **Persistent settings:** `config/config.ini` replaces `.env` — settings no longer lost after `docker-compose restart` (Issue #9)
- **Connection error banner:** Red banner at the top of the Web-UI when MQTT connection fails (e.g. wrong password, printer unreachable) (Issue #11)
- **Slicer estimated time:** Estimated total print time from GCode header shown in the progress panel
### Fixes
- README: OrcaSlicer connection documented explicitly with `http://` and port `:7125` (Issue #12)
- README: Direct download link for `extract_credentials` pointing to Gitea releases (Issue #13)
---
## [0.9.1-dev] ongoing (dev branch)
### New
- **Dev branch infrastructure:** Version scheme `0.9.1-dev+<hash>` — every build uniquely identifiable
- **Separate update channel:** Dev versions check for Gitea pre-releases with `-dev+` in the tag
- **AMS slot selection:** Setting "Default slot (single color)" in the Settings modal — pins a specific AMS channel or Auto (all loaded slots)
- **Auto-leveling:** Checkbox in Settings modal — controls `task_settings.auto_leveling` on print start
- **MQTT logging:** Structured TX/RX log with duplicate filter (`kobrax.mqtt` logger)
- **Server log in browser console:** Live stream via SSE (`/api/log/stream`) — all server logs appear in the Log tab
- **Log tab improvements:**
- Auto-scroll on/off — disables automatically on manual scroll-up, button to re-enable
- Text filter — live filtering of log entries
- Error badge — red counter on the tab button when errors/warnings occur while on another tab
- Clear button — empty the buffer
- Download button — last 500 entries as `kx-bridge.log`
- Log window now fills all available space (instead of fixed 160px height)
- **Log buffer:** 500 entries (server + browser unified)
- **Changelog in update dialog:** Release notes from Gitea loaded and shown directly in the update dialog
- **Slicer estimated time:** Estimated total print time from GCode header shown in the progress panel
---
## [0.9.1-beta15] 2026-04-26
### Fixes
- AMS: Empty slots are skipped on print start — no more `filament runout` for unloaded channels (Issue #5)
- AMS: Material type is now correctly read from the printer protocol (field `type` instead of `material_type`)
- AMS UI: Empty slots shown grey/transparent with "Empty" label
---
## [0.9.1-beta14] 2026-04-26
### Fixes
- Z axis: ▲ now moves up (Z+), ▼ moves down (Z) — arrows were reversed (Issue #4)
- Home All: correct axis code 5 — homes all axes XYZ (Issue #4)
- New "Home XY" button (axis=4) in the UI
- New "Motors Off" button (axis turnOff) in the UI
---
## [0.9.1-beta13] 2026-04-26
### Fixes (Windows)
- Self-update / Settings restart: `os.execv` now works correctly in PyInstaller binary
- Camera: `ffmpeg not found` no longer crashes — clean 503 response when ffmpeg is not installed
- Reconnect loop: Short empty TCP reads on Windows no longer trigger immediate reconnects
### Structure
- `bridge/`: Bridge files extracted from `05_scripts/`
- `tools/`: `extract_credentials.py` as standalone tool with its own README
- `_archive/`: RE research folders, analysis tools and old release checksums archived
- README fully rewritten: clear 3-step quick start
---
## [0.9.1-beta12] 2026-04-25
### Fixes
- Wrong MQTT credentials now shows a human-readable error instead of cryptic `CONNACK failed: 20020005`
---
## [0.9.1-beta11] 2026-04-25 ## [0.9.1-beta11] 2026-04-25
### Fixes ### Fixes
- Drucker-IP wird automatisch bereinigt wenn der Nutzer versehentlich den Port miteingibt (z.B. `192.168.1.102:9883``192.168.1.102`) - Printer IP is automatically cleaned if the user accidentally includes the port (e.g. `192.168.1.102:9883``192.168.1.102`)
- Settings-Modal: Hinweis erscheint wenn ein `:` in der IP erkannt wird - Settings modal: hint shown when `:` is detected in the IP field
- `docker-compose.yml`: `.env` wird als Volume in den Container gemountet Einstellungen bleiben nach `docker-compose restart` erhalten - `docker-compose.yml`: `.env` mounted as volume into the container — settings persist after `docker-compose restart`
--- ---
## [0.9.1-beta10] 2026-04-25 ## [0.9.1-beta10] 2026-04-25
### Neu ### New
- `start.sh` startet die Bridge per Docker, baut das Image automatisch beim ersten Aufruf - `start.sh` starts the bridge via Docker, builds the image automatically on first run
- Tests: pytest-Suite (19 Tests) für API-State, Moonraker-Endpunkte, Settings; Installations-Smoke-Test (`test_install.sh`) - Tests: pytest suite (19 tests) for API state, Moonraker endpoints, settings; install smoke test (`test_install.sh`)
- Settings-Modal öffnet sich beim ersten Start automatisch wenn keine Zugangsdaten hinterlegt sind - Settings modal opens automatically on first start when no credentials are configured
### Geändert ### Changed
- README (DE + EN): Schnellstart zeigt jetzt `./start.sh` statt manuellem `docker build` - README: Quick start now shows `./start.sh` instead of manual `docker build`
- README: LAN-Modus korrekt als Drucker-Menüoption beschrieben (kein WLAN-Bezug) - README: LAN mode correctly described as a printer menu option
- README: Versionsnummer wird ab jetzt automatisch bei jedem Release aktualisiert - README: Version number now updated automatically on each release
- `extract_credentials`: kein `--write-env` mehr empfohlen Werte im ⚙-Menü eintragen - `extract_credentials`: `--write-env` no longer recommended — enter values in the ⚙ menu
- Dockerfile im Release-Repo: Pfade ohne `05_scripts/`-Präfix (direkt aus Repo-Root) - Dockerfile in release repo: paths without `05_scripts/` prefix
- `release.sh`: Dockerfile für Release-Repo automatisch per `sed` angepasst - `release.sh`: Dockerfile for release repo automatically patched via `sed`
### Fixes ### Fixes
- Restdruckzeit (`remain_time`) wird jetzt korrekt aus `print/report` übernommen und in der UI angezeigt - Remaining print time (`remain_time`) now correctly taken from `print/report` and shown in UI
- Übersetzung: „Schrittweite" und „Ziel"-Placeholder in Temperatureingaben werden jetzt korrekt übersetzt - Translation: "Step size" and "Target" placeholders in temperature inputs now correctly translated
--- ---
## [0.9.1-beta9] 2026-04-25 ## [0.9.1-beta9] 2026-04-25
### Neu ### New
- OrcaSlicer-Profil (`kobra_x_orcaslicer_preset.zip`) als Release-Asset - OrcaSlicer profile (`kobra_x_orcaslicer_preset.zip`) as release asset
- `release.sh`: OrcaSlicer-Profil wird automatisch ins Release-Repo kopiert und hochgeladen - `release.sh`: OrcaSlicer profile automatically copied to release repo and uploaded
### Geändert ### Changed
- README: `extract_credentials` ohne `--write-env`, Werte manuell ins ⚙-Menü eintragen - README: `extract_credentials` without `--write-env`, values entered manually in the ⚙ menu
- README: Docker-Schnellstart vereinfacht (kein `.env` anlegen vor dem Start nötig) - README: Docker quick start simplified
--- ---
## [0.9.1-beta8] 2026-04-25 ## [0.9.1-beta8] 2026-04-25
### Neu ### New
- Restdruckzeit-Anzeige in der UI (≈ Xh Ym verbleibend) aus `remain_time`-Feld des Druckers - Remaining print time display in UI (≈ Xh Ym remaining) from `remain_time` field
- Settings-Modal: Verbindungseinstellungen und Self-Update direkt im Browser - Settings modal: connection settings and self-update directly in the browser
- Self-Update: Bridge prüft Gitea-Release-API auf neue Versionen und aktualisiert sich selbst - Self-update: bridge checks Gitea release API for new versions and updates itself
### Geändert ### Changed
- Bridge startet im Offline-Modus wenn Drucker nicht erreichbar (kein Absturz) - Bridge starts in offline mode when printer is unreachable (no crash)
- Verbinden/Trennen-Button im Header - Connect/Disconnect button in header
--- ---
## [0.9.1-beta7] 2026-04-22 ## [0.9.1-beta7] 2026-04-22
### Neu ### New
- Offline-Start: Bridge läuft auch ohne MQTT-Verbindung, verbindet automatisch sobald Drucker erreichbar - Offline start: bridge runs without MQTT connection, reconnects automatically when printer is reachable
- Verbinden/Trennen-Button im Header - Connect/Disconnect button in header
--- ---
## [0.9.1-beta6] 2026-04-20 ## [0.9.1-beta6] 2026-04-20
### Neu ### New
- Release-ZIPs: `kx-bridge-linux.zip`, `kx-bridge-windows.zip`, `anycubic-certs.zip` mit Zertifikaten - Release ZIPs: `kx-bridge-linux.zip`, `kx-bridge-windows.zip`, `anycubic-certs.zip` with certificates
### Fixes ### Fixes
- PyInstaller frozen-Binary: `__file__` durch `sys.executable`-Pfad ersetzt (Cert-Pfad-Fix) - PyInstaller frozen binary: `__file__` replaced with `sys.executable` path (cert path fix)
--- ---
## [0.9.1-beta5] 2026-04-19 ## [0.9.1-beta5] 2026-04-19
### Neu ### New
- `kx-bridge.exe` (Windows) wird automatisch via GitHub Actions gebaut - `kx-bridge.exe` (Windows) built automatically via GitHub Actions
--- ---
## [0.9.1-beta4] 2026-04-18 ## [0.9.1-beta4] 2026-04-18
### Neu ### New
- `release.sh`: baut Linux-Binary und Windows-EXE, lädt alle Assets auf Gitea hoch - `release.sh`: builds Linux binary and Windows EXE, uploads all assets to Gitea
- Englische README (`README.en.md`) - English README (`README.en.md`)
### Fixes ### Fixes
- `progress` und `filename` werden bei `stoped`/`canceled` korrekt auf 0 zurückgesetzt - `progress` and `filename` correctly reset to 0 on `stoped`/`canceled`
--- ---
## [0.9.1-beta3] 2026-04-17 ## [0.9.1-beta3] 2026-04-17
### Neu ### New
- Print-Speed-Card (Leise / Normal / Sport) - Print speed card (Silent / Normal / Sport)
- Übersetzungen (DE/EN) vervollständigt - Translations (DE/EN) completed
--- ---
## [0.9.1-beta2] 2026-04-17 ## [0.9.1-beta2] 2026-04-17
### Fixes ### Fixes
- Temperatursteuerung während eines laufenden Drucks - Temperature control during an active print
--- ---
## [0.9.1-beta1] 2026-04-17 ## [0.9.1-beta1] 2026-04-17
### Neu ### New
- UI-Komplettüberarbeitung: Settings-Modal, Self-Update, Dashboard, Responsive Design - Complete UI overhaul: Settings modal, self-update, dashboard, responsive design
- Neue Drucker-Zustände: `pausing`, `paused`, `resuming`, `resumed`, `stopping` - New printer states: `pausing`, `paused`, `resuming`, `resumed`, `stopping`
- `release.sh`: Version-Bump und Release-Sync Skript - `release.sh`: version bump and release sync script
--- ---
## [0.9.0-beta1] 2026-04-10 ## [0.9.0-beta1] 2026-04-10
### Neu ### New
- Erster öffentlicher Release - First public release
- Docker-Deployment, Linux-Binary, `extract_credentials`-Tool - Docker deployment, Linux binary, `extract_credentials` tool
- Moonraker-kompatible HTTP/WebSocket-Bridge für den Anycubic Kobra X - Moonraker-compatible HTTP/WebSocket bridge for the Anycubic Kobra X
- AMS Einziehen/Ausziehen, Licht- und Lüftersteuerung - AMS load/unload, light and fan control
- Web-UI mit Dashboard, Temperaturkarten, Achsensteuerung - Web-UI with dashboard, temperature cards, motion control

View File

@@ -3,14 +3,20 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY kobrax_moonraker_bridge.py . COPY kobrax_moonraker_bridge.py .
COPY config_loader.py .
COPY env_loader.py . COPY env_loader.py .
COPY kobrax_client.py . COPY kobrax_client.py .
COPY VERSION .
COPY anycubic_slicer.crt . COPY anycubic_slicer.crt .
COPY anycubic_slicer.key . COPY anycubic_slicer.key .
COPY config/config.ini.example /app/config/config.ini.example
# config/ ist ein Volume-Mountpoint beim Start wird config.ini aus .env migriert
# falls noch keine config.ini vorhanden ist.
RUN mkdir -p /app/config
EXPOSE 7125 EXPOSE 7125

137
README.de.md Normal file
View File

@@ -0,0 +1,137 @@
<p align="center"><img src="knlogo.png" alt="KX-Bridge Logo" width="180"/></p>
# KX-Bridge Anycubic Kobra X
**Version:** 0.9.6
Steuere deinen **Anycubic Kobra X** mit OrcaSlicer — ohne Klipper, ohne Raspberry Pi.
KX-Bridge ist eine Moonraker-kompatible Bridge die direkt mit dem Drucker kommuniziert.
---
## Schnellstart in 3 Schritten
### Schritt 1 Drucker vorbereiten
Den Kobra X in den LAN-Modus versetzen:
**Drucker-Display → Einstellungen → LAN-Modus einschalten**
### Schritt 2 Credentials holen
Die MQTT-Zugangsdaten sind druckerspezifisch. So holst du sie:
1. **AnycubicSlicerNext** öffnen und Drucker verbinden (bis Status angezeigt wird)
2. **`extract_credentials.exe`** (Windows) oder **`extract_credentials`** (Linux) ausführen — gibt Username, Password, Device-ID und Drucker-IP aus
3. Werte merken / kopieren
> **Download:** [gitea.it-drui.de/viewit/KX-Bridge-Release/releases](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases) → `extract_credentials.exe` (Windows) / `extract_credentials` (Linux) im jeweiligen Release-Asset
### Schritt 3 Bridge starten
```bash
./start.sh
```
Das Skript baut das Docker-Image automatisch beim ersten Aufruf.
**Web-UI öffnen:** `http://BRIDGE-IP:7125`
→ Das ⚙-Menü öffnet sich beim ersten Start automatisch
→ Credentials aus Schritt 2 eintragen → **Speichern & Neustart**
**OrcaSlicer verbinden:**
Drucker → Verbindungstyp **Moonraker** → Host: `http://BRIDGE-IP:7125`
> **Wichtig:** Verbindungstyp muss **Moonraker** sein (nicht „Bambu" oder „Klipper").
> Im Host-Feld vollständige URL mit `http://` und Port `:7125` angeben.
---
## ⚠️ Update von 0.9.1 oder älter
Ab **0.9.2** speichert KX-Bridge Einstellungen in `config/config.ini` statt in `.env`.
**Migration erfolgt automatisch** — keine manuelle Aktion nötig:
- Beim ersten Start nach dem Update liest die Bridge die vorhandene `.env` und erstellt `config/config.ini` automatisch
- Einstellungen bleiben ab sofort nach `docker-compose restart` und zukünftigen Updates erhalten
- Die `.env`-Datei bleibt read-only gemountet als Migrationsquelle — kann liegen bleiben
- Zum manuellen Anlegen einer `config.ini`: Vorlage unter `config/config.ini.example` kopieren
---
## Was wird unterstützt?
| Funktion | Details |
|----------|---------|
| Druckerstatus | Temperatur, Fortschritt, Zustand, Restzeit |
| Drucksteuerung | Start, Pause, Fortsetzen, Abbrechen |
| Temperaturregelung | Nozzle und Bett während des Drucks |
| Druckgeschwindigkeit | Leise / Normal / Sport |
| AMS-Farbwechsel | Filament einziehen / ausziehen |
| Licht & Lüfter | Drucklicht und Lüfterdrehzahl |
| Web-UI | Dashboard, Achsensteuerung, Kameraansicht |
| Self-Update | Neue Versionen direkt im Browser installieren |
| OrcaSlicer | Moonraker-Protokoll (HTTP + WebSocket) |
---
## Alternativen zu Docker
**Linux Binary** (kein Docker nötig):
```bash
chmod +x kx-bridge
./kx-bridge
```
**Python direkt:**
```bash
pip install aiohttp
python bridge/kobrax_moonraker_bridge.py
```
Web-UI jeweils unter `http://localhost:7125` — ⚙-Menü führt durch die Erstkonfiguration.
---
## Nützliche Befehle
```bash
# Logs anzeigen
docker-compose logs -f
# Bridge stoppen
docker-compose down
# Bridge neu starten (nach Update)
./start.sh
```
---
## Fehlerbehebung
**„Falsche MQTT-Zugangsdaten"** beim Start:
- AnycubicSlicerNext neu starten, Drucker verbinden, `extract_credentials` erneut ausführen
- Nur die IP-Adresse ins Feld eintragen, keinen Port (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`)
**Drucker nicht gefunden / kein LAN-Modus:**
- Am Drucker-Display: Einstellungen → LAN-Modus einschalten
- Drucker und Bridge müssen im selben Netzwerk sein
**Docker: Permission denied:**
```bash
sudo usermod -aG docker $USER # dann neu einloggen
```
---
## Sicherheitshinweise
- Die Bridge ist im lokalen Netzwerk erreichbar unter `http://<Host-IP>:7125` — nicht ins Internet freigeben
- `config/config.ini` enthält Drucker-Credentials — nicht öffentlich teilen
- Credentials haben keinen Zugang zu Anycubic-Cloud-Diensten
---
## Lizenz & Rechtliches
Interoperabilitätsforschung gem. §69e UrhG — ausschließlich private, nicht-kommerzielle Nutzung.

View File

@@ -1,256 +0,0 @@
# KX-Bridge Anycubic Kobra X Moonraker Bridge
**Version:** 0.9.1-beta10
**Status:** Public Beta suitable for home users, feedback welcome
KX-Bridge is a Moonraker-compatible HTTP/WebSocket bridge for the **Anycubic Kobra X** 3D printer. It allows you to control the printer through OrcaSlicer and other Moonraker-compatible software — no Klipper, no Raspberry Pi required.
---
## What's supported?
- Printer status (temperature, progress, state)
- File transfer and print start
- Print control: pause, resume, cancel
- Temperature control during an active print
- Print speed (Silent / Normal / Sport)
- AMS filament change (load / unload)
- Light and fan control
- Web UI with dashboard, temperature cards, axis control, and camera view
- Settings and self-update directly in the browser (⚙ menu)
- OrcaSlicer connection (Moonraker protocol)
---
## Requirements
- Anycubic Kobra X on your local network, with **LAN mode enabled** (printer menu → enable LAN mode)
- Printer MQTT credentials (→ see [Extracting credentials](#extracting-credentials))
- Docker **or** Python 3.9+ **or** the pre-built Linux binary
---
## Quick start Docker (recommended)
```bash
# 1. Start the bridge
./start.sh
```
`start.sh` builds the Docker image automatically on first run and starts the bridge.
```
# 2. Open the web UI: http://BRIDGE-IP:7125
# → Settings (⚙) open automatically on first start
# → Enter your credentials (→ see Extracting credentials)
# 3. In OrcaSlicer: add printer → "Moonraker" → http://BRIDGE-IP:7125
```
Check logs:
```bash
docker-compose logs -f
```
Stop:
```bash
docker-compose down
```
---
## Quick start Binary (Linux)
```bash
chmod +x kx-bridge
./kx-bridge
```
Open the web UI: `http://localhost:7125`
→ Settings (⚙) open automatically and guide you through the initial setup.
---
## Quick start Python directly
```bash
pip install aiohttp
python kobrax_moonraker_bridge.py
```
Open the web UI: `http://localhost:7125`
→ Settings (⚙) open automatically on first start.
---
## Extracting credentials
The MQTT credentials are printer-specific and are generated on first connection with AnycubicSlicerNext. The `extract_credentials` tool reads them from the memory of the running slicer.
**Requirement:** AnycubicSlicerNext must be running and connected to the printer (printer status is shown).
### Windows
```
extract_credentials.exe
```
### Linux
```bash
chmod +x extract_credentials
./extract_credentials
```
### Output
```
[*] Process found: AnycubicSlicerNext.exe (PID 1234)
[*] 1986 memory segments read (738.8 MB)
[*] Analyzing ... 100% (739 MB)
=======================================================
RESULTS
=======================================================
Username userXXXXXXXXXX (hits: 47)
Password *************** (hits: 1046)
Device-ID xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (hits: 3504)
Printer IP 192.168.x.x (hits: 3036)
=======================================================
```
Enter the displayed values in the bridge settings:
Open web UI → **⚙ Settings** → fill in the fields → **Save & Restart**
> If the result looks uncertain: `--verbose` shows all found candidates.
All credentials are **processed locally only** — nothing is sent to external servers.
---
## Configuration (.env)
```env
PRINTER_IP=192.168.x.x # Printer IP address
MQTT_PORT=9883 # Default, do not change
MQTT_USERNAME=userXXXXXXXX # Starts with "user"
MQTT_PASSWORD=XXXXXXXXXXXXXX # ~15 characters, mixed case
DEVICE_ID=xxxxxxxx... # 32-character hex string
MODE_ID=20030 # Kobra X default
```
---
## OrcaSlicer setup
1. Add printer → **Anycubic Kobra X** (or generic Klipper printer)
2. Connection type: **Moonraker**
3. Host: `http://BRIDGE-HOST:7125`
4. Test connection → should show "Online"
---
## Web UI
The bridge serves a web interface at `http://BRIDGE-HOST:7125`:
| Section | Function |
|---------|----------|
| Dashboard | Printer status, progress, temperature overview |
| Temperatures | Set nozzle and bed temperature directly |
| Motion | X/Y/Z movement, motor release |
| Print Speed | Silent / Normal / Sport |
| Fan / Light | Fan speed and work light |
| AMS | Load / unload filament |
| Camera | Live preview (if supported by printer) |
| ⚙ Settings | MQTT credentials, poll interval, self-update |
### Self-update
The ⚙ menu in the web UI lets you check for new versions and update the bridge in place — no reinstallation needed. After the download the bridge restarts automatically with the new version.
---
## bridge.sh (Linux service manager)
```bash
./bridge.sh start # Start bridge in background
./bridge.sh stop # Stop bridge
./bridge.sh restart # Restart
./bridge.sh status # Check status and port
./bridge.sh log 50 # Show last 50 log lines
```
---
## Printer states
The bridge translates internal Kobra states into Moonraker-compatible states:
| Kobra state | Meaning |
|-------------|---------|
| free | Ready |
| printing / busy | Printing |
| pausing / paused | Paused |
| resuming / resumed | Resuming |
| stopping / stoped | Stopping |
| finished | Complete |
| canceled | Cancelled |
| failed | Error |
---
## Troubleshooting
**Port 7125 already in use:**
```bash
./bridge.sh stop # or: fuser -k 7125/tcp
./bridge.sh start
```
**Invalid credentials / connection refused:**
- Start AnycubicSlicerNext, connect to the printer, then run `extract_credentials` again
- If the result looks uncertain: `extract_credentials --verbose` shows all candidates
- Manually enter the correct candidate in `.env` and restart the bridge
**Temperature changes are ignored:**
- During an active print, temperature changes are sent via a separate channel — this is normal and the bridge handles it automatically.
**Docker: Permission denied:**
```bash
sudo usermod -aG docker $USER
# Log out and back in
```
**Docker: .env not found:**
```bash
# .env must be in the same directory as docker-compose.yml
cp .env.example .env && nano .env
```
---
## Logs
```bash
# Docker
docker compose logs -f kx-bridge
# Binary / Python
tail -f /tmp/bridge.log # when using bridge.sh
```
---
## Security notes
- The bridge binds to `0.0.0.0:7125` by default — use on your local network only
- `.env` contains printer credentials — do not share publicly
- The credentials are printer-specific and have no access to Anycubic cloud services
---
## License & legal
This project was created through interoperability research under §69e UrhG (German copyright law).
For private, non-commercial use only.

279
README.md
View File

@@ -1,256 +1,137 @@
# KX-Bridge Anycubic Kobra X Moonraker Bridge <p align="center"><img src="knlogo.png" alt="KX-Bridge Logo" width="180"/></p>
**Version:** 0.9.1-beta10 # KX-Bridge Anycubic Kobra X
**Status:** Public Beta für Heimanwender geeignet, Feedback willkommen
KX-Bridge ist eine Moonraker-kompatible HTTP/WebSocket-Bridge für den **Anycubic Kobra X** 3D-Drucker. Sie ermöglicht die Steuerung des Druckers über OrcaSlicer und andere Moonraker-kompatible Software, ohne dass Klipper oder ein Raspberry Pi benötigt wird. **Version:** 0.9.6
Control your **Anycubic Kobra X** with OrcaSlicer — no Klipper, no Raspberry Pi.
KX-Bridge is a Moonraker-compatible bridge that communicates directly with the printer.
--- ---
## Was wird unterstützt? ## Quick Start in 3 Steps
- Druckerstatus (Temperatur, Fortschritt, Zustand) ### Step 1 Prepare the printer
- Dateiübertragung und Druckstart
- Drucksteuerung: Pause, Fortsetzen, Abbrechen
- Temperaturregelung während des laufenden Drucks
- Druckgeschwindigkeit (Leise / Normal / Sport)
- AMS-Farbwechsel (Einziehen / Ausziehen)
- Licht- und Lüftersteuerung
- Web-UI mit Dashboard, Temperaturkarten, Achsensteuerung und Kameraansicht
- Einstellungen und Self-Update direkt im Browser (⚙-Menü)
- OrcaSlicer-Verbindung (Moonraker-Protokoll)
--- Enable LAN mode on the Kobra X:
**Printer display → Settings → Enable LAN mode**
## Voraussetzungen ### Step 2 Get credentials
- Anycubic Kobra X im lokalen Netzwerk, mit aktiviertem **LAN-Modus** (Drucker-Menü → LAN-Modus einschalten) The MQTT credentials are printer-specific. Here's how to get them:
- MQTT-Credentials des Druckers (→ siehe [Credentials extrahieren](#credentials-extrahieren))
- Docker **oder** Python 3.9+ **oder** direkt die Linux-Binary
--- 1. Open **AnycubicSlicerNext** and connect the printer (wait until status is shown)
2. Run **`extract_credentials.exe`** (Windows) or **`extract_credentials`** (Linux) — outputs Username, Password, Device ID and printer IP
3. Note / copy the values
## Schnellstart Docker (empfohlen) > **Download:** [gitea.it-drui.de/viewit/KX-Bridge-Release/releases](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases) → `extract_credentials.exe` (Windows) / `extract_credentials` (Linux) in the release assets
### Step 3 Start the bridge
```bash ```bash
# 1. Bridge starten
./start.sh ./start.sh
``` ```
`start.sh` baut das Docker-Image automatisch beim ersten Aufruf und startet die Bridge. The script builds the Docker image automatically on first run.
``` **Open Web-UI:** `http://BRIDGE-IP:7125`
# 2. Web-UI öffnen: http://BRIDGE-IP:7125 → The ⚙ menu opens automatically on first start
# → Einstellungen (⚙) öffnen sich automatisch beim ersten Start → Enter credentials from Step 2 → **Save & Restart**
# → Zugangsdaten eintragen (→ siehe Credentials extrahieren)
# 3. In OrcaSlicer: Drucker → "Moonraker" → http://BRIDGE-IP:7125 **Connect OrcaSlicer:**
``` Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125`
Logs prüfen: > **Important:** Connection type must be **Moonraker** (not "Bambu" or "Klipper").
```bash > Enter the full URL including `http://` and port `:7125` in the host field.
docker-compose logs -f
```
Stoppen:
```bash
docker-compose down
```
--- ---
## Schnellstart Binary (Linux) ## ⚠️ Upgrading from 0.9.1 or earlier
Starting with **0.9.2**, KX-Bridge stores settings in `config/config.ini` instead of `.env`.
**Migration is automatic** — no manual action required:
- On first start after upgrade, the bridge reads your existing `.env` and creates `config/config.ini` automatically
- Settings now survive `docker-compose restart` and future updates
- The `.env` file stays mounted read-only as a migration source — you can keep it in place
- If you want to create a `config.ini` manually: copy `config/config.ini.example`
---
## What's supported?
| Feature | Details |
|---------|---------|
| Printer status | Temperature, progress, state, remaining time |
| Print control | Start, pause, resume, cancel |
| Temperature control | Nozzle and bed during print |
| Print speed | Silent / Normal / Sport |
| AMS filament change | Load / unload filament |
| Light & fan | Print light and fan speed |
| Web-UI | Dashboard, motion control, camera view |
| Self-update | Install new versions directly in the browser |
| OrcaSlicer | Moonraker protocol (HTTP + WebSocket) |
---
## Alternatives to Docker
**Linux binary** (no Docker needed):
```bash ```bash
chmod +x kx-bridge chmod +x kx-bridge
./kx-bridge ./kx-bridge
``` ```
Web-UI öffnen: `http://localhost:7125` **Python directly:**
→ Einstellungen (⚙) öffnen sich automatisch und führen durch die Erstkonfiguration.
---
## Schnellstart Python direkt
```bash ```bash
pip install aiohttp pip install aiohttp
python kobrax_moonraker_bridge.py python bridge/kobrax_moonraker_bridge.py
``` ```
Web-UI öffnen: `http://localhost:7125` Web-UI available at `http://localhost:7125` — the ⚙ menu guides through initial setup.
→ Einstellungen (⚙) öffnen sich automatisch beim ersten Start.
--- ---
## Credentials extrahieren ## Useful commands
Die MQTT-Zugangsdaten sind druckerspezifisch und werden beim ersten Verbindungsaufbau mit dem AnycubicSlicerNext generiert. Das Tool `extract_credentials` liest sie aus dem RAM des laufenden Slicers aus.
**Voraussetzung:** AnycubicSlicerNext muss gestartet und mit dem Drucker verbunden sein (Drucker-Status wird angezeigt).
### Windows
```
extract_credentials.exe
```
### Linux
```bash ```bash
chmod +x extract_credentials # Show logs
./extract_credentials docker-compose logs -f
```
### Ausgabe # Stop bridge
docker-compose down
``` # Restart bridge (after update)
[*] Prozess gefunden: AnycubicSlicerNext.exe (PID 1234) ./start.sh
[*] 1986 Speichersegmente gelesen (738.8 MB)
[*] Analysiere ... 100% (739 MB)
=======================================================
ERGEBNISSE
=======================================================
Username userXXXXXXXXXX (Treffer: 47)
Password *************** (Treffer: 1046)
Device-ID xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (Treffer: 3504)
Drucker-IP 192.168.x.x (Treffer: 3036)
=======================================================
```
Die angezeigten Werte in die Bridge-Einstellungen übertragen:
Web-UI öffnen → **⚙ Einstellungen** → Felder ausfüllen → **Speichern & Neustart**
> Falls das Ergebnis unsicher wirkt: `--verbose` zeigt alle gefundenen Kandidaten.
Alle Credentials werden **ausschließlich lokal verarbeitet** — keine Übertragung an externe Server.
---
## Konfiguration (.env)
```env
PRINTER_IP=192.168.x.x # IP des Druckers
MQTT_PORT=9883 # Standard, nicht ändern
MQTT_USERNAME=userXXXXXXXX # Beginnt mit "user"
MQTT_PASSWORD=XXXXXXXXXXXXXX # ~15 Zeichen, gemischt
DEVICE_ID=xxxxxxxx... # 32-stelliger Hex-String
MODE_ID=20030 # Kobra X Standard
``` ```
--- ---
## OrcaSlicer verbinden ## Troubleshooting
1. Drucker hinzufügen → **Anycubic Kobra X** (oder generischer Klipper-Drucker) **"Wrong MQTT credentials"** on start:
2. Verbindungstyp: **Moonraker** - Restart AnycubicSlicerNext, reconnect the printer, run `extract_credentials` again
3. IP: `http://BRIDGE-HOST:7125` - Enter only the IP address, no port (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`)
4. Verbindung testen → sollte "Online" anzeigen
--- **Printer not found / no LAN mode:**
- On the printer display: Settings → Enable LAN mode
## Web-UI - Printer and bridge must be on the same network
Die Bridge stellt unter `http://BRIDGE-HOST:7125` eine Web-Oberfläche bereit:
| Bereich | Funktion |
|---------|----------|
| Dashboard | Druckerstatus, Fortschritt, Temperaturübersicht |
| Temperaturen | Nozzle und Bett direkt setzen |
| Achsen | X/Y/Z-Bewegung, Motorfreigabe |
| Druckgeschwindigkeit | Leise / Normal / Sport |
| Lüfter / Licht | Lüfterdrehzahl und Drucklicht |
| AMS | Filament einziehen / ausziehen |
| Kamera | Live-Vorschau (falls Drucker unterstützt) |
| ⚙ Einstellungen | MQTT-Zugangsdaten, Poll-Intervall, Self-Update |
### Self-Update
Über das ⚙-Menü in der Web-UI kann die Bridge auf neue Versionen prüfen und sich selbst aktualisieren — ohne Neuinstallation. Nach dem Download startet die Bridge automatisch mit der neuen Version neu.
---
## bridge.sh (Linux Service-Manager)
```bash
./bridge.sh start # Bridge im Hintergrund starten
./bridge.sh stop # Bridge beenden
./bridge.sh restart # Neustarten
./bridge.sh status # Status und Port prüfen
./bridge.sh log 50 # Letzte 50 Log-Zeilen
```
---
## Druckerzustände
Die Bridge übersetzt die internen Kobra-Zustände in Moonraker-kompatible Zustände:
| Kobra-Zustand | Bedeutung |
|---------------|-----------|
| free | Bereit |
| printing / busy | Druckt |
| pausing / paused | Pausiert |
| resuming / resumed | Wird fortgesetzt |
| stopping / stoped | Wird gestoppt |
| finished | Abgeschlossen |
| canceled | Abgebrochen |
| failed | Fehler |
---
## Fehlerbehebung
**Port 7125 bereits belegt:**
```bash
./bridge.sh stop # oder: fuser -k 7125/tcp
./bridge.sh start
```
**Credentials ungültig / Verbindung abgelehnt:**
- AnycubicSlicerNext starten, mit Drucker verbinden, `extract_credentials` erneut ausführen
- Falls das Ergebnis unsicher wirkt: `extract_credentials --verbose` zeigt alle Kandidaten an
- Den richtigen Kandidaten manuell in `.env` eintragen und Bridge neu starten
**Temperaturänderungen werden ignoriert:**
- Während eines laufenden Drucks werden Temperaturänderungen über einen separaten Kanal gesendet — das ist normal und wird von der Bridge automatisch erkannt.
**Docker: Permission denied:** **Docker: Permission denied:**
```bash ```bash
sudo usermod -aG docker $USER sudo usermod -aG docker $USER # then log out and back in
# Neu einloggen
```
**Docker: .env nicht gefunden:**
```bash
# .env muss im gleichen Verzeichnis wie docker-compose.yml liegen
cp .env.example .env && nano .env
``` ```
--- ---
## Logs ## Security
```bash - The bridge is accessible on the local network at `http://<host-IP>:7125` — do not expose to the internet
# Docker - `config/config.ini` contains printer credentials — do not share publicly
docker compose logs -f kx-bridge - Credentials do not grant access to Anycubic cloud services
# Binary / Python
tail -f /tmp/bridge.log # bei Nutzung von bridge.sh
```
--- ---
## Sicherheitshinweise ## License
- Die Bridge bindet standardmäßig auf `0.0.0.0:7125` — nur im lokalen Netzwerk nutzen Interoperability research under §69e UrhG — private, non-commercial use only.
- `.env` enthält Drucker-Credentials — nicht öffentlich teilen
- Die Credentials sind druckerspezifisch und haben keinen Zugang zu Anycubic-Cloud-Diensten
---
## Lizenz & Rechtliches
Dieses Projekt entstand durch Interoperabilitätsforschung gem. §69e UrhG.
Ausschließlich für private, nicht-kommerzielle Nutzung.

View File

@@ -1 +1 @@
0.9.1-beta11 0.9.6

34
config.ini.example Normal file
View File

@@ -0,0 +1,34 @@
# KX-Bridge Konfigurationsdatei
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
# cp config.ini.example config.ini
#
# Credentials mit extract_credentials.exe (Windows) oder
# extract_credentials (Linux) aus dem laufenden AnycubicSlicerNext auslesen.
[connection]
# IP-Adresse des Druckers im lokalen Netzwerk
printer_ip = 192.168.x.x
# MQTT-Port (Anycubic Kobra X Standard: 9883)
mqtt_port = 9883
# MQTT-Zugangsdaten (druckerspezifisch, beginnt mit "user")
username = userXXXXXXXXXX
password = XXXXXXXXXXXXXXX
# Geräte-ID (32-stelliger Hex-String, druckerspezifisch)
device_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Modell-ID (Kobra X Standard: 20030)
mode_id = 20030
[print]
# Standard-AMS-Slot für Einfarbdruck (auto = alle belegten Slots, 0-3 = fixer Slot)
default_ams_slot = auto
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
auto_leveling = 1
[bridge]
# Poll-Intervall in Sekunden
poll_interval = 3

143
config_loader.py Normal file
View File

@@ -0,0 +1,143 @@
"""
config_loader.py lädt Verbindungsparameter aus config/config.ini (primär)
oder .env (Fallback / Migration).
Umgebungsvariablen haben immer Vorrang.
"""
import os
import sys
import pathlib
import configparser
_BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) else pathlib.Path(__file__).parent
CONFIG_SECTION_CONNECTION = "connection"
CONFIG_SECTION_PRINT = "print"
CONFIG_SECTION_BRIDGE = "bridge"
def _find_config_file() -> pathlib.Path | None:
for base in (_BASE, _BASE.parent):
p = base / "config" / "config.ini"
if p.is_file():
return p
return None
def _find_env_file() -> pathlib.Path | None:
for base in (_BASE, _BASE.parent):
p = base / ".env"
if p.is_file():
return p
return None
def _load_env_file(path: pathlib.Path):
"""Lädt .env-Datei als Fallback setzt nur Keys die noch nicht in os.environ sind."""
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
key = key.strip()
val = val.strip()
if key and key not in os.environ:
os.environ[key] = val
def _load_config_file(path: pathlib.Path):
"""Lädt config.ini und setzt Keys in os.environ (nur wenn nicht bereits gesetzt)."""
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
mapping = {
"PRINTER_IP": (CONFIG_SECTION_CONNECTION, "printer_ip"),
"MQTT_PORT": (CONFIG_SECTION_CONNECTION, "mqtt_port"),
"MQTT_USERNAME": (CONFIG_SECTION_CONNECTION, "username"),
"MQTT_PASSWORD": (CONFIG_SECTION_CONNECTION, "password"),
"MODE_ID": (CONFIG_SECTION_CONNECTION, "mode_id"),
"DEVICE_ID": (CONFIG_SECTION_CONNECTION, "device_id"),
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
}
for env_key, (section, option) in mapping.items():
if env_key not in os.environ:
try:
val = cfg.get(section, option)
if val:
os.environ[env_key] = val
except (configparser.NoSectionError, configparser.NoOptionError):
pass
def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
"""Einmalige Migration: .env → config.ini anlegen."""
env_vals: dict[str, str] = {}
with open(env_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
env_vals[k.strip()] = v.strip()
config_path.parent.mkdir(parents=True, exist_ok=True)
cfg = configparser.ConfigParser()
cfg[CONFIG_SECTION_CONNECTION] = {
"printer_ip": env_vals.get("PRINTER_IP", ""),
"mqtt_port": env_vals.get("MQTT_PORT", "9883"),
"username": env_vals.get("MQTT_USERNAME", ""),
"password": env_vals.get("MQTT_PASSWORD", ""),
"mode_id": env_vals.get("MODE_ID", ""),
"device_id": env_vals.get("DEVICE_ID", ""),
}
cfg[CONFIG_SECTION_PRINT] = {
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
}
cfg[CONFIG_SECTION_BRIDGE] = {
"poll_interval": "3",
}
with open(config_path, "w", encoding="utf-8") as f:
f.write("# KX-Bridge Konfigurationsdatei\n")
f.write("# Automatisch migriert aus .env\n\n")
cfg.write(f)
def find_config_path() -> pathlib.Path:
"""Gibt den Pfad zur config.ini zurück (auch wenn sie noch nicht existiert)."""
for base in (_BASE, _BASE.parent):
config_dir = base / "config"
if config_dir.is_dir():
return config_dir / "config.ini"
return _BASE / "config" / "config.ini"
# ─── Laden ───────────────────────────────────────────────────────────────────
_config_path = _find_config_file()
_env_path = _find_env_file()
if _config_path:
_load_config_file(_config_path)
elif _env_path:
# Kein config.ini vorhanden → aus .env migrieren
_target = find_config_path()
migrate_env_to_config(_env_path, _target)
_load_config_file(_target)
_config_path = _target
def get(key: str, default: str = "") -> str:
return os.environ.get(key, default)
# Häufig verwendete Shortcuts
PRINTER_IP = get("PRINTER_IP", "")
MQTT_PORT = int(get("MQTT_PORT", "9883"))
USERNAME = get("MQTT_USERNAME", "")
PASSWORD = get("MQTT_PASSWORD", "")
MODE_ID = get("MODE_ID", "")
DEVICE_ID = get("DEVICE_ID", "")
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))

View File

@@ -2,9 +2,9 @@ services:
kx-bridge: kx-bridge:
image: kx-bridge:latest image: kx-bridge:latest
build: . build: .
env_file: .env
volumes: volumes:
- ./.env:/app/.env - ./config:/app/config
- ./.env:/app/.env:ro
ports: ports:
- "7125:7125" - "7125:7125"
restart: unless-stopped restart: unless-stopped

View File

@@ -40,9 +40,11 @@ def get(key: str, default: str = "") -> str:
# Häufig verwendete Shortcuts # Häufig verwendete Shortcuts
PRINTER_IP = get("PRINTER_IP", "") PRINTER_IP = get("PRINTER_IP", "")
MQTT_PORT = int(get("MQTT_PORT", "9883")) MQTT_PORT = int(get("MQTT_PORT", "9883"))
USERNAME = get("MQTT_USERNAME", "") USERNAME = get("MQTT_USERNAME", "")
PASSWORD = get("MQTT_PASSWORD", "") PASSWORD = get("MQTT_PASSWORD", "")
MODE_ID = get("MODE_ID", "") MODE_ID = get("MODE_ID", "")
DEVICE_ID = get("DEVICE_ID", "") DEVICE_ID = get("DEVICE_ID", "")
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))

View File

@@ -1,23 +1,23 @@
""" """
extract_credentials.py Extrahiert Anycubic LAN-MQTT-Credentials aus dem RAM extract_credentials.py Extracts Anycubic LAN-MQTT credentials from the RAM
des laufenden AnycubicSlicerNext-Prozesses. of the running AnycubicSlicerNext process.
Voraussetzungen: Requirements:
- AnycubicSlicerNext läuft und ist mit dem Drucker verbunden - AnycubicSlicerNext is running and connected to the printer
- Gleiches Benutzerkonto wie der Slicer-Prozess (kein Admin nötig) - Same user account as the slicer process (no admin required)
Verwendung: Usage:
python3 extract_credentials.py [--write-env] [--env-file ../.env] python3 extract_credentials.py [--write-env] [--env-file ../.env]
Funktionsweise: How it works:
1. Prozess "AnycubicSlicer.exe" (Windows) bzw. "AnycubicSlicer" (Linux) finden 1. Find process "AnycubicSlicer.exe" (Windows) or "AnycubicSlicer" (Linux)
2. Speicherseiten des Prozesses durchsuchen (nur r/rw, keine Exec-Pages) 2. Scan memory pages of the process (only r/rw, no exec pages)
3. Nach MQTT-Credential-Patterns suchen: 3. Search for MQTT credential patterns:
Username: user[A-Za-z0-9]{8,12} Username: user[A-Za-z0-9]{8,12}
Password: [A-Za-z0-9+/]{13,18} Password: [A-Za-z0-9+/]{13,18}
Drucker-IP: d{1,3}.d{1,3}.d{1,3}.d{1,3} Printer IP: d{1,3}.d{1,3}.d{1,3}.d{1,3}
4. Kandidaten nach Plausibilität filtern und ausgeben 4. Filter candidates by plausibility and print results
5. Optional: .env-Datei schreiben 5. Optionally write .env file
""" """
import argparse import argparse
@@ -28,48 +28,48 @@ import sys
import platform import platform
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Plattform-Erkennung # Platform detection
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
IS_WINDOWS = platform.system() == "Windows" IS_WINDOWS = platform.system() == "Windows"
IS_LINUX = platform.system() == "Linux" IS_LINUX = platform.system() == "Linux"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Pattern # Patterns
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Username: "user" + 812 alphanumerische Zeichen (drucker-generiert) # Username: "user" + 812 alphanumeric characters (printer-generated)
RE_USERNAME = re.compile(rb'user[A-Za-z0-9]{8,12}(?=[^A-Za-z0-9]|$)') RE_USERNAME = re.compile(rb'user[A-Za-z0-9]{8,12}(?=[^A-Za-z0-9]|$)')
# Password: 1320 alphanumerische Zeichen (kein / da kein RTSP-Pfad) # Password: 1320 alphanumeric characters (no / since no RTSP path)
# Anycubic-Passwörter: gemischte Groß/Klein/Ziffern, kein Slash # Anycubic passwords: mixed upper/lower/digits, no slash
RE_PASSWORD = re.compile(rb'[A-Za-z0-9]{13,20}(?=[^A-Za-z0-9]|$)') RE_PASSWORD = re.compile(rb'[A-Za-z0-9]{13,20}(?=[^A-Za-z0-9]|$)')
# Kontext-Pattern: sucht Passwort das direkt nach "password" im Speicher steht # Context pattern: password directly following "password" in memory
RE_PASSWORD_CTX = re.compile(rb'(?:password|passwd|Password)\x00{0,4}([A-Za-z0-9]{10,25})(?=[^A-Za-z0-9]|$)', re.IGNORECASE) RE_PASSWORD_CTX = re.compile(rb'(?:password|passwd|Password)\x00{0,4}([A-Za-z0-9]{10,25})(?=[^A-Za-z0-9]|$)', re.IGNORECASE)
# Proximity-Pattern: Username gefolgt von Passwort in naher Umgebung (<512 Bytes) # Proximity pattern: username followed by password within close range (<512 bytes)
RE_USER_PASS_PROXIMITY = re.compile( RE_USER_PASS_PROXIMITY = re.compile(
rb'(user[A-Za-z0-9]{8,12}).{1,512}?([A-Za-z0-9]{13,20})(?=[^A-Za-z0-9]|$)', rb'(user[A-Za-z0-9]{8,12}).{1,512}?([A-Za-z0-9]{13,20})(?=[^A-Za-z0-9]|$)',
re.DOTALL re.DOTALL
) )
# IPv4-Adresse (kein localhost, kein Broadcast) # IPv4 address (no localhost, no broadcast)
RE_IP = re.compile(rb'(?<![.\d])(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?![.\d])') RE_IP = re.compile(rb'(?<![.\d])(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?![.\d])')
# mode_id: 5-stellige Zahl (z.B. 20030) # mode_id: 5-digit number (e.g. 20030)
RE_MODE_ID = re.compile(rb'(?<!\d)(2\d{4})(?!\d)') RE_MODE_ID = re.compile(rb'(?<!\d)(2\d{4})(?!\d)')
# device_id: 32 Hex-Zeichen (MD5-Format) # device_id: 32 hex characters (MD5 format)
RE_DEVICE_ID = re.compile(rb'[0-9a-f]{32}(?=[^0-9a-f]|$)') RE_DEVICE_ID = re.compile(rb'[0-9a-f]{32}(?=[^0-9a-f]|$)')
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Windows Speicher lesen via ctypes / ReadProcessMemory # Windows read memory via ctypes / ReadProcessMemory
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _win_find_pid(name: str) -> "int | None": def _win_find_pid(name: str) -> "int | None":
"""Findet die PID eines Prozesses anhand des Namens (case-insensitive).""" """Find the PID of a process by name (case-insensitive)."""
import ctypes import ctypes
import ctypes.wintypes import ctypes.wintypes
@@ -110,15 +110,15 @@ def _win_find_pid(name: str) -> "int | None":
def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]": def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
"""Liest alle lesbaren Speicherseiten eines Windows-Prozesses.""" """Read all readable memory pages of a Windows process."""
import ctypes import ctypes
import ctypes.wintypes import ctypes.wintypes
PROCESS_VM_READ = 0x0010 PROCESS_VM_READ = 0x0010
PROCESS_QUERY_INFORMATION = 0x0400 PROCESS_QUERY_INFORMATION = 0x0400
MEM_COMMIT = 0x1000 MEM_COMMIT = 0x1000
PAGE_NOACCESS = 0x01 PAGE_NOACCESS = 0x01
PAGE_GUARD = 0x100 PAGE_GUARD = 0x100
class MEMORY_BASIC_INFORMATION(ctypes.Structure): class MEMORY_BASIC_INFORMATION(ctypes.Structure):
_fields_ = [ _fields_ = [
@@ -134,7 +134,7 @@ def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
k32 = ctypes.windll.kernel32 k32 = ctypes.windll.kernel32
handle = k32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) handle = k32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
if not handle: if not handle:
raise PermissionError(f"OpenProcess fehlgeschlagen (PID {pid}): {ctypes.GetLastError()}") raise PermissionError(f"OpenProcess failed (PID {pid}): {ctypes.GetLastError()}")
chunks = [] chunks = []
addr = 0 addr = 0
@@ -147,7 +147,7 @@ def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
mbi.State != MEM_COMMIT or mbi.State != MEM_COMMIT or
mbi.Protect & PAGE_NOACCESS or mbi.Protect & PAGE_NOACCESS or
mbi.Protect & PAGE_GUARD or mbi.Protect & PAGE_GUARD or
mbi.RegionSize > 256 * 1024 * 1024 # >256 MB überspringen mbi.RegionSize > 256 * 1024 * 1024 # skip >256 MB regions
) )
if not skip: if not skip:
buf = ctypes.create_string_buffer(mbi.RegionSize) buf = ctypes.create_string_buffer(mbi.RegionSize)
@@ -156,7 +156,7 @@ def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
buf, mbi.RegionSize, ctypes.byref(read)): buf, mbi.RegionSize, ctypes.byref(read)):
chunks.append(bytes(buf[:read.value])) chunks.append(bytes(buf[:read.value]))
addr += mbi.RegionSize addr += mbi.RegionSize
if addr >= 0x7FFFFFFFFFFF: # Ende des User-Space (64-bit) if addr >= 0x7FFFFFFFFFFF: # end of user space (64-bit)
break break
finally: finally:
k32.CloseHandle(handle) k32.CloseHandle(handle)
@@ -165,11 +165,11 @@ def _win_read_memory(pid: int, chunk_size: int = 0x10000) -> "list[bytes]":
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Linux Speicher lesen via /proc/{pid}/mem # Linux read memory via /proc/{pid}/mem
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _linux_find_pid(name: str) -> "int | None": def _linux_find_pid(name: str) -> "int | None":
"""Findet PID anhand des Prozessnamens in /proc.""" """Find PID by process name in /proc."""
for entry in os.listdir("/proc"): for entry in os.listdir("/proc"):
if not entry.isdigit(): if not entry.isdigit():
continue continue
@@ -183,7 +183,7 @@ def _linux_find_pid(name: str) -> "int | None":
def _linux_read_memory(pid: int) -> "list[bytes]": def _linux_read_memory(pid: int) -> "list[bytes]":
"""Liest lesbare Speichersegmente aus /proc/{pid}/mem.""" """Read readable memory segments from /proc/{pid}/mem."""
chunks = [] chunks = []
maps_path = f"/proc/{pid}/maps" maps_path = f"/proc/{pid}/maps"
mem_path = f"/proc/{pid}/mem" mem_path = f"/proc/{pid}/mem"
@@ -193,8 +193,8 @@ def _linux_read_memory(pid: int) -> "list[bytes]":
mem = open(mem_path, "rb") mem = open(mem_path, "rb")
except PermissionError: except PermissionError:
raise PermissionError( raise PermissionError(
f"Kein Zugriff auf /proc/{pid}/mem — " f"No access to /proc/{pid}/mem — "
"Script als gleicher Benutzer wie der Slicer starten." "run the script as the same user as the slicer process."
) )
for line in maps: for line in maps:
@@ -202,16 +202,16 @@ def _linux_read_memory(pid: int) -> "list[bytes]":
if len(parts) < 2: if len(parts) < 2:
continue continue
perms = parts[1] perms = parts[1]
if "r" not in perms: # nur lesbare Seiten if "r" not in perms: # readable pages only
continue continue
if "x" in perms: # Code-Seiten überspringen (keine Strings) if "x" in perms: # skip code pages (no strings)
continue continue
try: try:
start, end = [int(x, 16) for x in parts[0].split("-")] start, end = [int(x, 16) for x in parts[0].split("-")]
except ValueError: except ValueError:
continue continue
size = end - start size = end - start
if size > 256 * 1024 * 1024: # >256 MB überspringen if size > 256 * 1024 * 1024: # skip >256 MB regions
continue continue
try: try:
mem.seek(start) mem.seek(start)
@@ -226,7 +226,7 @@ def _linux_read_memory(pid: int) -> "list[bytes]":
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Pattern-Suche # Pattern search
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _is_valid_ip(ip_bytes: bytes) -> bool: def _is_valid_ip(ip_bytes: bytes) -> bool:
@@ -245,7 +245,7 @@ def _is_valid_ip(ip_bytes: bytes) -> bool:
def search_chunks(chunks: "list[bytes]") -> dict: def search_chunks(chunks: "list[bytes]") -> dict:
"""Durchsucht Speicher-Chunks nach Credential-Patterns.""" """Search memory chunks for credential patterns."""
usernames = {} # value → count usernames = {} # value → count
passwords = {} passwords = {}
ips = {} ips = {}
@@ -256,12 +256,12 @@ def search_chunks(chunks: "list[bytes]") -> dict:
if i % 50 == 0 or i == total - 1: if i % 50 == 0 or i == total - 1:
pct = (i + 1) * 100 // total pct = (i + 1) * 100 // total
mb_done = sum(len(c) for c in chunks[:i+1]) / 1024 / 1024 mb_done = sum(len(c) for c in chunks[:i+1]) / 1024 / 1024
print(f"\r[*] Analysiere ... {pct:3d}% ({mb_done:.0f} MB)", end="", flush=True) print(f"\r[*] Scanning ... {pct:3d}% ({mb_done:.0f} MB)", end="", flush=True)
for m in RE_USERNAME.finditer(chunk): for m in RE_USERNAME.finditer(chunk):
v = m.group().decode("ascii", errors="replace") v = m.group().decode("ascii", errors="replace")
usernames[v] = usernames.get(v, 0) + 1 usernames[v] = usernames.get(v, 0) + 1
# Proximity: Passwort das innerhalb von 512 Bytes nach einem Username steht # Proximity: password within 512 bytes after a username
for m in RE_USER_PASS_PROXIMITY.finditer(chunk): for m in RE_USER_PASS_PROXIMITY.finditer(chunk):
pw = m.group(2).decode("ascii", errors="replace") pw = m.group(2).decode("ascii", errors="replace")
has_upper = any(c.isupper() for c in pw) has_upper = any(c.isupper() for c in pw)
@@ -272,7 +272,7 @@ def search_chunks(chunks: "list[bytes]") -> dict:
for m in RE_PASSWORD.finditer(chunk): for m in RE_PASSWORD.finditer(chunk):
v = m.group().decode("ascii", errors="replace") v = m.group().decode("ascii", errors="replace")
# Filter: mindestens 2 Großbuchstaben + 2 Kleinbuchstaben + 1 Ziffer # Filter: at least 2 uppercase + 2 lowercase + 1 digit
has_upper = sum(1 for c in v if c.isupper()) >= 2 has_upper = sum(1 for c in v if c.isupper()) >= 2
has_lower = sum(1 for c in v if c.islower()) >= 2 has_lower = sum(1 for c in v if c.islower()) >= 2
has_digit = sum(1 for c in v if c.isdigit()) >= 1 has_digit = sum(1 for c in v if c.isdigit()) >= 1
@@ -289,9 +289,9 @@ def search_chunks(chunks: "list[bytes]") -> dict:
v = m.group().decode("ascii") v = m.group().decode("ascii")
device_ids[v] = device_ids.get(v, 0) + 1 device_ids[v] = device_ids.get(v, 0) + 1
print() # Zeilenumbruch nach Fortschrittszeile print() # newline after progress line
# Nach Häufigkeit sortieren, häufigste zuerst # Sort by frequency, most frequent first
def top(d, n=10): def top(d, n=10):
return sorted(d.items(), key=lambda x: -x[1])[:n] return sorted(d.items(), key=lambda x: -x[1])[:n]
@@ -304,7 +304,7 @@ def search_chunks(chunks: "list[bytes]") -> dict:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Hauptprogramm # Main
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
SLICER_NAMES = [ SLICER_NAMES = [
@@ -359,95 +359,95 @@ def write_env(results: dict, env_path: str,
with open(env_path, "w") as f: with open(env_path, "w") as f:
f.writelines(lines) f.writelines(lines)
print(f"\n✓ Credentials in '{env_path}' gespeichert.") print(f"\n✓ Credentials saved to '{env_path}'.")
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Extrahiert MQTT-Credentials aus dem RAM des AnycubicSlicer-Prozesses" description="Extract MQTT credentials from the AnycubicSlicer process RAM"
) )
parser.add_argument("--write-env", action="store_true", parser.add_argument("--write-env", action="store_true",
help="Gefundene Credentials in .env schreiben") help="Write found credentials to .env file")
parser.add_argument("--env-file", default=None, parser.add_argument("--env-file", default=None,
help="Pfad zur .env-Datei (Standard: ../. env relativ zu diesem Script)") help="Path to .env file (default: ../.env relative to this script)")
parser.add_argument("--pid", type=int, default=None, parser.add_argument("--pid", type=int, default=None,
help="Prozess-PID direkt angeben (überspringt Auto-Erkennung)") help="Specify process PID directly (skips auto-detection)")
parser.add_argument("--verbose", action="store_true", parser.add_argument("--verbose", action="store_true",
help="Alle Kandidaten ausgeben, nicht nur den besten") help="Show all candidates, not just the best match")
args = parser.parse_args() args = parser.parse_args()
# .env-Pfad bestimmen # Determine .env path
if args.env_file: if args.env_file:
env_path = args.env_file env_path = args.env_file
else: else:
env_path = os.path.join(os.path.dirname(__file__), "..", ".env") env_path = os.path.join(os.path.dirname(__file__), "..", ".env")
env_path = os.path.normpath(env_path) env_path = os.path.normpath(env_path)
# Prozess finden # Find process
if args.pid: if args.pid:
pid, proc_name = args.pid, f"PID {args.pid}" pid, proc_name = args.pid, f"PID {args.pid}"
else: else:
print("[*] Suche AnycubicSlicer-Prozess ...") print("[*] Searching for AnycubicSlicer process ...")
pid, proc_name = find_slicer_pid() pid, proc_name = find_slicer_pid()
if not pid: if not pid:
print("✗ AnycubicSlicer nicht gefunden. Bitte den Slicer starten und " print("✗ AnycubicSlicer not found. Please start the slicer, connect it "
"mit dem Drucker verbinden, dann erneut ausführen.") "to the printer, then run this script again.")
sys.exit(1) sys.exit(1)
print(f"[*] Prozess gefunden: {proc_name} (PID {pid})") print(f"[*] Process found: {proc_name} (PID {pid})")
print(f"[*] Lese Prozess-Speicher ...") print(f"[*] Reading process memory ...")
try: try:
chunks = read_process(pid) chunks = read_process(pid)
except PermissionError as e: except PermissionError as e:
print(f"Zugriffsfehler: {e}") print(f"Permission error: {e}")
sys.exit(1) sys.exit(1)
total_mb = sum(len(c) for c in chunks) / 1024 / 1024 total_mb = sum(len(c) for c in chunks) / 1024 / 1024
print(f"[*] {len(chunks)} Speichersegmente gelesen ({total_mb:.1f} MB)") print(f"[*] {len(chunks)} memory segments read ({total_mb:.1f} MB)")
print(f"[*] Durchsuche nach Credentials ...") print(f"[*] Searching for credentials ...")
results = search_chunks(chunks) results = search_chunks(chunks)
# Ausgabe # Output
print("\n" + "="*55) print("\n" + "="*55)
print(" ERGEBNISSE") print(" RESULTS")
print("="*55) print("="*55)
def show(label, items, verbose): def show(label, items, verbose):
if not items: if not items:
print(f" {label:12s} — nicht gefunden") print(f" {label:12s} — not found")
return items[0][0] if items else "" return items[0][0] if items else ""
best = items[0][0] best = items[0][0]
print(f" {label:12s} {best} (Treffer: {items[0][1]})") print(f" {label:12s} {best} (matches: {items[0][1]})")
if verbose and len(items) > 1: if verbose and len(items) > 1:
for val, cnt in items[1:]: for val, cnt in items[1:]:
print(f" {'':12s} {val} (Treffer: {cnt})") print(f" {'':12s} {val} (matches: {cnt})")
return best return best
best_user = show("Username", results["usernames"], args.verbose) best_user = show("Username", results["usernames"], args.verbose)
best_pass = show("Password", results["passwords"], args.verbose) best_pass = show("Password", results["passwords"], args.verbose)
best_device = show("Device-ID", results["device_ids"], args.verbose) best_device = show("Device-ID", results["device_ids"], args.verbose)
# IP: 192.168.x.x bevorzugen # IP: prefer 192.168.x.x
lan_ips = [(ip, cnt) for ip, cnt in results["ips"] lan_ips = [(ip, cnt) for ip, cnt in results["ips"]
if ip.startswith("192.168.") or ip.startswith("10.") or ip.startswith("172.")] if ip.startswith("192.168.") or ip.startswith("10.") or ip.startswith("172.")]
if not lan_ips: if not lan_ips:
lan_ips = results["ips"] lan_ips = results["ips"]
best_ip = show("Drucker-IP", lan_ips, args.verbose) best_ip = show("Printer IP", lan_ips, args.verbose)
print("="*55) print("="*55)
if not best_user or not best_pass: if not best_user or not best_pass:
print("\nKeine vollständigen Credentials gefunden.") print("\nNo complete credentials found.")
print(" Stelle sicher dass der Slicer MIT dem Drucker verbunden ist.") print(" Make sure the slicer is connected to the printer.")
sys.exit(1) sys.exit(1)
if args.write_env: if args.write_env:
write_env(results, env_path, best_user, best_pass, best_ip, write_env(results, env_path, best_user, best_pass, best_ip,
device_id=best_device) device_id=best_device)
else: else:
print(f"\nHinweis: --write-env übergeben um Credentials in '{env_path}' zu speichern.") print(f"\nTip: pass --write-env to save credentials to '{env_path}'.")
if __name__ == "__main__": if __name__ == "__main__":

BIN
knlogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -16,7 +16,9 @@ Verwendung:
client.disconnect() client.disconnect()
""" """
import hashlib
import json import json
import logging
import os import os
import socket import socket
import ssl import ssl
@@ -28,6 +30,8 @@ from datetime import datetime
import env_loader import env_loader
log = logging.getLogger("kobrax.mqtt")
_SCRIPT_DIR = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__)) _SCRIPT_DIR = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__))
CERT_FILE = os.path.join(_SCRIPT_DIR, "anycubic_slicer.crt") CERT_FILE = os.path.join(_SCRIPT_DIR, "anycubic_slicer.crt")
KEY_FILE = os.path.join(_SCRIPT_DIR, "anycubic_slicer.key") KEY_FILE = os.path.join(_SCRIPT_DIR, "anycubic_slicer.key")
@@ -119,6 +123,13 @@ class KobraXClient:
# Optional callbacks: topic_suffix → callable(payload_dict) # Optional callbacks: topic_suffix → callable(payload_dict)
self.callbacks: dict[str, callable] = {} self.callbacks: dict[str, callable] = {}
# Dedup: last hash per topic suffix to suppress repeated identical messages
self._last_rx_hash: dict[str, str] = {}
# Fields that change every tick and should be stripped before dedup-hashing
_VOLATILE = {"timestamp", "msgid", "progress", "curr_layer",
"curr_nozzle_temp", "curr_hotbed_temp",
"target_nozzle_temp", "target_hotbed_temp"}
# -- Topics -------------------------------------------------------------- # -- Topics --------------------------------------------------------------
def _pub_topic(self, msg_type: str) -> str: def _pub_topic(self, msg_type: str) -> str:
@@ -144,18 +155,19 @@ class KobraXClient:
raw = socket.create_connection((self.host, self.port), timeout=5) raw = socket.create_connection((self.host, self.port), timeout=5)
self._sock = ctx.wrap_socket(raw) self._sock = ctx.wrap_socket(raw)
print(f"[kobrax] TLS: {self._sock.cipher()[0]}") log.info("TLS connected cipher=%s", self._sock.cipher()[0])
self._sock.sendall(_build_connect(self.client_id, self.username, self.password)) self._sock.sendall(_build_connect(self.client_id, self.username, self.password))
self._sock.settimeout(3) self._sock.settimeout(3)
r = self._sock.recv(64) r = self._sock.recv(64)
if len(r) < 4 or r[0] != 0x20 or r[3] != 0: if len(r) < 4 or r[0] != 0x20 or r[3] != 0:
raise RuntimeError(f"CONNACK failed: {r.hex()}") raise RuntimeError(f"CONNACK failed: {r.hex()}")
print(f"[kobrax] CONNACK rc=0") log.info("CONNACK rc=0")
self._sock.settimeout(0.2) self._sock.settimeout(0.2)
self._buf = b"" self._buf = b""
self._subscribe(self._sub_topic()) self._subscribe(self._sub_topic())
log.debug("MQTT connected to %s:%s", self.host, self.port)
def connect(self): def connect(self):
self._do_connect() self._do_connect()
@@ -172,7 +184,7 @@ class KobraXClient:
pass pass
def _reconnect(self): def _reconnect(self):
print("[kobrax] Verbindung verloren reconnect…") log.warning("Verbindung verloren reconnect…")
try: try:
self._sock.close() self._sock.close()
except Exception: except Exception:
@@ -180,10 +192,10 @@ class KobraXClient:
for delay in [2, 4, 8, 15, 30]: for delay in [2, 4, 8, 15, 30]:
try: try:
self._do_connect() self._do_connect()
print("[kobrax] Reconnect erfolgreich") log.info("Reconnect erfolgreich")
return True return True
except Exception as e: except Exception as e:
print(f"[kobrax] Reconnect fehlgeschlagen ({e}), warte {delay}s…") log.warning("Reconnect fehlgeschlagen (%s), warte %ss…", e, delay)
time.sleep(delay) time.sleep(delay)
return False return False
@@ -192,12 +204,13 @@ class KobraXClient:
pid = self._pid pid = self._pid
self._pid += 1 self._pid += 1
self._sock.sendall(_build_subscribe(topic, pid)) self._sock.sendall(_build_subscribe(topic, pid))
print(f"[kobrax] SUB {topic}") log.info("SUB %s", topic)
# -- Read loop ----------------------------------------------------------- # -- Read loop -----------------------------------------------------------
def _read_loop(self): def _read_loop(self):
last_ping = time.time() last_ping = time.time()
_empty_count = 0
while self._running: while self._running:
if time.time() - last_ping > 30: if time.time() - last_ping > 30:
with self._lock: with self._lock:
@@ -212,7 +225,12 @@ class KobraXClient:
try: try:
data = self._sock.recv(65536) data = self._sock.recv(65536)
if not data: if not data:
raise ConnectionResetError("EOF") # Windows SSL kann kurzzeitig b"" liefern ohne echten EOF
_empty_count += 1
if _empty_count >= 5:
raise ConnectionResetError("EOF")
continue
_empty_count = 0
self._buf += data self._buf += data
self._drain() self._drain()
except ssl.SSLWantReadError: except ssl.SSLWantReadError:
@@ -221,7 +239,7 @@ class KobraXClient:
continue continue
except Exception as e: except Exception as e:
if self._running: if self._running:
print(f"[kobrax] reader error: {e}") log.warning("reader error: %s", e)
if not self._reconnect(): if not self._reconnect():
break break
last_ping = time.time() last_ping = time.time()
@@ -260,9 +278,40 @@ class KobraXClient:
self._buf = buf[idx:] self._buf = buf[idx:]
def _dedup_hash(self, suffix: str, payload: dict) -> str:
"""Hash payload ignoring volatile per-tick fields for dedup check."""
stable = {k: v for k, v in payload.items()
if k not in {"timestamp", "msgid", "progress", "curr_layer",
"curr_nozzle_temp", "curr_hotbed_temp",
"target_nozzle_temp", "target_hotbed_temp"}}
return hashlib.md5(json.dumps(stable, sort_keys=True).encode(), usedforsecurity=False).hexdigest()
def _dispatch(self, topic: str, payload: dict): def _dispatch(self, topic: str, payload: dict):
# Resolve by report topic suffix (e.g. "info/report")
suffix = "/".join(topic.split("/")[-2:]) suffix = "/".join(topic.split("/")[-2:])
# Structured RX log with dedup suppression
h = self._dedup_hash(suffix, payload)
is_dup = self._last_rx_hash.get(suffix) == h
self._last_rx_hash[suffix] = h
if is_dup:
log.debug("RX [dup] %-25s state=%-12s", suffix, payload.get("state", ""))
else:
data = payload.get("data") or {}
state = payload.get("state", "")
if "progress" in data:
log.info("RX %-25s state=%-12s progress=%s%% layer=%s/%s",
suffix, state, data["progress"],
data.get("curr_layer", "?"), data.get("total_layers", "?"))
elif "curr_nozzle_temp" in data:
log.info("RX %-25s nozzle=%s°C/%s°C bed=%s°C/%s°C",
suffix,
data["curr_nozzle_temp"], data.get("target_nozzle_temp", 0),
data.get("curr_hotbed_temp", "?"), data.get("target_hotbed_temp", 0))
else:
log.info("RX %-25s state=%-12s data=%s",
suffix, state, json.dumps(payload.get("data"), ensure_ascii=False))
# Resolve by report topic suffix (e.g. "info/report")
if suffix in self._pending_report: if suffix in self._pending_report:
entry = self._pending_report[suffix] entry = self._pending_report[suffix]
entry["result"] = payload entry["result"] = payload
@@ -276,19 +325,18 @@ class KobraXClient:
entry["event"].set() entry["event"].set()
# User callbacks by topic suffix (last two path components) # User callbacks by topic suffix (last two path components)
suffix = "/".join(topic.split("/")[-2:])
if suffix in self.callbacks: if suffix in self.callbacks:
try: try:
self.callbacks[suffix](payload) self.callbacks[suffix](payload)
except Exception as e: except Exception as e:
print(f"[kobrax] callback error for {suffix}: {e}") log.error("callback error for %s: %s", suffix, e)
# Generic wildcard callback # Generic wildcard callback
if "*" in self.callbacks: if "*" in self.callbacks:
try: try:
self.callbacks["*"](topic, payload) self.callbacks["*"](topic, payload)
except Exception as e: except Exception as e:
print(f"[kobrax] wildcard callback error: {e}") log.error("wildcard callback error: %s", e)
# -- Publish + request/response ------------------------------------------ # -- Publish + request/response ------------------------------------------
@@ -316,11 +364,14 @@ class KobraXClient:
report_registered = True report_registered = True
topic = self._pub_topic(msg_type) topic = self._pub_topic(msg_type)
log.info("TX %-25s action=%-12s data=%s",
f"{msg_type}/request", action,
json.dumps(data, ensure_ascii=False) if data else "null")
try: try:
with self._lock: with self._lock:
self._sock.sendall(_build_publish(topic, payload)) self._sock.sendall(_build_publish(topic, payload))
except Exception as e: except Exception as e:
print(f"[kobrax] send error: {e}, reconnecting…") log.error("send error: %s, reconnecting…", e)
self._pending_msgid.pop(msgid, None) self._pending_msgid.pop(msgid, None)
if report_registered: if report_registered:
self._pending_report.pop(report_key, None) self._pending_report.pop(report_key, None)
@@ -361,11 +412,14 @@ class KobraXClient:
"data": data, "data": data,
}, separators=(",", ":")) }, separators=(",", ":"))
topic = self._web_topic(msg_type) topic = self._web_topic(msg_type)
log.info("TX(web) %-23s action=%-12s data=%s",
f"{msg_type}/request", action,
json.dumps(data, ensure_ascii=False) if data else "null")
try: try:
with self._lock: with self._lock:
self._sock.sendall(_build_publish(topic, payload)) self._sock.sendall(_build_publish(topic, payload))
except Exception as e: except Exception as e:
print(f"[kobrax] web send error: {e}") log.error("web send error: %s", e)
# -- High-level commands ------------------------------------------------- # -- High-level commands -------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,2 @@
aiohttp>=3.9 aiohttp>=3.9
imageio-ffmpeg>=0.4.9

View File

@@ -15,6 +15,15 @@ if [[ ! -f .env ]]; then
fi fi
fi fi
# config/ Verzeichnis und config.ini.example anlegen falls nicht vorhanden
mkdir -p config
if [[ ! -f config/config.ini ]] && [[ ! -f config/config.ini.example ]]; then
if [[ -f config.ini.example ]]; then
cp config.ini.example config/config.ini.example
echo "[start] config/config.ini.example aus config.ini.example erstellt"
fi
fi
# Docker verfügbar? # Docker verfügbar?
if ! docker info > /dev/null 2>&1; then if ! docker info > /dev/null 2>&1; then
echo "[start] Docker nicht gefunden bitte Docker installieren." echo "[start] Docker nicht gefunden bitte Docker installieren."
@@ -34,12 +43,12 @@ else
print(int(datetime.datetime.fromisoformat(s).replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null || echo 0) print(int(datetime.datetime.fromisoformat(s).replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null || echo 0)
for f in Dockerfile \ for f in Dockerfile \
05_scripts/kobrax_moonraker_bridge.py \ bridge/kobrax_moonraker_bridge.py \
05_scripts/kobrax_client.py \ bridge/kobrax_client.py \
05_scripts/env_loader.py \ bridge/env_loader.py \
05_scripts/requirements.txt \ bridge/requirements.txt \
05_scripts/anycubic_slicer.crt \ bridge/anycubic_slicer.crt \
05_scripts/anycubic_slicer.key; do bridge/anycubic_slicer.key; do
if [[ -f "$f" ]]; then if [[ -f "$f" ]]; then
FILE_TS=$(python3 -c "import os; print(int(os.path.getmtime('$f')))" 2>/dev/null || echo 0) FILE_TS=$(python3 -c "import os; print(int(os.path.getmtime('$f')))" 2>/dev/null || echo 0)
if [[ $FILE_TS -gt $IMAGE_TS ]]; then if [[ $FILE_TS -gt $IMAGE_TS ]]; then