diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md new file mode 100644 index 0000000..d5b385b --- /dev/null +++ b/CHANGELOG.de.md @@ -0,0 +1,212 @@ +# Changelog + +## [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+` — 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d9fd6..4669932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,166 +1,212 @@ # Changelog +## [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+` — 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: 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 und transparent dargestellt mit „Leer"-Label +- 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-Achse: ▲ fährt jetzt aufwärts (Z+), ▼ abwärts (Z−) – Pfeile waren vertauscht (Issue #4) -- Home All: korrekter axis-Code 5 – homed alle Achsen XYZ (Issue #4) -- Neuer Button „Home XY" (axis=4) in der UI -- Neuer Button „Motors Off" (axis turnOff) in der UI +- 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-Neustart: `os.execv` funktioniert jetzt korrekt in der PyInstaller-Binary (kein doppelter Pfad als Argument mehr) -- Kamera: `ffmpeg nicht gefunden` crasht nicht mehr – saubere 503-Antwort wenn ffmpeg nicht installiert ist -- Reconnect-Loop: Kurzeitige leere TCP-Reads unter Windows führen nicht mehr sofort zu Reconnects +- 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 -### 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 +### 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 -- Fehlermeldung bei falschen MQTT-Zugangsdaten ist jetzt verständlich: `Falsche MQTT-Zugangsdaten (falscher Benutzername, Passwort oder Device-ID)` statt kryptischem `CONNACK failed: 20020005` +- Wrong MQTT credentials now shows a human-readable error instead of cryptic `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` wird als Volume in den Container gemountet – Einstellungen bleiben nach `docker-compose restart` erhalten +- 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: hint shown when `:` is detected in the IP field +- `docker-compose.yml`: `.env` mounted as volume into the container — settings persist after `docker-compose restart` --- ## [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 +### New +- `start.sh` — starts the bridge via Docker, builds the image automatically on first run +- Tests: pytest suite (19 tests) for API state, Moonraker endpoints, settings; install smoke test (`test_install.sh`) +- Settings modal opens automatically on first start when no credentials are configured -### Geändert -- README (DE + EN): Schnellstart zeigt jetzt `./start.sh` statt manuellem `docker build` -- README: LAN-Modus korrekt als Drucker-Menüoption beschrieben (kein WLAN-Bezug) -- README: Versionsnummer wird ab jetzt automatisch bei jedem Release aktualisiert -- `extract_credentials`: kein `--write-env` mehr empfohlen – Werte im ⚙-Menü eintragen -- Dockerfile im Release-Repo: Pfade ohne `05_scripts/`-Präfix (direkt aus Repo-Root) -- `release.sh`: Dockerfile für Release-Repo automatisch per `sed` angepasst +### Changed +- README: Quick start now shows `./start.sh` instead of manual `docker build` +- README: LAN mode correctly described as a printer menu option +- README: Version number now updated automatically on each release +- `extract_credentials`: `--write-env` no longer recommended — enter values in the ⚙ menu +- Dockerfile in release repo: paths without `05_scripts/` prefix +- `release.sh`: Dockerfile for release repo automatically patched via `sed` ### Fixes -- Restdruckzeit (`remain_time`) wird jetzt korrekt aus `print/report` übernommen und in der UI angezeigt -- Übersetzung: „Schrittweite" und „Ziel"-Placeholder in Temperatureingaben werden jetzt korrekt übersetzt +- Remaining print time (`remain_time`) now correctly taken from `print/report` and shown in UI +- Translation: "Step size" and "Target" placeholders in temperature inputs now correctly translated --- ## [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 +### New +- OrcaSlicer profile (`kobra_x_orcaslicer_preset.zip`) as release asset +- `release.sh`: OrcaSlicer profile automatically copied to release repo and uploaded -### Geändert -- README: `extract_credentials` ohne `--write-env`, Werte manuell ins ⚙-Menü eintragen -- README: Docker-Schnellstart vereinfacht (kein `.env` anlegen vor dem Start nötig) +### Changed +- README: `extract_credentials` without `--write-env`, values entered manually in the ⚙ menu +- README: Docker quick start simplified --- ## [0.9.1-beta8] – 2026-04-25 -### Neu -- Restdruckzeit-Anzeige in der UI (≈ Xh Ym verbleibend) aus `remain_time`-Feld des Druckers -- Settings-Modal: Verbindungseinstellungen und Self-Update direkt im Browser -- Self-Update: Bridge prüft Gitea-Release-API auf neue Versionen und aktualisiert sich selbst +### New +- Remaining print time display in UI (≈ Xh Ym remaining) from `remain_time` field +- Settings modal: connection settings and self-update directly in the browser +- Self-update: bridge checks Gitea release API for new versions and updates itself -### Geändert -- Bridge startet im Offline-Modus wenn Drucker nicht erreichbar (kein Absturz) -- Verbinden/Trennen-Button im Header +### Changed +- Bridge starts in offline mode when printer is unreachable (no crash) +- Connect/Disconnect button in 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 +### New +- Offline start: bridge runs without MQTT connection, reconnects automatically when printer is reachable +- Connect/Disconnect button in header --- ## [0.9.1-beta6] – 2026-04-20 -### Neu -- Release-ZIPs: `kx-bridge-linux.zip`, `kx-bridge-windows.zip`, `anycubic-certs.zip` mit Zertifikaten +### New +- Release ZIPs: `kx-bridge-linux.zip`, `kx-bridge-windows.zip`, `anycubic-certs.zip` with certificates ### 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 -### Neu -- `kx-bridge.exe` (Windows) wird automatisch via GitHub Actions gebaut +### New +- `kx-bridge.exe` (Windows) built automatically via GitHub Actions --- ## [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`) +### New +- `release.sh`: builds Linux binary and Windows EXE, uploads all assets to Gitea +- English README (`README.en.md`) ### 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 -### Neu -- Print-Speed-Card (Leise / Normal / Sport) -- Übersetzungen (DE/EN) vervollständigt +### New +- Print speed card (Silent / Normal / Sport) +- Translations (DE/EN) completed --- ## [0.9.1-beta2] – 2026-04-17 ### Fixes -- Temperatursteuerung während eines laufenden Drucks +- Temperature control during an active print --- ## [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 +### New +- Complete UI overhaul: Settings modal, self-update, dashboard, responsive design +- New printer states: `pausing`, `paused`, `resuming`, `resumed`, `stopping` +- `release.sh`: version bump and release sync script --- ## [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 +### New +- First public release +- Docker deployment, Linux binary, `extract_credentials` tool +- Moonraker-compatible HTTP/WebSocket bridge for the Anycubic Kobra X +- AMS load/unload, light and fan control +- Web-UI with dashboard, temperature cards, motion control diff --git a/Dockerfile b/Dockerfile index 413dc64..7a505f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,14 +2,20 @@ FROM python:3.11-slim WORKDIR /app -COPY requirements.txt . +COPY bridge/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -COPY kobrax_moonraker_bridge.py . -COPY env_loader.py . -COPY kobrax_client.py . -COPY anycubic_slicer.crt . -COPY anycubic_slicer.key . +COPY bridge/kobrax_moonraker_bridge.py . +COPY bridge/config_loader.py . +COPY bridge/env_loader.py . +COPY bridge/kobrax_client.py . +COPY bridge/anycubic_slicer.crt . +COPY bridge/anycubic_slicer.key . +COPY bridge/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 diff --git a/README.de.md b/README.de.md new file mode 100644 index 0000000..323d742 --- /dev/null +++ b/README.de.md @@ -0,0 +1,137 @@ +

