Compare commits

...

9 Commits

Author SHA1 Message Date
3df73e89e3 release: v0.9.1-beta11 2026-04-25 22:16:35 +02:00
4026fcc60c release: v0.9.1-beta10 2026-04-25 17:09:03 +02:00
3687c28239 fix: Dockerfile Pfade für Release-Repo (kein 05_scripts/-Präfix) 2026-04-25 16:58:11 +02:00
68b282d170 docs: Version auf 0.9.1-beta10 korrigieren 2026-04-25 14:19:38 +02:00
f265a30994 docs: LAN-Modus korrekt beschreiben 2026-04-25 14:08:16 +02:00
3d2ac7b931 release: v0.9.1-beta10 2026-04-25 14:06:54 +02:00
98303f1197 release: v0.9.1-beta9 2026-04-25 14:02:51 +02:00
808ac13ce0 release: v0.9.1-beta8 2026-04-25 13:55:45 +02:00
14400dd799 release: v0.9.1-beta7 2026-04-22 00:27:37 +02:00
12 changed files with 554 additions and 174 deletions

125
CHANGELOG.md Normal file
View File

@@ -0,0 +1,125 @@
# Changelog
## [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
---
## [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 (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
### 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
---
## [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 ins ⚙-Menü eintragen
- README: Docker-Schnellstart vereinfacht (kein `.env` anlegen vor dem Start nötig)
---
## [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
### 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
- Print-Speed-Card (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

@@ -2,12 +2,15 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
COPY 05_scripts/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 05_scripts/kobrax_moonraker_bridge.py . COPY kobrax_moonraker_bridge.py .
COPY 05_scripts/env_loader.py . COPY env_loader.py .
COPY 05_scripts/kobrax_client.py . COPY kobrax_client.py .
COPY anycubic_slicer.crt .
COPY anycubic_slicer.key .
EXPOSE 7125 EXPOSE 7125

View File

@@ -1,6 +1,6 @@
# KX-Bridge Anycubic Kobra X Moonraker Bridge # KX-Bridge Anycubic Kobra X Moonraker Bridge
**Version:** 0.9.1-beta5 **Version:** 0.9.1-beta10
**Status:** Public Beta suitable for home users, feedback welcome **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. 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.
@@ -24,7 +24,7 @@ KX-Bridge is a Moonraker-compatible HTTP/WebSocket bridge for the **Anycubic Kob
## Requirements ## Requirements
- Anycubic Kobra X on your local network (LAN, no Wi-Fi client isolation) - Anycubic Kobra X on your local network, with **LAN mode enabled** (printer menu → enable LAN mode)
- Printer MQTT credentials (→ see [Extracting credentials](#extracting-credentials)) - Printer MQTT credentials (→ see [Extracting credentials](#extracting-credentials))
- Docker **or** Python 3.9+ **or** the pre-built Linux binary - Docker **or** Python 3.9+ **or** the pre-built Linux binary
@@ -33,24 +33,28 @@ KX-Bridge is a Moonraker-compatible HTTP/WebSocket bridge for the **Anycubic Kob
## Quick start Docker (recommended) ## Quick start Docker (recommended)
```bash ```bash
# 1. Create .env # 1. Start the bridge
cp .env.example .env ./start.sh
# Fill in your printer data (→ extract_credentials) ```
# 2. Start the bridge `start.sh` builds the Docker image automatically on first run and starts the bridge.
docker compose up -d
```
# 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 # 3. In OrcaSlicer: add printer → "Moonraker" → http://BRIDGE-IP:7125
``` ```
Check logs: Check logs:
```bash ```bash
docker compose logs -f docker-compose logs -f
``` ```
Update: Stop:
```bash ```bash
docker compose pull && docker compose up -d docker-compose down
``` ```
--- ---
@@ -59,11 +63,11 @@ docker compose pull && docker compose up -d
```bash ```bash
chmod +x kx-bridge chmod +x kx-bridge
./kx-bridge --printer-ip 192.168.x.x --username userXXXX --password XXXXX \ ./kx-bridge
--device-id XXXXX --mode-id 20030
``` ```
Or place a `.env` file in the same directory — the bridge reads it automatically. Open the web UI: `http://localhost:7125`
→ Settings (⚙) open automatically and guide you through the initial setup.
--- ---
@@ -71,32 +75,31 @@ Or place a `.env` file in the same directory — the bridge reads it automatical
```bash ```bash
pip install aiohttp pip install aiohttp
python kobrax_moonraker_bridge.py --printer-ip 192.168.x.x ...
# Or fill in .env and start without arguments
python kobrax_moonraker_bridge.py python kobrax_moonraker_bridge.py
``` ```
Open the web UI: `http://localhost:7125`
→ Settings (⚙) open automatically on first start.
--- ---
## Extracting credentials ## 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. 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. **Requirement:** AnycubicSlicerNext must be running and connected to the printer (printer status is shown).
### Windows ### Windows
``` ```
extract_credentials.exe --write-env extract_credentials.exe
``` ```
Writes the found credentials directly to `.env`.
### Linux ### Linux
```bash ```bash
chmod +x extract_credentials chmod +x extract_credentials
./extract_credentials --write-env ./extract_credentials
``` ```
### Output ### Output
@@ -114,10 +117,13 @@ chmod +x extract_credentials
Device-ID xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (hits: 3504) Device-ID xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (hits: 3504)
Printer IP 192.168.x.x (hits: 3036) Printer IP 192.168.x.x (hits: 3036)
======================================================= =======================================================
Hint: pass --write-env to save credentials to '.env'.
``` ```
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. All credentials are **processed locally only** — nothing is sent to external servers.
--- ---

269
README.md
View File

@@ -1,25 +1,9 @@
# KX-Bridge # KX-Bridge Anycubic Kobra X Moonraker Bridge
Verbindet den Anycubic Kobra X mit OrcaSlicer ohne Klipper, ohne Raspberry Pi. **Version:** 0.9.1-beta10
**Status:** Public Beta für Heimanwender geeignet, Feedback willkommen
KX-Bridge läuft auf deinem PC oder NAS und stellt eine Moonraker-kompatible Schnittstelle bereit, über die OrcaSlicer den Drucker direkt steuern kann: Druckstart, Temperatur, Fortschritt, Pause/Fortsetzen/Abbrechen, AMS-Farbwechsel, Druckgeschwindigkeit und mehr. 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.1-beta5
---
## Enthaltene Dateien
| Datei | Beschreibung |
|-------|-------------|
| `kobrax_moonraker_bridge.py` | Bridge-Hauptprogramm |
| `kx-bridge` | Vorkompilierte Linux-Binary |
| `extract_credentials.exe` | Zugangsdaten aus AnycubicSlicerNext auslesen (Windows) |
| `extract_credentials` | Zugangsdaten aus AnycubicSlicerNext auslesen (Linux) |
| `kobra_x_orcaslicer_preset.zip` | OrcaSlicer-Druckerprofil für den Kobra X |
| `bridge.sh` | Service-Manager für Linux |
| `Dockerfile` / `docker-compose.yml` | Docker-Deployment |
| `.env.example` | Konfigurationsvorlage |
--- ---
@@ -40,93 +24,135 @@ KX-Bridge läuft auf deinem PC oder NAS und stellt eine Moonraker-kompatible Sch
## Voraussetzungen ## Voraussetzungen
- Anycubic Kobra X im LAN-Modus (Drucker muss über LAN erreichbar sein, nicht nur über Anycubic-Cloud) - Anycubic Kobra X im lokalen Netzwerk, mit aktiviertem **LAN-Modus** (Drucker-Menü → LAN-Modus einschalten)
- PC, NAS oder Server im gleichen Netzwerk (Windows oder Linux) - MQTT-Credentials des Druckers (→ siehe [Credentials extrahieren](#credentials-extrahieren))
- Docker oder Python 3.9+ - Docker **oder** Python 3.9+ **oder** direkt die Linux-Binary
- MQTT-Zugangsdaten des Druckers → [Schritt 1](#schritt-1-zugangsdaten-ermitteln)
--- ---
## Schnellstart ## Schnellstart Docker (empfohlen)
### Schritt 1: Zugangsdaten ermitteln
Die Bridge benötigt druckerspezifische MQTT-Zugangsdaten.
> **Wichtig:** Der Drucker muss sich im LAN-Modus befinden. Nur wenn der Drucker direkt über LAN (nicht ausschließlich über die Anycubic-Cloud) erreichbar ist, können die Zugangsdaten ermittelt und die Bridge genutzt werden.
AnycubicSlicerNext starten und mit dem Drucker verbinden (bis der Drucker-Status angezeigt wird), dann:
**Windows:**
```
extract_credentials.exe --write-env
```
**Linux:**
```bash
chmod +x extract_credentials
./extract_credentials --write-env
```
Die Zugangsdaten werden automatisch in `.env` gespeichert.
> Falls das Ergebnis unsicher wirkt: `--verbose` zeigt alle gefundenen Kandidaten. Den richtigen Wert manuell in `.env` eintragen.
---
### Schritt 2: Konfiguration prüfen
```bash ```bash
cp .env.example .env # 1. Bridge starten
# .env öffnen und Werte kontrollieren ./start.sh
```
`start.sh` baut das Docker-Image automatisch beim ersten Aufruf und startet die Bridge.
```
# 2. Web-UI öffnen: http://BRIDGE-IP:7125
# → Einstellungen (⚙) öffnen sich automatisch beim ersten Start
# → Zugangsdaten eintragen (→ siehe Credentials extrahieren)
# 3. In OrcaSlicer: Drucker → "Moonraker" → http://BRIDGE-IP:7125
```
Logs prüfen:
```bash
docker-compose logs -f
```
Stoppen:
```bash
docker-compose down
``` ```
--- ---
### Schritt 3: Bridge starten ## Schnellstart Binary (Linux)
**Option A Docker (empfohlen):**
```bash
docker compose up -d
```
Läuft im Hintergrund, startet automatisch nach Systemneustart.
**Option B Linux Binary:**
```bash ```bash
chmod +x kx-bridge chmod +x kx-bridge
./kx-bridge ./kx-bridge
# Oder mit Service-Manager:
./bridge.sh start
``` ```
**Option C Python direkt:** Web-UI öffnen: `http://localhost:7125`
→ 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 kobrax_moonraker_bridge.py
``` ```
--- Web-UI öffnen: `http://localhost:7125`
→ Einstellungen (⚙) öffnen sich automatisch beim ersten Start.
### Schritt 4: OrcaSlicer-Profil installieren
1. `kobra_x_orcaslicer_preset.zip` in OrcaSlicer importieren:
Datei → Konfigurationen importieren → ZIP auswählen
2. Anycubic Kobra X als Drucker auswählen
--- ---
### Schritt 5: OrcaSlicer verbinden ## Credentials extrahieren
1. Drucker-Einstellungen öffnen 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
chmod +x extract_credentials
./extract_credentials
```
### Ausgabe
```
[*] 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
```
---
## OrcaSlicer verbinden
1. Drucker hinzufügen → **Anycubic Kobra X** (oder generischer Klipper-Drucker)
2. Verbindungstyp: **Moonraker** 2. Verbindungstyp: **Moonraker**
3. Adresse: `http://IP-DES-BRIDGE-PC:7125` eintragen 3. IP: `http://BRIDGE-HOST:7125`
4. Auf „Test" klicken bei erfolgreicher Verbindung erscheint eine Bestätigungsmeldung 4. Verbindung testen → sollte "Online" anzeigen
--- ---
## Web-UI ## Web-UI
Die Bridge stellt unter `http://BRIDGE-IP:7125` eine Web-Oberfläche bereit: Die Bridge stellt unter `http://BRIDGE-HOST:7125` eine Web-Oberfläche bereit:
| Bereich | Funktion | | Bereich | Funktion |
|---------|----------| |---------|----------|
@@ -136,55 +162,56 @@ Die Bridge stellt unter `http://BRIDGE-IP:7125` eine Web-Oberfläche bereit:
| Druckgeschwindigkeit | Leise / Normal / Sport | | Druckgeschwindigkeit | Leise / Normal / Sport |
| Lüfter / Licht | Lüfterdrehzahl und Drucklicht | | Lüfter / Licht | Lüfterdrehzahl und Drucklicht |
| AMS | Filament einziehen / ausziehen | | AMS | Filament einziehen / ausziehen |
| Kamera | Live-Vorschau (falls vom Drucker unterstützt) | | Kamera | Live-Vorschau (falls Drucker unterstützt) |
| ⚙ Einstellungen | MQTT-Zugangsdaten, Poll-Intervall, Self-Update | | ⚙ Einstellungen | MQTT-Zugangsdaten, Poll-Intervall, Self-Update |
### 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 sie automatisch neu. Ü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 Service-Manager (Linux) ## bridge.sh (Linux Service-Manager)
```bash ```bash
./bridge.sh start # Im Hintergrund starten ./bridge.sh start # Bridge im Hintergrund starten
./bridge.sh stop # Beenden ./bridge.sh stop # Bridge beenden
./bridge.sh restart # Neustarten ./bridge.sh restart # Neustarten
./bridge.sh status # Status anzeigen ./bridge.sh status # Status und Port prüfen
./bridge.sh log 50 # Letzte 50 Log-Zeilen ./bridge.sh log 50 # Letzte 50 Log-Zeilen
``` ```
--- ---
## Docker Nützliche Befehle ## Druckerzustände
```bash Die Bridge übersetzt die internen Kobra-Zustände in Moonraker-kompatible Zustände:
docker compose up -d # Starten
docker compose down # Stoppen | Kobra-Zustand | Bedeutung |
docker compose logs -f # Logs verfolgen |---------------|-----------|
docker compose pull && docker compose up -d # Update | free | Bereit |
``` | printing / busy | Druckt |
| pausing / paused | Pausiert |
| resuming / resumed | Wird fortgesetzt |
| stopping / stoped | Wird gestoppt |
| finished | Abgeschlossen |
| canceled | Abgebrochen |
| failed | Fehler |
--- ---
## Fehlerbehebung ## Fehlerbehebung
**Port 7125 belegt:** **Port 7125 bereits belegt:**
```bash ```bash
./bridge.sh stop ./bridge.sh stop # oder: fuser -k 7125/tcp
./bridge.sh start ./bridge.sh start
``` ```
**Verbindungstest in OrcaSlicer schlägt fehl:** **Credentials ungültig / Verbindung abgelehnt:**
- Firewall prüfen: Port 7125 muss erreichbar sein - AnycubicSlicerNext starten, mit Drucker verbinden, `extract_credentials` erneut ausführen
- Bridge-Log prüfen: `./bridge.sh log` oder `docker compose logs` - Falls das Ergebnis unsicher wirkt: `extract_credentials --verbose` zeigt alle Kandidaten an
- Drucker-IP in `.env` korrekt? - Den richtigen Kandidaten manuell in `.env` eintragen und Bridge neu starten
**Zugangsdaten werden abgelehnt:**
- AnycubicSlicerNext starten, mit Drucker verbinden
- `extract_credentials --verbose` ausführen und alle Kandidaten prüfen
- Richtigen Wert manuell in `.env` eintragen, Bridge neu starten
**Temperaturänderungen werden ignoriert:** **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. - Während eines laufenden Drucks werden Temperaturänderungen über einen separaten Kanal gesendet — das ist normal und wird von der Bridge automatisch erkannt.
@@ -192,21 +219,26 @@ docker compose pull && docker compose up -d # Update
**Docker: Permission denied:** **Docker: Permission denied:**
```bash ```bash
sudo usermod -aG docker $USER sudo usermod -aG docker $USER
# Neu einloggen, dann erneut versuchen # Neu einloggen
```
**Docker: .env nicht gefunden:**
```bash
# .env muss im gleichen Verzeichnis wie docker-compose.yml liegen
cp .env.example .env && nano .env
``` ```
--- ---
## Konfigurationsreferenz (.env) ## Logs
| Parameter | Beschreibung | Beispiel | ```bash
|-----------|-------------|---------| # Docker
| `PRINTER_IP` | IP-Adresse des Druckers | `192.168.1.100` | docker compose logs -f kx-bridge
| `MQTT_PORT` | MQTT-Port (nicht ändern) | `9883` |
| `MQTT_USERNAME` | Benutzername (beginnt mit „user") | `userXXXXXXXXXX` | # Binary / Python
| `MQTT_PASSWORD` | Passwort (~15 Zeichen) | `***` | tail -f /tmp/bridge.log # bei Nutzung von bridge.sh
| `DEVICE_ID` | Geräte-ID (32 Hex-Zeichen) | `xxxxxxxx...` | ```
| `MODE_ID` | Modell-ID (Kobra X Standard) | `20030` |
--- ---
@@ -214,14 +246,11 @@ sudo usermod -aG docker $USER
- Die Bridge bindet standardmäßig auf `0.0.0.0:7125` — nur im lokalen Netzwerk nutzen - Die Bridge bindet standardmäßig auf `0.0.0.0:7125` — nur im lokalen Netzwerk nutzen
- `.env` enthält Drucker-Credentials — nicht öffentlich teilen - `.env` enthält Drucker-Credentials — nicht öffentlich teilen
- Alle Zugangsdaten werden ausschließlich lokal verarbeitet — keine Übertragung an externe Server - Die Credentials sind druckerspezifisch und haben keinen Zugang zu Anycubic-Cloud-Diensten
--- ---
## Hinweis zur Nutzung ## Lizenz & Rechtliches
Dieses Projekt dient der privaten Nutzung und der Herstellung von Interoperabilität zwischen dem Anycubic Kobra X und freier Software (OrcaSlicer). Dieses Projekt entstand durch Interoperabilitätsforschung gem. §69e UrhG.
Ausschließlich für private, nicht-kommerzielle Nutzung.
`extract_credentials` liest ausschließlich den Arbeitsspeicher des auf deinem eigenen PC laufenden AnycubicSlicerNext-Prozesses. Es werden keine Daten übertragen oder gespeichert, außer in die lokale `.env`-Datei.
Das Projekt steht in keiner Verbindung zu Anycubic und wird nicht kommerziell betrieben.

View File

@@ -1 +1 @@
0.9.1-beta6 0.9.1-beta11

24
anycubic_slicer.crt Normal file
View File

@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgICAZAwDQYJKoZIhvcNAQEFBQAwgZsxCzAJBgNVBAYTAkNO
MRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56aGVuMREwDwYDVQQK
DAhBbnljdWJpYzERMA8GA1UECwwIQW55Y3ViaWMxEzARBgNVBAMMCkFDIFJvb3Qg
Q0ExKjAoBgkqhkiG9w0BCQEWG2FueWN1YmljX2Nsb3VkQGFueWN1YmljLmNvbTAg
Fw0yMzA3MjAwMzI3NTFaGA8yMTIzMDcyMTAzMjc1MVowgZ8xCzAJBgNVBAYTAkNO
MRIwEAYDVQQIDAlHdWFuZ2RvbmcxETAPBgNVBAcMCFNoZW56aGVuMREwDwYDVQQK
DAhBbnljdWJpYzERMA8GA1UECwwIQW55Y3ViaWMxFzAVBgNVBAMMDkFueWN1Ymlj
U2xpY2VyMSowKAYJKoZIhvcNAQkBFhthbnljdWJpY19jbG91ZEBhbnljdWJpYy5j
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdoQ7g2F/yecfpdlqT
b8W/84r3vQ4ZEWx2PbSTBcGD55HmzJp2lwABHFHbn4CltT9YzoJWpOiVMHYnyPep
43tkNUIcGm7z0jrTD5djyYjVAzEitkNzJspKK/xcVmZe/V7Q3IAWXtzgWCd0YpVk
K3J0HqoqJvcTSnYe4VXxbIGwbpeYyji9W/DuG1M4Z+sFiPDWeR9xo5IXRU5ZwaTP
8OiCCLSBbeKgf0UFWTIZdJ1JXJ7efbbstZOjf5L9LhBIC0hLdL4jlMpF7r0ThecJ
cTx9Bnw/hhy+i32rJTRzZDIaLhKg/bka9ZrORZdxxQRiPoMjLjoxtr4+AUaeLWkI
ajSJAgMBAAGjUzBRMB0GA1UdDgQWBBRI4P3/uKdYYFPEcFIwYxdv1p9gETAfBgNV
HSMEGDAWgBQlkDqpFERfr3u1rR9gNbNKtgrHIjAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBBQUAA4IBAQBP3ws80Y9eBR2lpjYP3rVvH8kA6+LnEXT4PpHj+fSw
jciaNskzpiwNvBy00m32ACR5YKlMUjevlQuyyw+LQbTUwAEOwyy9SDQpiXdjL6q3
SPQ4aB4A57nFXOGrthc/nb9yFcteWrZrKbwvVUu2vqU7U8n7lJKjhVuFRWSXS3SV
sPc9JZ21kpPYWKbGtfD6jUlW0Ip+PurLw9FrbVwnEcOMf/ezSlrH5c8mfJyo8pVk
aC/6PpReqijusOSRZ5oLyhPvtgddXseJFByun1Ud0CDlFA05nGGPmnVcXD+GMnHH
i6baCTeifwp5Jpdzv4imcCPvayKUNuX32vYNfNkWC/R5
-----END CERTIFICATE-----

28
anycubic_slicer.key Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdoQ7g2F/yecfp
dlqTb8W/84r3vQ4ZEWx2PbSTBcGD55HmzJp2lwABHFHbn4CltT9YzoJWpOiVMHYn
yPep43tkNUIcGm7z0jrTD5djyYjVAzEitkNzJspKK/xcVmZe/V7Q3IAWXtzgWCd0
YpVkK3J0HqoqJvcTSnYe4VXxbIGwbpeYyji9W/DuG1M4Z+sFiPDWeR9xo5IXRU5Z
waTP8OiCCLSBbeKgf0UFWTIZdJ1JXJ7efbbstZOjf5L9LhBIC0hLdL4jlMpF7r0T
hecJcTx9Bnw/hhy+i32rJTRzZDIaLhKg/bka9ZrORZdxxQRiPoMjLjoxtr4+AUae
LWkIajSJAgMBAAECggEASwRkC9lRiLqN30kvWW5g6hsec8KrTfLm2pMCVy2AlgxB
B3VD51YvKzERyBwSKITT/1RPK9K/4xe3NrpAkmGsd3vLd8W+vorvXFePr7gct7VP
4Wb+J7D+keKXlg2sswRiHqI0PN45Nzq/iBaCaJiIMiPbB0+PHBl9J/Cv7XsD3tq+
9WKhvXf2g1g9GMrLaCCcWXWCqcu0LlbqJnw3yMnJLSltmyFTmlVLjDHM75bMVz97
4emQzOlnRN2yA5cWWCaM+mgjNM2aWwUsXBZzCgwSqSaj1QD4B/epCuDBORWHS9D6
jL15w8xjly9q8OS+4d6beR5h9GiPyMK4Ff2wXImCXQKBgQDwXxtrL+kVZrQ/qftj
24F3+QDN0j5Z3lUMTfZPn6ng/E/aBfn8KcWJHj2vYkKZdB5wOXJr56BYe3Hukzfp
QF0E2+g1WAGskF1mb/vVab54geox5Y6CA+ionRn2kcCwybVkktR/0JK2UV9Qjb/z
k1WU+RUhNrW/GDBqYulaadnR+wKBgQDsCf2/yKGPxj4pIvAtn5RFSlfscddgkSnc
ouBkDXEp5ta+5PGrlrdzS/F0vFhvBPbfbVJxVwRnM/Oqj8c0/bj7oc5RpPxirciO
AaovKVPTiORaviytnB2HgkflkJfy5vdXv4ZQahAV/UwtSmLwBshe+Ya68MAFrQRa
7M4z6k4QSwKBgQCm7OVVoofzXMeADsONrTpT3pA4XvD95/CYAuwyj2ah35Z0igH4
o+mSN3YO/eXSO1mIBdz4Inqv98o/K+2ABjqSzUSNBvjipb63DL2Oj0i+1zmUPR6i
G6TOs4r8OGvgWbOmjHEV8fpwskHG5ymONZsRQYjy79N3SY0V1GrJZwjlUQKBgD0x
AeWcP7YkMK09b4KEYk3sTgrwIGPafj3Cw+VsTrAMNhPbCoPvWLO9NmWLBmoRoWae
0sarRmry3vKSv5QPSsuBURl9aiiy4NFfwRzk2+R1Eq4rqy1+0XD152muKJZCJlFL
R6jFNlJdDkiXhjqvp3ZnvfPswfs2tXBU/8gZsA8tAoGBALXfc5m9I5R1l1zN7tpa
ncA0S3EKzqmuCc3KzlS6OS0e9Lz1MsmfEsvxvW3w4SrdfTbwQpEy9RNg89dlgPtc
rdId1QdN2eWPY5M4lz9n9EYdzi9ufoKAEYu2a0lP+qz690JwmL1Jx49bvQEn5Nu0
4swn72uwBRlhjAw46MF77SBQ
-----END PRIVATE KEY-----

View File

@@ -3,6 +3,8 @@ services:
image: kx-bridge:latest image: kx-bridge:latest
build: . build: .
env_file: .env env_file: .env
volumes:
- ./.env:/app/.env
ports: ports:
- "7125:7125" - "7125:7125"
restart: unless-stopped restart: unless-stopped

View File

@@ -3,12 +3,14 @@ env_loader.py lädt Verbindungsparameter aus .env (Repo-Root oder Arbeitsver
Umgebungsvariablen haben Vorrang vor .env-Werten. Umgebungsvariablen haben Vorrang vor .env-Werten.
""" """
import os import os
import sys
import pathlib import pathlib
_BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) else pathlib.Path(__file__).parent
def _find_env_file() -> pathlib.Path | None: def _find_env_file() -> pathlib.Path | None:
# Suche .env im selben Verzeichnis, dann im Parent (Repo-Root) for base in (_BASE, _BASE.parent):
for base in (pathlib.Path(__file__).parent, pathlib.Path(__file__).parent.parent):
p = base / ".env" p = base / ".env"
if p.is_file(): if p.is_file():
return p return p

View File

@@ -20,6 +20,7 @@ import json
import os import os
import socket import socket
import ssl import ssl
import sys
import threading import threading
import time import time
import uuid import uuid
@@ -27,7 +28,7 @@ from datetime import datetime
import env_loader import env_loader
_SCRIPT_DIR = 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")

View File

@@ -24,8 +24,9 @@ import tempfile
import time import time
import threading import threading
# kobrax_client aus dem selben Verzeichnis importieren # Bei PyInstaller-Binary liegt alles neben sys.executable, sonst neben __file__
sys.path.insert(0, os.path.dirname(__file__)) _BASE = os.path.dirname(sys.executable) if getattr(sys, "frozen", False) else os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _BASE)
from kobrax_client import KobraXClient from kobrax_client import KobraXClient
try: try:
@@ -79,6 +80,7 @@ class KobraXBridge:
"filename": "", "filename": "",
"progress": 0.0, "progress": 0.0,
"print_duration": 0, "print_duration": 0,
"remain_time": 0,
"curr_layer": 0, "curr_layer": 0,
"total_layers": 0, "total_layers": 0,
"printer_name": "Anycubic Kobra X", "printer_name": "Anycubic Kobra X",
@@ -132,6 +134,8 @@ class KobraXBridge:
self._state["progress"] = float(d["progress"]) / 100.0 self._state["progress"] = float(d["progress"]) / 100.0
if "print_time" in d: if "print_time" in d:
self._state["print_duration"] = int(d["print_time"]) * 60 self._state["print_duration"] = int(d["print_time"]) * 60
if "remain_time" in d:
self._state["remain_time"] = int(d["remain_time"]) * 60
if "curr_layer" in d: if "curr_layer" in d:
self._state["curr_layer"] = d["curr_layer"] self._state["curr_layer"] = d["curr_layer"]
if "total_layers" in d: if "total_layers" in d:
@@ -248,6 +252,7 @@ class KobraXBridge:
"filename": s["filename"], "filename": s["filename"],
"print_duration": s["print_duration"], "print_duration": s["print_duration"],
"total_duration": s["print_duration"], "total_duration": s["print_duration"],
"remain_time": s["remain_time"],
"info": { "info": {
"current_layer": s["curr_layer"], "current_layer": s["curr_layer"],
"total_layer": s["total_layers"], "total_layer": s["total_layers"],
@@ -592,6 +597,12 @@ header{background:var(--card);border-bottom:1px solid var(--border);
.theme-btn{background:none;border:1px solid var(--border);color:var(--txt2); .theme-btn{background:none;border:1px solid var(--border);color:var(--txt2);
border-radius:8px;padding:6px 10px;cursor:pointer;font-size:13px;transition:.15s} border-radius:8px;padding:6px 10px;cursor:pointer;font-size:13px;transition:.15s}
.theme-btn:hover{border-color:var(--accent);color:var(--accent)} .theme-btn:hover{border-color:var(--accent);color:var(--accent)}
.conn-btn{border-radius:8px;padding:6px 12px;cursor:pointer;font-size:13px;
font-weight:600;border:none;transition:.15s}
.conn-btn.disconnected{background:var(--accent);color:#fff}
.conn-btn.disconnected:hover{opacity:.85}
.conn-btn.connected{background:transparent;border:1px solid var(--border);color:var(--txt2)}
.conn-btn.connected:hover{border-color:#e05;color:#e05}
/* ── LAYOUT ── */ /* ── LAYOUT ── */
.layout{display:flex;flex:1;min-height:0} .layout{display:flex;flex:1;min-height:0}
@@ -836,6 +847,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button> <button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
<button class="theme-btn" onclick="toggleLang()" id="lang-btn">EN</button> <button class="theme-btn" onclick="toggleLang()" id="lang-btn">EN</button>
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title="Einstellungen">⚙</button> <button class="theme-btn" onclick="openSettings()" id="settings-btn" title="Einstellungen">⚙</button>
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
</header> </header>
<!-- ═══ SETTINGS MODAL ═══ --> <!-- ═══ SETTINGS MODAL ═══ -->
@@ -851,6 +863,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<div class="modal-field"> <div class="modal-field">
<label id="lbl-printer-ip">Drucker-IP</label> <label id="lbl-printer-ip">Drucker-IP</label>
<input type="text" id="s-printer-ip" placeholder="192.168.x.x"> <input type="text" id="s-printer-ip" placeholder="192.168.x.x">
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
</div> </div>
<div class="modal-field"> <div class="modal-field">
<label id="lbl-mqtt-port">MQTT-Port</label> <label id="lbl-mqtt-port">MQTT-Port</label>
@@ -943,6 +956,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<div class="progress-bar" style="margin:8px 0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div> <div class="progress-bar" style="margin:8px 0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
<div class="meta-row" style="margin-top:6px"> <div class="meta-row" style="margin-top:6px">
<span id="d-elapsed"></span> <span id="d-elapsed"></span>
<span id="d-remain" style="color:var(--acc)"></span>
<span id="d-layers" class="layer-badge"></span> <span id="d-layers" class="layer-badge"></span>
</div> </div>
<div class="fname" id="d-fname" title="" style="margin-top:6px"></div> <div class="fname" id="d-fname" title="" style="margin-top:6px"></div>
@@ -1029,7 +1043,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<button class="joy" onclick="move(2,-1,getStep())" title="Z">▲</button> <button class="joy" onclick="move(2,-1,getStep())" title="Z">▲</button>
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▼</button> <button class="joy" onclick="move(2,1,getStep())" title="Z+">▼</button>
</div> </div>
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)">Schrittweite: <span id="step-display">1</span> mm</div> <div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
</div> </div>
<!-- Print Speed --> <!-- Print Speed -->
@@ -1111,7 +1125,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
<script> <script>
// ── State ── // ── State ──
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
print_state:'standby',filename:'',progress:0,print_duration:0, print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'', curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'',
camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,ams_slots:[]}; camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,ams_slots:[]};
var tempHistory={n:[],b:[]}; var tempHistory={n:[],b:[]};
@@ -1132,7 +1146,7 @@ var LANG_DE={
header_status_standby:'Bereit',header_status_printing:'Druckt',header_status_complete:'Fertig',header_status_error:'Fehler', header_status_standby:'Bereit',header_status_printing:'Druckt',header_status_complete:'Fertig',header_status_error:'Fehler',
kobra_free:'Bereit',kobra_busy:'Beschäftigt',kobra_printing:'Druckt',kobra_preheating:'Aufheizen',kobra_auto_leveling:'Nivellierung',kobra_checking:'Prüfung',kobra_updated:'Aktualisierung',kobra_init:'Initialisierung',kobra_pausing:'Pausiert...',kobra_paused:'Pausiert',kobra_resuming:'Fortsetzen...',kobra_resumed:'Fortgesetzt',kobra_stopping:'Stoppt...',kobra_stoped:'Gestoppt',kobra_finished:'Abgeschlossen',kobra_failed:'Fehler',kobra_canceled:'Abgebrochen',kobra_offline:'Offline', kobra_free:'Bereit',kobra_busy:'Beschäftigt',kobra_printing:'Druckt',kobra_preheating:'Aufheizen',kobra_auto_leveling:'Nivellierung',kobra_checking:'Prüfung',kobra_updated:'Aktualisierung',kobra_init:'Initialisierung',kobra_pausing:'Pausiert...',kobra_paused:'Pausiert',kobra_resuming:'Fortsetzen...',kobra_resumed:'Fortgesetzt',kobra_stopping:'Stoppt...',kobra_stoped:'Gestoppt',kobra_finished:'Abgeschlossen',kobra_failed:'Fehler',kobra_canceled:'Abgebrochen',kobra_offline:'Offline',
nav_dashboard:'Dashboard',nav_print:'Druck',nav_temps:'Temperaturen',nav_motion:'Achsen',nav_ams:'AMS',nav_extras:'Licht / Lüfter',nav_console:'Konsole', nav_dashboard:'Dashboard',nav_print:'Druck',nav_temps:'Temperaturen',nav_motion:'Achsen',nav_ams:'AMS',nav_extras:'Licht / Lüfter',nav_console:'Konsole',
card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera', card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen',lbl_remaining:'verbleibend',
speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen', lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen',
cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera', cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera',
@@ -1149,15 +1163,16 @@ var LANG_DE={
confirm_cancel:'Druck wirklich abbrechen?', confirm_cancel:'Druck wirklich abbrechen?',
settings_title:'Einstellungen',settings_connection:'Verbindung',settings_poll:'Poll-Intervall',settings_version:'Version', settings_title:'Einstellungen',settings_connection:'Verbindung',settings_poll:'Poll-Intervall',settings_version:'Version',
settings_save:'Speichern & Neustart',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port', settings_save:'Speichern & Neustart',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port',
settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID', settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)',
update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell', update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell',
update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler' update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler',
btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen'
}; };
var LANG_EN={ var LANG_EN={
header_status_standby:'Ready',header_status_printing:'Printing',header_status_complete:'Complete',header_status_error:'Error', header_status_standby:'Ready',header_status_printing:'Printing',header_status_complete:'Complete',header_status_error:'Error',
kobra_free:'Ready',kobra_busy:'Busy',kobra_printing:'Printing',kobra_preheating:'Preheating',kobra_auto_leveling:'Auto Leveling',kobra_checking:'Checking',kobra_updated:'Updating',kobra_init:'Initializing',kobra_pausing:'Pausing...',kobra_paused:'Paused',kobra_resuming:'Resuming...',kobra_resumed:'Resumed',kobra_stopping:'Stopping...',kobra_stoped:'Stopped',kobra_finished:'Finished',kobra_failed:'Error',kobra_canceled:'Cancelled',kobra_offline:'Offline', kobra_free:'Ready',kobra_busy:'Busy',kobra_printing:'Printing',kobra_preheating:'Preheating',kobra_auto_leveling:'Auto Leveling',kobra_checking:'Checking',kobra_updated:'Updating',kobra_init:'Initializing',kobra_pausing:'Pausing...',kobra_paused:'Paused',kobra_resuming:'Resuming...',kobra_resumed:'Resumed',kobra_stopping:'Stopping...',kobra_stoped:'Stopped',kobra_finished:'Finished',kobra_failed:'Error',kobra_canceled:'Cancelled',kobra_offline:'Offline',
nav_dashboard:'Dashboard',nav_print:'Print',nav_temps:'Temperatures',nav_motion:'Motion',nav_ams:'AMS',nav_extras:'Light / Fan',nav_console:'Console', nav_dashboard:'Dashboard',nav_print:'Print',nav_temps:'Temperatures',nav_motion:'Motion',nav_ams:'AMS',nav_extras:'Light / Fan',nav_console:'Console',
card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera', card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed',lbl_remaining:'remaining',
speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport', speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload', lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload',
cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera', cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera',
@@ -1174,9 +1189,10 @@ var LANG_EN={
confirm_cancel:'Really cancel the print?', confirm_cancel:'Really cancel the print?',
settings_title:'Settings',settings_connection:'Connection',settings_poll:'Poll Interval',settings_version:'Version', settings_title:'Settings',settings_connection:'Connection',settings_poll:'Poll Interval',settings_version:'Version',
settings_save:'Save & Restart',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port', settings_save:'Save & Restart',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port',
settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID', settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)',
update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date', update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date',
update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error' update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error',
btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect'
}; };
var currentLang='de'; var currentLang='de';
var T=LANG_DE; var T=LANG_DE;
@@ -1222,6 +1238,7 @@ function applyLang(){
document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z); document.querySelectorAll('.lbl-home-z').forEach(e=>e.textContent=T.btn_home_z);
document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all); document.querySelectorAll('.lbl-home-all').forEach(e=>e.textContent=T.btn_home_all);
document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step); document.querySelectorAll('.lbl-step').forEach(e=>e.textContent=T.label_step);
document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
// Console // Console
setText('ptitle-console',T.panel_console_title); setText('ptitle-console',T.panel_console_title);
// Settings modal // Settings modal
@@ -1245,6 +1262,8 @@ function applyLang(){
// AMS feed/unload // AMS feed/unload
document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed); document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed);
document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload); document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload);
// conn-btn text (nur wenn nicht im Übergangszustand)
updateConnBtn();
} }
function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;} function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;}
(function(){ (function(){
@@ -1253,7 +1272,13 @@ function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent
document.getElementById('lang-btn').textContent=l==='de'?'EN':'DE'; document.getElementById('lang-btn').textContent=l==='de'?'EN':'DE';
document.documentElement.setAttribute('lang',l); document.documentElement.setAttribute('lang',l);
// defer until DOM ready // defer until DOM ready
window.addEventListener('DOMContentLoaded',function(){applyLang();}); window.addEventListener('DOMContentLoaded',function(){
applyLang();
// Beim ersten Start (keine Zugangsdaten) Settings-Modal automatisch öffnen
fetch('/api/settings').then(function(r){return r.json()}).then(function(d){
if(!d.printer_ip||!d.device_id)openSettings();
}).catch(function(){});
});
})(); })();
// ── Panel nav ── // ── Panel nav ──
@@ -1312,6 +1337,8 @@ function applyState(){
var elapsed=fmtTime(s.print_duration); var elapsed=fmtTime(s.print_duration);
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=elapsed; var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=elapsed;
var remain=s.remain_time>0?''+fmtTime(s.remain_time)+' '+T.lbl_remaining:'';
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=remain;
var fn=s.filename||''; var fn=s.filename||'';
var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn}; var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn};
@@ -1370,6 +1397,33 @@ function applyState(){
if(s.print_state==='printing'&&!camOn&&s.camera_url){ if(s.print_state==='printing'&&!camOn&&s.camera_url){
camStart(); camStart();
} }
updateConnBtn();
}
function updateConnBtn(){
var btn=document.getElementById('conn-btn');
if(!btn)return;
var offline=S.kobra_state==='offline';
if(offline){
btn.className='conn-btn disconnected';
btn.textContent=T.btn_connect||'⚡ Verbinden';
} else {
btn.className='conn-btn connected';
btn.textContent=T.btn_disconnect||'✕ Trennen';
}
}
function toggleConnection(){
var btn=document.getElementById('conn-btn');
var offline=S.kobra_state==='offline';
btn.disabled=true;
btn.textContent='';
var url=offline?'/api/connect':'/api/disconnect';
post(url,{}).then(function(r){return r.json()}).then(function(r){
btn.disabled=false;
if(r.error)addLog('Error: '+r.error);
}).catch(function(){btn.disabled=false;});
} }
// ── Temp history + chart ── // ── Temp history + chart ──
@@ -1425,6 +1479,13 @@ function openSettings(){
function closeSettings(){ function closeSettings(){
document.getElementById('settings-modal').classList.remove('open'); document.getElementById('settings-modal').classList.remove('open');
} }
document.addEventListener('DOMContentLoaded',function(){
document.getElementById('s-printer-ip').addEventListener('input',function(){
var hint=document.getElementById('lbl-ip-hint');
if(this.value.includes(':')){hint.textContent=T.hint_ip_no_port;hint.style.display='block';}
else{hint.style.display='none';}
});
});
function setPoll(ms){ function setPoll(ms){
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')}); document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
var id='poll-'+Math.round(ms/1000); var id='poll-'+Math.round(ms/1000);
@@ -1684,6 +1745,28 @@ function toggleCam(){if(camOn)camStop();else camStart()}
self._state["fan_speed"] = speed self._state["fan_speed"] = speed
return web.json_response({"result": "ok"}) return web.json_response({"result": "ok"})
async def handle_api_connect(self, request):
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(None, self.client.connect)
self._state["print_state"] = "standby"
self._state["kobra_state"] = "free"
log.info("Manuell verbunden")
return web.json_response({"result": "connected"})
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
async def handle_api_disconnect(self, request):
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(None, self.client.disconnect)
except Exception:
pass
self._state["print_state"] = "error"
self._state["kobra_state"] = "offline"
log.info("Manuell getrennt")
return web.json_response({"result": "disconnected"})
async def handle_api_speed(self, request): async def handle_api_speed(self, request):
try: try:
body = await request.json() body = await request.json()
@@ -1893,6 +1976,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
"bed_target": s["bed_target"], "bed_target": s["bed_target"],
"progress": s["progress"], "progress": s["progress"],
"print_duration": s["print_duration"], "print_duration": s["print_duration"],
"remain_time": s["remain_time"],
"curr_layer": s["curr_layer"], "curr_layer": s["curr_layer"],
"total_layers": s["total_layers"], "total_layers": s["total_layers"],
"filename": s["filename"], "filename": s["filename"],
@@ -1960,7 +2044,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
def _find_env_path(self) -> pathlib.Path: def _find_env_path(self) -> pathlib.Path:
"""Gibt den Pfad zur .env-Datei zurück (neben Script oder im Parent).""" """Gibt den Pfad zur .env-Datei zurück (neben Script oder im Parent)."""
script_dir = pathlib.Path(__file__).parent script_dir = pathlib.Path(_BASE)
for base in (script_dir, script_dir.parent): for base in (script_dir, script_dir.parent):
p = base / ".env" p = base / ".env"
if p.is_file(): if p.is_file():
@@ -1992,7 +2076,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
lines.append(line) lines.append(line)
# Werte aktualisieren # Werte aktualisieren
mapping = { mapping = {
"PRINTER_IP": str(data.get("printer_ip", existing.get("PRINTER_IP", ""))), "PRINTER_IP": str(data.get("printer_ip", existing.get("PRINTER_IP", ""))).split(":")[0],
"MQTT_PORT": str(data.get("mqtt_port", existing.get("MQTT_PORT", "9883"))), "MQTT_PORT": str(data.get("mqtt_port", existing.get("MQTT_PORT", "9883"))),
"MQTT_USERNAME": str(data.get("username", existing.get("MQTT_USERNAME",""))), "MQTT_USERNAME": str(data.get("username", existing.get("MQTT_USERNAME",""))),
"MQTT_PASSWORD": str(data.get("password", existing.get("MQTT_PASSWORD",""))), "MQTT_PASSWORD": str(data.get("password", existing.get("MQTT_PASSWORD",""))),
@@ -2031,20 +2115,19 @@ function toggleCam(){if(camOn)camStop();else camStart()}
GITEA_RAW_BASE = "https://gitea.it-drui.de/viewit/KX-Bridge-Release/raw/tag" GITEA_RAW_BASE = "https://gitea.it-drui.de/viewit/KX-Bridge-Release/raw/tag"
def _read_version(self) -> str: def _read_version(self) -> str:
for base in (pathlib.Path(__file__).parent, pathlib.Path(__file__).parent.parent): for base in (pathlib.Path(_BASE), pathlib.Path(_BASE).parent):
p = base / "VERSION" p = base / "VERSION"
if p.is_file(): if p.is_file():
return p.read_text(encoding="utf-8").strip() return p.read_text(encoding="utf-8").strip()
return "unknown" return "unknown"
def _write_version(self, version: str): def _write_version(self, version: str):
for base in (pathlib.Path(__file__).parent, pathlib.Path(__file__).parent.parent): for base in (pathlib.Path(_BASE), pathlib.Path(_BASE).parent):
p = base / "VERSION" p = base / "VERSION"
if p.is_file(): if p.is_file():
p.write_text(version + "\n", encoding="utf-8") p.write_text(version + "\n", encoding="utf-8")
return return
# Fallback: neben dem Script (pathlib.Path(_BASE) / "VERSION").write_text(version + "\n", encoding="utf-8")
(pathlib.Path(__file__).parent.parent / "VERSION").write_text(version + "\n", encoding="utf-8")
@staticmethod @staticmethod
def _parse_version(v: str) -> "tuple[int, ...]": def _parse_version(v: str) -> "tuple[int, ...]":
@@ -2090,7 +2173,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
new_tag = data.get("tag", "") new_tag = data.get("tag", "")
if not download_url: if not download_url:
return web.json_response({"error": "download_url fehlt"}, status=400) return web.json_response({"error": "download_url fehlt"}, status=400)
script_path = pathlib.Path(__file__).resolve() script_path = pathlib.Path(sys.executable if getattr(sys, "frozen", False) else __file__).resolve()
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(download_url, timeout=aiohttp.ClientTimeout(total=30)) as resp: async with session.get(download_url, timeout=aiohttp.ClientTimeout(total=30)) as resp:
@@ -2267,7 +2350,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
return False return False
def _poll_loop(self, stop_event: threading.Event): def _poll_loop(self, stop_event: threading.Event):
_offline = False # True = Drucker zuletzt nicht erreichbar _offline = self._state["kobra_state"] == "offline"
_probe_interval = 10.0 # Sekunden zwischen TCP-Probes im Offline-Modus _probe_interval = 10.0 # Sekunden zwischen TCP-Probes im Offline-Modus
while not stop_event.is_set(): while not stop_event.is_set():
@@ -2355,6 +2438,8 @@ def build_app(bridge: KobraXBridge) -> web.Application:
# New API endpoints # New API endpoints
r.add_post("/api/light", bridge.handle_api_light) r.add_post("/api/light", bridge.handle_api_light)
r.add_post("/api/fan", bridge.handle_api_fan) r.add_post("/api/fan", bridge.handle_api_fan)
r.add_post("/api/connect", bridge.handle_api_connect)
r.add_post("/api/disconnect", bridge.handle_api_disconnect)
r.add_post("/api/speed", bridge.handle_api_speed) r.add_post("/api/speed", bridge.handle_api_speed)
r.add_post("/api/ams/feed", bridge.handle_api_ams_feed) r.add_post("/api/ams/feed", bridge.handle_api_ams_feed)
r.add_post("/api/axis", bridge.handle_api_axis) r.add_post("/api/axis", bridge.handle_api_axis)
@@ -2384,7 +2469,6 @@ def build_app(bridge: KobraXBridge) -> web.Application:
async def run_bridge(args): async def run_bridge(args):
log.info(f"Verbinde mit Drucker {args.printer_ip}:{args.mqtt_port}")
client = KobraXClient( client = KobraXClient(
host=args.printer_ip, host=args.printer_ip,
port=args.mqtt_port, port=args.mqtt_port,
@@ -2395,11 +2479,18 @@ async def run_bridge(args):
client_id="kobrax_bridge", client_id="kobrax_bridge",
) )
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, client.connect)
log.info("MQTT verbunden")
bridge = KobraXBridge(client, args=args) bridge = KobraXBridge(client, args=args)
# Verbindungsversuch beim Start bei Fehler im Offline-Modus weiterlaufen
loop = asyncio.get_event_loop()
log.info(f"Verbinde mit Drucker {args.printer_ip}:{args.mqtt_port}")
try:
await loop.run_in_executor(None, client.connect)
log.info("MQTT verbunden")
except Exception as e:
log.warning(f"Drucker nicht erreichbar ({e}) starte im Offline-Modus")
bridge._state["print_state"] = "error"
bridge._state["kobra_state"] = "offline"
app = build_app(bridge) app = build_app(bridge)
stop_event = threading.Event() stop_event = threading.Event()
@@ -2443,6 +2534,8 @@ def main():
parser.add_argument("--port", type=int, default=7125, parser.add_argument("--port", type=int, default=7125,
help="HTTP/WS-Port (Moonraker-Standard: 7125)") help="HTTP/WS-Port (Moonraker-Standard: 7125)")
args = parser.parse_args() args = parser.parse_args()
if args.printer_ip and ":" in args.printer_ip:
args.printer_ip = args.printer_ip.split(":")[0]
asyncio.run(run_bridge(args)) asyncio.run(run_bridge(args))

67
start.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# start.sh KX-Bridge starten (baut Docker-Image automatisch wenn nötig)
set -euo pipefail
cd "$(dirname "$0")"
# .env anlegen falls nicht vorhanden
if [[ ! -f .env ]]; then
if [[ -f .env.example ]]; then
cp .env.example .env
echo "[start] .env aus .env.example erstellt"
else
touch .env
fi
fi
# Docker verfügbar?
if ! docker info > /dev/null 2>&1; then
echo "[start] Docker nicht gefunden bitte Docker installieren."
exit 1
fi
# Prüfen ob Build nötig ist
NEEDS_BUILD=0
if ! docker image inspect kx-bridge:latest > /dev/null 2>&1; then
echo "[start] Image nicht vorhanden baue kx-bridge:latest ..."
NEEDS_BUILD=1
else
# Image-Erstellungszeit in Unix-Sekunden
IMAGE_TS=$(docker inspect --format='{{.Created}}' kx-bridge:latest \
| python3 -c "import sys,datetime; s=sys.stdin.read().strip(); \
s=s[:26].rstrip('Z').replace('T',' '); \
print(int(datetime.datetime.fromisoformat(s).replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null || echo 0)
for f in Dockerfile \
05_scripts/kobrax_moonraker_bridge.py \
05_scripts/kobrax_client.py \
05_scripts/env_loader.py \
05_scripts/requirements.txt \
05_scripts/anycubic_slicer.crt \
05_scripts/anycubic_slicer.key; do
if [[ -f "$f" ]]; then
FILE_TS=$(python3 -c "import os; print(int(os.path.getmtime('$f')))" 2>/dev/null || echo 0)
if [[ $FILE_TS -gt $IMAGE_TS ]]; then
echo "[start] '$f' ist neuer als das Image baue neu ..."
NEEDS_BUILD=1
break
fi
fi
done
fi
if [[ $NEEDS_BUILD -eq 1 ]]; then
docker build -t kx-bridge:latest .
fi
# Container starten
echo "[start] Starte KX-Bridge ..."
docker-compose down 2>/dev/null || true
docker-compose up -d
echo ""
echo " ✓ KX-Bridge läuft"
echo " Web-UI : http://$(hostname -I | awk '{print $1}'):7125"
echo " Logs : docker-compose logs -f"
echo " Stop : docker-compose down"