Compare commits
7 Commits
v0.9.1-bet
...
v0.9.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fb9a71390 | |||
| e81cdefd71 | |||
| a7469cce9a | |||
| 8394f77767 | |||
| c97253597b | |||
| 3ceee973c5 | |||
| 0c07f70fee |
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.
|
||||
80
README.md
80
README.md
@@ -2,7 +2,9 @@
|
||||
|
||||
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 |
|
||||
|-------|-------------|
|
||||
| `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 |
|
||||
@@ -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
|
||||
|
||||
- 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.
|
||||
|
||||
> 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
|
||||
```
|
||||
|
||||
Linux:
|
||||
**Linux:**
|
||||
```bash
|
||||
chmod +x extract_credentials
|
||||
./extract_credentials --write-env
|
||||
@@ -69,13 +85,13 @@ cp .env.example .env
|
||||
|
||||
### Schritt 3: Bridge starten
|
||||
|
||||
Option A – Docker (empfohlen):
|
||||
**Option A – Docker (empfohlen):**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
Läuft im Hintergrund, startet automatisch nach Systemneustart.
|
||||
|
||||
Option B – Linux Binary:
|
||||
**Option B – Linux Binary:**
|
||||
```bash
|
||||
chmod +x kx-bridge
|
||||
./kx-bridge
|
||||
@@ -83,7 +99,7 @@ chmod +x kx-bridge
|
||||
./bridge.sh start
|
||||
```
|
||||
|
||||
Option C – Python direkt:
|
||||
**Option C – Python direkt:**
|
||||
```bash
|
||||
pip install aiohttp
|
||||
python kobrax_moonraker_bridge.py
|
||||
@@ -93,7 +109,7 @@ python kobrax_moonraker_bridge.py
|
||||
|
||||
### Schritt 4: OrcaSlicer-Profil installieren
|
||||
|
||||
1. `kobra_x_orcaslicer_preset.zip` in OrcaSlicer importieren:
|
||||
1. `kobra_x_orcaslicer_preset.zip` in OrcaSlicer importieren:
|
||||
Datei → Konfigurationen importieren → ZIP auswählen
|
||||
2. Anycubic Kobra X als Drucker auswählen
|
||||
|
||||
@@ -102,9 +118,30 @@ python kobrax_moonraker_bridge.py
|
||||
### Schritt 5: OrcaSlicer verbinden
|
||||
|
||||
1. Drucker-Einstellungen öffnen
|
||||
2. Verbindungstyp: Moonraker
|
||||
2. Verbindungstyp: **Moonraker**
|
||||
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
|
||||
|
||||
Port 7125 belegt:
|
||||
**Port 7125 belegt:**
|
||||
```bash
|
||||
./bridge.sh stop
|
||||
./bridge.sh start
|
||||
```
|
||||
|
||||
Verbindungstest in OrcaSlicer schlägt fehl:
|
||||
**Verbindungstest in OrcaSlicer schlägt fehl:**
|
||||
- Firewall prüfen: Port 7125 muss erreichbar sein
|
||||
- Bridge-Log prüfen: `./bridge.sh log` oder `docker compose logs`
|
||||
- Drucker-IP in `.env` korrekt?
|
||||
|
||||
Zugangsdaten werden abgelehnt:
|
||||
**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
|
||||
|
||||
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
|
||||
sudo usermod -aG docker $USER
|
||||
# Neu einloggen, dann erneut versuchen
|
||||
@@ -172,7 +212,9 @@ sudo usermod -aG docker $USER
|
||||
|
||||
## 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).
|
||||
|
||||
`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.
|
||||
|
||||
@@ -124,6 +124,10 @@ class KobraXClient:
|
||||
return (f"anycubic/anycubicCloud/v1/slicer/printer/"
|
||||
f"{self.mode_id}/{self.device_id}/{msg_type}")
|
||||
|
||||
def _web_topic(self, msg_type: str) -> str:
|
||||
return (f"anycubic/anycubicCloud/v1/web/printer/"
|
||||
f"{self.mode_id}/{self.device_id}/{msg_type}")
|
||||
|
||||
def _sub_topic(self) -> str:
|
||||
return (f"anycubic/anycubicCloud/v1/printer/public/"
|
||||
f"{self.mode_id}/{self.device_id}/#")
|
||||
@@ -345,6 +349,23 @@ class KobraXClient:
|
||||
return None
|
||||
return entry["result"]
|
||||
|
||||
def publish_web(self, msg_type: str, action: str, data=None) -> None:
|
||||
"""Fire-and-forget publish on the web/printer topic (used for runtime updates during print)."""
|
||||
msgid = str(uuid.uuid4())
|
||||
payload = json.dumps({
|
||||
"type": msg_type,
|
||||
"action": action,
|
||||
"msgid": msgid,
|
||||
"timestamp": int(time.time() * 1000),
|
||||
"data": data,
|
||||
}, separators=(",", ":"))
|
||||
topic = self._web_topic(msg_type)
|
||||
try:
|
||||
with self._lock:
|
||||
self._sock.sendall(_build_publish(topic, payload))
|
||||
except Exception as e:
|
||||
print(f"[kobrax] web send error: {e}")
|
||||
|
||||
# -- High-level commands -------------------------------------------------
|
||||
|
||||
def query_info(self) -> dict | None:
|
||||
@@ -373,14 +394,14 @@ class KobraXClient:
|
||||
def stop_camera(self) -> dict | None:
|
||||
return self.publish("video", "stopCapture")
|
||||
|
||||
def pause_print(self) -> dict | None:
|
||||
return self.publish("print", "pause")
|
||||
def pause_print(self, taskid: str = "-1") -> dict | None:
|
||||
return self.publish("print", "pause", {"taskid": taskid})
|
||||
|
||||
def resume_print(self) -> dict | None:
|
||||
return self.publish("print", "resume")
|
||||
def resume_print(self, taskid: str = "-1") -> dict | None:
|
||||
return self.publish("print", "resume", {"taskid": taskid})
|
||||
|
||||
def stop_print(self) -> dict | None:
|
||||
return self.publish("print", "stop")
|
||||
def stop_print(self, taskid: str = "-1") -> dict | None:
|
||||
return self.publish("print", "stop", {"taskid": taskid})
|
||||
|
||||
# -- G-Code Upload -------------------------------------------------------
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ KOBRA_TO_KLIPPER_STATE = {
|
||||
"checking": "printing",
|
||||
"updated": "printing",
|
||||
"init": "printing",
|
||||
"pausing": "paused",
|
||||
"paused": "paused",
|
||||
"resuming": "printing",
|
||||
"resumed": "printing",
|
||||
"stopping": "printing",
|
||||
"stoped": "standby",
|
||||
"finished": "complete",
|
||||
"failed": "error",
|
||||
"canceled": "standby",
|
||||
@@ -82,6 +88,8 @@ class KobraXBridge:
|
||||
"fan_speed": 0,
|
||||
"light_on": False,
|
||||
"light_brightness": 80,
|
||||
"taskid": "-1",
|
||||
"print_speed_mode": 2,
|
||||
}
|
||||
self._ams_slots: list[dict] = []
|
||||
self._ams_loaded_slot: int = -1
|
||||
@@ -116,6 +124,9 @@ class KobraXBridge:
|
||||
self._state["print_state"] = KOBRA_TO_KLIPPER_STATE.get(kobra_state, "printing")
|
||||
if 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"])
|
||||
if "progress" in d:
|
||||
self._state["progress"] = float(d["progress"]) / 100.0
|
||||
@@ -125,6 +136,11 @@ class KobraXBridge:
|
||||
self._state["curr_layer"] = d["curr_layer"]
|
||||
if "total_layers" in d:
|
||||
self._state["total_layers"] = d["total_layers"]
|
||||
if "taskid" in d:
|
||||
self._state["taskid"] = str(d["taskid"])
|
||||
settings = d.get("settings") or {}
|
||||
if "print_speed_mode" in settings:
|
||||
self._state["print_speed_mode"] = int(settings["print_speed_mode"])
|
||||
self._push_status_update()
|
||||
|
||||
def _on_info(self, payload: dict):
|
||||
@@ -149,6 +165,9 @@ class KobraXBridge:
|
||||
fan = d.get("fan_speed_pct")
|
||||
if fan is not None:
|
||||
self._state["fan_speed"] = int(fan)
|
||||
speed_mode = d.get("print_speed_mode")
|
||||
if speed_mode is not None:
|
||||
self._state["print_speed_mode"] = int(speed_mode)
|
||||
self._push_status_update()
|
||||
|
||||
def _on_file(self, payload: dict):
|
||||
@@ -509,17 +528,20 @@ class KobraXBridge:
|
||||
|
||||
async def handle_print_pause(self, request):
|
||||
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"})
|
||||
|
||||
async def handle_print_resume(self, request):
|
||||
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"})
|
||||
|
||||
async def handle_print_cancel(self, request):
|
||||
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"})
|
||||
|
||||
async def handle_octoprint_version(self, request):
|
||||
@@ -634,6 +656,14 @@ main{flex:1;overflow-y:auto;padding:20px}
|
||||
.btn-cancel{background:#2d0d0d;color:var(--err);border:1px solid var(--err)}
|
||||
.btn-accent{background:var(--accent);color:#001a24}
|
||||
.btn-sm{padding:7px 12px;font-size:12px}
|
||||
.spd-btn{flex:1;border:1.5px solid var(--border);background:var(--raised);color:var(--txt);
|
||||
border-radius:10px;padding:14px 8px;font-size:13px;font-weight:600;cursor:pointer;
|
||||
transition:all .15s;display:flex;flex-direction:column;align-items:center;gap:4px}
|
||||
.spd-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.spd-btn.spd-active{border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)}
|
||||
.spd-btn .spd-icon{font-size:22px}
|
||||
.spd-bar{height:4px;border-radius:2px;background:var(--border);margin-top:10px;overflow:hidden}
|
||||
.spd-bar-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--accent2),var(--accent));transition:width .3s}
|
||||
|
||||
/* ── TEMPS ── */
|
||||
.temp-pair{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
@@ -884,9 +914,9 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
|
||||
<!-- Kamera -->
|
||||
<div class="card" style="grid-column:1/-1">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
|
||||
<div class="card-title" style="margin-bottom:0"><span>📷</span> Kamera</div>
|
||||
<div class="card-title" style="margin-bottom:0"><span>📷</span> <span id="d-card-cam">Kamera</span></div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:12px;color:var(--txt2)">💡 Licht</span>
|
||||
<span id="d-lbl-light" style="font-size:12px;color:var(--txt2)">💡 Licht</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="d-light-toggle" onchange="setLight()">
|
||||
<span class="toggle-track"></span>
|
||||
@@ -944,7 +974,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
|
||||
</div>
|
||||
</div>
|
||||
<div class="temp-block">
|
||||
<div class="temp-label">Bett</div>
|
||||
<div class="temp-label" id="d-lbl-bed">Bett</div>
|
||||
<div class="temp-row">
|
||||
<div class="temp-val" id="d-bt">–</div>
|
||||
<div class="temp-unit">°C</div>
|
||||
@@ -1002,6 +1032,28 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
|
||||
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)">Schrittweite: <span id="step-display">1</span> mm</div>
|
||||
</div>
|
||||
|
||||
<!-- Print Speed -->
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🏎</span> <span id="d-card-speed">Druckgeschwindigkeit</span></div>
|
||||
<div style="display:flex;gap:8px;margin-top:4px">
|
||||
<button class="spd-btn" id="d-spd-1" onclick="setSpeed(1)">
|
||||
<span class="spd-icon">🐢</span>
|
||||
<span id="d-spd-lbl-1">Leise</span>
|
||||
</button>
|
||||
<button class="spd-btn spd-active" id="d-spd-2" onclick="setSpeed(2)">
|
||||
<span class="spd-icon">⚡</span>
|
||||
<span id="d-spd-lbl-2">Normal</span>
|
||||
</button>
|
||||
<button class="spd-btn" id="d-spd-3" onclick="setSpeed(3)">
|
||||
<span class="spd-icon">🚀</span>
|
||||
<span id="d-spd-lbl-3">Sport</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="spd-bar" style="margin-top:12px">
|
||||
<div class="spd-bar-fill" id="d-spd-bar" style="width:50%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lüfter -->
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🌀</span> <span id="d-card-lightfan">Lüfter</span></div>
|
||||
@@ -1033,8 +1085,8 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
|
||||
<span id="ams-slot-label" style="min-width:48px;font-size:13px;font-weight:600">Slot 1</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<button class="btn" style="flex:1" onclick="amsFeed(1)">⬇ Einziehen</button>
|
||||
<button id="btn-unload" class="btn" style="flex:1" onclick="amsFeed(2)">⬆ Ausziehen</button>
|
||||
<button class="btn" style="flex:1" onclick="amsFeed(1)">⬇ <span class="lbl-feed">Einziehen</span></button>
|
||||
<button id="btn-unload" class="btn" style="flex:1" onclick="amsFeed(2)">⬆ <span class="lbl-unload">Ausziehen</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1061,7 +1113,7 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
|
||||
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
|
||||
print_state:'standby',filename:'',progress:0,print_duration:0,
|
||||
curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–',
|
||||
camera_url:'',fan_speed:0,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 camOn=false;
|
||||
var currentStep=1;
|
||||
@@ -1078,9 +1130,11 @@ function toggleTheme(){
|
||||
// ── i18n ──
|
||||
var LANG_DE={
|
||||
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',
|
||||
card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',
|
||||
card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',
|
||||
speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
||||
lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen',
|
||||
cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera',
|
||||
btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp',
|
||||
label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit',
|
||||
@@ -1101,9 +1155,11 @@ var LANG_DE={
|
||||
};
|
||||
var LANG_EN={
|
||||
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',
|
||||
card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',
|
||||
card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',
|
||||
speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
||||
lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload',
|
||||
cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera',
|
||||
btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop',
|
||||
label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed',
|
||||
@@ -1143,7 +1199,11 @@ function applyLang(){
|
||||
setText('d-card-progress',T.card_progress);
|
||||
setText('d-card-temps',T.card_temps);
|
||||
setText('d-card-lightfan',T.card_light_fan);
|
||||
setText('d-card-speed',T.card_speed);
|
||||
setText('d-card-cam',T.card_cam);
|
||||
setText('d-card-ams',T.panel_ams_title);
|
||||
setText('d-lbl-light',T.lbl_light);
|
||||
setText('d-lbl-bed',T.label_bed);
|
||||
// Dashboard buttons
|
||||
setText('d-btn-pause',T.btn_pause);
|
||||
setText('d-btn-resume',T.btn_resume);
|
||||
@@ -1178,6 +1238,13 @@ function applyLang(){
|
||||
setText('lbl-mode-id',T.settings_mode_id);
|
||||
setText('lbl-update-check',T.update_check);
|
||||
setText('lbl-update-apply',T.update_apply);
|
||||
// Speed buttons
|
||||
setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,''));
|
||||
setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,''));
|
||||
setText('d-spd-lbl-3',T.speed_sport.replace(/^\S+\s/,''));
|
||||
// AMS feed/unload
|
||||
document.querySelectorAll('.lbl-feed').forEach(e=>e.textContent=T.lbl_feed);
|
||||
document.querySelectorAll('.lbl-unload').forEach(e=>e.textContent=T.lbl_unload);
|
||||
}
|
||||
function setText(id,txt){var el=document.getElementById(id);if(el)el.textContent=txt;}
|
||||
(function(){
|
||||
@@ -1268,6 +1335,15 @@ function applyState(){
|
||||
var dfan=document.getElementById('d-fan');if(dfan)dfan.value=s.fan_speed;
|
||||
var dfanval=document.getElementById('d-fan-val');if(dfanval)dfanval.textContent=s.fan_speed;
|
||||
|
||||
// speed mode buttons
|
||||
var spdWidths={1:25,2:55,3:90};
|
||||
[1,2,3].forEach(function(m){
|
||||
var b=document.getElementById('d-spd-'+m);
|
||||
if(b) b.classList.toggle('spd-active', s.print_speed_mode===m);
|
||||
});
|
||||
var spdBar=document.getElementById('d-spd-bar');
|
||||
if(spdBar) spdBar.style.width=(spdWidths[s.print_speed_mode]||55)+'%';
|
||||
|
||||
// AMS
|
||||
if(s.ams_slots&&s.ams_slots.length){
|
||||
var html='';
|
||||
@@ -1475,7 +1551,7 @@ function setNozzle(){
|
||||
function setBed(){
|
||||
var v=parseFloat(document.getElementById('p-bed-inp').value||0);
|
||||
post('/api/temperature',{nozzle:S.nozzle_target,bed:v})
|
||||
.then(function(){clog('Bett → '+v+'°C','msg-ok')})
|
||||
.then(function(){clog(T.label_bed+' → '+v+'°C','msg-ok')})
|
||||
.catch(function(e){clog('Temp-Fehler: '+e,'msg-err')});
|
||||
}
|
||||
|
||||
@@ -1487,6 +1563,17 @@ function setLight(){
|
||||
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
|
||||
}
|
||||
|
||||
// ── Print Speed ──
|
||||
function setSpeed(mode){
|
||||
S.print_speed_mode=mode;
|
||||
[1,2,3].forEach(function(m){
|
||||
var b=document.getElementById('d-spd-'+m);
|
||||
if(b) b.classList.toggle('spd-active',m===mode);
|
||||
});
|
||||
post('/api/speed',{mode:mode})
|
||||
.catch(function(e){clog('Speed-Fehler: '+e,'msg-err')});
|
||||
}
|
||||
|
||||
// ── Fan ──
|
||||
function setFan(){
|
||||
var v=parseInt(document.getElementById('d-fan').value);
|
||||
@@ -1507,7 +1594,7 @@ function quickFan(v){
|
||||
function amsFeed(type){
|
||||
var slot=parseInt(document.getElementById('ams-slot-sel').value);
|
||||
post('/api/ams/feed',{slot_index:slot,type:type})
|
||||
.then(function(){clog((type===1?'Einziehen':'Ausziehen')+' Slot '+(slot+1),'msg-ok')})
|
||||
.then(function(){clog((type===1?T.lbl_feed:T.lbl_unload)+' Slot '+(slot+1),'msg-ok')})
|
||||
.catch(function(e){clog('AMS-Fehler: '+e,'msg-err')});
|
||||
}
|
||||
|
||||
@@ -1597,6 +1684,21 @@ function toggleCam(){if(camOn)camStop();else camStart()}
|
||||
self._state["fan_speed"] = speed
|
||||
return web.json_response({"result": "ok"})
|
||||
|
||||
async def handle_api_speed(self, request):
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
mode = int(body.get("mode", 2))
|
||||
loop = asyncio.get_event_loop()
|
||||
taskid = self._state.get("taskid", "-1")
|
||||
await loop.run_in_executor(None, lambda: self.client.publish_web(
|
||||
"print", "update",
|
||||
{"taskid": taskid, "settings": {"print_speed_mode": mode}},
|
||||
))
|
||||
self._state["print_speed_mode"] = mode
|
||||
return web.json_response({"result": "ok"})
|
||||
|
||||
async def handle_api_ams_feed(self, request):
|
||||
try:
|
||||
body = await request.json()
|
||||
@@ -1642,18 +1744,29 @@ function toggleCam(){if(camOn)camStop();else camStart()}
|
||||
nozzle = body.get("nozzle")
|
||||
bed = body.get("bed")
|
||||
loop = asyncio.get_event_loop()
|
||||
if nozzle is not None:
|
||||
n = int(float(nozzle))
|
||||
printing = self._state.get("print_state") == "printing"
|
||||
if printing:
|
||||
# During print: runtime update via web/printer topic, one setting at a time
|
||||
taskid = self._state.get("taskid", "-1")
|
||||
if nozzle is not None:
|
||||
n = int(float(nozzle))
|
||||
await loop.run_in_executor(None, lambda: self.client.publish_web(
|
||||
"print", "update",
|
||||
{"taskid": taskid, "settings": {"target_nozzle_temp": n}},
|
||||
))
|
||||
if bed is not None:
|
||||
b = int(float(bed))
|
||||
await loop.run_in_executor(None, lambda: self.client.publish_web(
|
||||
"print", "update",
|
||||
{"taskid": taskid, "settings": {"target_hotbed_temp": b}},
|
||||
))
|
||||
else:
|
||||
# Idle: standard tempature/set with both values
|
||||
n = int(float(nozzle)) if nozzle is not None else int(self._state["nozzle_target"])
|
||||
b = int(float(bed)) if bed is not None else int(self._state["bed_target"])
|
||||
await loop.run_in_executor(None, lambda: self.client.publish(
|
||||
"tempature", "set",
|
||||
{"type": 0, "target_nozzle_temp": n, "target_hotbed_temp": 0},
|
||||
timeout=0
|
||||
))
|
||||
if bed is not None:
|
||||
b = int(float(bed))
|
||||
await loop.run_in_executor(None, lambda: self.client.publish(
|
||||
"tempature", "set",
|
||||
{"type": 1, "target_hotbed_temp": b, "target_nozzle_temp": 0},
|
||||
{"target_nozzle_temp": n, "target_hotbed_temp": b},
|
||||
timeout=0
|
||||
))
|
||||
return web.json_response({"result": "ok"})
|
||||
@@ -1785,6 +1898,7 @@ function toggleCam(){if(camOn)camStop();else camStart()}
|
||||
"filename": s["filename"],
|
||||
"camera_url": s["camera_url"],
|
||||
"fan_speed": s["fan_speed"],
|
||||
"print_speed_mode": s["print_speed_mode"],
|
||||
"light_on": s["light_on"],
|
||||
"light_brightness": s["light_brightness"],
|
||||
"ams_slots": self._ams_slots,
|
||||
@@ -2241,6 +2355,7 @@ def build_app(bridge: KobraXBridge) -> web.Application:
|
||||
# New API endpoints
|
||||
r.add_post("/api/light", bridge.handle_api_light)
|
||||
r.add_post("/api/fan", bridge.handle_api_fan)
|
||||
r.add_post("/api/speed", bridge.handle_api_speed)
|
||||
r.add_post("/api/ams/feed", bridge.handle_api_ams_feed)
|
||||
r.add_post("/api/axis", bridge.handle_api_axis)
|
||||
r.add_post("/api/temperature", bridge.handle_api_temperature)
|
||||
|
||||
Reference in New Issue
Block a user