KX-Bridge Logo

+ +# KX-Bridge – Anycubic Kobra X + +**Version:** 0.9.2 + +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://: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. diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 13b9a5d..0000000 --- a/README.en.md +++ /dev/null @@ -1,258 +0,0 @@ -

KX-Bridge Logo

- -# KX-Bridge – Anycubic Kobra X Klipper Bridge - -**Version:** 0.9.1-beta15 -**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 is accessible on your local network via `http://:7125` — do not expose to the internet -- `.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. diff --git a/README.md b/README.md index 4bcf9de..37ac04c 100644 --- a/README.md +++ b/README.md @@ -1,258 +1,137 @@

KX-Bridge Logo

-# KX-Bridge – Anycubic Kobra X Klipper Bridge +# KX-Bridge – Anycubic Kobra X -**Version:** 0.9.1-beta15 -**Status:** Public Beta – für Heimanwender geeignet, Feedback willkommen +**Version:** 0.9.2 -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. +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) -- 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) +### Step 1 – Prepare the printer ---- +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) -- MQTT-Credentials des Druckers (→ siehe [Credentials extrahieren](#credentials-extrahieren)) -- Docker **oder** Python 3.9+ **oder** direkt die Linux-Binary +The MQTT credentials are printer-specific. Here's how to get them: ---- +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 -# 1. Bridge starten ./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. -``` -# 2. Web-UI öffnen: http://BRIDGE-IP:7125 -# → Einstellungen (⚙) öffnen sich automatisch beim ersten Start -# → Zugangsdaten eintragen (→ siehe Credentials extrahieren) +**Open Web-UI:** `http://BRIDGE-IP:7125` +→ The ⚙ menu opens automatically on first start +→ Enter credentials from Step 2 → **Save & Restart** -# 3. In OrcaSlicer: Drucker → "Moonraker" → http://BRIDGE-IP:7125 -``` +**Connect OrcaSlicer:** +Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125` -Logs prüfen: -```bash -docker-compose logs -f -``` - -Stoppen: -```bash -docker-compose down -``` +> **Important:** Connection type must be **Moonraker** (not "Bambu" or "Klipper"). +> Enter the full URL including `http://` and port `:7125` in the host field. --- -## 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 chmod +x kx-bridge ./kx-bridge ``` -Web-UI öffnen: `http://localhost:7125` -→ Einstellungen (⚙) öffnen sich automatisch und führen durch die Erstkonfiguration. - ---- - -## Schnellstart – Python direkt - +**Python directly:** ```bash pip install aiohttp -python kobrax_moonraker_bridge.py +python bridge/kobrax_moonraker_bridge.py ``` -Web-UI öffnen: `http://localhost:7125` -→ Einstellungen (⚙) öffnen sich automatisch beim ersten Start. +Web-UI available at `http://localhost:7125` — the ⚙ menu guides through initial setup. --- -## Credentials extrahieren - -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 +## Useful commands ```bash -chmod +x extract_credentials -./extract_credentials -``` +# Show logs +docker-compose logs -f -### Ausgabe +# Stop bridge +docker-compose down -``` -[*] Prozess gefunden: AnycubicSlicerNext.exe (PID 1234) -[*] 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 +# Restart bridge (after update) +./start.sh ``` --- -## OrcaSlicer verbinden +## Troubleshooting -1. Drucker hinzufügen → **Anycubic Kobra X** (oder generischer Klipper-Drucker) -2. Verbindungstyp: **Moonraker** -3. IP: `http://BRIDGE-HOST:7125` -4. Verbindung testen → sollte "Online" anzeigen +**"Wrong MQTT credentials"** on start: +- Restart AnycubicSlicerNext, reconnect the printer, run `extract_credentials` again +- Enter only the IP address, no port (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`) ---- - -## Web-UI - -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. +**Printer not found / no LAN mode:** +- On the printer display: Settings → Enable LAN mode +- Printer and bridge must be on the same network **Docker: Permission denied:** ```bash -sudo usermod -aG docker $USER -# Neu einloggen -``` - -**Docker: .env nicht gefunden:** -```bash -# .env muss im gleichen Verzeichnis wie docker-compose.yml liegen -cp .env.example .env && nano .env +sudo usermod -aG docker $USER # then log out and back in ``` --- -## Logs +## Security -```bash -# Docker -docker compose logs -f kx-bridge - -# Binary / Python -tail -f /tmp/bridge.log # bei Nutzung von bridge.sh -``` +- The bridge is accessible on the local network at `http://:7125` — do not expose to the internet +- `config/config.ini` contains printer credentials — do not share publicly +- Credentials do not grant access to Anycubic cloud services --- -## Sicherheitshinweise +## License -- Die Bridge ist im lokalen Netzwerk erreichbar unter `http://:7125` — nicht ins Internet freigeben -- `.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. +Interoperability research under §69e UrhG — private, non-commercial use only. diff --git a/VERSION b/VERSION index 0f59c45..2003b63 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.1-beta15 +0.9.2 diff --git a/config_loader.py b/config_loader.py new file mode 100644 index 0000000..c59080c --- /dev/null +++ b/config_loader.py @@ -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")) diff --git a/docker-compose.yml b/docker-compose.yml index 75bcda6..3a0d0cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,9 @@ services: kx-bridge: image: kx-bridge:latest build: . - env_file: .env volumes: - - ./.env:/app/.env + - ./config:/app/config + - ./.env:/app/.env:ro ports: - "7125:7125" restart: unless-stopped diff --git a/env_loader.py b/env_loader.py index 2235cf4..342df7b 100644 --- a/env_loader.py +++ b/env_loader.py @@ -40,9 +40,11 @@ def get(key: str, default: str = "") -> str: # 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", "") +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")) diff --git a/kobrax_client.py b/kobrax_client.py index 2aea55a..a6689ac 100644 --- a/kobrax_client.py +++ b/kobrax_client.py @@ -16,7 +16,9 @@ Verwendung: client.disconnect() """ +import hashlib import json +import logging import os import socket import ssl @@ -28,6 +30,8 @@ from datetime import datetime 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__)) CERT_FILE = os.path.join(_SCRIPT_DIR, "anycubic_slicer.crt") KEY_FILE = os.path.join(_SCRIPT_DIR, "anycubic_slicer.key") @@ -119,6 +123,13 @@ class KobraXClient: # Optional callbacks: topic_suffix → callable(payload_dict) 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 -------------------------------------------------------------- def _pub_topic(self, msg_type: str) -> str: @@ -144,18 +155,19 @@ class KobraXClient: raw = socket.create_connection((self.host, self.port), timeout=5) 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.settimeout(3) r = self._sock.recv(64) if len(r) < 4 or r[0] != 0x20 or r[3] != 0: raise RuntimeError(f"CONNACK failed: {r.hex()}") - print(f"[kobrax] CONNACK rc=0") + log.info("CONNACK rc=0") self._sock.settimeout(0.2) self._buf = b"" self._subscribe(self._sub_topic()) + log.debug("MQTT connected to %s:%s", self.host, self.port) def connect(self): self._do_connect() @@ -172,7 +184,7 @@ class KobraXClient: pass def _reconnect(self): - print("[kobrax] Verbindung verloren – reconnect…") + log.warning("Verbindung verloren – reconnect…") try: self._sock.close() except Exception: @@ -180,10 +192,10 @@ class KobraXClient: for delay in [2, 4, 8, 15, 30]: try: self._do_connect() - print("[kobrax] Reconnect erfolgreich") + log.info("Reconnect erfolgreich") return True 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) return False @@ -192,7 +204,7 @@ class KobraXClient: pid = self._pid self._pid += 1 self._sock.sendall(_build_subscribe(topic, pid)) - print(f"[kobrax] SUB {topic}") + log.info("SUB %s", topic) # -- Read loop ----------------------------------------------------------- @@ -227,7 +239,7 @@ class KobraXClient: continue except Exception as e: if self._running: - print(f"[kobrax] reader error: {e}") + log.warning("reader error: %s", e) if not self._reconnect(): break last_ping = time.time() @@ -266,9 +278,40 @@ class KobraXClient: 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): - # Resolve by report topic suffix (e.g. "info/report") 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)[:120]) + + # Resolve by report topic suffix (e.g. "info/report") if suffix in self._pending_report: entry = self._pending_report[suffix] entry["result"] = payload @@ -282,19 +325,18 @@ class KobraXClient: entry["event"].set() # User callbacks by topic suffix (last two path components) - suffix = "/".join(topic.split("/")[-2:]) if suffix in self.callbacks: try: self.callbacks[suffix](payload) except Exception as e: - print(f"[kobrax] callback error for {suffix}: {e}") + log.error("callback error for %s: %s", suffix, e) # Generic wildcard callback if "*" in self.callbacks: try: self.callbacks["*"](topic, payload) except Exception as e: - print(f"[kobrax] wildcard callback error: {e}") + log.error("wildcard callback error: %s", e) # -- Publish + request/response ------------------------------------------ @@ -322,11 +364,14 @@ class KobraXClient: report_registered = True 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)[:120] if data else "null") try: with self._lock: self._sock.sendall(_build_publish(topic, payload)) except Exception as e: - print(f"[kobrax] send error: {e}, reconnecting…") + log.error("send error: %s, reconnecting…", e) self._pending_msgid.pop(msgid, None) if report_registered: self._pending_report.pop(report_key, None) @@ -367,11 +412,14 @@ class KobraXClient: "data": data, }, separators=(",", ":")) 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)[:120] if data else "null") try: with self._lock: self._sock.sendall(_build_publish(topic, payload)) except Exception as e: - print(f"[kobrax] web send error: {e}") + log.error("web send error: %s", e) # -- High-level commands ------------------------------------------------- diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index f2bad13..05c5931 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -11,7 +11,10 @@ OrcaSlicer-Konfiguration: """ import argparse -import env_loader +try: + import config_loader as env_loader +except ImportError: + import env_loader import asyncio import hashlib import json @@ -49,10 +52,37 @@ except ImportError: print("Fehler: aiohttp nicht installiert. Bitte: pip install aiohttp") sys.exit(1) -logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s %(message)s", +logging.basicConfig(level=logging.INFO, + format="[%(asctime)s] %(levelname)-5s %(name)s: %(message)s", datefmt="%H:%M:%S") log = logging.getLogger("bridge") +# Ring-Buffer für Browser-Log-Stream (letzte 200 Einträge) +import collections as _collections +_log_buffer: "_collections.deque[dict]" = _collections.deque(maxlen=500) +_log_sse_queues: "list[asyncio.Queue]" = [] + +class _BrowserLogHandler(logging.Handler): + """Sendet Log-Records in den Ring-Buffer und alle offenen SSE-Queues.""" + _fmt = logging.Formatter(datefmt="%H:%M:%S") + + def emit(self, record: logging.LogRecord): + entry = { + "ts": self._fmt.formatTime(record, "%H:%M:%S"), + "lvl": record.levelname, + "name": record.name, + "msg": record.getMessage(), + } + _log_buffer.append(entry) + for q in list(_log_sse_queues): + try: + q.put_nowait(entry) + except Exception: + pass + +_browser_handler = _BrowserLogHandler() +logging.getLogger().addHandler(_browser_handler) + KOBRA_TO_KLIPPER_STATE = { "free": "standby", "busy": "printing", @@ -77,6 +107,23 @@ MOONRAKER_VERSION = "v0.9.3-1" KLIPPER_VERSION = "v0.12.0-1" +def _parse_gcode_estimated_time(data: bytes) -> int: + """Liest '; estimated printing time (normal mode) = Xh Ym Zs' aus GCode-Header. + Gibt Sekunden zurück, 0 wenn nicht gefunden. Sucht nur in den ersten 8KB.""" + import re + header = data[:8192].decode("utf-8", errors="ignore") + m = re.search(r";\s*estimated printing time \(normal mode\)\s*=\s*(.*)", header) + if not m: + return 0 + parts = re.findall(r"(\d+)\s*([hms])", m.group(1)) + secs = 0 + for val, unit in parts: + if unit == "h": secs += int(val) * 3600 + elif unit == "m": secs += int(val) * 60 + elif unit == "s": secs += int(val) + return secs + + class KobraXBridge: def __init__(self, client: KobraXClient, args=None): self.client = client @@ -91,6 +138,7 @@ class KobraXBridge: "print_state": "standby", "kobra_state": "free", "filename": "", + "slicer_time": 0, "progress": 0.0, "print_duration": 0, "remain_time": 0, @@ -105,6 +153,7 @@ class KobraXBridge: "light_brightness": 80, "taskid": "-1", "print_speed_mode": 2, + "connection_error": "", } self._ams_slots: list[dict] = [] self._ams_loaded_slot: int = -1 @@ -112,7 +161,7 @@ class KobraXBridge: self._serve_dir = tempfile.TemporaryDirectory(prefix="kobrax_serve_") self._serve_dir_path: str = self._serve_dir.name - self._thumbnail_b64: str = "" # base64-PNG aus file/report + self._thumbnail_b64: str = "" # Register MQTT push callbacks client.callbacks["tempature/report"] = self._on_temp @@ -226,6 +275,74 @@ class KobraXBridge: log.info(f"AMS-Slots empfangen: {len(slots)}, loaded_slot={self._ams_loaded_slot}") self._push_status_update() + # OrcaSlicer filament preset IDs (MoonrakerPrinterAgent.cpp mapping) + _TRAY_INFO_IDX = { + "PLA": "OGFL99", "PLA-CF": "OGFL98", "PLA SILK": "OGFL96", + "PETG": "OGFG99", "PETG-CF": "OGFG98", + "ABS": "OGFB99", "ASA": "OGFB98", + "TPU": "OGFT99", "PA": "OGFP99", "PA-CF": "OGFP98", + "PC": "OGFC99", "HIPS": "OGFH99", "PVA": "OGFV99", + } + + def _build_lane_data(self) -> dict: + """Baut BBL-AMS-JSON für OrcaSlicer DevFilaSystemParser::ParseV1_0.""" + slots = self._ams_slots + total = len(slots) + if total == 0: + return {"ams": [], "ams_exist_bits": "0", "tray_exist_bits": "0"} + + ams_count = (total + 3) // 4 + ams_exist_bits = 0 + tray_exist_bits = 0 + ams_array = [] + + for ams_id in range(ams_count): + ams_exist_bits |= (1 << ams_id) + tray_array = [] + max_slot = min(3, total - ams_id * 4 - 1) + for slot_id in range(max_slot + 1): + slot_index = ams_id * 4 + slot_id + slot = slots[slot_index] if slot_index < total else {} + occupied = slot.get("status") == 5 + + if occupied: + tray_exist_bits |= (1 << slot_index) + color_raw = slot.get("color", [255, 255, 255]) + if isinstance(color_raw, list) and len(color_raw) >= 3: + color_hex = "{:02X}{:02X}{:02X}FF".format( + int(color_raw[0]), int(color_raw[1]), int(color_raw[2]) + ) + elif isinstance(color_raw, str) and len(color_raw) >= 6: + color_hex = color_raw[:6].upper() + "FF" + else: + color_hex = "FFFFFFFF" + material = slot.get("type", "PLA").upper() + tray_info_idx = self._TRAY_INFO_IDX.get(material, "OGFL99") + tray_array.append({ + "id": str(slot_id), + "tag_uid": "0000000000000000", + "tray_info_idx": tray_info_idx, + "tray_type": material, + "tray_color": color_hex, + }) + else: + tray_array.append({ + "id": str(slot_id), + "tag_uid": "0000000000000000", + "tray_info_idx": "", + "tray_type": "", + "tray_color": "00000000", + "tray_slot_placeholder": "1", + }) + + ams_array.append({"id": str(ams_id), "info": "0002", "tray": tray_array}) + + return { + "ams": ams_array, + "ams_exist_bits": format(ams_exist_bits, "X"), + "tray_exist_bits": format(tray_exist_bits, "X"), + } + # ------------------------------------------------------------------------- # WebSocket push # ------------------------------------------------------------------------- @@ -408,6 +525,9 @@ class KobraXBridge: file_md5 = hashlib.md5(file_data).hexdigest() file_size = len(file_data) + # Slicer-Zeitschätzung aus GCode-Header auslesen + self._state["slicer_time"] = _parse_gcode_estimated_time(file_data) + # Datei auf Disk ablegen (temp-Verzeichnis) damit Drucker sie per HTTP abrufen kann safe_name = os.path.basename(remote_filename) # keine Pfad-Traversal serve_path = os.path.join(self._serve_dir_path, safe_name) @@ -458,7 +578,19 @@ class KobraXBridge: }, status=201) def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0): - loaded = [(i, s) for i, s in enumerate(self._ams_slots) if s.get("status") == 5] + default_slot = getattr(self._args, "default_ams_slot", "auto") + all_loaded = [(i, s) for i, s in enumerate(self._ams_slots) if s.get("status") == 5] + if default_slot != "auto": + try: + slot_idx = int(default_slot) + loaded = [(i, s) for i, s in all_loaded if i == slot_idx] + if not loaded: + log.warning(f"Standard-Slot {slot_idx} ist leer – fallback auf Auto") + loaded = all_loaded + except ValueError: + loaded = all_loaded + else: + loaded = all_loaded use_ams = len(loaded) > 0 ams_box_mapping = [ { @@ -471,6 +603,7 @@ class KobraXBridge: for i, s in loaded ] log.info(f"AMS-Slots: {len(loaded)}/{len(self._ams_slots)} belegt → {[i for i,_ in loaded]}") + auto_leveling = getattr(self._args, "auto_leveling", 1) payload = { "taskid": "-1", "url": url, @@ -485,7 +618,7 @@ class KobraXBridge: "ams_box_mapping": ams_box_mapping, }, "task_settings": { - "auto_leveling": 1, + "auto_leveling": auto_leveling, "vibration_compensation": 0, "flow_calibration": 0, "dry_mode": 0, @@ -519,16 +652,34 @@ class KobraXBridge: log.info(f"Druck starten: {filename}") # AMS-Mapping aus gecachtem State — leere Slots (status != 5) überspringen + default_slot = getattr(self._args, "default_ams_slot", "auto") ams_box_mapping = [] for i, slot in enumerate(self._ams_slots): if slot.get("status") != 5: log.info(f"AMS-Slot {i} leer (status={slot.get('status')}) – übersprungen") continue + if default_slot != "auto": + try: + if i != int(default_slot): + continue + except ValueError: + pass ams_box_mapping.append({ "slot_index": i, "material_type": slot.get("type", "PLA"), "color": slot.get("color", [255, 255, 255]), }) + # Fallback auf alle belegten Slots wenn gewählter Slot leer war + if default_slot != "auto" and not ams_box_mapping: + log.warning(f"Standard-Slot {default_slot} leer – fallback auf alle belegten Slots") + for i, slot in enumerate(self._ams_slots): + if slot.get("status") != 5: + continue + ams_box_mapping.append({ + "slot_index": i, + "material_type": slot.get("type", "PLA"), + "color": slot.get("color", [255, 255, 255]), + }) use_ams = len(ams_box_mapping) > 0 @@ -758,7 +909,7 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background: /* ── CONSOLE ── */ .console{background:#0a0a0e;border-radius:8px;padding:10px;font-family:var(--mono); - font-size:11px;color:#8888aa;height:160px;overflow-y:auto;line-height:1.6} + font-size:11px;color:#8888aa;overflow-y:auto;line-height:1.6} .console .ts{color:#444;margin-right:6px} .console .msg-info{color:#8888aa} .console .msg-ok{color:var(--ok)} @@ -858,6 +1009,8 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0; + +
Anycubic Kobra X
@@ -905,6 +1058,24 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0; +
+ + + +
+
@@ -924,6 +1095,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0; +
@@ -934,8 +1106,8 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
@@ -977,6 +1149,9 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
+
+ +
@@ -1128,8 +1303,21 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
-
Ereignis-Log
-
+
+ Ereignis-Log + ⬇ Download +
+
+ + + +
+
@@ -1137,7 +1325,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;