Compare commits

..

11 Commits

14 changed files with 117 additions and 206 deletions

View File

@@ -1,30 +1,5 @@
# Changelog
## [0.9.20] 2026-06-08
### Neu
- **Französische Sprachunterstützung (PR #45 von @Nathacks)**
- **Z-Höhe in der Print-UI (PR #49 von @Nathacks).** Zeigt die aktuelle
Z-Position in mm unterhalb des Layer-Zählers.
### Behoben
- **Kamera-Autostart ignorierte das "Kamera bei Druckstart einschalten"-
Setting nach einem Bridge-Restart (Issue #50).** Das Setting wurde in
der Prozessumgebung gecacht — nach dem Speichern in der UI überlebte
der alte Wert den Restart und der neue Wert aus `config.ini` wurde
nicht gelesen.
- **Kamera startete nach manuellem Stopp während eines Drucks automatisch
neu (Issue #50).** Ein neues `_camera_user_stopped`-Flag unterdrückt
den Autostart für die aktuelle Drucksitzung. Es wird beim Druckende
zurückgesetzt.
- **Falscher "Stream nicht verfügbar"-Fehler-Toast beim manuellen
Kamera-Stopp.** Der Bild-Fehler-Handler war noch registriert als
`img.src` geleert wurde.
- **JS-Fehler (`ReferenceError: br is not defined`) beim Licht-Toggle.**
Variable wurde aus dem falschen Scope referenziert.
- Webcam-URLs sind jetzt absolut, damit Mobileraker/Obico-Clients sie
erreichen können.
## [0.9.19.1] 2026-06-04
### Behoben

View File

@@ -1,51 +1,24 @@
# Changelog
## [0.9.20] 2026-06-08
### New
- **French language support (PR #45 by @Nathacks)**
- **Z height display in the print UI (PR #49 by @Nathacks).** Shows
current Z position in mm below the layer counter.
### Fixed
- **Camera auto-start ignored "Enable camera on print start" setting
after a bridge restart (issue #50).** The setting was cached in the
process environment — after saving it in the UI, the old value
survived the restart and the new value from `config.ini` was never
read.
- **Camera restarted automatically after manual stop during a print
(issue #50).** A new `_camera_user_stopped` flag suppresses
auto-restart for the current print session. It resets when the
print ends.
- **Spurious "stream unavailable" error toast when stopping the camera
manually.** The image error handler was still registered when
`img.src` was cleared.
- **JS error (`ReferenceError: br is not defined`) when toggling the
light.** Variable was referenced from the wrong scope.
- Webcam URLs are now absolute so that Mobileraker/Obico clients can
reach them.
## [0.9.19.1] 2026-06-04
### Fixed
- Standalone binaries (Linux/Windows) reported `vunknown` as their
version. The `VERSION` file is now embedded into the PyInstaller
onefile bundle.
- When the TLS certificates (`anycubic_slicer.crt`/`.key`) were
missing, the bridge only logged the raw `[Errno 2] No such file
or directory`. It now states clearly where the files need to be
placed and that `anycubic-certs.zip` from the Gitea release is the
source.
- Standalone-Binaries (Linux/Windows) zeigten `vunknown` als Version.
Die `VERSION`-Datei ist jetzt ins PyInstaller-Onefile eingebettet.
- Bei fehlenden TLS-Zertifikaten (`anycubic_slicer.crt`/`.key`) gab
es nur den rohen Fehler `[Errno 2] No such file or directory`. Die
Bridge meldet jetzt klar, wo die Dateien hingelegt werden müssen
und dass `anycubic-certs.zip` aus dem Gitea-Release stammt.
### Changed
- Filament profile list re-curated: 209 entries instead of 399.
Profiles that only exist inside printer-specific vendor bundles
(e.g. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) were dropped —
OrcaSlicer wouldn't have found them in a default Kobra X setup
anyway, because the matching vendor bundle is only loaded when
the corresponding printer vendor is active. For those filaments
the custom profile import (issue #41) remains the way.
- Filament-Profil-Liste neu kuratiert: 209 statt 399 Einträge.
Profile die nur für drucker-spezifische Vendor-Bundles existieren
(z.B. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) sind rausgeflogen
OrcaSlicer hätte sie im Standard-Kobra-X-Setup beim Sync
ohnehin nicht gefunden, weil die jeweiligen Vendor-Bundles nur
bei aktivem Drucker-Vendor geladen werden. Für solche Filamente
bleibt der Custom-Profile-Import (Issue #41) der Weg.
## [0.9.19] 2026-06-02

View File

@@ -2,8 +2,6 @@ FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

View File

@@ -67,17 +67,44 @@ docker compose up -d
**Linux-Binary (kein Docker):**
```bash
chmod +x kx-bridge && ./kx-bridge
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
```
**Windows-EXE (kein Docker):**
```
kx-bridge.exe
```
> `config\` und `data\` werden neben der EXE angelegt — portabel.
> Bei Linux- und Windows-Binary liegen `config/` und `data/` (Einstellungen, SQLite,
> GCode-Store) jeweils neben dem Programm. Einfach den ganzen Ordner kopieren = umziehen.
> ⚠️ **TLS-Zertifikate für Standalone-Binary nötig**
>
> Die Bridge spricht per mTLS mit dem Drucker-MQTT und braucht zwei
> Zertifikat-Dateien **direkt neben dem Binary**:
>
> - `anycubic_slicer.crt`
> - `anycubic_slicer.key`
>
> Beide liegen im **`anycubic-certs.zip`** auf derselben Release-Seite.
> Lade die ZIP herunter und entpacke die beiden Dateien in dasselbe
> Verzeichnis wie `kx-bridge-linux-amd64` bzw. `kx-bridge.exe`. Ohne
> die Zertifikate siehst du `Verbindung fehlgeschlagen: TLS-Zertifikate
> fehlen …` (0.9.19.1+) oder `[Errno 2] No such file or directory`
> (ältere Builds).
>
> So muss es aussehen:
> ```
> ~/kx-bridge/
> ├── kx-bridge-linux-amd64 (oder kx-bridge.exe)
> ├── anycubic_slicer.crt ← aus anycubic-certs.zip
> ├── anycubic_slicer.key ← aus anycubic-certs.zip
> └── config/ (wird beim ersten Start angelegt)
> ```
>
> Docker-User müssen das nicht machen — die Zertifikate sind im Image
> enthalten.
> Bei Linux- und Windows-Binary liegen `config/` und `data/` (Einstellungen,
> SQLite, GCode-Store) jeweils neben dem Programm. Einfach den ganzen Ordner
> kopieren = umziehen.
**Python direkt:**
```bash
@@ -130,7 +157,7 @@ Für sauberen AMS-Filament-Sync gibt es einen **gepatchten OrcaSlicer-Build**:
- Vendor-Match wenn `tray_info_idx` gesetzt ist, das Preset aber inkompatibel
- Zwei-Pass-Suche: erst kompatible Presets, dann alle sichtbaren
**Warum das zusammen wichtig ist:** ohne #13719 landen die AMS-Slots in OrcaSlicer alle auf `Generic PLA` / `Generic PETG`, obwohl die Bridge die konkrete Marke schon mitsendet (`name + vendor_name + gate_filament_name`). Mit dem KX-Build matched OrcaSlicer deine echten User-Presets — auch die, die du via [Eigene OrcaSlicer-Profile importieren](docs/filament-preset-bridge-guide.md) in die Bridge gezogen hast.
**Warum das zusammen wichtig ist:** ohne #13719 landen die AMS-Slots in OrcaSlicer alle auf `Generic PLA` / `Generic PETG`, obwohl die Bridge die konkrete Marke schon mitsendet (`name + vendor_name + gate_filament_name`). Mit dem KX-Build matched OrcaSlicer deine echten User-Presets — auch die, die du via [Eigene OrcaSlicer-Profile importieren](https://gitea.it-drui.de/viewit/KX-Bridge-Release/src/branch/master/docs/filament-preset-bridge-guide.md) in die Bridge gezogen hast.
Stock-Upstream-OrcaSlicer funktioniert für Slicing und Drucken weiterhin — nur das Per-Slot-Vendor-Matching beim AMS-Sync fällt dann weg. Material und Farbe pro Slot kannst du auch ohne den KX-Build über die Bridge ans Drucker-Display schreiben (das läuft über MQTT, nicht über den Slicer).

View File

@@ -30,7 +30,7 @@ officially tested or supported. Feedback welcome.</sub>
---
## Features
## Features
| | |
|---|---|
@@ -50,7 +50,7 @@ officially tested or supported. Feedback welcome.</sub>
---
## 🚀 Quick Start
## Quick Start
### 1. Prepare the printer
@@ -66,17 +66,43 @@ docker compose up -d
**Linux binary (no Docker):**
```bash
chmod +x kx-bridge && ./kx-bridge
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
```
**Windows EXE (no Docker):**
```
kx-bridge.exe
```
> `config\` and `data\` are created next to the EXE — portable.
> With the Linux and Windows binaries, `config/` and `data/` (settings, SQLite, GCode store)
> live next to the program. Copy the whole folder = move the installation.
> ⚠️ **TLS certificates required for the standalone binary**
>
> The bridge talks to the printer's MQTT over mTLS and needs two
> certificate files **right next to the binary**:
>
> - `anycubic_slicer.crt`
> - `anycubic_slicer.key`
>
> Both ship inside **`anycubic-certs.zip`** on the same release page.
> Download it and extract the two files into the same directory as
> `kx-bridge-linux-amd64` / `kx-bridge.exe`. Without them you'll see
> `Verbindung fehlgeschlagen: TLS-Zertifikate fehlen …` (0.9.19.1+)
> or `[Errno 2] No such file or directory` (older builds).
>
> Working layout:
> ```
> ~/kx-bridge/
> ├── kx-bridge-linux-amd64 (or kx-bridge.exe)
> ├── anycubic_slicer.crt ← from anycubic-certs.zip
> ├── anycubic_slicer.key ← from anycubic-certs.zip
> └── config/ (auto-created on first run)
> ```
>
> Docker users don't need to do this — the certs are baked into the
> image.
> With the Linux and Windows binaries, `config/` and `data/` (settings,
> SQLite, GCode store) live next to the program. Copy the whole folder
> = move the installation.
**Python directly:**
```bash
@@ -104,13 +130,13 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125`
---
## 📺 Video Tutorial
## Video Tutorial
[![KX-Bridge Setup & Usage](https://img.youtube.com/vi/1Ql4wfH27fM/hqdefault.jpg)](https://www.youtube.com/watch?v=1Ql4wfH27fM)
---
## 🎨 Recommended Slicer
## Recommended Slicer
For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
@@ -129,7 +155,7 @@ For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
- Vendor match when `tray_info_idx` is set but its preset is incompatible
- Two-pass lookup: first compatible presets, then all visible ones
**Why this matters:** without #13719 the AMS slots in OrcaSlicer all fall back to `Generic PLA` / `Generic PETG` even though the bridge already sends the concrete brand (`name + vendor_name + gate_filament_name`). With the KX build OrcaSlicer matches your actual user presets — including profiles you imported into the bridge via the [Import your own OrcaSlicer profiles](docs/filament-preset-bridge-guide.md) flow.
**Why this matters:** without #13719 the AMS slots in OrcaSlicer all fall back to `Generic PLA` / `Generic PETG` even though the bridge already sends the concrete brand (`name + vendor_name + gate_filament_name`). With the KX build OrcaSlicer matches your actual user presets — including profiles you imported into the bridge via the [Import your own OrcaSlicer profiles](https://gitea.it-drui.de/viewit/KX-Bridge-Release/src/branch/master/docs/filament-preset-bridge-guide.md) flow.
Stock upstream OrcaSlicer still works for slicing and printing — you just lose the per-slot brand matching on AMS sync. Slot material + colour can still be pushed bridge → printer either way (that goes over MQTT, not via the slicer).
@@ -137,7 +163,7 @@ OrcaSlicer-KX is a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer
---
## 🏠 Community & Integrations
## Community & Integrations
- **[Home Assistant integration](https://github.com/gangoke/kobrax-lan-hass-component)**
by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls,
@@ -154,7 +180,7 @@ OrcaSlicer-KX is a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer
---
## 🔧 Getting credentials manually
## Getting credentials manually
Normally not needed — *"+ Add printer"* does this automatically. If you do need it:
@@ -171,7 +197,7 @@ Alternatively (if the IP is unknown): open AnycubicSlicerNext, connect the print
---
## ⚙️ Useful commands
## Useful commands
```bash
docker compose logs -f # show logs
@@ -182,7 +208,7 @@ docker compose up -d --build # rebuild locally (instead of pulling)
---
## 🩹 Troubleshooting
## Troubleshooting
<details>
<summary><b>"Wrong MQTT credentials" on start</b></summary>
@@ -216,7 +242,7 @@ Migration runs automatically on first start after the upgrade — no action requ
---
## 🔒 Security
## Security
- The bridge is reachable on the local network at `http://<host-IP>:7125`**do not** expose it to the internet
- `config/config.ini` contains printer credentials — do not share publicly
@@ -224,7 +250,7 @@ Migration runs automatically on first start after the upgrade — no action requ
---
## 📄 License
## License
[![License: GPL v3](https://img.shields.io/badge/License-GPL_v3-blue.svg)](LICENSE)

View File

@@ -1 +1 @@
0.9.20
0.9.19.1

View File

@@ -222,7 +222,7 @@ def _parse_gcode_estimated_time(data: bytes) -> int:
elif unit == "m": secs += int(val) * 60
elif unit == "s": secs += int(val)
if secs:
log.info(f"Slicer estimate: {secs}s ({m.group(1).strip()})")
log.info(f"Slicer-Schätzzeit: {secs}s ({m.group(1).strip()})")
return secs
@@ -798,7 +798,6 @@ class KobraXBridge:
self._serve_dir_path: str = self._store._gcode_dir
self._current_job_id: str = ""
self._camera_autostarted: bool = False
self._camera_user_stopped: bool = False # User hat Kamera während Druck manuell gestoppt
self.camera_cache: CameraCache = CameraCache()
self._thumbnail_b64: str = ""
@@ -810,7 +809,7 @@ class KobraXBridge:
# Theme-Name prüfen (keine Sonderzeichen oder Umlaute)
raw_theme = (getattr(args, "ui_theme", None) or "default").strip()
if not _UI_THEME_NAME_RE.match(raw_theme):
log.warning("Invalid UI theme name %r using default", raw_theme)
log.warning("Ungültiger UI-Theme-Name %r nutze default", raw_theme)
raw_theme = "default"
self._ui_theme = raw_theme
self._index_tpl_cache: str | None = None
@@ -915,9 +914,7 @@ class KobraXBridge:
# Zentral hier, damit es alle Druck-Startwege abdeckt (OrcaSlicer + UI).
# _camera_autostarted verhindert Mehrfach-Trigger pro Druck.
if kobra_state == "printing":
if (getattr(self._args, "camera_on_print", 0)
and not self._camera_autostarted
and not self._camera_user_stopped):
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
self._camera_autostarted = True
try:
self.client.start_camera()
@@ -926,7 +923,6 @@ class KobraXBridge:
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
elif kobra_state in ("free", "finished", "stoped", "canceled"):
self._camera_autostarted = False
self._camera_user_stopped = False # für nächsten Druck freigeben
# Job-History: Druckstart erkennen
if kobra_state == "printing" and not self._current_job_id:
@@ -938,7 +934,7 @@ class KobraXBridge:
gcode_file_id=gf["id"],
printer_id=self._printer_id,
)
log.info(f"Job started: {self._current_job_id} for {filename}")
log.info(f"Job gestartet: {self._current_job_id} für {filename}")
# Job-History: Druckende erkennen
if kobra_state in ("finished",) and self._current_job_id:
@@ -1005,9 +1001,7 @@ class KobraXBridge:
# Kamera-Autostart auch hier (OrcaSlicer meldet Start oft via info/report).
# _camera_autostarted-Guard verhindert Doppel-Start mit _on_print.
if kobra_state == "printing":
if (getattr(self._args, "camera_on_print", 0)
and not self._camera_autostarted
and not self._camera_user_stopped):
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
self._camera_autostarted = True
try:
self.client.start_camera()
@@ -1016,7 +1010,6 @@ class KobraXBridge:
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
elif kobra_state in ("free", "finished", "stoped", "canceled"):
self._camera_autostarted = False
self._camera_user_stopped = False # für nächsten Druck freigeben
if project:
if "filename" in project:
self._state["filename"] = project["filename"]
@@ -1082,7 +1075,7 @@ class KobraXBridge:
if filename:
try:
self._store.update_file_objects(filename, objs, svg)
log.info(f"Skip objects for {filename}: {len(objs)} ({'with SVG' if svg else 'no SVG'})")
log.info(f"Skip-Objekte für {filename}: {len(objs)} ({'mit SVG' if svg else 'ohne SVG'})")
except Exception as e:
log.warning(f"update_file_objects fehlgeschlagen: {e}")
self._push_status_update()
@@ -1891,41 +1884,6 @@ class KobraXBridge:
"gcode_macro TIMELAPSE_TAKE_FRAME": {
"is_paused": False,
},
# configfile stub — Mobileraker und andere Clients crashen ohne
# dieses Objekt (Missing field: configFile). Werte aus der
# entschlüsselten avata_main.conf (ACCFG1.0 — Kobra X Firmware).
"configfile": {
"config": {},
"settings": {
"printer": {
"kinematics": "cartesian",
"max_velocity": 450,
"max_accel": 10000,
"max_z_velocity": 12,
"max_z_accel": 100,
"square_corner_velocity": 20.0,
},
"extruder": {
"nozzle_diameter": 0.4,
"min_temp": 0,
"max_temp": 320,
"min_extrude_temp": 10,
},
"heater_bed": {
"min_temp": 0,
"max_temp": 120,
},
"stepper_x": {"position_min": -18.5, "position_max": 280},
"stepper_y": {"position_min": -6.5, "position_max": 272.5},
"stepper_z": {"position_min": -4, "position_max": 262},
"virtual_sdcard": {"path": "/data/gcodes"},
"pause_resume": {},
"display_status": {},
},
"warnings": [],
"save_config_pending": False,
"save_config_pending_items": {},
},
}
# -------------------------------------------------------------------------
@@ -2697,20 +2655,7 @@ class KobraXBridge:
return web.json_response({"result": {"count": len(result_jobs), "jobs": result_jobs}})
async def handle_webcams_list(self, request):
"""Moonraker /server/webcams/list — Obico holt die Webcam-URLs hier.
Wenn der Client von einem anderen Host kommt (z.B. moonraker-obico auf
separatem Server), braucht er absolute URLs damit er den Stream erreicht.
Host-Header mit localhost/127.0.0.1 wird durch die echte LAN-IP ersetzt."""
host_hdr = request.headers.get("Host", "") if request else ""
host_name = (host_hdr or "").split(":")[0]
port_part = f":{host_hdr.split(':')[1]}" if ":" in (host_hdr or "") else f":{self._args.port}"
local_ip = getattr(self, "_local_ip", None) or host_name
if host_name in ("localhost", "127.0.0.1", ""):
host_name = local_ip
base = f"http://{host_name}{port_part}"
stream_url = f"{base}/api/camera/stream"
snapshot_url = f"{base}/api/camera/snapshot"
"""Moonraker /server/webcams/list — Obico holt die Webcam-URLs hier."""
return web.json_response({
"result": {
"webcams": [
@@ -2722,8 +2667,8 @@ class KobraXBridge:
"icon": "mdiWebcam",
"target_fps": 5,
"target_fps_idle": 2,
"stream_url": stream_url,
"snapshot_url": snapshot_url,
"stream_url": "/api/camera/stream",
"snapshot_url": "/api/camera/snapshot",
"flip_horizontal": False,
"flip_vertical": False,
"rotation": 0,
@@ -2908,7 +2853,7 @@ class KobraXBridge:
log.info(f"print/start → {filename} url={url} ams={len(ams_box_mapping)} slots mode={self._filament_mode}")
result = self.client.publish("print", "start", payload, timeout=15.0)
if result:
log.info(f"Print start confirmed: state={result.get('state')}")
log.info(f"Druckstart bestätigt: state={result.get('state')}")
else:
log.warning("Druckstart: keine Antwort vom Drucker")
@@ -3163,7 +3108,7 @@ class KobraXBridge:
return web.json_response({"result": "disconnected"})
async def handle_api_restart(self, request):
log.info("Restart requested via API")
log.info("Neustart über API angefordert")
response = web.json_response({"status": "restarting"})
asyncio.get_event_loop().call_later(0.3, self._restart_bridge)
return response
@@ -3443,9 +3388,6 @@ class KobraXBridge:
await loop.run_in_executor(None, lambda: self.client.publish(
"video", "stopCapture", None, timeout=0
))
# Verhindert dass der Autostart-Guard die Kamera während des
# laufenden Drucks wieder einschaltet (State-Flicker-Problem).
self._camera_user_stopped = True
return web.json_response({"result": "ok"})
async def handle_api_camera_snapshot(self, request):
@@ -3505,7 +3447,7 @@ class KobraXBridge:
stderr=asyncio.subprocess.DEVNULL,
)
except (FileNotFoundError, OSError) as e:
log.warning("Camera: ffmpeg not found camera stream unavailable")
log.warning("Kamera: ffmpeg nicht gefunden Kamerastream nicht verfügbar")
return web.Response(status=503, text="ffmpeg not found")
except Exception as e:
log.warning(f"Kamera: ffmpeg konnte nicht gestartet werden: {e}")
@@ -3600,7 +3542,7 @@ class KobraXBridge:
if not os.path.isfile(serve_path):
return web.Response(status=404, text="not found")
size = os.path.getsize(serve_path)
log.info(f"Printer downloading file: {filename} ({size} bytes)")
log.info(f"Drucker lädt Datei ab: {filename} ({size} bytes)")
return web.FileResponse(serve_path, headers={
"Content-Disposition": f'attachment; filename="{filename}"'
})
@@ -3638,7 +3580,6 @@ class KobraXBridge:
"remain_time": s["remain_time"],
"curr_layer": s["curr_layer"],
"total_layers": s["total_layers"],
"z_mm": self._estimate_current_z(),
"filename": s["filename"],
"slicer_time": slicer_time,
"camera_url": s["camera_url"],
@@ -3916,7 +3857,7 @@ class KobraXBridge:
with open(config_path, "w", encoding="utf-8") as f:
f.write("# KX-Bridge Konfigurationsdatei\n\n")
cfg.write(f)
log.info(f"Printer '{name or creds['model']}' added as {sec} (port {new_port})")
log.info(f"Drucker '{name or creds['model']}' als {sec} hinzugefügt (Port {new_port})")
response = self._json_cors({"status": "restarting", "section": sec, "http_port": new_port})
asyncio.get_event_loop().call_later(0.5, self._restart_bridge)
return response
@@ -3991,13 +3932,13 @@ class KobraXBridge:
# die alten Werte statt der geänderten config.ini.
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "BRIDGE_PRINTER_NAME"):
"BRIDGE_PRINTER_NAME"):
os.environ.pop(_k, None)
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
if in_docker:
# Docker/systemd: Prozess beenden reicht der Supervisor startet neu (frische environ)
log.info("Container environment detected exiting for supervisor restart")
log.info("Container-Umgebung erkannt beende Prozess für Supervisor-Restart")
os._exit(0)
frozen = getattr(sys, "frozen", False)
@@ -4176,7 +4117,7 @@ class KobraXBridge:
if fname == "kobrax_moonraker_bridge.py":
return web.json_response(
{"error": f"Download {fname}: HTTP {resp.status}"}, status=502)
log.warning(f"Update: {fname} not found in release ({resp.status}) skipped")
log.warning(f"Update: {fname} nicht im Release ({resp.status}) übersprungen")
continue
downloaded.append((app_dir / fname, await resp.read()))
# Phase 2: atomar ersetzen (erst nach komplettem, erfolgreichem Download)
@@ -4453,14 +4394,11 @@ class KobraXBridge:
# Obico registriert obico_remote_event-Callback. Wir akzeptieren leer.
result = "ok"
elif method == "server.webcams.list":
# WS-Variante: absolute URL mit echter LAN-IP statt localhost
_lip = getattr(self, "_local_ip", None) or "127.0.0.1"
_base = f"http://{_lip}:{self._args.port}"
# WS-Variante des HTTP-Endpoints
result = {"webcams": [{
"name": "KX-Bridge", "location": "printer", "service": "mjpegstreamer",
"enabled": True,
"stream_url": f"{_base}/api/camera/stream",
"snapshot_url": f"{_base}/api/camera/snapshot",
"enabled": True, "stream_url": "/api/camera/stream",
"snapshot_url": "/api/camera/snapshot",
"flip_horizontal": False, "flip_vertical": False, "rotation": 0,
"target_fps": 5, "aspect_ratio": "16:9",
}]}
@@ -4510,7 +4448,7 @@ class KobraXBridge:
log.debug(f"Unbekannte RPC-Methode: {method}")
result = {}
except Exception as e:
log.error(f"RPC error for {method}: {e}")
log.error(f"RPC-Fehler für {method}: {e}")
error = {"code": -32603, "message": str(e)}
if rpc_id is not None:
@@ -4824,7 +4762,7 @@ async def run_bridge(args):
site = web.TCPSite(runner, args.host, per_args.port)
await site.start()
runners.append((runner, client, pid))
log.info(f"[Printer {pid}] Bridge running on http://{args.host}:{per_args.port}")
log.info(f"[Drucker {pid}] Bridge läuft auf http://{args.host}:{per_args.port}")
import socket as _socket
try:
@@ -4833,9 +4771,6 @@ async def run_bridge(args):
_local_ip = _s.getsockname()[0]
except Exception:
_local_ip = args.host
# An alle Bridge-Instanzen weitergeben — wird für absolute Webcam-URLs genutzt
for _b in all_bridges.values():
_b._local_ip = _local_ip
log.info(f"OrcaSlicer → Klipper → Host: {_local_ip} Ports: " +
", ".join(str(getattr(b._args, 'port', 0)) for b in all_bridges.values()))
log.info("Ctrl-C zum Beenden")

View File

@@ -1,12 +1,11 @@
// ── State ──
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
curr_layer:0,total_layers:0,z_mm: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:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]},web_upload_warning:1};
var tempHistory={n:[],b:[]};
var camOn=false;
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
var currentStep=1;
var currentPanel='dashboard';
var aceAutoRefillPrefs=(function(){
@@ -102,7 +101,6 @@ function tr(key,fallback){
function _langToggleLabel(lang){
if(lang==='de')return 'Deutsch';
if(lang==='en')return 'English';
if(lang==='fr')return 'Français';
if(lang==='zh-cn')return '简体中文';
return 'Espanol';
}
@@ -284,7 +282,6 @@ function applyLang(){
setText('d-lbl-remain',T.lbl_remaining);
setText('d-slicer-label',T.lbl_slicer_time);
setText('d-lbl-layers',T.lbl_layers);
setText('d-lbl-zpos',T.lbl_zpos);
setText('d-lbl-light',T.lbl_light);
setText('d-lbl-nozzle',T.label_nozzle);
setText('d-lbl-bed',T.label_bed);
@@ -662,7 +659,6 @@ function applyState(){
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'';
var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers;
var dzpos=document.getElementById('d-zpos');if(dzpos)dzpos.textContent=s.z_mm>0?s.z_mm.toFixed(2)+' mm':'';
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration);
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'';
@@ -826,14 +822,10 @@ function applyState(){
var co=document.getElementById('cam-overlay');
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
// auto-start camera during print (unless user explicitly stopped it)
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){
// auto-start camera during print
if(s.print_state==='printing'&&!camOn&&s.camera_url){
camStart();
}
// reset user-stopped flag when print ends so next print auto-starts again
if(s.print_state!=='printing'){
camUserStopped=false;
}
updateConnBtn();
}
@@ -1445,8 +1437,7 @@ function setBed(){
function setLight(){
var on=document.getElementById('d-light-toggle').checked;
post('/api/light',{on:on,brightness:80})
.then(function(){clog('Licht '+(on?'an, 80%':'aus'),'msg-ok')})
.then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')})
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
}
@@ -1527,13 +1518,11 @@ function camStart(){
function camStop(){
var img=document.getElementById('cam-img');
img.onerror=null; // deregister error handler before clearing src to avoid spurious error toast
post('/api/camera/stop',{}).catch(function(){});
img.src='';
img.style.display='none';
document.getElementById('cam-placeholder').style.display='flex';
camOn=false;
camUserStopped=true; // suppress auto-restart for remainder of this print
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
clog(tr('log_cam_stop'),'msg-ok');
}

View File

@@ -267,15 +267,9 @@
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center">
<div class="time-label" id="d-lbl-layers"></div>
<div class="time-val" style="font-size:16px" id="d-layers"></div>
</div>
<div class="time-block" style="padding:4px 10px;min-width:72px;text-align:center">
<div class="time-label" id="d-lbl-zpos">Z</div>
<div class="time-val" style="font-size:13px" id="d-zpos"></div>
</div>
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0">
<div class="time-label" id="d-lbl-layers"></div>
<div class="time-val" style="font-size:16px" id="d-layers"></div>
</div>
</div>
<div class="time-grid">

View File

@@ -37,7 +37,6 @@
"lbl_remaining": "Restzeit:",
"lbl_slicer_time": "Slicer-Schätzung:",
"lbl_layers": "Layer",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 Leise",
"speed_normal": "⚡ Normal",
"speed_sport": "🚀 Sport",

View File

@@ -37,7 +37,6 @@
"lbl_remaining": "Remaining:",
"lbl_slicer_time": "Slicer estimate:",
"lbl_layers": "Layer",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 Silent",
"speed_normal": "⚡ Normal",
"speed_sport": "🚀 Sport",

View File

@@ -37,7 +37,6 @@
"lbl_remaining": "Restante:",
"lbl_slicer_time": "Estimación del slicer:",
"lbl_layers": "Capa",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 Silencioso",
"speed_normal": "⚡ Normal",
"speed_sport": "🚀 Sport",

View File

@@ -37,7 +37,6 @@
"lbl_remaining": "Restant :",
"lbl_slicer_time": "Estimation slicer :",
"lbl_layers": "Couche",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 Silencieux",
"speed_normal": "⚡ Normal",
"speed_sport": "🚀 Sport",
@@ -246,4 +245,3 @@
"ss_name": "AZ Nom",
"ss_dur": "⏱ Durée d'impression"
}

View File

@@ -37,7 +37,6 @@
"lbl_remaining": "剩余时间:",
"lbl_slicer_time": "切片预估:",
"lbl_layers": "层",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 静音",
"speed_normal": "⚡ 标准",
"speed_sport": "🚀 运动",