Compare commits
8 Commits
v0.9.1-bet
...
v0.9.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d2ac7b931 | |||
| 98303f1197 | |||
| 808ac13ce0 | |||
| 14400dd799 | |||
| 0fb9a71390 | |||
| e81cdefd71 | |||
| a7469cce9a | |||
| 8394f77767 |
@@ -3,11 +3,14 @@ FROM python:3.11-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY 05_scripts/requirements.txt .
|
COPY 05_scripts/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 05_scripts/kobrax_moonraker_bridge.py .
|
||||||
COPY 05_scripts/env_loader.py .
|
COPY 05_scripts/env_loader.py .
|
||||||
COPY 05_scripts/kobrax_client.py .
|
COPY 05_scripts/kobrax_client.py .
|
||||||
|
COPY 05_scripts/anycubic_slicer.crt .
|
||||||
|
COPY 05_scripts/anycubic_slicer.key .
|
||||||
|
|
||||||
EXPOSE 7125
|
EXPOSE 7125
|
||||||
|
|
||||||
|
|||||||
250
README.en.md
Normal file
250
README.en.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# KX-Bridge – Anycubic Kobra X Moonraker Bridge
|
||||||
|
|
||||||
|
**Version:** 0.9.1-beta5
|
||||||
|
**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 (LAN, no Wi-Fi client isolation)
|
||||||
|
- 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. Create .env
|
||||||
|
cp .env.example .env
|
||||||
|
# Fill in your printer data (→ extract_credentials)
|
||||||
|
|
||||||
|
# 2. Start the bridge
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 3. In OrcaSlicer: add printer → "Moonraker" → http://BRIDGE-IP:7125
|
||||||
|
```
|
||||||
|
|
||||||
|
Check logs:
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Update:
|
||||||
|
```bash
|
||||||
|
docker compose pull && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start – Binary (Linux)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x kx-bridge
|
||||||
|
./kx-bridge --printer-ip 192.168.x.x --username userXXXX --password XXXXX \
|
||||||
|
--device-id XXXXX --mode-id 20030
|
||||||
|
```
|
||||||
|
|
||||||
|
Or place a `.env` file in the same directory — the bridge reads it automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start – Python directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
```
|
||||||
|
extract_credentials.exe --write-env
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes the found credentials directly to `.env`.
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x extract_credentials
|
||||||
|
./extract_credentials --write-env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
=======================================================
|
||||||
|
|
||||||
|
Hint: pass --write-env to save credentials to '.env'.
|
||||||
|
```
|
||||||
|
|
||||||
|
All credentials are **processed locally only** — nothing is sent to external servers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration (.env)
|
||||||
|
|
||||||
|
```env
|
||||||
|
PRINTER_IP=192.168.x.x # Printer IP address
|
||||||
|
MQTT_PORT=9883 # Default, do not change
|
||||||
|
MQTT_USERNAME=userXXXXXXXX # Starts with "user"
|
||||||
|
MQTT_PASSWORD=XXXXXXXXXXXXXX # ~15 characters, mixed case
|
||||||
|
DEVICE_ID=xxxxxxxx... # 32-character hex string
|
||||||
|
MODE_ID=20030 # Kobra X default
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OrcaSlicer setup
|
||||||
|
|
||||||
|
1. Add printer → **Anycubic Kobra X** (or generic Klipper printer)
|
||||||
|
2. Connection type: **Moonraker**
|
||||||
|
3. Host: `http://BRIDGE-HOST:7125`
|
||||||
|
4. Test connection → should show "Online"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web UI
|
||||||
|
|
||||||
|
The bridge serves a web interface at `http://BRIDGE-HOST:7125`:
|
||||||
|
|
||||||
|
| Section | Function |
|
||||||
|
|---------|----------|
|
||||||
|
| Dashboard | Printer status, progress, temperature overview |
|
||||||
|
| Temperatures | Set nozzle and bed temperature directly |
|
||||||
|
| Motion | X/Y/Z movement, motor release |
|
||||||
|
| Print Speed | Silent / Normal / Sport |
|
||||||
|
| Fan / Light | Fan speed and work light |
|
||||||
|
| AMS | Load / unload filament |
|
||||||
|
| Camera | Live preview (if supported by printer) |
|
||||||
|
| ⚙ Settings | MQTT credentials, poll interval, self-update |
|
||||||
|
|
||||||
|
### Self-update
|
||||||
|
|
||||||
|
The ⚙ menu in the web UI lets you check for new versions and update the bridge in place — no reinstallation needed. After the download the bridge restarts automatically with the new version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## bridge.sh (Linux service manager)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bridge.sh start # Start bridge in background
|
||||||
|
./bridge.sh stop # Stop bridge
|
||||||
|
./bridge.sh restart # Restart
|
||||||
|
./bridge.sh status # Check status and port
|
||||||
|
./bridge.sh log 50 # Show last 50 log lines
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Printer states
|
||||||
|
|
||||||
|
The bridge translates internal Kobra states into Moonraker-compatible states:
|
||||||
|
|
||||||
|
| Kobra state | Meaning |
|
||||||
|
|-------------|---------|
|
||||||
|
| free | Ready |
|
||||||
|
| printing / busy | Printing |
|
||||||
|
| pausing / paused | Paused |
|
||||||
|
| resuming / resumed | Resuming |
|
||||||
|
| stopping / stoped | Stopping |
|
||||||
|
| finished | Complete |
|
||||||
|
| canceled | Cancelled |
|
||||||
|
| failed | Error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Port 7125 already in use:**
|
||||||
|
```bash
|
||||||
|
./bridge.sh stop # or: fuser -k 7125/tcp
|
||||||
|
./bridge.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Invalid credentials / connection refused:**
|
||||||
|
- Start AnycubicSlicerNext, connect to the printer, then run `extract_credentials` again
|
||||||
|
- If the result looks uncertain: `extract_credentials --verbose` shows all candidates
|
||||||
|
- Manually enter the correct candidate in `.env` and restart the bridge
|
||||||
|
|
||||||
|
**Temperature changes are ignored:**
|
||||||
|
- During an active print, temperature changes are sent via a separate channel — this is normal and the bridge handles it automatically.
|
||||||
|
|
||||||
|
**Docker: Permission denied:**
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
# Log out and back in
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker: .env not found:**
|
||||||
|
```bash
|
||||||
|
# .env must be in the same directory as docker-compose.yml
|
||||||
|
cp .env.example .env && nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker
|
||||||
|
docker compose logs -f kx-bridge
|
||||||
|
|
||||||
|
# Binary / Python
|
||||||
|
tail -f /tmp/bridge.log # when using bridge.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security notes
|
||||||
|
|
||||||
|
- The bridge binds to `0.0.0.0:7125` by default — use on your local network only
|
||||||
|
- `.env` contains printer credentials — do not share publicly
|
||||||
|
- The credentials are printer-specific and have no access to Anycubic cloud services
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License & legal
|
||||||
|
|
||||||
|
This project was created through interoperability research under §69e UrhG (German copyright law).
|
||||||
|
For private, non-commercial use only.
|
||||||
78
README.md
78
README.md
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
Verbindet den Anycubic Kobra X mit OrcaSlicer – ohne Klipper, ohne Raspberry Pi.
|
Verbindet den Anycubic Kobra X mit OrcaSlicer – ohne Klipper, ohne Raspberry Pi.
|
||||||
|
|
||||||
KX-Bridge läuft auf deinem PC oder NAS und stellt eine Schnittstelle bereit, über die OrcaSlicer den Drucker direkt steuern kann: Druckstart, Temperatur, Fortschritt, AMS-Farbwechsel.
|
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.
|
||||||
|
|
||||||
|
**Version:** 0.9.1-beta5
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ KX-Bridge läuft auf deinem PC oder NAS und stellt eine Schnittstelle bereit, ü
|
|||||||
| Datei | Beschreibung |
|
| Datei | Beschreibung |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `kobrax_moonraker_bridge.py` | Bridge-Hauptprogramm |
|
| `kobrax_moonraker_bridge.py` | Bridge-Hauptprogramm |
|
||||||
|
| `kx-bridge` | Vorkompilierte Linux-Binary |
|
||||||
| `extract_credentials.exe` | Zugangsdaten aus AnycubicSlicerNext auslesen (Windows) |
|
| `extract_credentials.exe` | Zugangsdaten aus AnycubicSlicerNext auslesen (Windows) |
|
||||||
| `extract_credentials` | Zugangsdaten aus AnycubicSlicerNext auslesen (Linux) |
|
| `extract_credentials` | Zugangsdaten aus AnycubicSlicerNext auslesen (Linux) |
|
||||||
| `kobra_x_orcaslicer_preset.zip` | OrcaSlicer-Druckerprofil für den Kobra X |
|
| `kobra_x_orcaslicer_preset.zip` | OrcaSlicer-Druckerprofil für den Kobra X |
|
||||||
@@ -20,6 +23,21 @@ KX-Bridge läuft auf deinem PC oder NAS und stellt eine Schnittstelle bereit, ü
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Was wird unterstützt?
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Anycubic Kobra X im LAN-Modus (Drucker muss über LAN erreichbar sein, nicht nur über Anycubic-Cloud)
|
- Anycubic Kobra X im LAN-Modus (Drucker muss über LAN erreichbar sein, nicht nur über Anycubic-Cloud)
|
||||||
@@ -35,18 +53,16 @@ KX-Bridge läuft auf deinem PC oder NAS und stellt eine Schnittstelle bereit, ü
|
|||||||
|
|
||||||
Die Bridge benötigt druckerspezifische MQTT-Zugangsdaten.
|
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.
|
> **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.
|
||||||
|
|
||||||
Die Zugangsdaten werden mit `extract_credentials` aus dem laufenden AnycubicSlicerNext ausgelesen.
|
AnycubicSlicerNext starten und mit dem Drucker verbinden (bis der Drucker-Status angezeigt wird), dann:
|
||||||
|
|
||||||
Vorbereitung: AnycubicSlicerNext starten und mit dem Drucker verbinden (bis der Drucker-Status angezeigt wird).
|
**Windows:**
|
||||||
|
|
||||||
Windows:
|
|
||||||
```
|
```
|
||||||
extract_credentials.exe --write-env
|
extract_credentials.exe --write-env
|
||||||
```
|
```
|
||||||
|
|
||||||
Linux:
|
**Linux:**
|
||||||
```bash
|
```bash
|
||||||
chmod +x extract_credentials
|
chmod +x extract_credentials
|
||||||
./extract_credentials --write-env
|
./extract_credentials --write-env
|
||||||
@@ -69,13 +85,13 @@ cp .env.example .env
|
|||||||
|
|
||||||
### Schritt 3: Bridge starten
|
### Schritt 3: Bridge starten
|
||||||
|
|
||||||
Option A – Docker (empfohlen):
|
**Option A – Docker (empfohlen):**
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
Läuft im Hintergrund, startet automatisch nach Systemneustart.
|
Läuft im Hintergrund, startet automatisch nach Systemneustart.
|
||||||
|
|
||||||
Option B – Linux Binary:
|
**Option B – Linux Binary:**
|
||||||
```bash
|
```bash
|
||||||
chmod +x kx-bridge
|
chmod +x kx-bridge
|
||||||
./kx-bridge
|
./kx-bridge
|
||||||
@@ -83,7 +99,7 @@ chmod +x kx-bridge
|
|||||||
./bridge.sh start
|
./bridge.sh start
|
||||||
```
|
```
|
||||||
|
|
||||||
Option C – Python direkt:
|
**Option C – Python direkt:**
|
||||||
```bash
|
```bash
|
||||||
pip install aiohttp
|
pip install aiohttp
|
||||||
python kobrax_moonraker_bridge.py
|
python kobrax_moonraker_bridge.py
|
||||||
@@ -102,9 +118,30 @@ python kobrax_moonraker_bridge.py
|
|||||||
### Schritt 5: OrcaSlicer verbinden
|
### Schritt 5: OrcaSlicer verbinden
|
||||||
|
|
||||||
1. Drucker-Einstellungen öffnen
|
1. Drucker-Einstellungen öffnen
|
||||||
2. Verbindungstyp: Moonraker
|
2. Verbindungstyp: **Moonraker**
|
||||||
3. Adresse: `http://IP-DES-BRIDGE-PC:7125` eintragen
|
3. Adresse: `http://IP-DES-BRIDGE-PC:7125` eintragen
|
||||||
4. Auf "Test" klicken – bei erfolgreicher Verbindung erscheint eine Bestätigungsmeldung
|
4. Auf „Test" klicken – bei erfolgreicher Verbindung erscheint eine Bestätigungsmeldung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web-UI
|
||||||
|
|
||||||
|
Die Bridge stellt unter `http://BRIDGE-IP: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 vom 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 sie automatisch neu.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -133,23 +170,26 @@ docker compose pull && docker compose up -d # Update
|
|||||||
|
|
||||||
## Fehlerbehebung
|
## Fehlerbehebung
|
||||||
|
|
||||||
Port 7125 belegt:
|
**Port 7125 belegt:**
|
||||||
```bash
|
```bash
|
||||||
./bridge.sh stop
|
./bridge.sh stop
|
||||||
./bridge.sh start
|
./bridge.sh start
|
||||||
```
|
```
|
||||||
|
|
||||||
Verbindungstest in OrcaSlicer schlägt fehl:
|
**Verbindungstest in OrcaSlicer schlägt fehl:**
|
||||||
- Firewall prüfen: Port 7125 muss erreichbar sein
|
- Firewall prüfen: Port 7125 muss erreichbar sein
|
||||||
- Bridge-Log prüfen: `./bridge.sh log` oder `docker compose logs`
|
- Bridge-Log prüfen: `./bridge.sh log` oder `docker compose logs`
|
||||||
- Drucker-IP in `.env` korrekt?
|
- Drucker-IP in `.env` korrekt?
|
||||||
|
|
||||||
Zugangsdaten werden abgelehnt:
|
**Zugangsdaten werden abgelehnt:**
|
||||||
- AnycubicSlicerNext starten, mit Drucker verbinden
|
- AnycubicSlicerNext starten, mit Drucker verbinden
|
||||||
- `extract_credentials --verbose` ausführen und alle Kandidaten prüfen
|
- `extract_credentials --verbose` ausführen und alle Kandidaten prüfen
|
||||||
- Richtigen Wert manuell in `.env` eintragen, Bridge neu starten
|
- Richtigen Wert manuell in `.env` eintragen, Bridge neu starten
|
||||||
|
|
||||||
Docker: Permission denied:
|
**Temperaturänderungen werden ignoriert:**
|
||||||
|
- Während eines laufenden Drucks werden Temperaturänderungen über einen separaten Kanal gesendet — das ist normal und wird von der Bridge automatisch erkannt.
|
||||||
|
|
||||||
|
**Docker: Permission denied:**
|
||||||
```bash
|
```bash
|
||||||
sudo usermod -aG docker $USER
|
sudo usermod -aG docker $USER
|
||||||
# Neu einloggen, dann erneut versuchen
|
# Neu einloggen, dann erneut versuchen
|
||||||
@@ -172,7 +212,9 @@ sudo usermod -aG docker $USER
|
|||||||
|
|
||||||
## Sicherheitshinweise
|
## Sicherheitshinweise
|
||||||
|
|
||||||
- Alle Zugangsdaten werden ausschließlich lokal verarbeitet
|
- Die Bridge bindet standardmäßig auf `0.0.0.0:7125` — nur im lokalen Netzwerk nutzen
|
||||||
|
- `.env` enthält Drucker-Credentials — nicht öffentlich teilen
|
||||||
|
- Alle Zugangsdaten werden ausschließlich lokal verarbeitet — keine Übertragung an externe Server
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -180,6 +222,6 @@ sudo usermod -aG docker $USER
|
|||||||
|
|
||||||
Dieses Projekt dient der privaten Nutzung und der Herstellung von Interoperabilität zwischen dem Anycubic Kobra X und freier Software (OrcaSlicer).
|
Dieses Projekt dient der privaten Nutzung und der Herstellung von Interoperabilität zwischen dem Anycubic Kobra X und freier Software (OrcaSlicer).
|
||||||
|
|
||||||
`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 Tool funktioniert nur für den Prozess des Druckers, dem du selbst gehörst.
|
`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.
|
Das Projekt steht in keiner Verbindung zu Anycubic und wird nicht kommerziell betrieben.
|
||||||
|
|||||||
24
anycubic_slicer.crt
Normal file
24
anycubic_slicer.crt
Normal 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
28
anycubic_slicer.key
Normal 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-----
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
@@ -394,14 +395,14 @@ class KobraXClient:
|
|||||||
def stop_camera(self) -> dict | None:
|
def stop_camera(self) -> dict | None:
|
||||||
return self.publish("video", "stopCapture")
|
return self.publish("video", "stopCapture")
|
||||||
|
|
||||||
def pause_print(self) -> dict | None:
|
def pause_print(self, taskid: str = "-1") -> dict | None:
|
||||||
return self.publish("print", "pause")
|
return self.publish("print", "pause", {"taskid": taskid})
|
||||||
|
|
||||||
def resume_print(self) -> dict | None:
|
def resume_print(self, taskid: str = "-1") -> dict | None:
|
||||||
return self.publish("print", "resume")
|
return self.publish("print", "resume", {"taskid": taskid})
|
||||||
|
|
||||||
def stop_print(self) -> dict | None:
|
def stop_print(self, taskid: str = "-1") -> dict | None:
|
||||||
return self.publish("print", "stop")
|
return self.publish("print", "stop", {"taskid": taskid})
|
||||||
|
|
||||||
# -- G-Code Upload -------------------------------------------------------
|
# -- G-Code Upload -------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -48,6 +49,12 @@ KOBRA_TO_KLIPPER_STATE = {
|
|||||||
"checking": "printing",
|
"checking": "printing",
|
||||||
"updated": "printing",
|
"updated": "printing",
|
||||||
"init": "printing",
|
"init": "printing",
|
||||||
|
"pausing": "paused",
|
||||||
|
"paused": "paused",
|
||||||
|
"resuming": "printing",
|
||||||
|
"resumed": "printing",
|
||||||
|
"stopping": "printing",
|
||||||
|
"stoped": "standby",
|
||||||
"finished": "complete",
|
"finished": "complete",
|
||||||
"failed": "error",
|
"failed": "error",
|
||||||
"canceled": "standby",
|
"canceled": "standby",
|
||||||
@@ -73,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",
|
||||||
@@ -118,11 +126,16 @@ class KobraXBridge:
|
|||||||
self._state["print_state"] = KOBRA_TO_KLIPPER_STATE.get(kobra_state, "printing")
|
self._state["print_state"] = KOBRA_TO_KLIPPER_STATE.get(kobra_state, "printing")
|
||||||
if kobra_state:
|
if kobra_state:
|
||||||
self._state["kobra_state"] = kobra_state
|
self._state["kobra_state"] = kobra_state
|
||||||
|
if kobra_state in ("stoped", "canceled"):
|
||||||
|
self._state["progress"] = 0.0
|
||||||
|
self._state["filename"] = ""
|
||||||
self._state["filename"] = d.get("filename", self._state["filename"])
|
self._state["filename"] = d.get("filename", self._state["filename"])
|
||||||
if "progress" in d:
|
if "progress" in d:
|
||||||
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:
|
||||||
@@ -239,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"],
|
||||||
@@ -519,17 +533,20 @@ class KobraXBridge:
|
|||||||
|
|
||||||
async def handle_print_pause(self, request):
|
async def handle_print_pause(self, request):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
await loop.run_in_executor(None, self.client.pause_print)
|
taskid = self._state.get("taskid", "-1")
|
||||||
|
await loop.run_in_executor(None, lambda: self.client.pause_print(taskid))
|
||||||
return web.json_response({"result": "ok"})
|
return web.json_response({"result": "ok"})
|
||||||
|
|
||||||
async def handle_print_resume(self, request):
|
async def handle_print_resume(self, request):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
await loop.run_in_executor(None, self.client.resume_print)
|
taskid = self._state.get("taskid", "-1")
|
||||||
|
await loop.run_in_executor(None, lambda: self.client.resume_print(taskid))
|
||||||
return web.json_response({"result": "ok"})
|
return web.json_response({"result": "ok"})
|
||||||
|
|
||||||
async def handle_print_cancel(self, request):
|
async def handle_print_cancel(self, request):
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
await loop.run_in_executor(None, self.client.stop_print)
|
taskid = self._state.get("taskid", "-1")
|
||||||
|
await loop.run_in_executor(None, lambda: self.client.stop_print(taskid))
|
||||||
return web.json_response({"result": "ok"})
|
return web.json_response({"result": "ok"})
|
||||||
|
|
||||||
async def handle_octoprint_version(self, request):
|
async def handle_octoprint_version(self, request):
|
||||||
@@ -580,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}
|
||||||
@@ -824,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 ═══ -->
|
||||||
@@ -931,6 +955,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>
|
||||||
@@ -1017,7 +1042,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 -->
|
||||||
@@ -1099,7 +1124,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:[]};
|
||||||
@@ -1118,9 +1143,9 @@ function toggleTheme(){
|
|||||||
// ── i18n ──
|
// ── i18n ──
|
||||||
var LANG_DE={
|
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_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',
|
||||||
@@ -1139,13 +1164,14 @@ var LANG_DE={
|
|||||||
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',
|
||||||
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_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',
|
||||||
@@ -1164,7 +1190,8 @@ var LANG_EN={
|
|||||||
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',
|
||||||
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;
|
||||||
@@ -1210,6 +1237,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
|
||||||
@@ -1233,6 +1261,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(){
|
||||||
@@ -1241,7 +1271,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 ──
|
||||||
@@ -1300,6 +1336,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};
|
||||||
@@ -1358,6 +1396,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 ──
|
||||||
@@ -1672,6 +1737,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()
|
||||||
@@ -1881,6 +1968,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"],
|
||||||
@@ -1948,7 +2036,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():
|
||||||
@@ -2019,20 +2107,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, ...]":
|
||||||
@@ -2078,7 +2165,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:
|
||||||
@@ -2255,7 +2342,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():
|
||||||
@@ -2343,6 +2430,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)
|
||||||
@@ -2372,7 +2461,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,
|
||||||
@@ -2383,11 +2471,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()
|
||||||
|
|||||||
67
start.sh
Executable file
67
start.sh
Executable 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"
|
||||||
Reference in New Issue
Block a user