diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md index bf03f56..cea68e6 100644 --- a/CHANGELOG.de.md +++ b/CHANGELOG.de.md @@ -1,5 +1,30 @@ # 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index f8247fe..7931b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,51 @@ # 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) 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. +- 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. ### Changed -- 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. +- 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. ## [0.9.19] – 2026-06-02 diff --git a/Dockerfile b/Dockerfile index 18ac4f6..2e34be3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,8 @@ 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 diff --git a/README.de.md b/README.de.md index ea80161..e6931e4 100644 --- a/README.de.md +++ b/README.de.md @@ -67,44 +67,17 @@ docker compose up -d **Linux-Binary (kein Docker):** ```bash -chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64 +chmod +x kx-bridge && ./kx-bridge ``` **Windows-EXE (kein Docker):** ``` kx-bridge.exe ``` +> `config\` und `data\` werden neben der EXE angelegt — portabel. -> ⚠️ **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. +> 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 diff --git a/README.md b/README.md index 50b806d..7bf3199 100644 --- a/README.md +++ b/README.md @@ -66,43 +66,17 @@ docker compose up -d **Linux binary (no Docker):** ```bash -chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64 +chmod +x kx-bridge && ./kx-bridge ``` **Windows EXE (no Docker):** ``` kx-bridge.exe ``` +> `config\` and `data\` are created next to the EXE — portable. -> ⚠️ **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. +> 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 diff --git a/VERSION b/VERSION index a696a42..bd758c9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.19.1 +0.9.20 diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 0a3b163..4c2f19e 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -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-Schätzzeit: {secs}s ({m.group(1).strip()})") + log.info(f"Slicer estimate: {secs}s ({m.group(1).strip()})") return secs @@ -798,6 +798,7 @@ 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 = "" @@ -809,7 +810,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("Ungültiger UI-Theme-Name %r – nutze default", raw_theme) + log.warning("Invalid UI theme name %r – using default", raw_theme) raw_theme = "default" self._ui_theme = raw_theme self._index_tpl_cache: str | None = None @@ -914,7 +915,9 @@ 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 getattr(self, "_camera_autostarted", False): + if (getattr(self._args, "camera_on_print", 0) + and not self._camera_autostarted + and not self._camera_user_stopped): self._camera_autostarted = True try: self.client.start_camera() @@ -923,6 +926,7 @@ 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: @@ -934,7 +938,7 @@ class KobraXBridge: gcode_file_id=gf["id"], printer_id=self._printer_id, ) - log.info(f"Job gestartet: {self._current_job_id} für {filename}") + log.info(f"Job started: {self._current_job_id} for {filename}") # Job-History: Druckende erkennen if kobra_state in ("finished",) and self._current_job_id: @@ -1001,7 +1005,9 @@ 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 getattr(self, "_camera_autostarted", False): + if (getattr(self._args, "camera_on_print", 0) + and not self._camera_autostarted + and not self._camera_user_stopped): self._camera_autostarted = True try: self.client.start_camera() @@ -1010,6 +1016,7 @@ 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"] @@ -1075,7 +1082,7 @@ class KobraXBridge: if filename: try: self._store.update_file_objects(filename, objs, svg) - log.info(f"Skip-Objekte für {filename}: {len(objs)} ({'mit SVG' if svg else 'ohne SVG'})") + log.info(f"Skip objects for {filename}: {len(objs)} ({'with SVG' if svg else 'no SVG'})") except Exception as e: log.warning(f"update_file_objects fehlgeschlagen: {e}") self._push_status_update() @@ -1884,6 +1891,41 @@ 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": {}, + }, } # ------------------------------------------------------------------------- @@ -2655,7 +2697,20 @@ 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.""" + """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" return web.json_response({ "result": { "webcams": [ @@ -2667,8 +2722,8 @@ class KobraXBridge: "icon": "mdiWebcam", "target_fps": 5, "target_fps_idle": 2, - "stream_url": "/api/camera/stream", - "snapshot_url": "/api/camera/snapshot", + "stream_url": stream_url, + "snapshot_url": snapshot_url, "flip_horizontal": False, "flip_vertical": False, "rotation": 0, @@ -2853,7 +2908,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"Druckstart bestätigt: state={result.get('state')}") + log.info(f"Print start confirmed: state={result.get('state')}") else: log.warning("Druckstart: keine Antwort vom Drucker") @@ -3108,7 +3163,7 @@ class KobraXBridge: return web.json_response({"result": "disconnected"}) async def handle_api_restart(self, request): - log.info("Neustart über API angefordert") + log.info("Restart requested via API") response = web.json_response({"status": "restarting"}) asyncio.get_event_loop().call_later(0.3, self._restart_bridge) return response @@ -3388,6 +3443,9 @@ 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): @@ -3447,7 +3505,7 @@ class KobraXBridge: stderr=asyncio.subprocess.DEVNULL, ) except (FileNotFoundError, OSError) as e: - log.warning("Kamera: ffmpeg nicht gefunden – Kamerastream nicht verfügbar") + log.warning("Camera: ffmpeg not found – camera stream unavailable") return web.Response(status=503, text="ffmpeg not found") except Exception as e: log.warning(f"Kamera: ffmpeg konnte nicht gestartet werden: {e}") @@ -3542,7 +3600,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"Drucker lädt Datei ab: {filename} ({size} bytes)") + log.info(f"Printer downloading file: {filename} ({size} bytes)") return web.FileResponse(serve_path, headers={ "Content-Disposition": f'attachment; filename="{filename}"' }) @@ -3580,6 +3638,7 @@ 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"], @@ -3857,7 +3916,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"Drucker '{name or creds['model']}' als {sec} hinzugefügt (Port {new_port})") + log.info(f"Printer '{name or creds['model']}' added as {sec} (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 @@ -3932,13 +3991,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", - "BRIDGE_PRINTER_NAME"): + "CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "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-Umgebung erkannt – beende Prozess für Supervisor-Restart") + log.info("Container environment detected – exiting for supervisor restart") os._exit(0) frozen = getattr(sys, "frozen", False) @@ -4117,7 +4176,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} nicht im Release ({resp.status}) – übersprungen") + log.warning(f"Update: {fname} not found in release ({resp.status}) – skipped") continue downloaded.append((app_dir / fname, await resp.read())) # Phase 2: atomar ersetzen (erst nach komplettem, erfolgreichem Download) @@ -4394,11 +4453,14 @@ class KobraXBridge: # Obico registriert obico_remote_event-Callback. Wir akzeptieren leer. result = "ok" elif method == "server.webcams.list": - # WS-Variante des HTTP-Endpoints + # 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}" result = {"webcams": [{ "name": "KX-Bridge", "location": "printer", "service": "mjpegstreamer", - "enabled": True, "stream_url": "/api/camera/stream", - "snapshot_url": "/api/camera/snapshot", + "enabled": True, + "stream_url": f"{_base}/api/camera/stream", + "snapshot_url": f"{_base}/api/camera/snapshot", "flip_horizontal": False, "flip_vertical": False, "rotation": 0, "target_fps": 5, "aspect_ratio": "16:9", }]} @@ -4448,7 +4510,7 @@ class KobraXBridge: log.debug(f"Unbekannte RPC-Methode: {method}") result = {} except Exception as e: - log.error(f"RPC-Fehler für {method}: {e}") + log.error(f"RPC error for {method}: {e}") error = {"code": -32603, "message": str(e)} if rpc_id is not None: @@ -4762,7 +4824,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"[Drucker {pid}] Bridge läuft auf http://{args.host}:{per_args.port}") + log.info(f"[Printer {pid}] Bridge running on http://{args.host}:{per_args.port}") import socket as _socket try: @@ -4771,6 +4833,9 @@ 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") diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 158dc44..284dd89 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -1,11 +1,12 @@ // ── 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,printer_name:'Kobra X',firmware_version:'–', + curr_layer:0,total_layers:0,z_mm: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(){ @@ -101,6 +102,7 @@ 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'; } @@ -108,10 +110,10 @@ function _langToggleLabel(lang){ function _mapSupportedLang(lang){ if(!lang)return ''; var l=String(lang).toLowerCase().replace(/_/g,'-').trim(); - if(l==='de'||l==='en'||l==='es'||l==='zh-cn')return l; + if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='zh-cn')return l; var base=l.split('-')[0]; - if(base==='de'||base==='en'||base==='es')return base; + if(base==='de'||base==='en'||base==='es'||base==='fr')return base; if(base==='zh'){ if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn'; @@ -282,6 +284,7 @@ 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); @@ -659,6 +662,7 @@ 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):'–'; @@ -822,10 +826,14 @@ 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 - if(s.print_state==='printing'&&!camOn&&s.camera_url){ + // auto-start camera during print (unless user explicitly stopped it) + if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){ camStart(); } + // reset user-stopped flag when print ends so next print auto-starts again + if(s.print_state!=='printing'){ + camUserStopped=false; + } updateConnBtn(); } @@ -1437,7 +1445,8 @@ 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, '+br+'%':'aus'),'msg-ok')}) + .then(function(){clog('Licht '+(on?'an, 80%':'aus'),'msg-ok')}) + .catch(function(e){clog('Licht-Fehler: '+e,'msg-err')}); } @@ -1518,11 +1527,13 @@ 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'); } diff --git a/web/themes/default/index.html b/web/themes/default/index.html index 5023291..d662a58 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -38,6 +38,7 @@ + @@ -266,9 +267,15 @@