From 3f6ea269e6961b43d0042039810453760d47c5b7 Mon Sep 17 00:00:00 2001 From: viewit Date: Mon, 8 Jun 2026 23:14:50 +0200 Subject: [PATCH 1/9] build: sources for v0.9.20 --- CHANGELOG.de.md | 25 ++++ CHANGELOG.md | 55 ++++++-- Dockerfile | 2 + README.de.md | 35 +---- README.md | 34 +---- VERSION | 2 +- kobrax_moonraker_bridge.py | 109 ++++++++++++--- web/themes/default/app.js | 23 +++- web/themes/default/index.html | 13 +- web/translations/de.json | 1 + web/translations/en.json | 1 + web/translations/es.json | 1 + web/translations/fr.json | 249 ++++++++++++++++++++++++++++++++++ web/translations/zh-cn.json | 1 + 14 files changed, 444 insertions(+), 107 deletions(-) create mode 100644 web/translations/fr.json 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 @@
0%
-
-
-
+
+
+
+
+
+
+
Z
+
+
diff --git a/web/translations/de.json b/web/translations/de.json index 57a3520..1d00c22 100644 --- a/web/translations/de.json +++ b/web/translations/de.json @@ -37,6 +37,7 @@ "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", diff --git a/web/translations/en.json b/web/translations/en.json index 7458e63..6bed741 100644 --- a/web/translations/en.json +++ b/web/translations/en.json @@ -37,6 +37,7 @@ "lbl_remaining": "Remaining:", "lbl_slicer_time": "Slicer estimate:", "lbl_layers": "Layer", + "lbl_zpos": "Z (mm)", "speed_silent": "🐢 Silent", "speed_normal": "⚡ Normal", "speed_sport": "🚀 Sport", diff --git a/web/translations/es.json b/web/translations/es.json index 3c78f93..8964dc5 100644 --- a/web/translations/es.json +++ b/web/translations/es.json @@ -37,6 +37,7 @@ "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", diff --git a/web/translations/fr.json b/web/translations/fr.json new file mode 100644 index 0000000..0c040be --- /dev/null +++ b/web/translations/fr.json @@ -0,0 +1,249 @@ +{ + "header_status_standby": "Prêt", + "header_status_printing": "Impression", + "header_status_complete": "Terminé", + "header_status_error": "Erreur", + "kobra_free": "Disponible", + "kobra_busy": "Occupé", + "kobra_printing": "Impression", + "kobra_preheating": "Préchauffage", + "kobra_auto_leveling": "Mise à niveau auto", + "kobra_checking": "Vérification", + "kobra_updated": "Mise à jour", + "kobra_init": "Initialisation", + "kobra_pausing": "Pause en cours…", + "kobra_paused": "En pause", + "kobra_resuming": "Reprise en cours…", + "kobra_resumed": "Repris", + "kobra_stopping": "Arrêt en cours…", + "kobra_stoped": "Arrêté", + "kobra_finished": "Terminé", + "kobra_failed": "Erreur", + "kobra_canceled": "Annulé", + "kobra_offline": "Hors ligne", + "nav_dashboard": "Tableau de bord", + "nav_print": "Impression", + "nav_temps": "Températures", + "nav_motion": "Mouvement", + "nav_ams": "AMS", + "nav_extras": "Lumière / Ventilateur", + "nav_console": "Console", + "card_progress": "Progression", + "card_temps": "Températures", + "card_light_fan": "Ventilateur", + "card_speed": "Vitesse d'impression", + "card_cam": "Caméra", + "lbl_elapsed": "Écoulé :", + "lbl_remaining": "Restant :", + "lbl_slicer_time": "Estimation slicer :", + "lbl_layers": "Couche", + "lbl_zpos": "Z (mm)", + "speed_silent": "🐢 Silencieux", + "speed_normal": "⚡ Normal", + "speed_sport": "🚀 Sport", + "lbl_light": "💡 Lumière", + "lbl_feed": "Charger", + "lbl_unload": "Décharger", + "card_ace_dry": "Séchage ACE", + "ace_dry_dryer": "Séchoir", + "ace_dry_status_off": "Statut : Arrêté", + "ace_dry_status_on": "Statut : Actif", + "ace_dry_status_remaining": "Restant", + "ace_dry_humidity": "Humidité", + "ace_dry_current_temp": "Température", + "ace_dry_chart": "Historique (Temp/Humidité)", + "ace_dry_temp": "Température (°C)", + "ace_dry_duration": "Durée (min)", + "ace_dry_start": "▶ Démarrer", + "ace_dry_stop": "■ Arrêter", + "ace_dry_auto_refill": "Remplissage auto", + "ace_dry_enable": "Activer le séchage", + "ace_dry_temp_line": "Température de séchage", + "ace_dry_time_line": "Durée de séchage", + "ace_dry_ui_pending": "(Interface seule, backend suivant)", + "ace_dry_dialog_title": "Réglages Temp/Durée du séchoir", + "ace_dry_dialog_temp": "Température (30-80°C)", + "ace_dry_dialog_time": "Temps restant (h:m:s)", + "ace_dry_dialog_confirm": "Confirmer", + "ace_dry_dialog_cancel": "Annuler", + "ace_dry_dialog_save_restart": "Enregistrer et redémarrer", + "ace_dry_dialog_custom_name": "Nom personnalisé", + "ace_dry_dialog_reset_default": "Réinitialiser", + "cam_placeholder": "📷 Caméra non démarrée", + "cam_stream_unavailable": "Flux indisponible", + "btn_cam_start": "▶ Caméra", + "btn_cam_stop": "◼ Caméra", + "btn_pause": "⏸ Pause", + "btn_resume": "▶ Reprendre", + "btn_cancel": "✕ Arrêter", + "label_nozzle": "Buse", + "label_bed": "Plateau", + "label_fan": "🌀 Ventilateur", + "label_light": "💡 Lumière", + "label_on_off": "On / Off", + "label_speed": "Vitesse", + "panel_print_title": "Contrôle impression", + "panel_print_btn_pause": "⏸ Pause", + "panel_print_btn_resume": "▶ Reprendre", + "panel_print_btn_cancel": "✕ Annuler", + "panel_print_temps_live": "Températures (en direct)", + "label_set": "Définir", + "label_off": "Éteint", + "panel_temps_nozzle": "Buse", + "panel_temps_bed": "Plateau chauffant", + "panel_temps_chart": "Historique (60 dernières valeurs)", + "label_target_c": "Cible :", + "panel_motion_xy": "Axes XY", + "panel_motion_z": "Axe Z", + "label_step": "Pas :", + "btn_home_z": "Origine Z", + "btn_home_xy": "Origine XY", + "btn_home_all": "Origine Tout", + "btn_disable_motors": "Moteurs Off", + "panel_ams_title": "Filament", + "card_ams": "Filament", + "ams_no_data": "Aucune donnée AMS reçue", + "label_slot": "Slot", + "ams_empty": "Vide", + "panel_extras_light": "Lumière", + "panel_extras_fan": "Ventilateur", + "panel_extras_camera": "Caméra", + "btn_cam_start2": "▶ Démarrer", + "btn_cam_stop2": "◼ Arrêter", + "panel_console_title": "Journal d'événements", + "log_light_on": "Lumière allumée", + "log_light_off": "Lumière éteinte", + "log_fan": "Ventilateur →", + "log_nozzle": "Buse →", + "log_bed": "Plateau →", + "log_axis": "Axe", + "log_home": "Origine", + "log_home_all": "Origine Tout", + "log_cam_start": "Caméra démarrée :", + "log_cam_stop": "Caméra arrêtée", + "log_poll_error": "Erreur de sondage :", + "log_error": "Erreur :", + "confirm_cancel": "Vraiment annuler l'impression ?", + "settings_title": "Paramètres", + "settings_connection": "Connexion", + "settings_print": "Paramètres d'impression", + "settings_poll": "Intervalle de sondage", + "settings_version": "Version", + "settings_save": "Enregistrer et redémarrer", + "settings_printer_name": "Nom de l'imprimante", + "settings_printer_ip": "IP de l'imprimante", + "settings_mqtt_port": "Port MQTT", + "settings_username": "Nom d'utilisateur MQTT", + "settings_password": "Mot de passe MQTT", + "settings_device_id": "ID de l'appareil", + "settings_mode_id": "ID du mode", + "hint_ip_no_port": "Adresse IP uniquement, sans port (ex. 192.168.1.102)", + "settings_default_slot": "Slot par défaut (couleur unique)", + "settings_slot_auto": "Auto (tous les slots chargés)", + "settings_auto_leveling": "Mise à niveau auto avant impression", + "settings_camera_on_print": "Activer la caméra au démarrage de l'impression", + "settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web", + "update_check": "Vérifier les mises à jour", + "update_checking": "Vérification…", + "update_available": "disponible", + "update_none": "Déjà à jour", + "update_apply": "Installer maintenant", + "update_applying": "Téléchargement…", + "update_restarting": "Redémarrage…", + "update_error": "Erreur", + "btn_connect": "⚡ Connecter", + "btn_disconnect": "✕ Déconnecter", + "lbl_conn_error": "Erreur de connexion :", + "slot_edit_title": "Modifier le slot", + "slot_edit_color": "Couleur", + "slot_edit_material": "Matériau", + "slot_edit_load": "⬇ Charger", + "slot_edit_unload": "⬆ Décharger", + "slot_edit_save": "💾 Enregistrer", + "slot_edit_custom": "ex. PLA, PETG, ABS…", + "slot_edit_ok": "Slot AMS", + "slot_edit_profile": "Profil OrcaSlicer", + "slot_edit_profile_hint": "Envoyé lors de la synchronisation OrcaSlicer comme marque spécifique au lieu de \"Générique\"", + "slot_edit_profile_default": "— Générique (défaut) —", + "orca_profile_section": "Profils OrcaSlicer", + "orca_profile_hint": "Importez vos propres profils de filament OrcaSlicer (ouvrez le dossier utilisateur via Aide → Afficher le dossier de configuration)", + "orca_profile_import_btn": "Importer des profils", + "orca_profile_import_link": "★ Importer mes profils…", + "orca_profile_import_title": "Importer vos profils OrcaSlicer", + "orca_profile_help_html": "Déposez un ZIP de votre dossier filament OrcaSlicer ou des fichiers .json individuels.
Dans OrcaSlicer : Aide → Afficher le dossier de configuration → user/<id>/filament/", + "orca_profile_dropmsg": "Déposez ici ou cliquez", + "orca_profile_list_label": "Profils importés", + "orca_profile_user_label": "Mes profils", + "orca_profile_user_empty": "– aucun –", + "orca_profile_uploading": "Envoi en cours…", + "orca_profile_done": "Importé", + "orca_profile_skipped": "ignoré", + "log_dir_all": "Tout", + "log_lvl_label": "Niveau :", + "file_ready_btn": "▶ Lancer l'impression", + "file_slots_btn": "🎨 Choisir les slots", + "file_cancel_btn": "✕ Annuler", + "nav_printers": "Imprimantes", + "skip_title": "✂ Ignorer des objets", + "skip_hint": "Décochez les objets que vous ne souhaitez plus imprimer :", + "skip_btn_label": "Objets", + "skip_no_objects": "Aucun objet dans cette impression.", + "skip_already": "ignoré", + "skip_select_at_least_one": "Veuillez sélectionner au moins un objet.", + "skip_sending": "Envoi …", + "skip_success": "Les objets seront ignorés.", + "fd_objects_hint": "Ignorer des objets (optionnel) :", + "fd_slots_hint": "Associer le canal GCode au slot AMS :", + "fd_cancel": "Annuler", + "fd_print": "▶ Imprimer", + "fd_no_slots_msg": "Aucun slot AMS chargé.{br}Lancer l'impression quand même ?", + "fd_slot": "Slot", + "fd_no_matching_material": "Aucun matériau correspondant", + "fd_used": "UTILISÉ", + "add_printer": "Ajouter une imprimante", + "apd_lbl_ip": "IP de l'imprimante", + "apd_lbl_name": "Nom (optionnel)", + "apd_placeholder_name": "ex. Kobra X Salon", + "apd_cancel": "Annuler", + "apd_confirm": "Ajouter", + "apd_fetching": "Récupération des données de l'imprimante…", + "apd_success": "Imprimante ajoutée, redémarrage du bridge…", + "apd_err_ip": "Veuillez saisir une adresse IP", + "printers_remove": "Supprimer l'imprimante", + "printers_remove_confirm": "Supprimer l'imprimante \"{name}\" ? Le bridge va redémarrer.", + "printers_active": "● actif", + "printers_switch": "Changer →", + "printers_current": "Imprimante actuelle", + "printers_loading": "Chargement…", + "printers_none": "Aucune imprimante configurée.", + "printers_empty_hint": "Aucune imprimante configurée.", + "nav_browser": "Navigateur", + "panel_browser_title": "Explorateur de fichiers", + "store_search_placeholder": "🔍 Rechercher…", + "store_empty": "Aucun fichier uploadé.", + "store_refresh": "↻ Actualiser", + "store_print": "▶ Imprimer", + "store_download": "⬇ Télécharger", + "store_delete_confirm": "Supprimer le fichier ?", + "store_print_confirm": "Imprimer le fichier ?", + "store_web_verify_title": "Vérifier le fichier", + "store_web_verify_msg": "Veuillez vérifier que ce fichier a été créé pour l'Anycubic Kobra X.", + "store_web_verify_confirm": "Confirmer", + "store_web_verify_abort": "Annuler", + "store_no_results": "Aucun fichier trouvé.", + "store_never": "jamais imprimé", + "store_estimate": "Estimation", + "store_upload_label_prefix": "Déposez un GCode ici ou ", + "store_upload_label_browse": "parcourir", + "store_upload_busy": "⏳ Envoi en cours…", + "store_upload_success": "✓ {file}", + "store_upload_error": "✗ {error}", + "sf_all": "Tout", + "sf_ok": "✓ Terminés", + "sf_err": "✗ Échoués", + "sf_new": "Nouveau", + "ss_date": "↓ Date", + "ss_name": "A–Z Nom", + "ss_dur": "⏱ Durée d'impression" +} + diff --git a/web/translations/zh-cn.json b/web/translations/zh-cn.json index 867eb35..cf4280e 100644 --- a/web/translations/zh-cn.json +++ b/web/translations/zh-cn.json @@ -37,6 +37,7 @@ "lbl_remaining": "剩余时间:", "lbl_slicer_time": "切片预估:", "lbl_layers": "层", + "lbl_zpos": "Z (mm)", "speed_silent": "🐢 静音", "speed_normal": "⚡ 标准", "speed_sport": "🚀 运动", From 636889bdbc74f5ecd4bc4a7f011fefcb4319bff7 Mon Sep 17 00:00:00 2001 From: viewit Date: Tue, 9 Jun 2026 12:41:23 +0200 Subject: [PATCH 2/9] =?UTF-8?q?docs:=20Filament-Preset-Anleitung=20als=20d?= =?UTF-8?q?ocs/filament-preset-bridge-guide.md=20hinzugef=C3=BCgt=20+=20RE?= =?UTF-8?q?ADME-Links=20aktualisiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.de.md | 2 +- README.md | 2 +- docs/filament-preset-bridge-guide.md | 344 +++++++++++++++++++++++++++ 3 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 docs/filament-preset-bridge-guide.md diff --git a/README.de.md b/README.de.md index e6931e4..2d33da4 100644 --- a/README.de.md +++ b/README.de.md @@ -130,7 +130,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](#-features) 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](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). diff --git a/README.md b/README.md index 7bf3199..ac77fc0 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,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](#-features) 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](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). diff --git a/docs/filament-preset-bridge-guide.md b/docs/filament-preset-bridge-guide.md new file mode 100644 index 0000000..30862e2 --- /dev/null +++ b/docs/filament-preset-bridge-guide.md @@ -0,0 +1,344 @@ +# Eigene Filament-Presets anlegen, prüfen und mit KX-Bridge verknüpfen + +> **Gilt für:** OrcaSlicer-KX v2.4.0-alpha-kx2 oder neuer + +--- + +## Was ist die `filament_id` und warum ist sie wichtig? + +Jedes Filament-Preset in OrcaSlicer hat eine interne `filament_id`. Diese ID wird von der KX-Bridge genutzt, um beim AMS-Sync das richtige Preset zuzuordnen. + +- System-Presets (z.B. "Polymaker PolyTerra PLA") haben eine feste ID wie `GFL99` oder `OGFL04`. +- **Eigene (User-)Presets** bekommen in OrcaSlicer-KX automatisch eine eindeutige ID, die mit `P` beginnt (z.B. `P3a7f2c1`). + +Ohne eindeutige ID zeigt OrcaSlicer beim Sync immer "Generic PLA" — auch wenn das Preset existiert. + +--- + +## 1. Eigenes Filament-Preset anlegen + +1. OrcaSlicer-KX starten +2. Rechts oben im **Filament-Dropdown** ein passendes Basis-Preset wählen (z.B. "Generic PLA" oder ein Hersteller-Preset) +3. Einstellungen nach Wunsch anpassen (Temperaturen, Kühlung, etc.) +4. Auf das **Speichern-Symbol** (Diskette) klicken → **"Save as new preset"** +5. Namen eingeben — z.B. `SUNLU PLA+ 2.0` + > Der Name muss später exakt so in der Bridge eingetragen werden. +6. Drucker auswählen: **Anycubic Kobra X 0.4 nozzle** — wichtig für die Kompatibilität! +7. **Speichern** klicken +8. OrcaSlicer **einmal neu starten** — erst dann wird die `filament_id` dauerhaft gespeichert. + +--- + +## 2. Eindeutige ID prüfen + +Nach dem Neustart prüfen, ob die ID korrekt gesetzt wurde: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +Die Datei öffnen und nach `filament_id` suchen: + +```json +{ + "filament_id": "P3a7f2c1", + ... +} +``` + +✅ Korrekt: ID beginnt mit `P` gefolgt von 7 Hex-Zeichen +❌ Fehlt oder leer: OrcaSlicer-KX zu alt — Update auf v2.4.0-alpha-kx2 oder neuer + +--- + +## 3. Preset auf einen anderen PC übertragen (Import) + +### Exportieren (Quell-PC) + +Die Preset-Datei einfach kopieren: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +### Importieren (Ziel-PC) + +**Methode A — Datei direkt kopieren:** +1. Die `.json`-Datei in das gleiche Verzeichnis auf dem Ziel-PC kopieren +2. OrcaSlicer neu starten → Preset erscheint im Dropdown + +**Methode B — OrcaSlicer Import-Funktion:** +1. In OrcaSlicer: **File → Import → Import Configs...** +2. Die `.json`-Datei auswählen +3. OrcaSlicer neu starten + +> **Wichtig:** Die `filament_id` in der Datei bleibt erhalten — das Preset wird auf dem Ziel-PC genauso erkannt wie auf dem Quell-PC. + +--- + +## 4. Preset in KX-Bridge verknüpfen + +1. KX-Bridge UI öffnen +2. **Filament-Verwaltung** → AMS-Slot auswählen +3. Im Feld **Filament-Name** exakt den OrcaSlicer-Preset-Namen eintragen: + ``` + SUNLU PLA+ 2.0 + ``` +4. Speichern + +Die Bridge sendet beim Sync `filament_name: "SUNLU PLA+ 2.0"` → OrcaSlicer findet das Preset anhand von Name und `filament_id` → zeigt es korrekt an. + +--- + +## Wichtige Hinweise + +| Was | Warum | +|-----|-------| +| Name in OrcaSlicer und Bridge müssen **exakt** übereinstimmen | Groß-/Kleinschreibung und Sonderzeichen werden verglichen | +| Preset muss für **Anycubic Kobra X 0.4 nozzle** kompatibel sein | Beim Speichern den richtigen Drucker auswählen | +| Nach dem ersten Speichern OrcaSlicer **neu starten** | Erst dann wird die `filament_id` persistent geschrieben | +| **OrcaSlicer-KX v2.4.0-alpha-kx2** oder neuer verwenden | Ältere Versionen generieren keine eindeutige `filament_id` für User-Presets | + +--- +--- + +# How to Create, Verify and Import Custom Filament Presets for KX-Bridge + +> **Requires:** OrcaSlicer-KX v2.4.0-alpha-kx2 or newer + +--- + +## What is the `filament_id` and why does it matter? + +Every filament preset in OrcaSlicer has an internal `filament_id`. The KX-Bridge uses this ID to match the correct preset during AMS sync. + +- System presets (e.g. "Polymaker PolyTerra PLA") have a fixed ID like `GFL99` or `OGFL04`. +- **Custom (user) presets** automatically receive a unique ID starting with `P` (e.g. `P3a7f2c1`) in OrcaSlicer-KX. + +Without a unique ID, OrcaSlicer will always show "Generic PLA" during sync — even if the preset exists. + +--- + +## 1. Create a Custom Filament Preset + +1. Launch OrcaSlicer-KX +2. Select a suitable base preset from the **filament dropdown** (e.g. "Generic PLA" or a vendor preset) +3. Adjust settings as needed (temperatures, cooling, etc.) +4. Click the **save icon** (floppy disk) → **"Save as new preset"** +5. Enter a name — e.g. `SUNLU PLA+ 2.0` + > This name must be entered in the bridge exactly as typed here. +6. Select printer: **Anycubic Kobra X 0.4 nozzle** — required for compatibility! +7. Click **Save** +8. **Restart OrcaSlicer once** — the `filament_id` is only written permanently after a restart. + +--- + +## 2. Verify the Unique ID + +After restarting, check that the ID was set correctly: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +Open the file and look for `filament_id`: + +```json +{ + "filament_id": "P3a7f2c1", + ... +} +``` + +✅ Correct: ID starts with `P` followed by 7 hex characters +❌ Missing or empty: Your OrcaSlicer-KX version is too old — update to v2.4.0-alpha-kx2 or newer + +--- + +## 3. Transfer a Preset to Another PC (Import) + +### Export (source PC) + +Simply copy the preset file: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +### Import (target PC) + +**Method A — Copy file directly:** +1. Copy the `.json` file to the same directory on the target PC +2. Restart OrcaSlicer → preset appears in the dropdown + +**Method B — OrcaSlicer import function:** +1. In OrcaSlicer: **File → Import → Import Configs...** +2. Select the `.json` file +3. Restart OrcaSlicer + +> **Note:** The `filament_id` inside the file is preserved — the preset will be recognized on the target PC exactly as on the source PC. + +--- + +## 4. Link the Preset in KX-Bridge + +1. Open the KX-Bridge UI +2. Go to **Filament Management** → select the AMS slot +3. In the **Filament Name** field, enter the OrcaSlicer preset name exactly: + ``` + SUNLU PLA+ 2.0 + ``` +4. Save + +The bridge sends `filament_name: "SUNLU PLA+ 2.0"` during sync → OrcaSlicer matches by name and `filament_id` → displays the preset correctly. + +--- + +## Quick Reference + +| What | Why | +|------|-----| +| Name in OrcaSlicer and Bridge must match **exactly** | Case and special characters are compared | +| Preset must be compatible with **Anycubic Kobra X 0.4 nozzle** | Select the correct printer when saving | +| **Restart OrcaSlicer** after saving for the first time | The `filament_id` is only written persistently after a restart | +| Use **OrcaSlicer-KX v2.4.0-alpha-kx2** or newer | Older versions do not generate a unique `filament_id` for user presets | + +--- +--- + +# Cómo crear, verificar e importar perfiles de filamento personalizados para KX-Bridge + +> **Requiere:** OrcaSlicer-KX v2.4.0-alpha-kx2 o superior + +--- + +## ¿Qué es el `filament_id` y por qué es importante? + +Cada perfil de filamento en OrcaSlicer tiene un `filament_id` interno. KX-Bridge usa este ID para asignar el perfil correcto durante la sincronización AMS. + +- Los perfiles del sistema (p. ej. "Polymaker PolyTerra PLA") tienen un ID fijo como `GFL99` o `OGFL04`. +- Los **perfiles personalizados (usuario)** reciben automáticamente un ID único que empieza por `P` (p. ej. `P3a7f2c1`) en OrcaSlicer-KX. + +Sin un ID único, OrcaSlicer mostrará siempre "Generic PLA" durante la sincronización, aunque el perfil exista. + +--- + +## 1. Crear un perfil de filamento personalizado + +1. Iniciar OrcaSlicer-KX +2. Seleccionar un perfil base adecuado en el **menú desplegable de filamento** (p. ej. "Generic PLA" o un perfil de fabricante) +3. Ajustar la configuración según sea necesario (temperaturas, refrigeración, etc.) +4. Hacer clic en el **icono de guardar** (disquete) → **"Save as new preset"** +5. Introducir un nombre — p. ej. `SUNLU PLA+ 2.0` + > Este nombre debe introducirse en la bridge exactamente igual. +6. Seleccionar impresora: **Anycubic Kobra X 0.4 nozzle** — ¡necesario para la compatibilidad! +7. Hacer clic en **Guardar** +8. **Reiniciar OrcaSlicer una vez** — el `filament_id` solo se escribe de forma permanente tras un reinicio. + +--- + +## 2. Verificar el ID único + +Tras reiniciar, comprobar que el ID se ha establecido correctamente: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +Abrir el archivo y buscar `filament_id`: + +```json +{ + "filament_id": "P3a7f2c1", + ... +} +``` + +✅ Correcto: el ID empieza por `P` seguido de 7 caracteres hexadecimales +❌ Falta o está vacío: la versión de OrcaSlicer-KX es demasiado antigua — actualizar a v2.4.0-alpha-kx2 o superior + +--- + +## 3. Transferir un perfil a otro PC (importar) + +### Exportar (PC de origen) + +Simplemente copiar el archivo del perfil: + +**Windows:** +``` +%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json +``` + +**Linux:** +``` +~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json +``` + +### Importar (PC de destino) + +**Método A — Copiar el archivo directamente:** +1. Copiar el archivo `.json` al mismo directorio en el PC de destino +2. Reiniciar OrcaSlicer → el perfil aparece en el menú desplegable + +**Método B — Función de importación de OrcaSlicer:** +1. En OrcaSlicer: **File → Import → Import Configs...** +2. Seleccionar el archivo `.json` +3. Reiniciar OrcaSlicer + +> **Nota:** El `filament_id` dentro del archivo se conserva — el perfil se reconocerá en el PC de destino exactamente igual que en el de origen. + +--- + +## 4. Vincular el perfil en KX-Bridge + +1. Abrir la interfaz de KX-Bridge +2. Ir a **Gestión de filamentos** → seleccionar la ranura AMS +3. En el campo **Nombre de filamento**, introducir el nombre exacto del perfil de OrcaSlicer: + ``` + SUNLU PLA+ 2.0 + ``` +4. Guardar + +La bridge envía `filament_name: "SUNLU PLA+ 2.0"` durante la sincronización → OrcaSlicer busca por nombre y `filament_id` → muestra el perfil correctamente. + +--- + +## Referencia rápida + +| Qué | Por qué | +|-----|---------| +| El nombre en OrcaSlicer y en Bridge debe coincidir **exactamente** | Se comparan mayúsculas, minúsculas y caracteres especiales | +| El perfil debe ser compatible con **Anycubic Kobra X 0.4 nozzle** | Seleccionar la impresora correcta al guardar | +| **Reiniciar OrcaSlicer** tras guardar por primera vez | El `filament_id` solo se escribe de forma permanente tras un reinicio | +| Usar **OrcaSlicer-KX v2.4.0-alpha-kx2** o superior | Las versiones anteriores no generan un `filament_id` único para perfiles de usuario | From 930e3774afeb7d1063f21b5add1d072af410a8b9 Mon Sep 17 00:00:00 2001 From: viewit Date: Sat, 13 Jun 2026 00:01:07 +0200 Subject: [PATCH 3/9] docs: Portainer-Deployment-Anleitung + docker-compose.portainer.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Named-Volume-Compose ohne ENV-Pflichtfelder — Bridge startet im Offline-Modus, User trägt Drucker-IP in der UI ein. --- README.de.md | 14 ++++++++++++++ README.md | 14 ++++++++++++++ docker-compose.portainer.yml | 29 +++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 docker-compose.portainer.yml diff --git a/README.de.md b/README.de.md index 2d33da4..a6d9b27 100644 --- a/README.de.md +++ b/README.de.md @@ -105,6 +105,20 @@ Drucker → Verbindungstyp **Moonraker** → Host: `http://BRIDGE-IP:7125` --- +## 🐳 Portainer + +[`docker-compose.portainer.yml`](docker-compose.portainer.yml) in +**Portainer → Stacks → Add stack → Web editor** einfügen und auf *Deploy* klicken. + +Keine Konfiguration nötig — die Bridge startet im Offline-Modus. +**`http://HOST-IP:7125`** öffnen, den **Drucker-Tab** aufrufen und **+ Drucker hinzufügen** klicken. +Nur die Drucker-IP eingeben — Zugangsdaten werden automatisch geholt und gespeichert. + +Alle Daten (Konfiguration, GCode-Store, Datenbank) liegen in Named Docker Volumes +(`kx-bridge-config`, `kx-bridge-data`), die Portainer verwaltet. + +--- + ## 📺 Video-Tutorial [![KX-Bridge Setup & Usage](https://img.youtube.com/vi/1Ql4wfH27fM/hqdefault.jpg)](https://www.youtube.com/watch?v=1Ql4wfH27fM) diff --git a/README.md b/README.md index ac77fc0..b11bd71 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,20 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125` --- +## 🐳 Portainer + +Paste [`docker-compose.portainer.yml`](docker-compose.portainer.yml) into +**Portainer → Stacks → Add stack → Web editor** and click *Deploy*. + +No configuration needed upfront — the bridge starts in offline mode. +Open **`http://HOST-IP:7125`**, go to the **Printers** tab and click **+ Add printer**. +Enter the printer's IP address — credentials are fetched and saved automatically. + +All data (config, GCode store, database) is stored in named Docker volumes +(`kx-bridge-config`, `kx-bridge-data`) managed by Portainer. + +--- + ## 📺 Video Tutorial [![KX-Bridge Setup & Usage](https://img.youtube.com/vi/1Ql4wfH27fM/hqdefault.jpg)](https://www.youtube.com/watch?v=1Ql4wfH27fM) diff --git a/docker-compose.portainer.yml b/docker-compose.portainer.yml new file mode 100644 index 0000000..43a8db1 --- /dev/null +++ b/docker-compose.portainer.yml @@ -0,0 +1,29 @@ +# KX-Bridge — Portainer Stack +# +# Paste this into Portainer → Stacks → Add stack → Web editor +# +# No configuration needed upfront — just deploy, open http://HOST-IP:7125 +# and add your printer via the UI (IP only, credentials are fetched automatically). +# +# All data (config, GCode store, database) is stored in named Docker volumes +# managed by Portainer. + +services: + kx-bridge: + image: gitea.it-drui.de/viewit/kx-bridge:latest + volumes: + - kx-bridge-config:/app/config + - kx-bridge-data:/app/data + ports: + # Port 7125 = first printer. Add 7126, 7127, … for each additional printer. + - "7125:7125" + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +volumes: + kx-bridge-config: + kx-bridge-data: From 1f300589d179ff10a51f9e95264aebd704c12728 Mon Sep 17 00:00:00 2001 From: viewit Date: Sun, 14 Jun 2026 11:00:32 +0200 Subject: [PATCH 4/9] build: sources for v0.9.21 --- CHANGELOG.de.md | 19 +++++++++++++++++++ CHANGELOG.md | 19 +++++++++++++++++++ Dockerfile | 2 +- README.de.md | 16 +--------------- README.md | 16 +--------------- VERSION | 2 +- kobrax_client.py | 6 ++++-- kobrax_moonraker_bridge.py | 10 ++++++---- web/themes/default/app.js | 31 +++++++++++++++++-------------- 9 files changed, 69 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md index cea68e6..b7f203e 100644 --- a/CHANGELOG.de.md +++ b/CHANGELOG.de.md @@ -1,5 +1,24 @@ # Changelog +## [0.9.21] – 2026-06-14 + +### Behoben +- **Kamera-Stream auf Android (Chrome / Firefox) nicht sichtbar.** Android-Browser + unterstützen `multipart/x-mixed-replace` (MJPEG) nicht. Die UI erkennt Android + jetzt automatisch und fällt auf Snapshot-Polling mit 5 fps zurück + (`/api/camera/snapshot` alle 200 ms) — keine Server-Änderung nötig. + +### Geändert +- Docker-Image auf **Debian 12 (Bookworm)** gepinnt (`python:3.11-slim-bookworm`), + um Kompatibilitätsprobleme mit glibc 2.41 zu vermeiden, die das aktuell von + `python:3.11-slim` gezogene Debian 13 Basis-Image mitbringt. +- MQTT- und HTTP-Verbindungen erzwingen jetzt **IPv4** (`AF_INET`), um + Verbindungsfehler auf Hosts zu verhindern, bei denen der Drucker nur über IPv4 + erreichbar ist, das OS aber IPv6 bevorzugt. +- Extruder-Stub in der Moonraker-`configfile`-Antwort enthält jetzt `sensor_type` + und `filament_diameter` — behebt einen Mobileraker-Absturz + (`Null is not a subtype of Object`, Issue #48). + ## [0.9.20] – 2026-06-08 ### Neu diff --git a/CHANGELOG.md b/CHANGELOG.md index 7931b9a..d075487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [0.9.21] – 2026-06-14 + +### Fixed +- **Camera stream not visible on Android (Chrome / Firefox).** Android + browsers do not support `multipart/x-mixed-replace` (MJPEG). The UI + now detects Android and falls back to snapshot-polling at 5 fps + (`/api/camera/snapshot` every 200 ms) — no server-side change needed. + +### Changed +- Docker image now pinned to **Debian 12 (Bookworm)** (`python:3.11-slim-bookworm`) + to avoid glibc 2.41 compatibility issues introduced by the Debian 13 + base image that `python:3.11-slim` recently started pulling. +- MQTT and HTTP connections now **force IPv4** (`AF_INET`) to prevent + connection failures on hosts where the printer is only reachable via + IPv4 but the OS prefers IPv6. +- Extruder stub in the Moonraker `configfile` response now includes + `sensor_type` and `filament_diameter` — fixes a Mobileraker crash + (`Null is not a subtype of Object`, issue #48). + ## [0.9.20] – 2026-06-08 ### New diff --git a/Dockerfile b/Dockerfile index 2e34be3..553475a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim +FROM python:3.11-slim-bookworm WORKDIR /app diff --git a/README.de.md b/README.de.md index a6d9b27..e6931e4 100644 --- a/README.de.md +++ b/README.de.md @@ -105,20 +105,6 @@ Drucker → Verbindungstyp **Moonraker** → Host: `http://BRIDGE-IP:7125` --- -## 🐳 Portainer - -[`docker-compose.portainer.yml`](docker-compose.portainer.yml) in -**Portainer → Stacks → Add stack → Web editor** einfügen und auf *Deploy* klicken. - -Keine Konfiguration nötig — die Bridge startet im Offline-Modus. -**`http://HOST-IP:7125`** öffnen, den **Drucker-Tab** aufrufen und **+ Drucker hinzufügen** klicken. -Nur die Drucker-IP eingeben — Zugangsdaten werden automatisch geholt und gespeichert. - -Alle Daten (Konfiguration, GCode-Store, Datenbank) liegen in Named Docker Volumes -(`kx-bridge-config`, `kx-bridge-data`), die Portainer verwaltet. - ---- - ## 📺 Video-Tutorial [![KX-Bridge Setup & Usage](https://img.youtube.com/vi/1Ql4wfH27fM/hqdefault.jpg)](https://www.youtube.com/watch?v=1Ql4wfH27fM) @@ -144,7 +130,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](#-features) 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). diff --git a/README.md b/README.md index b11bd71..7bf3199 100644 --- a/README.md +++ b/README.md @@ -104,20 +104,6 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125` --- -## 🐳 Portainer - -Paste [`docker-compose.portainer.yml`](docker-compose.portainer.yml) into -**Portainer → Stacks → Add stack → Web editor** and click *Deploy*. - -No configuration needed upfront — the bridge starts in offline mode. -Open **`http://HOST-IP:7125`**, go to the **Printers** tab and click **+ Add printer**. -Enter the printer's IP address — credentials are fetched and saved automatically. - -All data (config, GCode store, database) is stored in named Docker volumes -(`kx-bridge-config`, `kx-bridge-data`) managed by Portainer. - ---- - ## 📺 Video Tutorial [![KX-Bridge Setup & Usage](https://img.youtube.com/vi/1Ql4wfH27fM/hqdefault.jpg)](https://www.youtube.com/watch?v=1Ql4wfH27fM) @@ -143,7 +129,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](#-features) 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). diff --git a/VERSION b/VERSION index bd758c9..b216023 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.20 +0.9.21 diff --git a/kobrax_client.py b/kobrax_client.py index fa50ce3..e7080fc 100644 --- a/kobrax_client.py +++ b/kobrax_client.py @@ -167,7 +167,8 @@ class KobraXClient: ctx.set_ciphers("DEFAULT:@SECLEVEL=0") ctx.load_cert_chain(CERT_FILE, KEY_FILE) - raw = socket.create_connection((self.host, self.port), timeout=5) + _ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM) + raw = socket.create_connection(_ai[0][4], timeout=5) self._sock = ctx.wrap_socket(raw) log.info("TLS connected cipher=%s", self._sock.cipher()[0]) @@ -596,7 +597,8 @@ class KobraXClient: # langsamerem WLAN am Drucker dauert das Schieben sonst >30 s und # würde den Connect-Timeout fälschlich auslösen. Read-Timeout danach # generös (Drucker verarbeitet die Datei bevor er antwortet). - sock = socket.create_connection((self.host, 18910), timeout=10) + _ai = socket.getaddrinfo(self.host, 18910, socket.AF_INET, socket.SOCK_STREAM) + sock = socket.create_connection(_ai[0][4], timeout=10) sock.settimeout(None) # blocking während Send sock.sendall(headers + body) sock.settimeout(180) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 4c2f19e..8caf876 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -1906,10 +1906,12 @@ class KobraXBridge: "square_corner_velocity": 20.0, }, "extruder": { - "nozzle_diameter": 0.4, - "min_temp": 0, - "max_temp": 320, - "min_extrude_temp": 10, + "nozzle_diameter": 0.4, + "filament_diameter": 1.75, + "sensor_type": "ATC Semitec 104GT-2", + "min_temp": 0, + "max_temp": 320, + "min_extrude_temp": 10, }, "heater_bed": { "min_temp": 0, diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 284dd89..a84ba72 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -7,6 +7,7 @@ var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, var tempHistory={n:[],b:[]}; var camOn=false; var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print +var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support) var currentStep=1; var currentPanel='dashboard'; var aceAutoRefillPrefs=(function(){ @@ -1501,23 +1502,24 @@ function camStart(){ img.style.display='none'; sp.style.display='block'; post('/api/camera/start',{}).then(function(){ - img.onerror=function(){ - sp.style.display='none'; - img.style.display='none'; - ph.style.display='flex'; - camOn=false; - document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start'); - clog(tr('log_error')+' '+tr('cam_stream_unavailable'),'msg-err'); - }; - img.src='/api/camera/stream?t='+Date.now(); camOn=true; document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_stop'); clog(tr('log_cam_start'),'msg-ok'); - // MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden - setTimeout(function(){ - sp.style.display='none'; - img.style.display='block'; - },1200); + setTimeout(function(){ sp.style.display='none'; img.style.display='block'; },1200); + if(/Android/i.test(navigator.userAgent)){ + // Android browsers don't support multipart/x-mixed-replace (MJPEG) — poll snapshots instead + _camPollInterval=setInterval(function(){ img.src='/api/camera/snapshot?t='+Date.now(); },200); + } else { + img.onerror=function(){ + sp.style.display='none'; + img.style.display='none'; + ph.style.display='flex'; + camOn=false; + document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start'); + clog(tr('log_error')+' '+tr('cam_stream_unavailable'),'msg-err'); + }; + img.src='/api/camera/stream?t='+Date.now(); + } }).catch(function(e){ sp.style.display='none'; ph.style.display='flex'; @@ -1527,6 +1529,7 @@ function camStart(){ function camStop(){ var img=document.getElementById('cam-img'); + if(_camPollInterval){clearInterval(_camPollInterval);_camPollInterval=null;} img.onerror=null; // deregister error handler before clearing src to avoid spurious error toast post('/api/camera/stop',{}).catch(function(){}); img.src=''; From ed30568092504ff24b15cd44876336f213033563 Mon Sep 17 00:00:00 2001 From: viewit Date: Tue, 16 Jun 2026 13:12:04 +0200 Subject: [PATCH 5/9] build: sources for v0.9.22 --- CHANGELOG.md | 38 +++++ VERSION | 2 +- config_loader.py | 46 +++++- kobrax_moonraker_bridge.py | 231 +++++++++++++++++++++-------- web/themes/default/app.js | 243 +++++++++++++++++++++++++----- web/themes/default/index.html | 272 +++++++++++++++++++++------------- web/themes/default/style.css | 18 +++ web/translations/de.json | 15 +- web/translations/en.json | 15 +- web/translations/es.json | 15 +- web/translations/fr.json | 15 +- web/translations/zh-cn.json | 15 +- 12 files changed, 715 insertions(+), 210 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d075487..5ed96cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## [0.9.22] – 2026-06-16 + +### New +- **Restructured Settings panel.** The settings modal has been replaced by a + persistent Master-Detail panel with five categories: Connection, Printer, + Appearance, Filament, and System. Poll interval is now adjustable live. +- **Vendor visibility filter (issue #41).** A new checklist in the Filament + settings lets you restrict the slot profile dropdown to specific manufacturers. + "Generic" and your own imported profiles are always visible. The list updates + automatically after a profile import. +- **Idle file actions in the progress card (issue #55).** After uploading a file + while the printer is idle, three quick-action buttons appear directly in the + progress card: ▶ Print, ⚙ Map Slots, and ✕ Clear — matching the file browser + workflow without navigating away. + +### Fixed +- **Mobileraker: app crashed with `Null is not a subtype of Object` in + `ConfigExtruder.fromJson` (issue #48).** `configfile.config` was returned as + an empty object `{}`. Mobileraker parses both `configfile.settings` and + `configfile.config` through the same strict Dart parser — both are now + populated with the same extruder/bed/stepper stub. +- **Mobileraker: app hung indefinitely on refresh (issue #48).** The WebSocket + `server.files.metadata` handler called a non-existent store method + (`get_file_by_filename`), always returning empty metadata. Mobileraker retried + this thousands of times per second. Both the HTTP and WS paths now share a + single `_build_file_metadata()` method. +- **Mobileraker: ETA / remaining time not shown (issue #48).** A side effect of + the metadata loop fix — once `currentFile` resolves, Mobileraker can calculate + ETA from `estimated_time`. +- **Mobileraker: `notify_status_update` triggered repeated `ConfigFile.parse` + (issue #48).** Static objects (`configfile`, `webhooks`, `heaters`, `history`) + were included in every live status push. They are now filtered out; only live + telemetry is broadcast. +- `motion_report` (`live_position`, `live_velocity`) added to printer objects + for Mobileraker motion display. +- Saving filament slot profiles no longer silently drops the `visible_vendors` + setting from `config.ini`. + ## [0.9.21] – 2026-06-14 ### Fixed diff --git a/VERSION b/VERSION index b216023..ea3f0d7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.21 +0.9.22 diff --git a/config_loader.py b/config_loader.py index 807f911..ff14dd1 100644 --- a/config_loader.py +++ b/config_loader.py @@ -227,10 +227,17 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool: return False cfg = configparser.ConfigParser() cfg.read(path, encoding="utf-8") + # visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der + # Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren. + preserved_vendors = None + if cfg.has_option("filament_profiles", "visible_vendors"): + preserved_vendors = cfg.get("filament_profiles", "visible_vendors") if cfg.has_section("filament_profiles"): cfg.remove_section("filament_profiles") - if profiles: + if profiles or preserved_vendors: cfg["filament_profiles"] = {} + if preserved_vendors: + cfg["filament_profiles"]["visible_vendors"] = preserved_vendors for slot_idx in sorted(profiles.keys()): entry = profiles[slot_idx] or {} if entry.get("vendor"): @@ -244,6 +251,43 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool: return True +def list_visible_vendors() -> list[str]: + """Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini. + + Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A). + Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren). + """ + path = _find_config_file() + if not path: + return [] + cfg = configparser.ConfigParser() + cfg.read(path, encoding="utf-8") + if not cfg.has_option("filament_profiles", "visible_vendors"): + return [] + raw = cfg.get("filament_profiles", "visible_vendors") + return [v.strip() for v in raw.split(",") if v.strip()] + + +def save_visible_vendors(vendors: list[str]) -> bool: + """Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings + (slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder.""" + path = _find_config_file() + if not path: + return False + cfg = configparser.ConfigParser() + cfg.read(path, encoding="utf-8") + if not cfg.has_section("filament_profiles"): + cfg.add_section("filament_profiles") + clean = [v.strip() for v in (vendors or []) if v and v.strip()] + if clean: + cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean) + elif cfg.has_option("filament_profiles", "visible_vendors"): + cfg.remove_option("filament_profiles", "visible_vendors") + with open(path, "w", encoding="utf-8") as f: + cfg.write(f) + return True + + def get(key: str, default: str = "") -> str: return os.environ.get(key, default) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 8caf876..cea6819 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -34,6 +34,7 @@ except ImportError: import env_loader import asyncio import hashlib +import copy import json import logging import os @@ -751,6 +752,13 @@ class KobraXBridge: self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles() except Exception: self._filament_profiles = {} + # Vendor-Sichtbarkeitsfilter fürs Slot-Profil-Dropdown (Issue #41 Option A). + # Leere Liste = alle Vendoren sichtbar (rückwärtskompatibel). + try: + import config_loader as _cl + self._visible_vendors: list[str] = _cl.list_visible_vendors() + except Exception: + self._visible_vendors = [] self._last_state: dict = {} self._state = { "nozzle_temp": 0.0, @@ -1694,13 +1702,22 @@ class KobraXBridge: # WebSocket push # ------------------------------------------------------------------------- + # Statische Objekte, die sich zur Laufzeit nie ändern. Sie werden einmalig + # per objects.query/subscribe ausgeliefert, aber NICHT in jedem + # notify_status_update mitgeschickt — sonst läuft Mobilerakers + # ConfigFile.parse (teuer + strikt) bei jedem Status-Tick erneut und die App + # hängt/crasht beim Refresh (Issue #48). + _STATIC_STATUS_OBJECTS = ("configfile", "webhooks", "heaters", "history") + def _push_status_update(self): if not self.ws_clients: return + objs = self._build_printer_objects() + live = {k: v for k, v in objs.items() if k not in self._STATIC_STATUS_OBJECTS} msg = { "jsonrpc": "2.0", "method": "notify_status_update", - "params": [self._build_printer_objects(), time.time()], + "params": [live, time.time()], } text = json.dumps(msg) dead = set() @@ -1858,6 +1875,17 @@ class KobraXBridge: "homing_origin": [0, 0, 0, 0], "position": [0, 0, self._estimate_current_z(), 0], }, + # motion_report: Mobileraker liest die Live-Geschwindigkeit hier + # (live_velocity). Der Kobra-X-MQTT liefert KEINE echte mm/s, nur + # einen print_speed_mode (1-4). live_velocity bleibt daher 0 — das + # Objekt muss aber existieren, sonst zeigt Mobileraker nichts an + # (motion_report war zuvor null). live_position spiegelt die + # geschätzte Z-Höhe (wie gcode_move). + "motion_report": { + "live_position": [0, 0, self._estimate_current_z(), 0], + "live_velocity": 0.0, + "live_extruder_velocity": 0.0, + }, "fan": { "speed": (int(s.get("fan_speed") or 0)) / 100.0, "rpm": None, @@ -1894,40 +1922,78 @@ class KobraXBridge: # 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, - "filament_diameter": 1.75, - "sensor_type": "ATC Semitec 104GT-2", - "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": {}, + # Mobileraker (Issue #48) parst BEIDE Zweige config + settings durch + # denselben ConfigFile.parse → ConfigExtruder.fromJson; ein leeres + # config:{} ließ den nicht-nullbaren Dart-Parser crashen. Daher wird + # config identisch zu settings gespiegelt. + "configfile": self._klipper_configfile_stub(), + } + + def _klipper_configfile_stub(self) -> dict: + """Minimaler Klipper-configfile-Stub für Mobileraker/OctoApp (Issue #48). + + Mobileraker parst BEIDE Zweige `config` und `settings` durch denselben + ConfigFile.parse → ConfigExtruder.fromJson. Ein leeres `config: {}` + ließ den nicht-nullbaren Dart-Parser crashen, daher wird `config` + identisch zu `settings` gespiegelt. Werte aus der entschlüsselten + avata_main.conf (ACCFG1.0 — Kobra X Firmware). + """ + 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, + "filament_diameter": 1.75, + "sensor_type": "ATC Semitec 104GT-2", + "min_temp": 0, + "max_temp": 320, + "min_extrude_temp": 10, + # Mobileraker ConfigExtruder erwartet diese Felder non-nullable + # (max_extrude_only_distance, max_power) bzw. als Key präsent + # (max_extrude_only_velocity/accel dürfen null sein). Fehlen = + # Crash in ConfigExtruder.fromJson (Issue #48). + "max_extrude_only_distance": 100.0, + "max_power": 1.0, + "max_extrude_only_velocity": None, + "max_extrude_only_accel": None, + }, + "heater_bed": { + # Mobileraker ConfigHeaterBed: heater_pin, sensor_type, control + # sind non-nullable. Werte sind Platzhalter (Bridge kennt die + # echten Pins nicht — Anycubic-Firmware, kein Klipper-printer.cfg). + "heater_pin": "PA0", + "sensor_type": "ATC Semitec 104GT-2", + "control": "pid", + "min_temp": 0, + "max_temp": 120, + }, + # stepper_* mit non-nullable Pflichtfeldern (step_pin, dir_pin, + # rotation_distance) füllen, sonst crasht ConfigStepper.fromJson. + "stepper_x": {"step_pin": "PA1", "dir_pin": "PA2", "rotation_distance": 40, + "position_min": -18.5, "position_max": 280}, + "stepper_y": {"step_pin": "PA3", "dir_pin": "PA4", "rotation_distance": 40, + "position_min": -6.5, "position_max": 272.5}, + "stepper_z": {"step_pin": "PA5", "dir_pin": "PA6", "rotation_distance": 8, + "position_min": -4, "position_max": 262}, + "virtual_sdcard": {"path": "/data/gcodes"}, + "pause_resume": {}, + "display_status": {}, + } + # config + settings müssen dieselben Felder enthalten — Mobileraker + # parst beide. deepcopy, damit kein Client durch geteilte Referenz + # versehentlich beide Zweige mutiert. + return { + "config": copy.deepcopy(settings), + "settings": settings, + "warnings": [], + "save_config_pending": False, + "save_config_pending_items": {}, } # ------------------------------------------------------------------------- @@ -2247,6 +2313,31 @@ class KobraXBridge: "name": entry.get("name", ""), "id": entry.get("id", "")}) + async def handle_kx_visible_vendors(self, request): + """GET/POST /kx/filament/visible_vendors — Vendor-Sichtbarkeitsfilter + fürs Slot-Profil-Dropdown (Issue #41 Option A). + + GET → {"result": ["Polymaker", "eSUN", ...]} + POST {"vendors": [...]} → speichert in config.ini [filament_profiles] + visible_vendors. Leere Liste = alle sichtbar. KEIN Bridge-Neustart + nötig (nur Anzeigefilter).""" + if request.method == "POST": + try: + data = await request.json() + except Exception: + data = {} + vendors = data.get("vendors") or [] + if not isinstance(vendors, list): + return self._json_cors({"error": "vendors must be a list"}, status=400) + self._visible_vendors = [str(v).strip() for v in vendors if str(v).strip()] + try: + import config_loader as _cl + _cl.save_visible_vendors(self._visible_vendors) + except Exception as e: + log.warning(f"save_visible_vendors failed: {e}") + return self._json_cors({"error": str(e)}, status=500) + return self._json_cors({"result": self._visible_vendors}) + def _load_orca_filaments(self) -> list[dict]: """Lädt System- + User-Profile aus dem Cache. System-Profile kommen aus bridge/data/orca_filaments.json (Image-embedded), User-Profile @@ -2591,18 +2682,16 @@ class KobraXBridge: }) return web.json_response({"result": files}) - async def handle_files_metadata(self, request): - """Moonraker /server/files/metadata — moonraker-obico-Plugin holt das - einmal pro Druck und liest daraus `object_height` (für `currentZ`- - Anzeige im Obico-UI: `mmProgress` braucht maxZ), `layer_count`, - `layer_height` und `first_layer_height` (für die Layer-Berechnung). + def _build_file_metadata(self, filename: str) -> dict: + """Baut die Moonraker-file-metadata für eine Datei. Gemeinsame Quelle + für HTTP /server/files/metadata UND den WS-RPC server.files.metadata + (vorher hatte der WS-Pfad eigene, kaputte Logik mit einer nicht + existierenden Store-Methode → leere Antwort → Mobileraker fragte in + Endlosschleife, App hing beim Refresh, Issue #48). - Quelle: aktueller `_state` + GCode-Store-Eintrag wenn vorhanden. - Wenn Layer-Heights weder im State noch im Store sind, Fallback auf die - OrcaSlicer-Default-Filename-Heuristik (`_layer_height_from_filename`).""" - filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "") - if not filename: - return web.json_response({"result": {}}) + Liefert Mobileraker-kompatible Pflichtfelder: `filename`, `size`, + `modified` sind in GCodeFile non-nullable; `print_start_time` und die + Slicer-Felder optional.""" s = self._state layer_h = float(s.get("layer_height") or 0.0) first_h = float(s.get("first_layer_height") or 0.0) @@ -2626,16 +2715,27 @@ class KobraXBridge: if layer_h and not first_h: first_h = layer_h object_height = round(first_h + max(0, total_layers - 1) * layer_h, 3) if (layer_h and total_layers) else 0.0 - return web.json_response({"result": { + return { "filename": filename, - "size": size_bytes, + # GCodeFile (Mobileraker) verlangt size als non-nullable int. + "size": size_bytes or 1, "modified": time.time(), "estimated_time": est_time or None, "layer_height": layer_h or None, "first_layer_height": first_h or None, "layer_count": total_layers or None, "object_height": object_height or None, - }}) + "thumbnails": [], + } + + async def handle_files_metadata(self, request): + """Moonraker /server/files/metadata — moonraker-obico + Mobileraker + holen Datei-Metadaten (Slicer-Zeit, Layer, object_height). + Logik in _build_file_metadata (gemeinsam mit WS-RPC).""" + filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "") + if not filename: + return web.json_response({"result": {}}) + return web.json_response({"result": self._build_file_metadata(filename)}) # ── Moonraker-Stubs für moonraker-obico ────────────────────────────────── async def handle_access_api_key(self, request): @@ -3797,6 +3897,9 @@ class KobraXBridge: "auto_leveling": getattr(self._args, "auto_leveling", 1), "camera_on_print": getattr(self._args, "camera_on_print", 0), "web_upload_warning": getattr(self._args, "web_upload_warning", 1), + "poll_interval": getattr(self._args, "poll_interval", 3), + "filament_profiles": {str(k): v for k, v in self._filament_profiles.items()}, + "visible_vendors": self._visible_vendors, "ace_dry_presets": self._ace_dry_presets, }) @@ -3827,7 +3930,13 @@ class KobraXBridge: cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))) cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0)))))) cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1)))))) - if not cfg.has_option("bridge", "poll_interval"): + if "poll_interval" in data: + try: + pi = max(1, min(60, int(data["poll_interval"]))) + except (TypeError, ValueError): + pi = 3 + cfg.set("bridge", "poll_interval", str(pi)) + elif not cfg.has_option("bridge", "poll_interval"): cfg.set("bridge", "poll_interval", "3") printer_name = str(data.get("printer_name", "")).strip() if printer_name: @@ -4491,23 +4600,13 @@ class KobraXBridge: elif method == "machine.update.status": result = {"busy": False, "version_info": {}} elif method == "server.files.metadata": - # Obico fragt nach Metadaten zu einer Datei (filename in params) + # Obico + Mobileraker fragen Metadaten zu einer Datei. Dieselbe + # Logik wie der HTTP-Endpoint (vorher eigener kaputter Pfad mit + # nicht existierender Store-Methode → leere Antwort → + # Mobileraker-Endlosschleife, Issue #48). fname = (params or {}).get("filename") if isinstance(params, dict) else None - meta = {} - if fname: - try: - rec = self._store.get_file_by_filename(fname) if hasattr(self._store, "get_file_by_filename") else None - except Exception: - rec = None - if rec: - meta = { - "filename": rec.get("filename"), - "size": rec.get("size_bytes") or 0, - "modified": time.time(), - "estimated_time": rec.get("est_print_time_sec") or 0, - "thumbnails": [], - } - result = meta + fname = fname or self._state.get("filename", "") + result = self._build_file_metadata(fname) if fname else {} else: log.debug(f"Unbekannte RPC-Methode: {method}") result = {} @@ -4711,6 +4810,8 @@ def build_app(bridge: KobraXBridge) -> web.Application: r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots) r.add_get("/kx/filament/profiles", bridge.handle_kx_filament_profiles) r.add_post("/kx/filament/slots/{idx}/profile", bridge.handle_kx_filament_slot_profile) + r.add_get("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors) + r.add_post("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors) # Custom-Profile-Import (Issue #41) — User lädt eigene Orca-Filament- # Profile als ZIP/JSON hoch (z.B. aus ~/.config/OrcaSlicer/user//filament/), # weil die Bridge typischerweise nicht auf demselben Host wie OrcaSlicer läuft. diff --git a/web/themes/default/app.js b/web/themes/default/app.js index a84ba72..44e425f 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -8,6 +8,7 @@ var tempHistory={n:[],b:[]}; var camOn=false; var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support) +var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55) var currentStep=1; var currentPanel='dashboard'; var aceAutoRefillPrefs=(function(){ @@ -167,6 +168,8 @@ async function setLanguage(lang){ localStorage.setItem('lang',l); var langSel=document.getElementById('lang-select'); if(langSel)langSel.value=l; + var sLangSel=document.getElementById('s-lang-select'); + if(sLangSel)sLangSel.value=l; document.documentElement.setAttribute('lang',l); applyLang(); } @@ -310,12 +313,26 @@ function applyLang(){ document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':',''))); // Console setText('ptitle-console',T.panel_console_title); - // Settings modal - setText('modal-title-settings',T.settings_title); + // Settings-Panel setText('modal-sec-connection',T.settings_connection); setText('modal-sec-print',T.settings_print); - setText('modal-sec-poll',T.settings_poll); setText('modal-sec-version',T.settings_version); + // Nav + Kategorie-Labels (mit Fallback falls i18n-Key noch fehlt) + setText('nav-settings',T.nav_settings||'Einstellungen'); + setText('setcat-lbl-connection',T.settings_connection||'Verbindung'); + setText('setcat-lbl-printer',T.settings_print||'Drucker'); + setText('setcat-lbl-display',T.settings_cat_display||'Darstellung'); + setText('setcat-lbl-display2',T.settings_cat_display||'Darstellung'); + setText('setcat-lbl-filament',T.settings_cat_filament||'Filament'); + setText('setcat-lbl-system',T.settings_version||'System'); + setText('lbl-set-lang',T.settings_cat_language||'Sprache'); + setText('lbl-set-theme',T.settings_cat_theme||'Hell / Dunkel umschalten'); + setText('lbl-poll-interval',T.settings_poll||'Poll-Intervall (Sekunden)'); + setText('lbl-filament-mapping',T.settings_filament_mapping||'Filament-Profil-Mapping (pro Slot)'); + setText('lbl-filament-mapping-save',T.settings_filament_mapping_save||'Mapping speichern'); + setText('lbl-visible-vendors',T.settings_visible_vendors||'Sichtbare Hersteller (Profil-Dropdown)'); + setText('visible-vendors-hint',T.settings_visible_vendors_hint||'Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.'); + setText('lbl-visible-vendors-save',T.settings_visible_vendors_save||'Auswahl speichern'); // Custom-Profile-Import (Issue #41) setText('modal-sec-orca-profiles',T.orca_profile_section); setText('orca-profiles-hint',T.orca_profile_hint); @@ -343,6 +360,10 @@ function applyLang(){ setText('lbl-update-check',T.update_check); setText('lbl-update-apply',T.update_apply); + // Progress-Karten-Aktionen für geladene/idle Datei (Issue #55) + setText('d-idle-print-lbl',T.progress_action_print||'Drucken'); + setText('d-idle-slots-lbl',T.progress_action_slots||'Slots zuordnen'); + setText('d-idle-clear-lbl',T.progress_action_clear||'Leeren'); // Speed buttons setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,'')); setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,'')); @@ -462,6 +483,15 @@ function showPanel(id){ var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active'); var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active'); currentPanel=id; + if(id==='settings')openSettings(); +} + +// Settings-Kategorie umschalten (Master-Detail) +function showSettingsCat(name){ + document.querySelectorAll('.set-group').forEach(g=>g.classList.remove('active')); + document.querySelectorAll('.set-cat').forEach(b=>b.classList.remove('active')); + var g=document.getElementById('setgrp-'+name);if(g)g.classList.add('active'); + var c=document.getElementById('setcat-'+name);if(c)c.classList.add('active'); } // ── Console log ── @@ -619,11 +649,13 @@ function applyState(){ // connection error banner – nur wenn überhaupt ein Drucker konfiguriert ist var banner=document.getElementById('conn-error-banner'); if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+tr('lbl_conn_error')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}} + var bannerVisible=false; var frb=document.getElementById('file-ready-banner'); if(frb){ if(s.file_ready&&s.print_state==='standby'){ document.getElementById('file-ready-name').textContent=s.file_ready; frb.style.display='flex'; + bannerVisible=true; }else{frb.style.display='none';} } // skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird @@ -635,6 +667,25 @@ function applyState(){ var ctrlBtns=document.getElementById('d-ctrl-btns'); if(ctrlBtns) ctrlBtns.style.display=printing?'':'none'; updatePauseResumeBtn(); + // Zuletzt geladene Datei merken (Issue #55): solange sie über den State + // sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename + // (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen. + if(s.file_ready) _lastLoadedFile=s.file_ready; + else if(s.filename) _lastLoadedFile=s.filename; + // Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine + // Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion + // anbietet. + var idleBtns=document.getElementById('d-idle-btns'); + if(idleBtns){ + var showIdle=(!printing && _lastLoadedFile && !bannerVisible); + idleBtns.style.display=showIdle?'':'none'; + if(showIdle){ + var dfn=document.getElementById('d-fname'); + if(dfn && (!dfn.textContent || dfn.textContent==='–')){ + dfn.textContent=_lastLoadedFile;dfn.title=_lastLoadedFile; + } + } + } // header var b=document.getElementById('h-badge'); @@ -896,10 +947,6 @@ function drawChart(id,_,series){ var _updateTag=''; var _updateUrl=''; function openSettings(){ - // Titel mit aktivem Drucker-Namen aktualisieren - var pname=_activePrinter&&_activePrinter.name?_activePrinter.name:null; - var title=document.getElementById('modal-title-settings'); - if(title)title.textContent=T.settings_title+(pname?' – '+pname:''); fetch(_apiUrl('/api/settings')).then(function(r){return r.json()}).then(function(d){ document.getElementById('s-printer-name').value=d.printer_name||''; document.getElementById('s-printer-ip').value=d.printer_ip||''; @@ -912,23 +959,118 @@ function openSettings(){ document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling); var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print; var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning); + // Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage + var pi=document.getElementById('s-poll-interval'); + if(pi){ + var sec=d.poll_interval||Math.round((parseInt(localStorage.getItem('pollInterval')||'2000'))/1000)||3; + pi.value=sec; + } + renderFilamentMapping(d.filament_profiles||{}); }); - var v=localStorage.getItem('pollInterval')||'2000'; - document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')}); - var pb=document.getElementById('poll-'+Math.round(parseInt(v)/1000)); - if(pb)pb.classList.add('active'); + // Sprach-Select im Settings-Panel mit aktueller Sprache spiegeln + var ls=document.getElementById('s-lang-select'); + if(ls)ls.value=(localStorage.getItem('lang')||document.documentElement.lang||'de'); document.getElementById('s-version-label').textContent='v'+('__VERSION__'||'?'); document.getElementById('update-status').textContent=''; document.getElementById('btn-update-apply').style.display='none'; var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none'; _updateTag='';_updateUrl=''; - document.getElementById('settings-modal').classList.add('open'); - // Custom-Profile-Liste laden (Issue #41 — Verwaltung von User-importierten - // OrcaSlicer-Filament-Profilen) + // Custom-Profile-Liste laden (Issue #41) refreshUserProfileList(); + // Vendor-Sichtbarkeitsfilter (Issue #41 Option A) + loadVendorChecklist(); } function closeSettings(){ - document.getElementById('settings-modal').classList.remove('open'); + // Panel-Variante: zurück zum Dashboard + showPanel('dashboard'); +} + +// Poll-Intervall-Feld → Live-Poll sofort übernehmen (Persistenz erst beim Speichern) +function onPollIntervalInput(){ + var pi=document.getElementById('s-poll-interval'); + if(!pi)return; + var sec=parseInt(pi.value,10); + if(sec>=1&&sec<=60)setPoll(sec*1000); +} + +// ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ── +function renderFilamentMapping(map){ + var el=document.getElementById('filament-mapping-list'); + if(!el)return; + var rows=''; + for(var i=0;i<4;i++){ + var m=map[i]||map[String(i)]||{}; + var idHint=m.id?' ('+m.id+')':''; + rows+=''; + } + el.innerHTML=rows; +} +function saveFilamentMapping(){ + // Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend). + // Leere Felder = Mapping entfernen. + var chain=Promise.resolve(); + for(var i=0;i<4;i++){ + (function(slot){ + var vendor=((document.getElementById('fmap-'+slot+'-vendor')||{}).value||'').trim(); + var name=((document.getElementById('fmap-'+slot+'-name')||{}).value||'').trim(); + chain=chain.then(function(){ + return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'), + {method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({vendor:vendor,name:name})}); + }); + })(i); + } + chain.then(function(){ + clog(tr('log_filament_mapping_saved')||'Filament-Mapping gespeichert','msg-ok'); + openSettings(); // neu laden → ID-Hints aktualisieren + }).catch(function(e){clog('Mapping-Fehler: '+e,'msg-err');}); +} + +// ── Vendor-Sichtbarkeitsfilter (Issue #41 Option A) ── +var _vendorChecklistSel={}; // {vendor:true} — laufende Auswahl im UI +function loadVendorChecklist(){ + // aktuelle Auswahl aus Backend, dann alle verfügbaren Vendoren rendern + _visibleVendors=null; // Cache invalidieren + _loadVisibleVendors(function(vis){ + _vendorChecklistSel={}; + (vis||[]).forEach(function(v){_vendorChecklistSel[v]=true;}); + renderVendorChecklist(); + }); +} +function renderVendorChecklist(){ + var el=document.getElementById('visible-vendors-list'); + if(!el)return; + _loadOrcaFilaments(function(profiles){ + // alle System-Vendoren (ohne Generic — der ist immer sichtbar) sammeln + var set={}; + profiles.forEach(function(p){ if(!p.is_user && p.vendor && p.vendor!=='Generic') set[p.vendor]=1; }); + var vendors=Object.keys(set).sort(); + var q=((document.getElementById('vendor-filter-search')||{}).value||'').toLowerCase(); + if(q)vendors=vendors.filter(function(v){return v.toLowerCase().indexOf(q)>=0;}); + el.innerHTML=vendors.map(function(v){ + var ck=_vendorChecklistSel[v]?'checked':''; + var safe=v.replace(/"/g,'"'); + return ''; + }).join('')||''; + }); +} +function _vendorCheck(cb){ + var v=cb.getAttribute('data-vendor'); + if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v]; +} +function saveVisibleVendors(){ + var vendors=Object.keys(_vendorChecklistSel); + fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({vendors:vendors})}).then(function(r){return r.json();}).then(function(){ + _visibleVendors=vendors.slice(); // Dropdown-Cache aktualisieren + clog(tr('log_visible_vendors_saved')||'Hersteller-Auswahl gespeichert','msg-ok'); + }).catch(function(e){clog('Vendor-Filter-Fehler: '+e,'msg-err');}); } // ── Custom Filament Profile Import (Issue #41) ── @@ -1002,6 +1144,9 @@ function doProfileImportUpload(files){ _orcaFilamentCache=null; refreshImportDialogList(); refreshUserProfileList(); + // Vendor-Checkliste neu aufbauen — ein Import kann einen bisher + // unbekannten System-Vendor mitbringen (Issue #41). + if(document.getElementById('visible-vendors-list')) renderVendorChecklist(); // Wenn Slot-Edit offen ist, Dropdown gleich neu befüllen var mat=document.getElementById('slot-edit-mat'); if(mat && document.getElementById('slot-edit-modal').classList.contains('open')){ @@ -1042,6 +1187,14 @@ function updateSlotEditFeedButton(){ btn.textContent=_slotEditLoaded?tr('slot_edit_unload'):tr('slot_edit_load'); } var _orcaFilamentCache=null; // [{id,name,vendor,type,color}, …] +var _visibleVendors=null; // Vendor-Sichtbarkeitsfilter (Issue #41); [] = alle +function _loadVisibleVendors(cb){ + if(_visibleVendors!==null){ cb(_visibleVendors); return; } + fetch(_apiUrl('/kx/filament/visible_vendors')).then(function(r){return r.json();}).then(function(d){ + _visibleVendors=d.result||[]; + cb(_visibleVendors); + }).catch(function(){ _visibleVendors=[]; cb([]); }); +} function _loadOrcaFilaments(cb){ if(_orcaFilamentCache){ cb(_orcaFilamentCache); return; } fetch(_apiUrl('/kx/filament/profiles')).then(function(r){return r.json();}).then(function(d){ @@ -1087,13 +1240,23 @@ function _fillSlotProfileDropdown(material, currentVendor, currentName){ userProfs.forEach(function(p){ _appendOption(gUser, p); }); sel.appendChild(gUser); } - // System-Profile nach Vendor gruppieren - var byVendor={}; - systemProfs.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); }); - Object.keys(byVendor).sort().forEach(function(v){ - var g=document.createElement('optgroup'); g.label=v; - byVendor[v].forEach(function(p){ _appendOption(g, p); }); - sel.appendChild(g); + // Vendor-Sichtbarkeitsfilter (Issue #41 Option A): nur gewählte Vendoren + + // Generic. Leere Liste = alle (rückwärtskompatibel). Eigene Profile (is_user) + // sind oben bereits unkonditional drin. + _loadVisibleVendors(function(vis){ + var filtered=systemProfs; + if(vis&&vis.length){ + var allow={};vis.forEach(function(v){allow[v]=1;}); + allow['Generic']=1; + filtered=systemProfs.filter(function(p){return allow[p.vendor];}); + } + var byVendor={}; + filtered.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); }); + Object.keys(byVendor).sort().forEach(function(v){ + var g=document.createElement('optgroup'); g.label=v; + byVendor[v].forEach(function(p){ _appendOption(g, p); }); + sel.appendChild(g); + }); }); }); } @@ -1147,15 +1310,16 @@ function slotEditFeed(){ }) .catch(function(){}); } -function startReadyFile(){ - var currentFile=(storeFiles||[]).find(function(f){return f.filename===S.file_ready;}); +function startReadyFile(filename){ + var fn=filename||S.file_ready; + var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(currentFile, function(){ startReadyFile(); }); + maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); }); return; } var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:S.file_ready}) + post('/printer/print/start',{filename:fn}) .then(function(r){return r.json();}) .then(function(r){ document.getElementById('file-ready-banner').style.display='none'; @@ -1170,6 +1334,20 @@ function cancelReadyFile(){ post('/api/file_ready/clear',{}) .then(function(){document.getElementById('file-ready-banner').style.display='none';}); } + +// ── Aktionen für geladene/idle Datei in der Progress-Karte (Issue #55) ── +function startIdleFile(){ + if(_lastLoadedFile) startReadyFile(_lastLoadedFile); +} +function startIdleFileWithSlots(){ + if(_lastLoadedFile) startReadyFileWithSlots(_lastLoadedFile); +} +function clearIdleFile(){ + _lastLoadedFile=null; + var ib=document.getElementById('d-idle-btns');if(ib)ib.style.display='none'; + var fn=document.getElementById('d-fname');if(fn){fn.textContent='–';fn.title='';} + post('/api/file_ready/clear',{}).catch(function(){}); +} function selectMatPreset(m){ document.getElementById('slot-edit-mat').value=m; highlightMatBtn(m); @@ -1257,9 +1435,6 @@ document.addEventListener('DOMContentLoaded',function(){ }); }); function setPoll(ms){ - document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')}); - var id='poll-'+Math.round(ms/1000); - var pb=document.getElementById(id);if(pb)pb.classList.add('active'); localStorage.setItem('pollInterval',ms); clearInterval(pollTimer); pollTimer=setInterval(poll,ms); @@ -1281,6 +1456,7 @@ function saveSettings(){ auto_leveling: document.getElementById('s-auto-leveling').checked?1:0, camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0, web_upload_warning:webUploadWarning, + poll_interval: Math.min(60,Math.max(1,parseInt((document.getElementById('s-poll-interval')||{}).value,10)||3)), }).then(function(){ btn.textContent=T.update_restarting; setTimeout(function(){ @@ -2063,14 +2239,15 @@ function confirmStoreWebVerify(){ }); } -function startReadyFileWithSlots(){ - var currentFile=(storeFiles||[]).find(function(f){return f.filename===S.file_ready;}); +function startReadyFileWithSlots(filename){ + var fn=filename||S.file_ready; + var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(); }); + maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn); }); return; } _filamentDialogMode='banner'; - _storeFilename=S.file_ready||''; + _storeFilename=fn||''; // Banner must never reuse stale store-file context. _storeFileId=null; _gcodeFilaments=[]; diff --git a/web/themes/default/index.html b/web/themes/default/index.html index d662a58..5be145c 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -42,115 +42,12 @@
- + - + + +
@@ -520,6 +425,162 @@
+ + +
+
+
+ + + + + +
+ +
+ +
+
+
🔌 Verbindung
+ + + + + + + +
+
+ + +
+
+
🖨 Druckeinstellungen
+ + + + +
+
+ + +
+
+
🎨 Darstellung
+ + + +
+
+ + +
+
+
🧵 OrcaSlicer-Profile
+
+ Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder) +
+
+ +
+
+
🎯 Filament-Profil-Mapping (pro Slot)
+
+ Festes Orca-Profil pro AMS-Slot. Beim Slicer-Sync sendet die Bridge dieses Profil statt „Generic". +
+
+ +
+
+
👁 Sichtbare Hersteller (Profil-Dropdown)
+
+ Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar. +
+ +
+ +
+
+ + +
+
+
Version
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -528,6 +589,7 @@ + diff --git a/web/themes/default/style.css b/web/themes/default/style.css index a2a4572..369830f 100644 --- a/web/themes/default/style.css +++ b/web/themes/default/style.css @@ -212,6 +212,24 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background: .panel{display:none} .panel.active{display:block} +/* ── SETTINGS (Master-Detail) ── */ +.settings-wrap{display:grid;grid-template-columns:200px 1fr;gap:16px;align-items:start} +.settings-cats{display:flex;flex-direction:column;gap:4px;position:sticky;top:12px} +.set-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px; + border:1px solid transparent;background:var(--raised);color:var(--txt2);cursor:pointer; + font-size:13px;text-align:left;transition:background .15s,color .15s} +.set-cat:hover{color:var(--txt)} +.set-cat.active{background:var(--accent);color:#fff;border-color:var(--accent)} +.set-group{display:none} +.set-group.active{display:block} +.set-group .card{margin-bottom:14px} +@media(max-width:768px){ + .settings-wrap{grid-template-columns:1fr} + .settings-cats{flex-direction:row;flex-wrap:wrap;position:static;overflow-x:auto} + .set-cat{flex:1;min-width:auto;justify-content:center;padding:8px 6px;font-size:12px} + .set-cat .nav-text{display:inline} +} + /* ── FILE BROWSER UPLOAD ZONE ── */ #store-upload-zone{ display:flex;flex-direction:column;align-items:center;justify-content:center; diff --git a/web/translations/de.json b/web/translations/de.json index 1d00c22..2c9c7d5 100644 --- a/web/translations/de.json +++ b/web/translations/de.json @@ -127,8 +127,21 @@ "settings_title": "Einstellungen", "settings_connection": "Verbindung", "settings_print": "Druckeinstellungen", - "settings_poll": "Poll-Intervall", + "settings_poll": "Poll-Intervall (Sekunden)", "settings_version": "Version", + "nav_settings": "Einstellungen", + "settings_cat_display": "Darstellung", + "settings_cat_filament": "Filament", + "settings_cat_language": "Sprache", + "settings_cat_theme": "Hell / Dunkel umschalten", + "settings_filament_mapping": "Filament-Profil-Mapping (pro Slot)", + "settings_filament_mapping_save": "Mapping speichern", + "settings_visible_vendors": "Sichtbare Hersteller (Profil-Dropdown)", + "settings_visible_vendors_hint": "Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic\" und eigene Profile sind immer sichtbar.", + "settings_visible_vendors_save": "Auswahl speichern", + "progress_action_print": "Drucken", + "progress_action_slots": "Slots zuordnen", + "progress_action_clear": "Leeren", "settings_save": "Speichern & Neustart", "settings_printer_name": "Drucker-Name", "settings_printer_ip": "Drucker-IP", diff --git a/web/translations/en.json b/web/translations/en.json index 6bed741..8343491 100644 --- a/web/translations/en.json +++ b/web/translations/en.json @@ -127,7 +127,20 @@ "settings_title": "Settings", "settings_connection": "Connection", "settings_print": "Print Settings", - "settings_poll": "Poll Interval", + "settings_poll": "Poll Interval (seconds)", + "nav_settings": "Settings", + "settings_cat_display": "Appearance", + "settings_cat_filament": "Filament", + "settings_cat_language": "Language", + "settings_cat_theme": "Toggle light / dark", + "settings_filament_mapping": "Filament profile mapping (per slot)", + "settings_filament_mapping_save": "Save mapping", + "settings_visible_vendors": "Visible vendors (profile dropdown)", + "settings_visible_vendors_hint": "Only these vendors appear in the slot profile dropdown. Nothing selected = show all. \"Generic\" and your own profiles are always visible.", + "settings_visible_vendors_save": "Save selection", + "progress_action_print": "Print", + "progress_action_slots": "Map slots", + "progress_action_clear": "Clear", "settings_version": "Version", "settings_save": "Save & Restart", "settings_printer_name": "Printer Name", diff --git a/web/translations/es.json b/web/translations/es.json index 8964dc5..5b0e879 100644 --- a/web/translations/es.json +++ b/web/translations/es.json @@ -127,7 +127,20 @@ "settings_title": "Configuración", "settings_connection": "Conexión", "settings_print": "Ajustes de impresión", - "settings_poll": "Intervalo de sondeo", + "settings_poll": "Intervalo de sondeo (segundos)", + "nav_settings": "Ajustes", + "settings_cat_display": "Apariencia", + "settings_cat_filament": "Filamento", + "settings_cat_language": "Idioma", + "settings_cat_theme": "Alternar claro / oscuro", + "settings_filament_mapping": "Asignación de perfil de filamento (por ranura)", + "settings_filament_mapping_save": "Guardar asignación", + "settings_visible_vendors": "Fabricantes visibles (lista de perfiles)", + "settings_visible_vendors_hint": "Solo estos fabricantes aparecen en la lista de perfiles de ranura. Nada seleccionado = mostrar todos. «Generic» y tus propios perfiles siempre son visibles.", + "settings_visible_vendors_save": "Guardar selección", + "progress_action_print": "Imprimir", + "progress_action_slots": "Asignar ranuras", + "progress_action_clear": "Vaciar", "settings_version": "Versión", "settings_save": "Guardar y reiniciar", "settings_printer_name": "Nombre de impresora", diff --git a/web/translations/fr.json b/web/translations/fr.json index 0c040be..8c32693 100644 --- a/web/translations/fr.json +++ b/web/translations/fr.json @@ -127,7 +127,20 @@ "settings_title": "Paramètres", "settings_connection": "Connexion", "settings_print": "Paramètres d'impression", - "settings_poll": "Intervalle de sondage", + "settings_poll": "Intervalle de sondage (secondes)", + "nav_settings": "Paramètres", + "settings_cat_display": "Apparence", + "settings_cat_filament": "Filament", + "settings_cat_language": "Langue", + "settings_cat_theme": "Basculer clair / sombre", + "settings_filament_mapping": "Mappage du profil de filament (par emplacement)", + "settings_filament_mapping_save": "Enregistrer le mappage", + "settings_visible_vendors": "Fabricants visibles (liste des profils)", + "settings_visible_vendors_hint": "Seuls ces fabricants apparaissent dans la liste des profils d'emplacement. Rien de sélectionné = tout afficher. « Generic » et vos propres profils sont toujours visibles.", + "settings_visible_vendors_save": "Enregistrer la sélection", + "progress_action_print": "Imprimer", + "progress_action_slots": "Affecter les emplacements", + "progress_action_clear": "Vider", "settings_version": "Version", "settings_save": "Enregistrer et redémarrer", "settings_printer_name": "Nom de l'imprimante", diff --git a/web/translations/zh-cn.json b/web/translations/zh-cn.json index cf4280e..048cf67 100644 --- a/web/translations/zh-cn.json +++ b/web/translations/zh-cn.json @@ -127,7 +127,20 @@ "settings_title": "设置", "settings_connection": "连接", "settings_print": "打印设置", - "settings_poll": "轮询间隔", + "settings_poll": "轮询间隔(秒)", + "nav_settings": "设置", + "settings_cat_display": "外观", + "settings_cat_filament": "耗材", + "settings_cat_language": "语言", + "settings_cat_theme": "切换浅色 / 深色", + "settings_filament_mapping": "耗材配置映射(每槽位)", + "settings_filament_mapping_save": "保存映射", + "settings_visible_vendors": "可见厂商(配置下拉框)", + "settings_visible_vendors_hint": "仅这些厂商会出现在槽位配置下拉框中。未选择 = 显示全部。“Generic”和您自己的配置始终可见。", + "settings_visible_vendors_save": "保存选择", + "progress_action_print": "打印", + "progress_action_slots": "分配槽位", + "progress_action_clear": "清除", "settings_version": "版本", "settings_save": "保存并重启", "settings_printer_name": "打印机名称", From 6b9ad9d42662762b6a82ff6ede5b0f0f764aef9a Mon Sep 17 00:00:00 2001 From: viewit Date: Tue, 16 Jun 2026 15:15:47 +0200 Subject: [PATCH 6/9] build: sources for v0.9.23 --- CHANGELOG.de.md | 41 ++++++++++++++++++++ CHANGELOG.md | 21 ++++++++++ VERSION | 2 +- config_loader.py | 14 +++++++ env_loader.py | 1 + kobrax_moonraker_bridge.py | 73 ++++++++++++++++++++++++++++++++--- web/themes/default/app.js | 44 ++++++++++++++++++--- web/themes/default/index.html | 14 +++++++ web/translations/de.json | 30 +++++++++++++- web/translations/en.json | 30 +++++++++++++- web/translations/es.json | 30 +++++++++++++- web/translations/fr.json | 31 ++++++++++++++- web/translations/zh-cn.json | 30 +++++++++++++- 13 files changed, 343 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md index b7f203e..f61fe1a 100644 --- a/CHANGELOG.de.md +++ b/CHANGELOG.de.md @@ -1,5 +1,46 @@ # Changelog +## [0.9.23] – 2026-06-16 + +### Neu +- **Druckdialog nach Upload automatisch öffnen.** Eine neue Einstellung + `print_start_dialog` (Einstellungen → Drucker → „Druckstart-Verhalten") steuert, + was nach einem Upload bei leerlaufendem Drucker passiert: „Print-Dialog" öffnet + den Slot-Zuordnungs-Dialog automatisch, „Print-Leiste" behält das bisherige + Banner. Basiert auf PR #56 von @gangoke. +- **Auto-Leveling-Schalter pro Druck.** Der Druckdialog hat jetzt eine eigene + Auto-Leveling-Checkbox, die den globalen Standard für einen einzelnen Druck + überschreibt. + +### Behoben +- **Objekt-Skip wurde beim Druckstart still ignoriert (PR #56, @gangoke).** Der + Skip-Befehl wurde gesendet, *bevor* der Drucker im `printing`-Status war, und + daher verworfen. Der Skip wird nun in einer Retry-Schleife erneut angewendet, + sobald der Druck bestätigt läuft — mit einer Pending-Sperre, damit die UI den + Skip-Status nicht vorzeitig zurücksetzt. +- **Upload während eines laufenden Drucks überschrieb die Vorschau des laufenden + Auftrags.** Ein neuer Upload während des Drucks ersetzt nicht mehr Thumbnail / + file_ready des Auftrags auf dem Druckbett. + +## [0.9.22] – 2026-06-16 + +### Neu +- **Neu strukturiertes Einstellungs-Panel.** Das Einstellungs-Modal wurde durch + ein dauerhaftes Master-Detail-Panel mit fünf Kategorien ersetzt: Verbindung, + Drucker, Darstellung, Filament und System. Das Poll-Intervall ist nun live + einstellbar. +- **Vendor-Sichtbarkeitsfilter (Issue #41).** Eine neue Checkliste in den + Filament-Einstellungen beschränkt das Slot-Profil-Dropdown auf bestimmte + Hersteller. „Generic" und eigene importierte Profile sind immer sichtbar. +- **Idle-Datei-Aktionen in der Fortschritts-Karte (Issue #55).** Nach einem + Upload bei leerlaufendem Drucker erscheinen drei Schnellaktionen direkt in der + Fortschritts-Karte: ▶ Drucken, ⚙ Slots zuordnen und ✕ Leeren. + +### Behoben +- **Mobileraker-Kompatibilität (Issue #48).** Absturz in `ConfigExtruder.fromJson` + (leeres `configfile.config`), Hänger beim Refresh (Metadata-Endlosschleife) und + fehlende ETA/Restzeit behoben. + ## [0.9.21] – 2026-06-14 ### Behoben diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed96cd..f8b1b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [0.9.23] – 2026-06-16 + +### New +- **Auto-open print dialog after upload.** A new `print_start_dialog` setting + (Settings → Printer → "Start Print Behavior") controls what happens after a + file is uploaded while the printer is idle: `Print Dialog` opens the + slot-assignment dialog automatically, `Print Bar` keeps the previous banner + behaviour. Based on PR #56 by @gangoke. +- **Per-print auto-leveling toggle.** The print dialog now has its own + auto-leveling checkbox that overrides the global default for a single print. + +### Fixed +- **Object skip was silently ignored at print start (PR #56, @gangoke).** The + skip command was sent *before* the printer entered the `printing` state, so it + was dropped. The skip is now re-applied in a retry loop once the print is + confirmed running, with a pending-lock so the UI doesn't reset the skip state + prematurely. +- **Upload during an active print overwrote the running job's preview.** + Uploading a new file while printing no longer replaces the thumbnail / + file_ready of the job currently on the bed. + ## [0.9.22] – 2026-06-16 ### New diff --git a/VERSION b/VERSION index ea3f0d7..68a5945 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.22 +0.9.23 diff --git a/config_loader.py b/config_loader.py index ff14dd1..b1012e1 100644 --- a/config_loader.py +++ b/config_loader.py @@ -61,6 +61,7 @@ def _load_config_file(path: pathlib.Path): "AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"), "CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"), "WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"), + "PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"), "BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"), } for env_key, (section, option) in mapping.items(): @@ -73,6 +74,18 @@ def _load_config_file(path: pathlib.Path): pass + # Backward compatibility: old key FILE_READY_DIALOG → PRINT_START_DIALOG + if "PRINT_START_DIALOG" not in os.environ: + try: + legacy = cfg.get(CONFIG_SECTION_PRINT, "file_ready_dialog") + if legacy: + os.environ["PRINT_START_DIALOG"] = legacy + except (configparser.NoSectionError, configparser.NoOptionError): + pass + if "PRINT_START_DIALOG" not in os.environ and "FILE_READY_DIALOG" in os.environ: + os.environ["PRINT_START_DIALOG"] = os.environ["FILE_READY_DIALOG"] + + def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path): """Einmalige Migration: .env → config.ini anlegen.""" env_vals: dict[str, str] = {} @@ -303,3 +316,4 @@ DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto") AUTO_LEVELING = int(get("AUTO_LEVELING","1")) CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0")) WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1")) +PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1"))) diff --git a/env_loader.py b/env_loader.py index 342df7b..5ed9379 100644 --- a/env_loader.py +++ b/env_loader.py @@ -48,3 +48,4 @@ MODE_ID = get("MODE_ID", "") DEVICE_ID = get("DEVICE_ID", "") DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto") AUTO_LEVELING = int(get("AUTO_LEVELING", "1")) +PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1"))) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index cea6819..d9e25e5 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -791,6 +791,7 @@ class KobraXBridge: "print_speed_mode": 2, "connection_error": "", "file_ready": "", + "print_start_dialog": getattr(args, "print_start_dialog", 1), "filament_mode": "toolhead", "ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None}, } @@ -814,6 +815,9 @@ class KobraXBridge: # Part-Skip: zuletzt vom Drucker gemeldete Skip-Liste (v0.9.10) self._skip_state: dict = {"objects": [], "skipped": [], "ts": 0} + # Pre-Print-Skip: pending until printer enters printing state + self._pending_preprint_skip: list[str] = [] + self._pending_preprint_skip_deadline: float = 0.0 # Theme-Name prüfen (keine Sonderzeichen oder Umlaute) raw_theme = (getattr(args, "ui_theme", None) or "default").strip() @@ -1067,7 +1071,16 @@ class KobraXBridge: """ d = payload.get("data") or {} skipped = d.get("objects_skip_parts") or d.get("skipped") or d.get("skipped_parts") or [] - # Liste immer (auch leer) übernehmen – sonst bleibt sie auf alten Stand + # Während ein Pre-Print-Skip noch pending ist, leere Früh-Reports ignorieren + # damit die UI nicht sofort zurückspringt bevor der Drucker den Skip bestätigt. + now = time.time() + if (not skipped and self._pending_preprint_skip + and now <= self._pending_preprint_skip_deadline): + return + # Pending-Lock aufheben sobald Drucker die gewünschten Objekte bestätigt + if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip): + self._pending_preprint_skip = [] + self._pending_preprint_skip_deadline = 0.0 self._skip_state = { "skipped": list(skipped), "ts": int(time.time()), @@ -1079,14 +1092,19 @@ class KobraXBridge: d = payload.get("data") or {} details = d.get("file_details") or {} thumb = details.get("thumbnail") or details.get("png_image") or "" - if thumb: + file_name = d.get("filename") or details.get("filename") or self._last_uploaded_file + active_print = self._state.get("print_state") in ("printing", "paused") + current_print_file = self._state.get("filename") or "" + # Uploads während eines laufenden Drucks dürfen die aktive + # Fortschritts-Vorschau nicht überschreiben. + if thumb and (not active_print or (file_name and file_name == current_print_file)): self._thumbnail_b64 = thumb log.info(f"Vorschaubild empfangen: {len(thumb)} Zeichen base64") # Part-Skip: Objekt-Liste + optionales SVG (v0.9.10) objs = details.get("objects_skip_parts") or [] svg = details.get("svg_image") or "" if objs: - filename = d.get("filename") or details.get("filename") or self._last_uploaded_file + filename = file_name if filename: try: self._store.update_file_objects(filename, objs, svg) @@ -1095,6 +1113,33 @@ class KobraXBridge: log.warning(f"update_file_objects fehlgeschlagen: {e}") self._push_status_update() + def _apply_preprint_skip_after_start(self, names: list[str], retries: int = 20, delay_s: float = 0.75): + """Sendet Skip-Befehl erst nachdem Drucker in printing-State gewechselt hat. + + Vorher sendet der Drucker den Befehl ins Leere (kein aktiver Druck). + """ + wanted = [str(n) for n in (names or []) if isinstance(n, str) and n] + if not wanted: + return False + for i in range(max(1, int(retries))): + try: + if self._state.get("kobra_state") != "printing": + time.sleep(max(0.1, float(delay_s))) + continue + resp = self.client.skip_objects(wanted) + if resp is not None: + log.info(f"Pre-Print skip applied ({len(wanted)} objects) on attempt {i+1}/{retries}") + self._pending_preprint_skip = [] + self._pending_preprint_skip_deadline = 0.0 + return True + except Exception as e: + log.debug(f"Pre-Print skip attempt {i+1}/{retries} failed: {e}") + time.sleep(max(0.1, float(delay_s))) + log.warning(f"Pre-Print skip could not be confirmed after {retries} attempts") + self._pending_preprint_skip = [] + self._pending_preprint_skip_deadline = 0.0 + return False + @staticmethod def _detect_filament_mode(boxes: list, head_tools_model: int = -1) -> str: """Detect active filament topology mode. @@ -2538,7 +2583,7 @@ class KobraXBridge: ams_box_mapping = self._build_auto_ams_box_mapping() use_ams = len(ams_box_mapping) > 0 - auto_leveling = getattr(self._args, "auto_leveling", 1) + auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1))) filename = gcode_file["filename"] file_path = gcode_file["path"] @@ -2570,6 +2615,15 @@ class KobraXBridge: }, } + # Pre-Print-Skip sofort im UI-Status spiegeln + self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())} + if excluded_objects: + self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n] + self._pending_preprint_skip_deadline = time.time() + 12.0 + else: + self._pending_preprint_skip = [] + self._pending_preprint_skip_deadline = 0.0 + log.info(f"KX-Store Druckstart: {filename} ams={len(ams_box_mapping)} slots assignments={bool(assignments)} excluded={len(excluded_objects)}") loop = asyncio.get_event_loop() result = await loop.run_in_executor( @@ -2578,6 +2632,9 @@ class KobraXBridge: if result is None: return self._json_cors({"error": "Keine Antwort vom Drucker"}, status=504) + if excluded_objects: + loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects)) + # Job in History starten self._current_job_id = self._store.start_job( gcode_file_id=gcode_file["id"], @@ -3759,6 +3816,7 @@ class KobraXBridge: "thumbnail": thumbnail, "connection_error": s["connection_error"], "file_ready": s["file_ready"], + "print_start_dialog": s.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1)), "version": self._read_version(), }) @@ -3897,6 +3955,7 @@ class KobraXBridge: "auto_leveling": getattr(self._args, "auto_leveling", 1), "camera_on_print": getattr(self._args, "camera_on_print", 0), "web_upload_warning": getattr(self._args, "web_upload_warning", 1), + "print_start_dialog": getattr(self._args, "print_start_dialog", 1), "poll_interval": getattr(self._args, "poll_interval", 3), "filament_profiles": {str(k): v for k, v in self._filament_profiles.items()}, "visible_vendors": self._visible_vendors, @@ -3930,6 +3989,7 @@ class KobraXBridge: cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))) cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0)))))) cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1)))))) + cfg.set("print", "print_start_dialog", str(int(bool(data.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1)))))) if "poll_interval" in data: try: pi = max(1, min(60, int(data["poll_interval"]))) @@ -4102,7 +4162,8 @@ 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"): + "CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "PRINT_START_DIALOG", + "FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME"): os.environ.pop(_k, None) in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER") @@ -4987,6 +5048,8 @@ def main(): parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING) parser.add_argument("--camera-on-print", type=int, default=env_loader.CAMERA_ON_PRINT) parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING) + parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG) + parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int) parser.add_argument("--host", default="0.0.0.0", help="Bind-Adresse für den Bridge-Server") diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 44e425f..5b2e773 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -9,6 +9,9 @@ var camOn=false; var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support) var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55) +var _fdDialogOpen=false; // Dialog ist gerade offen +var _fdAutoOpenedFile=null; // Dateiname für den der Dialog auto-geöffnet wurde +var _fdUserCancelled=false; // User hat den Auto-Open-Dialog abgebrochen var currentStep=1; var currentPanel='dashboard'; var aceAutoRefillPrefs=(function(){ @@ -355,8 +358,13 @@ function applyLang(){ setText('lbl-default-slot',T.settings_default_slot); setText('opt-slot-auto',T.settings_slot_auto); setText('lbl-auto-leveling',T.settings_auto_leveling); + setText('lbl-file-ready-mode',T.settings_file_ready_mode); + setText('opt-file-ready-dialog',T.settings_file_ready_dialog); + setText('opt-file-ready-banner',T.settings_file_ready_banner); setText('lbl-camera-on-print',T.settings_camera_on_print); setText('lbl-web-upload-warning',T.settings_web_upload_warning); + setText('fd-options-title',T.fd_options_title); + setText('fd-lbl-auto-leveling',T.print_auto_leveling); setText('lbl-update-check',T.update_check); setText('lbl-update-apply',T.update_apply); @@ -652,10 +660,18 @@ function applyState(){ var bannerVisible=false; var frb=document.getElementById('file-ready-banner'); if(frb){ + var shouldAutoOpen=(s.print_start_dialog===undefined?true:!!s.print_start_dialog); if(s.file_ready&&s.print_state==='standby'){ document.getElementById('file-ready-name').textContent=s.file_ready; - frb.style.display='flex'; - bannerVisible=true; + // Neue Datei → Abbruch-Sperre aufheben + if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready) _fdUserCancelled=false; + if(shouldAutoOpen&&!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ + _fdAutoOpenedFile=s.file_ready; + startReadyFileWithSlots(s.file_ready,true); + } else { + frb.style.display='flex'; + bannerVisible=true; + } }else{frb.style.display='none';} } // skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird @@ -958,6 +974,7 @@ function openSettings(){ document.getElementById('s-default-slot').value=d.default_ams_slot||'auto'; document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling); var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print; + var frm=document.getElementById('s-file-ready-mode');if(frm)frm.value=(d.print_start_dialog===undefined?'1':String(d.print_start_dialog?1:0)); var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning); // Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage var pi=document.getElementById('s-poll-interval'); @@ -1455,6 +1472,7 @@ function saveSettings(){ default_ams_slot: document.getElementById('s-default-slot').value, auto_leveling: document.getElementById('s-auto-leveling').checked?1:0, camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0, + print_start_dialog: parseInt((document.getElementById('s-file-ready-mode')||{}).value||'1',10), web_upload_warning:webUploadWarning, poll_interval: Math.min(60,Math.max(1,parseInt((document.getElementById('s-poll-interval')||{}).value,10)||3)), }).then(function(){ @@ -2239,7 +2257,8 @@ function confirmStoreWebVerify(){ }); } -function startReadyFileWithSlots(filename){ +function startReadyFileWithSlots(filename,_autoOpen){ + if(!_autoOpen) _fdAutoOpenedFile=null; // manueller Aufruf → Auto-Open-Sperre aufheben var fn=filename||S.file_ready; var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){ @@ -2252,10 +2271,16 @@ function startReadyFileWithSlots(filename){ _storeFileId=null; _gcodeFilaments=[]; + var _autoOpenFile=_autoOpen?fn:null; + if(_autoOpen) _fdDialogOpen=true; // bereits während Fetch sperren function openWithSlots(){ fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){ + if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;} openFilamentDialog(d.result||[]); - }).catch(function(){openFilamentDialog([]);}); + }).catch(function(){ + if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;} + openFilamentDialog([]); + }); } var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); @@ -2325,6 +2350,9 @@ function openFilamentDialog(slots){ var title=document.getElementById('fd-title'); var body=document.getElementById('fd-slots'); if(title)title.textContent='▶ '+_storeFilename; + // Auto-Leveling-Checkbox mit globalem Default vorbelegen + var fdAl=document.getElementById('fd-auto-leveling'); + if(fdAl) fdAl.checked=(S.auto_leveling===undefined?true:!!S.auto_leveling); // Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID) _printObjects=[]; _printObjectsSvg=''; @@ -2432,6 +2460,8 @@ function openFilamentDialog(slots){ function closeFilamentDialog(){ var dlg=document.getElementById('filament-dialog'); if(dlg)dlg.classList.remove('open'); + _fdDialogOpen=false; + if(_fdAutoOpenedFile) _fdUserCancelled=true; } function confirmFilamentPrint(){ @@ -2471,12 +2501,14 @@ function confirmFilamentPrint(){ } // Pre-Print Skip: Namen der abgehakten Objekte sammeln var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;}); + var fdAlEl=document.getElementById('fd-auto-leveling'); + var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0); closeFilamentDialog(); if(_filamentDialogMode==='banner'){ // Banner-Modus: normaler print/start mit Slot-Override var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects}) + post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling}) .then(function(r){return r.json();}) .then(function(){ document.getElementById('file-ready-banner').style.display='none'; @@ -2491,7 +2523,7 @@ function confirmFilamentPrint(){ fetch(_apiUrl('/kx/print'),{ method:'POST', headers:{'Content-Type':'application/json'}, - body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects}) + body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling}) }).then(function(r){return r.json()}).then(function(d){ if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');} else{clog('Druckfehler: '+(d.error||'?'),'msg-err');} diff --git a/web/themes/default/index.html b/web/themes/default/index.html index 5be145c..424ea10 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -492,6 +492,13 @@ + +
+
Druckoptionen
+
+ + +
+
diff --git a/web/translations/de.json b/web/translations/de.json index 2c9c7d5..8367d5d 100644 --- a/web/translations/de.json +++ b/web/translations/de.json @@ -257,5 +257,33 @@ "sf_new": "Neu", "ss_date": "↓ Datum", "ss_name": "A–Z Name", - "ss_dur": "⏱ Druckzeit" + "ss_dur": "⏱ Druckzeit", + "ace_dry_preset_pla": "PLA", + "ace_dry_preset_pla_plus": "PLA+", + "ace_dry_preset_petg": "PETG", + "ace_dry_preset_tpu": "TPU", + "ace_dry_preset_abs_asa": "ABS / ASA", + "ace_dry_preset_pa_pc": "PA / PC", + "ace_dry_preset_custom": "Custom", + "fd_options_title": "Optionen", + "print_auto_leveling": "Auto-Leveling für diesen Druck", + "settings_file_ready_mode": "Druckdialog starten", + "settings_file_ready_banner": "Druckleiste", + "settings_file_ready_dialog": "Druckdialog", + "log_dir_rx": "RX", + "log_dir_tx": "TX", + "log_dir_label": "Richtung:", + "log_lvl_err": "⛔ Fehler", + "log_lvl_warn": "⚠ Warnung", + "log_topic_label": "Thema:", + "log_topic_ams": "AMS", + "log_topic_print": "Druck", + "log_topic_info": "Info", + "log_topic_status": "Status", + "log_download": "⬇ Download", + "log_auto": "⬇ Auto", + "log_clear": "✕ Leeren", + "log_filter_placeholder": "Filtern…", + "skip_cancel": "Abbrechen", + "skip_confirm": "Überspringen" } diff --git a/web/translations/en.json b/web/translations/en.json index 8343491..ab80b64 100644 --- a/web/translations/en.json +++ b/web/translations/en.json @@ -69,6 +69,13 @@ "ace_dry_dialog_save_restart": "Save & Restart", "ace_dry_dialog_custom_name": "Custom Name", "ace_dry_dialog_reset_default": "Reset to Default", + "ace_dry_preset_pla": "PLA", + "ace_dry_preset_pla_plus": "PLA+", + "ace_dry_preset_petg": "PETG", + "ace_dry_preset_tpu": "TPU", + "ace_dry_preset_abs_asa": "ABS / ASA", + "ace_dry_preset_pa_pc": "PA / PC", + "ace_dry_preset_custom": "Custom", "cam_placeholder": "📷 Camera not started", "cam_stream_unavailable": "Stream unavailable", "btn_cam_start": "▶ Camera", @@ -153,7 +160,12 @@ "hint_ip_no_port": "IP address only, no port (e.g. 192.168.1.102)", "settings_default_slot": "Default Slot (single color)", "settings_slot_auto": "Auto (all loaded slots)", - "settings_auto_leveling": "Auto-Leveling before print", + "settings_auto_leveling": "Auto-Leveling Default", + "fd_options_title": "Print Options", + "print_auto_leveling": "Auto-Leveling", + "settings_file_ready_mode": "Start Print Behavior", + "settings_file_ready_banner": "Print Bar", + "settings_file_ready_dialog": "Print Dialog", "settings_camera_on_print": "Turn camera on at print start", "settings_web_upload_warning": "Show warning when printing web uploads", "update_check": "Check for Updates", @@ -192,7 +204,21 @@ "orca_profile_done": "Imported", "orca_profile_skipped": "skipped", "log_dir_all": "All", + "log_dir_rx": "RX", + "log_dir_tx": "TX", + "log_dir_label": "Dir:", "log_lvl_label": "Level:", + "log_lvl_err": "⛔ Errors", + "log_lvl_warn": "⚠ Warn", + "log_topic_label": "Topic:", + "log_topic_ams": "AMS", + "log_topic_print": "Print", + "log_topic_info": "Info", + "log_topic_status": "Status", + "log_download": "⬇ Download", + "log_auto": "⬇ Auto", + "log_clear": "✕ Clear", + "log_filter_placeholder": "Filter…", "file_ready_btn": "▶ Start Print", "file_slots_btn": "🎨 Select Slots", "file_cancel_btn": "✕ Cancel", @@ -202,6 +228,8 @@ "skip_btn_label": "Objects", "skip_no_objects": "No objects in this print.", "skip_already": "skipped", + "skip_cancel": "Cancel", + "skip_confirm": "Skip", "skip_select_at_least_one": "Please pick at least one object.", "skip_sending": "Sending …", "skip_success": "Objects will be skipped.", diff --git a/web/translations/es.json b/web/translations/es.json index 5b0e879..069d215 100644 --- a/web/translations/es.json +++ b/web/translations/es.json @@ -257,5 +257,33 @@ "sf_new": "Nuevo", "ss_date": "↓ Fecha", "ss_name": "A–Z Nombre", - "ss_dur": "⏱ Tiempo de impresión" + "ss_dur": "⏱ Tiempo de impresión", + "ace_dry_preset_pla": "PLA", + "ace_dry_preset_pla_plus": "PLA+", + "ace_dry_preset_petg": "PETG", + "ace_dry_preset_tpu": "TPU", + "ace_dry_preset_abs_asa": "ABS / ASA", + "ace_dry_preset_pa_pc": "PA / PC", + "ace_dry_preset_custom": "Personalizado", + "fd_options_title": "Opciones", + "print_auto_leveling": "Autonivelado para esta impresión", + "settings_file_ready_mode": "Iniciar diálogo de impresión", + "settings_file_ready_banner": "Barra de impresión", + "settings_file_ready_dialog": "Diálogo de impresión", + "log_dir_rx": "RX", + "log_dir_tx": "TX", + "log_dir_label": "Dirección:", + "log_lvl_err": "⛔ Errores", + "log_lvl_warn": "⚠ Avisos", + "log_topic_label": "Tema:", + "log_topic_ams": "AMS", + "log_topic_print": "Impresión", + "log_topic_info": "Info", + "log_topic_status": "Estado", + "log_download": "⬇ Descargar", + "log_auto": "⬇ Auto", + "log_clear": "✕ Limpiar", + "log_filter_placeholder": "Filtrar…", + "skip_cancel": "Cancelar", + "skip_confirm": "Omitir" } diff --git a/web/translations/fr.json b/web/translations/fr.json index 8c32693..0f24a0e 100644 --- a/web/translations/fr.json +++ b/web/translations/fr.json @@ -257,6 +257,33 @@ "sf_new": "Nouveau", "ss_date": "↓ Date", "ss_name": "A–Z Nom", - "ss_dur": "⏱ Durée d'impression" + "ss_dur": "⏱ Durée d'impression", + "ace_dry_preset_pla": "PLA", + "ace_dry_preset_pla_plus": "PLA+", + "ace_dry_preset_petg": "PETG", + "ace_dry_preset_tpu": "TPU", + "ace_dry_preset_abs_asa": "ABS / ASA", + "ace_dry_preset_pa_pc": "PA / PC", + "ace_dry_preset_custom": "Personnalisé", + "fd_options_title": "Options", + "print_auto_leveling": "Mise à niveau auto pour cette impression", + "settings_file_ready_mode": "Démarrer le dialogue d'impression", + "settings_file_ready_banner": "Barre d'impression", + "settings_file_ready_dialog": "Dialogue d'impression", + "log_dir_rx": "RX", + "log_dir_tx": "TX", + "log_dir_label": "Sens :", + "log_lvl_err": "⛔ Erreurs", + "log_lvl_warn": "⚠ Avert.", + "log_topic_label": "Sujet :", + "log_topic_ams": "AMS", + "log_topic_print": "Impression", + "log_topic_info": "Info", + "log_topic_status": "Statut", + "log_download": "⬇ Télécharger", + "log_auto": "⬇ Auto", + "log_clear": "✕ Effacer", + "log_filter_placeholder": "Filtrer…", + "skip_cancel": "Annuler", + "skip_confirm": "Ignorer" } - diff --git a/web/translations/zh-cn.json b/web/translations/zh-cn.json index 048cf67..3672a87 100644 --- a/web/translations/zh-cn.json +++ b/web/translations/zh-cn.json @@ -257,5 +257,33 @@ "sf_new": "新", "ss_date": "↓ 日期", "ss_name": "A–Z 名称", - "ss_dur": "⏱ 打印时间" + "ss_dur": "⏱ 打印时间", + "ace_dry_preset_pla": "PLA", + "ace_dry_preset_pla_plus": "PLA+", + "ace_dry_preset_petg": "PETG", + "ace_dry_preset_tpu": "TPU", + "ace_dry_preset_abs_asa": "ABS / ASA", + "ace_dry_preset_pa_pc": "PA / PC", + "ace_dry_preset_custom": "自定义", + "fd_options_title": "选项", + "print_auto_leveling": "本次打印自动调平", + "settings_file_ready_mode": "开始打印对话框", + "settings_file_ready_banner": "打印栏", + "settings_file_ready_dialog": "打印对话框", + "log_dir_rx": "RX", + "log_dir_tx": "TX", + "log_dir_label": "方向:", + "log_lvl_err": "⛔ 错误", + "log_lvl_warn": "⚠ 警告", + "log_topic_label": "主题:", + "log_topic_ams": "AMS", + "log_topic_print": "打印", + "log_topic_info": "信息", + "log_topic_status": "状态", + "log_download": "⬇ 下载", + "log_auto": "⬇ 自动", + "log_clear": "✕ 清空", + "log_filter_placeholder": "筛选…", + "skip_cancel": "取消", + "skip_confirm": "跳过" } From 303297bfbfc4d03390eff5089195ff45e08378dd Mon Sep 17 00:00:00 2001 From: viewit Date: Tue, 16 Jun 2026 21:45:34 +0200 Subject: [PATCH 7/9] build: sources for v0.9.24 --- CHANGELOG.de.md | 13 +++++++++++ CHANGELOG.md | 13 +++++++++++ VERSION | 2 +- web/themes/default/app.js | 43 ++++++++++++++++++++++++++++++----- web/themes/default/index.html | 13 ++++++++--- web/translations/de.json | 1 + web/translations/en.json | 1 + web/translations/es.json | 1 + web/translations/fr.json | 1 + web/translations/zh-cn.json | 1 + 10 files changed, 79 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md index f61fe1a..103fd85 100644 --- a/CHANGELOG.de.md +++ b/CHANGELOG.de.md @@ -1,5 +1,18 @@ # Changelog +## [0.9.24] – 2026-06-16 + +### Neu +- **Objekte überspringen in jedem Druck-Flow (Issue #57).** Der „Objekte + überspringen"-Bereich im Slot-Mapper erschien bisher nur beim Druck aus dem + Browser-Tab. Er ist jetzt in allen Flows verfügbar (inkl. Upload / Print-Leiste), + standardmäßig eingeklappt hinter einem `✂ Objekte überspringen (N)`-Header, damit + der Dialog kompakt bleibt — Klick klappt Vorschau + Checkliste auf. +- **Slot-Mapper zeigt konkreten Profilnamen (Issue #57).** Jeder Slot zeigt nun das + zugeordnete Filament-Profil (z.B. „PolyTerra PLA — Polymaker") in den Dropdown- + Optionen und als Hover-Tooltip am Slot-Marker, statt nur des generischen Typs. + Fällt auf den generischen Typ zurück, wenn kein Profil gemappt ist. + ## [0.9.23] – 2026-06-16 ### Neu diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b1b95..4cc2a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.9.24] – 2026-06-16 + +### New +- **Skip Objects available in every print flow (issue #57).** The "Skip objects" + panel in the Slot Mapper used to appear only when printing from the Browser tab. + It now shows in all flows (upload / print bar included), collapsed by default + behind a `✂ Skip objects (N)` header to keep the dialog compact, expanding on + click with the object preview and checklist. +- **Slot Mapper shows the specific profile name (issue #57).** Each slot now + displays its mapped filament profile (e.g. "PolyTerra PLA — Polymaker") in the + dropdown options and as a hover tooltip on the slot marker, instead of just the + generic type. Falls back to the generic type when no profile is mapped. + ## [0.9.23] – 2026-06-16 ### New diff --git a/VERSION b/VERSION index 68a5945..37d8b0b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.23 +0.9.24 diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 5b2e773..20a381b 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -259,7 +259,7 @@ function applyLang(){ setText('skip-title',T.skip_title); setText('skip-hint',T.skip_hint); setText('d-btn-skip-label',T.skip_btn_label); - setText('fd-objects-hint',T.fd_objects_hint); + setText('fd-objects-toggle-lbl',T.fd_objects_toggle); setText('apd-lbl-ip',T.apd_lbl_ip); setText('apd-lbl-name',T.apd_lbl_name); var apn=document.getElementById('apd-name');if(apn)apn.setAttribute('placeholder',T.apd_placeholder_name); @@ -2329,6 +2329,18 @@ function _normalizeMaterialKey(material){ function _materialsCompatible(a,b){ return _normalizeMaterialKey(a)===_normalizeMaterialKey(b); } +// Issue #57 Punkt 4: konkreter Profilname (User-Override) statt generischem Typ. +// Fällt auf den Material-Typ zurück wenn kein Profil gemappt ist. +function _slotProfileLabel(slot){ + if(!slot)return ''; + if(slot.filament_name){ + return slot.filament_name+(slot.filament_vendor?' — '+slot.filament_vendor:''); + } + return slot.material||''; +} +function _escAttr(s){ + return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>'); +} function _updateSlotMarker(sel){ var opt=sel.options[sel.selectedIndex]; var color=opt&&opt.dataset.color?opt.dataset.color:'#888'; @@ -2339,6 +2351,7 @@ function _updateSlotMarker(sel){ marker.style.background=color; marker.style.color=_contrastText(color); marker.textContent=(slotIdx+1); + marker.title=(opt&&opt.dataset.profile)?opt.dataset.profile:''; } } @@ -2353,12 +2366,19 @@ function openFilamentDialog(slots){ // Auto-Leveling-Checkbox mit globalem Default vorbelegen var fdAl=document.getElementById('fd-auto-leveling'); if(fdAl) fdAl.checked=(S.auto_leveling===undefined?true:!!S.auto_leveling); - // Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID) + // Objekt-Liste laden — sobald eine File-ID auflösbar ist (Issue #57 Punkt 3: + // Skip-Parität auch im Banner-/Upload-Modus, nicht nur im Store-Modus). + // startReadyFileWithSlots() setzt _storeFileId auch im Banner-Modus per + // filename→fileObj-Lookup, daher reicht hier die _storeFileId-Prüfung. _printObjects=[]; _printObjectsSvg=''; var objSection=document.getElementById('fd-objects-section'); + var objBody=document.getElementById('fd-objects-body'); + var objArrow=document.getElementById('fd-objects-arrow'); if(objSection)objSection.style.display='none'; - if(_filamentDialogMode==='store'&&_storeFileId){ + if(objBody)objBody.style.display='none'; // immer eingeklappt starten + if(objArrow)objArrow.style.transform=''; + if(_storeFileId){ fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects')) .then(function(r){return r.json()}) .then(function(d){ @@ -2367,6 +2387,8 @@ function openFilamentDialog(slots){ if(names.length>=2){ _printObjects=names.map(function(n){return {name:n,skip:false};}); renderObjectChecklist(); renderObjectSvg(); + var cnt=document.getElementById('fd-objects-count'); + if(cnt)cnt.textContent='('+names.length+')'; if(objSection)objSection.style.display='block'; } }).catch(function(){}); @@ -2430,8 +2452,8 @@ function openFilamentDialog(slots){ var defaultSlot=compatible.find(function(s){return s.slot_index===defaultSlotIndex;})||null; var opts=compatible.map(function(s){ var sel=(defaultSlot&&s.slot_index===defaultSlot.slot_index)?'selected':''; - return ''; + return ''; }).join(''); if(!compatible.length){ opts=''; @@ -2448,7 +2470,7 @@ function openFilamentDialog(slots){ ''+gc.material+''+ usedBadge+ ''+ - ''+(defaultSlot?defaultSlot.slot_index+1:'?')+''+ + ''+(defaultSlot?defaultSlot.slot_index+1:'?')+''+ ''+ '
'; @@ -2549,6 +2571,15 @@ function _toggleObjectSkip(idx,val){ if(_printObjects[idx])_printObjects[idx].skip=!!val; renderObjectSvg(); } +// Issue #57 Punkt 3: Skip-Objekte-Bereich ein-/ausklappen +function toggleFdObjects(){ + var body=document.getElementById('fd-objects-body'); + var arrow=document.getElementById('fd-objects-arrow'); + if(!body)return; + var open=body.style.display!=='none'; + body.style.display=open?'none':'block'; + if(arrow)arrow.style.transform=open?'':'rotate(90deg)'; +} function renderObjectSvg(){ var box=document.getElementById('fd-objects-svg'); if(!box)return; diff --git a/web/themes/default/index.html b/web/themes/default/index.html index 424ea10..a783654 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -626,9 +626,16 @@

GCode-Kanal → AMS-Slot zuweisen:

Druckoptionen
diff --git a/web/translations/de.json b/web/translations/de.json index 8367d5d..c11dda1 100644 --- a/web/translations/de.json +++ b/web/translations/de.json @@ -206,6 +206,7 @@ "skip_sending": "Sende …", "skip_success": "Objekte werden übersprungen.", "fd_objects_hint": "Objekte überspringen (optional):", + "fd_objects_toggle": "Objekte überspringen", "fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:", "fd_cancel": "Abbrechen", "fd_print": "▶ Drucken", diff --git a/web/translations/en.json b/web/translations/en.json index ab80b64..3e1b1e8 100644 --- a/web/translations/en.json +++ b/web/translations/en.json @@ -234,6 +234,7 @@ "skip_sending": "Sending …", "skip_success": "Objects will be skipped.", "fd_objects_hint": "Skip objects (optional):", + "fd_objects_toggle": "Skip objects", "fd_slots_hint": "Assign GCode channel to AMS slot:", "fd_cancel": "Cancel", "fd_print": "▶ Print", diff --git a/web/translations/es.json b/web/translations/es.json index 069d215..6544dbf 100644 --- a/web/translations/es.json +++ b/web/translations/es.json @@ -206,6 +206,7 @@ "skip_sending": "Enviando …", "skip_success": "Se omitirán los objetos.", "fd_objects_hint": "Omitir objetos (opcional):", + "fd_objects_toggle": "Omitir objetos", "fd_slots_hint": "Asignar canal GCode a la ranura AMS:", "fd_cancel": "Cancelar", "fd_print": "▶ Imprimir", diff --git a/web/translations/fr.json b/web/translations/fr.json index 0f24a0e..e1a4c88 100644 --- a/web/translations/fr.json +++ b/web/translations/fr.json @@ -206,6 +206,7 @@ "skip_sending": "Envoi …", "skip_success": "Les objets seront ignorés.", "fd_objects_hint": "Ignorer des objets (optionnel) :", + "fd_objects_toggle": "Ignorer des objets", "fd_slots_hint": "Associer le canal GCode au slot AMS :", "fd_cancel": "Annuler", "fd_print": "▶ Imprimer", diff --git a/web/translations/zh-cn.json b/web/translations/zh-cn.json index 3672a87..af584d3 100644 --- a/web/translations/zh-cn.json +++ b/web/translations/zh-cn.json @@ -206,6 +206,7 @@ "skip_sending": "发送中 …", "skip_success": "对象将被跳过。", "fd_objects_hint": "跳过对象 (可选):", + "fd_objects_toggle": "跳过对象", "fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:", "fd_cancel": "取消", "fd_print": "▶ 打印", From eea570052fcf1c5c84468b265e8175b41fa101de Mon Sep 17 00:00:00 2001 From: viewit Date: Wed, 17 Jun 2026 07:15:31 +0200 Subject: [PATCH 8/9] build: sources for v0.9.25 --- CHANGELOG.de.md | 19 ++++++ CHANGELOG.md | 18 ++++++ VERSION | 2 +- kobrax_client.py | 113 +++++++++++++++++++++++++++--------- kobrax_moonraker_bridge.py | 12 ++++ web/themes/default/app.js | 8 +++ web/translations/de.json | 1 + web/translations/en.json | 1 + web/translations/es.json | 1 + web/translations/fr.json | 1 + web/translations/zh-cn.json | 1 + 11 files changed, 150 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md index 103fd85..ed5a469 100644 --- a/CHANGELOG.de.md +++ b/CHANGELOG.de.md @@ -1,5 +1,24 @@ # Changelog +## [0.9.25] – 2026-06-17 + +### Behoben +- **Zufällige Abstürze / Container-Restarts — Segfault in `libcrypto.so.3` + (Issue #53).** Der MQTT-über-TLS-Client teilte einen einzelnen SSL-Socket + zwischen dem Reader-Thread (`recv`) und den Sender-Threads (`sendall`), ohne sie + zu serialisieren. CPythons `ssl`-Modul erlaubt kein gleichzeitiges Lesen und + Schreiben auf demselben Socket — die Überlappung korrumpierte den internen + OpenSSL-Zustand und löste eine Heap-Corruption + Segfault aus, die auf manchen + Hosts timing-bedingt zuverlässig auftrat. Sämtliche Socket-Zugriffe (recv / + sendall / close / reconnect) werden nun unter einem einzigen Lock serialisiert; + der Reader prüft die Bereitschaft mit `select()` außerhalb des Locks, damit die + Sender nie ausgehungert werden. Reconnect und Disconnect tauschen den Socket + jetzt atomar. Dank an @BasK für den detaillierten Fault-Handler-Trace. +- **File-Browser akzeptierte Nicht-GCode-Uploads (Issue #59).** Drag & Drop umging + den `accept`-Filter des Dateidialogs, sodass z.B. ein JPG hochgeladen werden + konnte. Uploads werden jetzt client- und serverseitig validiert; nur `.gcode`, + `.gcode.3mf`, `.3mf` und `.bgcode` werden akzeptiert. Dank an @gangoke. + ## [0.9.24] – 2026-06-16 ### Neu diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc2a1e..282b6a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [0.9.25] – 2026-06-17 + +### Fixed +- **Random crashes / container restarts — segfault in `libcrypto.so.3` (issue #53).** + The MQTT-over-TLS client shared a single SSL socket between the reader thread + (`recv`) and the sender threads (`sendall`) without serializing them. CPython's + `ssl` module does not allow concurrent read and write on the same socket — the + overlap corrupted OpenSSL's internal state, causing a heap corruption and a + segfault that manifested reliably on some hosts (timing-dependent). All socket + access (recv / sendall / close / reconnect) is now serialized under a single + lock; the reader probes readiness with `select()` outside the lock so senders + are never starved. Reconnect and disconnect now swap the socket atomically. + Thanks to @BasK for the detailed fault-handler trace that pinpointed this. +- **File browser accepted non-GCode uploads (issue #59).** Drag & drop bypassed + the file picker's `accept` filter, so e.g. a JPG could be uploaded. Uploads are + now validated both client- and server-side; only `.gcode`, `.gcode.3mf`, `.3mf` + and `.bgcode` are accepted. Thanks @gangoke. + ## [0.9.24] – 2026-06-16 ### New diff --git a/VERSION b/VERSION index 37d8b0b..ec9b691 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.24 +0.9.25 diff --git a/kobrax_client.py b/kobrax_client.py index e7080fc..ec81391 100644 --- a/kobrax_client.py +++ b/kobrax_client.py @@ -27,6 +27,7 @@ import hashlib import json import logging import os +import select import socket import ssl import sys @@ -120,6 +121,10 @@ class KobraXClient: self._buf = b"" self._pid = 1 self._lock = threading.Lock() + # Generations-Marker: wird bei jedem Socket-Swap/Close erhöht, damit der + # Reader-Thread erkennt wenn _reconnect/_do_connect den Socket unter ihm + # ersetzt hat (Issue #53). Schützt gegen recv auf einem stale fd. + self._sock_gen = 0 self._running = False # Pending requests by msgid (for response ACK) @@ -167,21 +172,31 @@ class KobraXClient: ctx.set_ciphers("DEFAULT:@SECLEVEL=0") ctx.load_cert_chain(CERT_FILE, KEY_FILE) - _ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM) - raw = socket.create_connection(_ai[0][4], timeout=5) - self._sock = ctx.wrap_socket(raw) - log.info("TLS connected cipher=%s", self._sock.cipher()[0]) + # Socket als lokale Variable aufbauen — der Handshake (Connect + CONNACK) + # läuft OHNE gehaltenes Lock, damit ein langsamer Connect die Sender nicht + # einfriert. Erst der fertige Socket wird unter Lock eingeschwenkt (#53). + _ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM) + raw = socket.create_connection(_ai[0][4], timeout=5) + new_sock = ctx.wrap_socket(raw) + log.info("TLS connected cipher=%s", new_sock.cipher()[0]) - self._sock.sendall(_build_connect(self.client_id, self.username, self.password)) - self._sock.settimeout(3) - r = self._sock.recv(64) + new_sock.sendall(_build_connect(self.client_id, self.username, self.password)) + new_sock.settimeout(3) + r = new_sock.recv(64) if len(r) < 4 or r[0] != 0x20 or r[3] != 0: + try: + new_sock.close() + except Exception: + pass raise RuntimeError(f"CONNACK failed: {r.hex()}") log.info("CONNACK rc=0") - self._sock.settimeout(0.2) - self._buf = b"" - self._subscribe(self._sub_topic()) + new_sock.settimeout(0.2) + with self._lock: + self._sock = new_sock + self._sock_gen += 1 + self._buf = b"" + self._subscribe(self._sub_topic()) # nimmt das Lock selbst — nicht verschachteln log.debug("MQTT connected to %s:%s", self.host, self.port) def connect(self): @@ -207,10 +222,14 @@ class KobraXClient: def disconnect(self): self._running = False - try: - self._sock.close() - except Exception: - pass + with self._lock: + try: + if self._sock is not None: + self._sock.close() + except Exception: + pass + self._sock = None + self._sock_gen += 1 def _reconnect(self): """Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder @@ -219,10 +238,16 @@ class KobraXClient: nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht ausgeschaltet) zu vermeiden.""" log.warning("Verbindung verloren – reconnect…") - try: - self._sock.close() - except Exception: - pass + # Close + Invalidierung unter Lock, damit kein Sender mitten im sendall + # auf den gerade geschlossenen Socket trifft (Issue #53). + with self._lock: + try: + if self._sock is not None: + self._sock.close() + except Exception: + pass + self._sock = None + self._sock_gen += 1 delays = [2, 4, 8, 15, 30, 60] attempt = 0 while self._running: @@ -246,7 +271,8 @@ class KobraXClient: with self._lock: pid = self._pid self._pid += 1 - self._sock.sendall(_build_subscribe(topic, pid)) + if self._sock is not None: + self._sock.sendall(_build_subscribe(topic, pid)) log.info("SUB %s", topic) # -- Read loop ----------------------------------------------------------- @@ -256,17 +282,52 @@ class KobraXClient: _empty_count = 0 while self._running: if time.time() - last_ping > 30: + ping_ok = False with self._lock: try: - self._sock.sendall(_build_pingreq()) + if self._sock is not None: + self._sock.sendall(_build_pingreq()) + ping_ok = True except Exception: - if self._running and not self._reconnect(): - break - last_ping = time.time() - continue + ping_ok = False + # _reconnect() AUSSERHALB des Locks aufrufen — es nimmt das Lock + # selbst, und threading.Lock ist nicht reentrant (sonst Deadlock). + if not ping_ok: + if self._running and not self._reconnect(): + break last_ping = time.time() + # Aktuellen Socket + Generation unter Lock greifen, damit ein + # paralleler _reconnect/_do_connect-Swap uns nicht auf einem stale + # fd pollen lässt (Issue #53). + with self._lock: + sock = self._sock + gen = self._sock_gen + if sock is None: + time.sleep(0.05) + continue + + # Idle-Wartezeit OHNE Lock — select probt nur die Bereitschaft, so + # blockiert der Reader während Leerlauf nie das gemeinsame Lock. try: - data = self._sock.recv(65536) + ready, _, _ = select.select([sock], [], [], 0.2) + except (OSError, ValueError): + # fd geschlossen/ungültig (Reconnect oder Disconnect mitten im select) + if not self._running: + break + time.sleep(0.05) + continue + if not ready: + continue # Leerlauf, kein Lock gehalten + + # Daten liegen an: Lock kurz greifen für das eine recv, serialisiert + # gegen alle sendall-Caller. recv blockiert nicht lange (select sagte + # ready, Socket-Timeout ist 0.2s). + try: + with self._lock: + # Socket könnte zwischen select und hier ersetzt worden sein. + if self._sock_gen != gen or self._sock is not sock: + continue + data = sock.recv(65536) if not data: # Windows SSL kann kurzzeitig b"" liefern ohne echten EOF _empty_count += 1 @@ -275,7 +336,7 @@ class KobraXClient: continue _empty_count = 0 self._buf += data - self._drain() + self._drain() # außerhalb des Locks — Dispatch/event.set() bleibt prompt except ssl.SSLWantReadError: continue except socket.timeout: diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index d9e25e5..774e7e5 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -2925,6 +2925,18 @@ class KobraXBridge: if not file_data: return web.json_response({"error": "no file received"}, status=400) + # Nur druckbare Dateien zulassen (Issue #59) — verhindert dass z.B. ein + # JPG im File-Browser landet. OrcaSlicer-Uploads sind .gcode/.gcode.3mf, + # der Kobra X akzeptiert .gcode/.3mf/.bgcode. + _allowed_ext = (".gcode", ".gcode.3mf", ".3mf", ".bgcode") + _fn_lower = (remote_filename or "").lower() + if not _fn_lower.endswith(_allowed_ext): + log.warning(f"Upload abgelehnt (kein GCode): {remote_filename}") + return web.json_response( + {"error": f"only GCode files allowed ({', '.join(_allowed_ext)})"}, + status=400, + ) + file_md5 = hashlib.md5(file_data).hexdigest() file_size = len(file_data) diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 20a381b..02e0de0 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -2006,6 +2006,14 @@ function uploadGcode(file){ var zone=document.getElementById('store-upload-zone'); var status=document.getElementById('store-upload-status'); var label=document.getElementById('store-upload-label'); + // Nur druckbare Dateien zulassen (Issue #59) — Drag&Drop umgeht das + // accept-Attribut, daher hier explizit prüfen. + var _fn=(file.name||'').toLowerCase(); + if(!/\.(gcode|gcode\.3mf|3mf|bgcode)$/.test(_fn)){ + if(status){ status.textContent=T.store_upload_only_gcode||'Only GCode files allowed'; status.style.display=''; status.className='upload-status-err'; } + clog('Upload abgelehnt (kein GCode): '+file.name,'msg-err'); + return; + } if(status) { status.textContent=T.store_upload_busy; status.style.display=''; status.className='upload-status-busy'; } if(label) label.style.display='none'; if(zone) zone.style.pointerEvents='none'; diff --git a/web/translations/de.json b/web/translations/de.json index c11dda1..c5011df 100644 --- a/web/translations/de.json +++ b/web/translations/de.json @@ -252,6 +252,7 @@ "store_upload_busy": "⏳ Hochladen…", "store_upload_success": "✓ {file}", "store_upload_error": "✗ {error}", + "store_upload_only_gcode": "✗ Nur GCode-Dateien erlaubt (.gcode, .3mf, .bgcode)", "sf_all": "Alle", "sf_ok": "✓ Erfolgreich", "sf_err": "✗ Fehler", diff --git a/web/translations/en.json b/web/translations/en.json index 3e1b1e8..61c5c86 100644 --- a/web/translations/en.json +++ b/web/translations/en.json @@ -280,6 +280,7 @@ "store_upload_busy": "⏳ Uploading…", "store_upload_success": "✓ {file}", "store_upload_error": "✗ {error}", + "store_upload_only_gcode": "✗ Only GCode files allowed (.gcode, .3mf, .bgcode)", "sf_all": "All", "sf_ok": "✓ Completed", "sf_err": "✗ Failed", diff --git a/web/translations/es.json b/web/translations/es.json index 6544dbf..822cf4b 100644 --- a/web/translations/es.json +++ b/web/translations/es.json @@ -252,6 +252,7 @@ "store_upload_busy": "⏳ Subiendo…", "store_upload_success": "✓ {file}", "store_upload_error": "✗ {error}", + "store_upload_only_gcode": "✗ Solo se permiten archivos GCode (.gcode, .3mf, .bgcode)", "sf_all": "Todos", "sf_ok": "✓ Completado", "sf_err": "✗ Fallido", diff --git a/web/translations/fr.json b/web/translations/fr.json index e1a4c88..01fc6f6 100644 --- a/web/translations/fr.json +++ b/web/translations/fr.json @@ -252,6 +252,7 @@ "store_upload_busy": "⏳ Envoi en cours…", "store_upload_success": "✓ {file}", "store_upload_error": "✗ {error}", + "store_upload_only_gcode": "✗ Seuls les fichiers GCode sont autorisés (.gcode, .3mf, .bgcode)", "sf_all": "Tout", "sf_ok": "✓ Terminés", "sf_err": "✗ Échoués", diff --git a/web/translations/zh-cn.json b/web/translations/zh-cn.json index af584d3..8225ee0 100644 --- a/web/translations/zh-cn.json +++ b/web/translations/zh-cn.json @@ -252,6 +252,7 @@ "store_upload_busy": "⏳ 上传中…", "store_upload_success": "✓ {file}", "store_upload_error": "✗ {error}", + "store_upload_only_gcode": "✗ 仅允许 GCode 文件 (.gcode, .3mf, .bgcode)", "sf_all": "全部", "sf_ok": "✓ 已完成", "sf_err": "✗ 失败", From 5bff7adad002ec4a63cc1eb734352db5fd528d5c Mon Sep 17 00:00:00 2001 From: viewit Date: Sun, 21 Jun 2026 21:46:38 +0200 Subject: [PATCH 9/9] build: sources for v0.9.26 --- CHANGELOG.de.md | 16 ++ CHANGELOG.md | 35 ++++ VERSION | 2 +- env_loader.py | 2 + kobrax_moonraker_bridge.py | 115 ++++++++--- web/themes/default/app.js | 352 +++++++++++++++++++++++++--------- web/themes/default/index.html | 2 + web/translations/it.json | 291 ++++++++++++++++++++++++++++ 8 files changed, 696 insertions(+), 119 deletions(-) create mode 100644 web/translations/it.json diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md index ed5a469..440e5b7 100644 --- a/CHANGELOG.de.md +++ b/CHANGELOG.de.md @@ -1,5 +1,21 @@ # Changelog +## [0.9.26] – 2026-06-21 + +### Neu +- **Italienische Sprachunterstützung** (PR #66, @Alex_M). Die Bridge-UI ist jetzt vollständig auf Italienisch verfügbar. + +### Behoben +- **Kamera startete immer beim Druckbeginn** (Issue #50). `camera_on_print` fehlte in der `/api/state`-Antwort — JavaScript las `undefined` und startete die Kamera unabhängig vom Setting. Jetzt korrekt im State enthalten. +- **Auto-Leveling-Setting wurde im Moonraker-Druckpfad ignoriert** (Issue #57). `handle_print_start` las den Wert nur aus den Bridge-Args, nicht aus dem Request-Body — Dialog-Checkbox und Per-Print-Override hatten keine Wirkung. Verhält sich jetzt identisch zum direkten Druckpfad. +- **Filament-Mapping: Freitext-Felder durch Dropdowns ersetzt** (Issue #57). Falsch getippte Vendor/Name-Kombination brach das Profil-Matching ohne Fehlermeldung; Felder sind jetzt Dropdowns (Vendor → Profil, vendor-gefiltert), sodass nur gültige Kombinationen gespeichert werden können. +- **Dashboard zeigte generischen Materialtyp statt Profilname** (Issue #57). AMS-Slot-Karten zeigen jetzt den gemappten Profilnamen (z.B. „eSUN PLA-Basic") statt nur „PLA". Fallback auf generischen Typ wenn kein Profil gemappt ist. +- **Ghost-Profil auf leerem Slot** (Issue #57). Verwaiste Mappings für leere Slots wurden weiterhin angezeigt; leere Slots zeigen jetzt korrekt „–". +- **Skip-Objects-Panel fehlte im Orca-Upload-Flow** (Issue #57). Panel erscheint jetzt in allen Druckflows; bei frischem Upload fragt die Bridge `fileDetails` beim Drucker nach und pollt die Objektliste bis zu 6 Sekunden nach. +- **Banner und Dialog erschienen gleichzeitig** (Issue #57). Settings-Save setzt jetzt den Dialog-Cancel-State zurück, sodass der Slot-Mapper nach Wechsel des Start-Print-Verhaltens zuverlässig öffnet. +- **„Leeren" lud idle-Datei beim nächsten Poll nach** (Issue #57). Leeren setzt jetzt den lokalen State sofort zurück (`file_ready`, `filename`, `thumbnail`) und löscht alle Dialog-Sperren — Vorschaubild und Aktions-Buttons verschwinden sofort und kommen nicht zurück. +- **Material-Matching für „PLA Silk", „Matte PLA" etc.** (PR #64, @p2l). Modifier+Basis-Muster in beliebiger Wortreihenfolge werden jetzt auf den Basis-Typ normalisiert; Dash-Varianten (PLA-CF) bleiben weiterhin korrekt inkompatibel mit ihrem Basis-Typ. + ## [0.9.25] – 2026-06-17 ### Behoben diff --git a/CHANGELOG.md b/CHANGELOG.md index 282b6a0..40bb001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## [0.9.26] – 2026-06-21 + +### New +- **Italian language support** (PR #66, @Alex_M). The bridge UI is now fully + available in Italian. + +### Fixed +- **Camera always started at print begin** (issue #50). `camera_on_print` was + missing from the `/api/state` response — JavaScript read `undefined` and started + the camera regardless of the setting. Now correctly exposed in state. +- **Auto-leveling setting ignored in Moonraker print path** (issue #57). + `handle_print_start` read the value only from bridge args, not from the request + body, so the dialog checkbox and the per-print override had no effect. Now + behaves identically to the direct print path. +- **Filament mapping free-text fields replaced by dropdowns** (issue #57). A + mistyped vendor/name broke profile matching silently; fields are now dropdowns + (vendor → profile, vendor-filtered) so only valid combinations can be saved. +- **Dashboard showed generic material type instead of profile name** (issue #57). + AMS slot cards now display the mapped profile name (e.g. "eSUN PLA-Basic") + instead of just "PLA". Falls back to the generic type when no profile is mapped. +- **Ghost profile shown on empty slot** (issue #57). Stale mappings for empty + slots were still rendered; empty slots now correctly show "–". +- **Skip-Objects panel missing in Orca upload flow** (issue #57). Panel now + appears in all print flows; on fresh upload the bridge requests `fileDetails` + from the printer and retries the object list for up to 6 s. +- **Banner and dialog appeared simultaneously** (issue #57). Settings save now + resets the dialog cancel state so the slot mapper reliably opens after toggling + Start Print Behavior. +- **"Clear" reloaded idle file on next poll** (issue #57). Clear now immediately + resets local state (`file_ready`, `filename`, `thumbnail`) and clears all dialog + locks — the preview and action buttons disappear instantly and do not return. +- **Material matching for "PLA Silk", "Matte PLA" etc.** (PR #64, @p2l). + Modifier+base patterns in any word order are now normalised to the base type; + dash-suffix variants (PLA-CF) remain correctly incompatible with their base. + ## [0.9.25] – 2026-06-17 ### Fixed diff --git a/VERSION b/VERSION index ec9b691..46e7a71 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.25 +0.9.26 diff --git a/env_loader.py b/env_loader.py index 5ed9379..0ff9433 100644 --- a/env_loader.py +++ b/env_loader.py @@ -48,4 +48,6 @@ MODE_ID = get("MODE_ID", "") DEVICE_ID = get("DEVICE_ID", "") DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto") AUTO_LEVELING = int(get("AUTO_LEVELING", "1")) +CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0")) +WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1")) PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1"))) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 774e7e5..930c34f 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -1077,6 +1077,25 @@ class KobraXBridge: if (not skipped and self._pending_preprint_skip and now <= self._pending_preprint_skip_deadline): return + + # Während eines aktiven Drucks sind Skip-Zustände effektiv monoton. + # Manche Firmware-Reports kommen zwischenzeitlich leer/teilweise zurück; + # diese dürfen bereits bestätigte Skip-Objekte nicht aus der UI löschen. + existing_skipped = [str(n) for n in (self._skip_state.get("skipped") or []) if n] + existing_set = set(existing_skipped) + incoming_skipped = [str(n) for n in (skipped or []) if n] + incoming_set = set(incoming_skipped) + active_print = self._state.get("print_state") in ("printing", "paused") + if active_print and existing_set: + if not incoming_set: + skipped = list(existing_skipped) + elif not incoming_set.issuperset(existing_set): + merged = list(existing_skipped) + for n in incoming_skipped: + if n not in existing_set: + merged.append(n) + skipped = merged + # Pending-Lock aufheben sobald Drucker die gewünschten Objekte bestätigt if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip): self._pending_preprint_skip = [] @@ -1123,7 +1142,7 @@ class KobraXBridge: return False for i in range(max(1, int(retries))): try: - if self._state.get("kobra_state") != "printing": + if self._state.get("print_state") not in ("printing", "paused"): time.sleep(max(0.1, float(delay_s))) continue resp = self.client.skip_objects(wanted) @@ -2453,6 +2472,18 @@ class KobraXBridge: names = json.loads(f.get("objects_skip_parts") or "[]") except Exception: names = [] + # Noch keine Objekte im Store (frischer Orca-/Web-Upload): einmal aktiv + # file/fileDetails beim Drucker anfragen. _on_file() füllt den Store nach, + # das Frontend pollt diesen Endpoint und bekommt die Liste beim nächsten + # Versuch (Issue #57 — Skip-Parität auch außerhalb des File-Browsers). + if not names: + fn = f.get("filename") or "" + if fn: + try: + self.client.publish("file", "fileDetails", + {"root": "local", "filename": fn}, timeout=0) + except Exception as e: + log.debug(f"fileDetails-Nachfrage fehlgeschlagen: {e}") return self._json_cors({ "result": { "names": names, @@ -2479,29 +2510,8 @@ class KobraXBridge: return self._json_cors({"error": str(e)}, status=502) return self._json_cors({"result": "ok", "names": names}) - async def handle_kx_skip_query(self, request): - """Druck-Objektliste vom Drucker neu abfragen. - - POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten - Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state). - """ - try: - loop = asyncio.get_event_loop() - await loop.run_in_executor(None, lambda: self.client.query_skip_objects()) - except Exception as e: - return self._json_cors({"error": str(e)}, status=502) - return self._json_cors({"result": self._skip_state}) - - async def handle_kx_skip_state(self, request): - """Aktueller Skip-State. - - Kombiniert: - - Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell - laufenden filename (file/report beim Druckstart hat die Liste gefüllt). - skip/query_obj liefert nämlich NUR die bereits geskippten zurück, - nicht die Gesamtliste. - - Geskippt: aus self._skip_state (von skip/report aktualisiert). - """ + def _build_skip_state_result(self) -> dict: + """Baut den kombinierten Skip-State für UI-Endpunkte.""" filename = self._state.get("filename", "") all_objects: list[str] = [] svg = "" @@ -2513,14 +2523,46 @@ class KobraXBridge: svg = f.get("svg_image") or "" except Exception as e: log.warning(f"skip_state lookup failed: {e}") - result = { + return { "objects": all_objects, "skipped": list(self._skip_state.get("skipped", [])), "svg_b64": svg, "ts": self._skip_state.get("ts", 0), "filename": filename, } - return self._json_cors({"result": result}) + + async def handle_kx_skip_query(self, request): + """Druck-Objektliste vom Drucker neu abfragen. + + POST /kx/skip/query → triggert skip/query_obj, wartet kurz auf den + async skip/report und gibt den zusammengeführten Skip-State zurück. + """ + prev_ts = int(self._skip_state.get("ts", 0) or 0) + try: + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, lambda: self.client.query_skip_objects()) + except Exception as e: + return self._json_cors({"error": str(e)}, status=502) + + deadline = time.time() + 1.5 + while time.time() < deadline: + if int(self._skip_state.get("ts", 0) or 0) > prev_ts: + break + await asyncio.sleep(0.1) + + return self._json_cors({"result": self._build_skip_state_result()}) + + async def handle_kx_skip_state(self, request): + """Aktueller Skip-State. + + Kombiniert: + - Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell + laufenden filename (file/report beim Druckstart hat die Liste gefüllt). + skip/query_obj liefert nämlich NUR die bereits geskippten zurück, + nicht die Gesamtliste. + - Geskippt: aus self._skip_state (von skip/report aktualisiert). + """ + return self._json_cors({"result": self._build_skip_state_result()}) async def handle_kx_printers(self, request): # Aktive Drucker (mit IP) sammeln @@ -2615,8 +2657,8 @@ class KobraXBridge: }, } - # Pre-Print-Skip sofort im UI-Status spiegeln - self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())} + # UI erst nach echter Drucker-Bestätigung als "geskippt" markieren. + self._skip_state = {"skipped": [], "ts": int(time.time())} if excluded_objects: self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n] self._pending_preprint_skip_deadline = time.time() + 12.0 @@ -3139,7 +3181,8 @@ class KobraXBridge: ams_box_mapping = self._build_auto_ams_box_mapping() use_ams = len(ams_box_mapping) > 0 - auto_leveling = getattr(self._args, "auto_leveling", 1) + # Dialog-Checkbox (body) hat Vorrang, sonst Setting-Default (wie handle_kx_print). + auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1))) url = self._state.get("last_upload_url", "") filesize = self._state.get("last_upload_size", 0) md5 = self._state.get("last_upload_md5", "") @@ -3169,6 +3212,15 @@ class KobraXBridge: }, } + # UI erst nach echter Drucker-Bestätigung als "geskippt" markieren. + self._skip_state = {"skipped": [], "ts": int(time.time())} + if excluded_objects: + self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n] + self._pending_preprint_skip_deadline = time.time() + 12.0 + else: + self._pending_preprint_skip = [] + self._pending_preprint_skip_deadline = 0.0 + log.info( f"print/start api=1 mode={self._filament_mode} " f"ams={len(ams_box_mapping)} slots assignments={filament_assignments is not None}" @@ -3181,6 +3233,9 @@ class KobraXBridge: if result is None: return web.json_response({"error": "Keine Antwort vom Drucker"}, status=504) + if excluded_objects: + loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects)) + return web.json_response({"result": "ok"}) async def handle_print_pause(self, request): @@ -3815,6 +3870,8 @@ class KobraXBridge: "camera_url": s["camera_url"], "fan_speed": s["fan_speed"], "print_speed_mode": s["print_speed_mode"], + "auto_leveling": getattr(self._args, "auto_leveling", 1), + "camera_on_print": getattr(self._args, "camera_on_print", 0), "web_upload_warning": getattr(self._args, "web_upload_warning", 1), "light_on": s["light_on"], "light_brightness": s["light_brightness"], diff --git a/web/themes/default/app.js b/web/themes/default/app.js index 02e0de0..52bacf9 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -9,9 +9,10 @@ var camOn=false; var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support) var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55) +var _idleCleared=false; // User hat idle-Datei explizit „geleert" → kein Nachladen von s.filename (Issue #57) var _fdDialogOpen=false; // Dialog ist gerade offen -var _fdAutoOpenedFile=null; // Dateiname für den der Dialog auto-geöffnet wurde -var _fdUserCancelled=false; // User hat den Auto-Open-Dialog abgebrochen +var _fdAutoOpenedFile=sessionStorage.getItem('fdAutoOpenedFile')||null; +var _fdUserCancelled=sessionStorage.getItem('fdUserCancelled')==='1'; var currentStep=1; var currentPanel='dashboard'; var aceAutoRefillPrefs=(function(){ @@ -108,6 +109,7 @@ function _langToggleLabel(lang){ if(lang==='de')return 'Deutsch'; if(lang==='en')return 'English'; if(lang==='fr')return 'Français'; + if(lang==='it')return 'Italiano'; if(lang==='zh-cn')return '简体中文'; return 'Espanol'; } @@ -115,10 +117,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==='fr'||l==='zh-cn')return l; + if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='it'||l==='zh-cn')return l; var base=l.split('-')[0]; - if(base==='de'||base==='en'||base==='es'||base==='fr')return base; + if(base==='de'||base==='en'||base==='es'||base==='fr'||base==='it')return base; if(base==='zh'){ if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn'; @@ -664,10 +666,18 @@ function applyState(){ if(s.file_ready&&s.print_state==='standby'){ document.getElementById('file-ready-name').textContent=s.file_ready; // Neue Datei → Abbruch-Sperre aufheben - if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready) _fdUserCancelled=false; - if(shouldAutoOpen&&!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ - _fdAutoOpenedFile=s.file_ready; - startReadyFileWithSlots(s.file_ready,true); + if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready){ + _fdUserCancelled=false; + sessionStorage.removeItem('fdUserCancelled'); + sessionStorage.removeItem('fdAutoOpenedFile'); + } + if(shouldAutoOpen){ + // Dialog-Modus: Banner niemals anzeigen. + frb.style.display='none'; + if(!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){ + _fdAutoOpenedFile=s.file_ready; + startReadyFileWithSlots(s.file_ready,true); + } } else { frb.style.display='flex'; bannerVisible=true; @@ -686,8 +696,11 @@ function applyState(){ // Zuletzt geladene Datei merken (Issue #55): solange sie über den State // sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename // (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen. + // Echte ready-Datei oder laufender Druck hebt einen vorherigen „Clear" auf. + if(s.file_ready||printing) _idleCleared=false; if(s.file_ready) _lastLoadedFile=s.file_ready; - else if(s.filename) _lastLoadedFile=s.filename; + else if(s.filename && !_idleCleared) _lastLoadedFile=s.filename; + else if(_idleCleared) _lastLoadedFile=null; // Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine // Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion // anbietet. @@ -864,7 +877,13 @@ function applyState(){ var activity=(slot.activity||''); var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'–'); var slotLabel=T.label_slot+' '+(globalIdx+1); - var profile=(window._slotProfileMap||{})[globalIdx]; + // Gemapptes Profil nur für belegte Slots verwenden — sonst zeigt ein + // verwaistes Mapping (Slot wurde geleert) ein „Geister"-Profil (Issue #57). + var profile=empty?null:(window._slotProfileMap||{})[globalIdx]; + var genericType=(slot.type||slot.material_type||'–'); + // Material-Label: bei belegtem Slot mit Mapping den konkreten Profilnamen + // (z.B. „eSUN PLA+") statt nur des generischen Typs zeigen (Issue #57 Punkt 4). + var materialLabel=empty?'–':((profile&&profile.name)?profile.name:genericType); var vendorBadge=''; if(!empty && profile && profile.vendor){ var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':''); @@ -873,7 +892,7 @@ function applyState(){ html+='
' +'
' - +'
'+(empty?'–':(slot.type||slot.material_type||'–'))+'
' + +'
'+materialLabel+'
' +vendorBadge +'
'+slotLabel+'
' +'
'+pct+'
' @@ -895,7 +914,7 @@ function applyState(){ 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){ + if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped&&s.camera_on_print){ camStart(); } // reset user-stopped flag when print ends so next print auto-starts again @@ -1011,6 +1030,11 @@ function onPollIntervalInput(){ } // ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ── +// Pro Slot ein einzelnes Profil-Dropdown (vendor+name gemeinsam, gekeyt per +// _profileKey). Kein Freitext mehr → das (vendor,name)→id-Matching kann nicht +// mehr durch manuelle Eingabe brechen (Issue #57 Punkt 1). Optionen werden aus +// /kx/filament/profiles geladen, nach Vendor gruppiert, User-Profile zuerst, +// mit demselben Vendor-Sichtbarkeitsfilter wie das Slot-Edit-Dropdown. function renderFilamentMapping(map){ var el=document.getElementById('filament-mapping-list'); if(!el)return; @@ -1020,21 +1044,61 @@ function renderFilamentMapping(map){ var idHint=m.id?' ('+m.id+')':''; rows+=''; + +'' + +'
'; } el.innerHTML=rows; + // Dropdowns befüllen (async, geteilter Profil-Cache + Vendor-Filter) + for(var j=0;j<4;j++){ _fillMappingDropdown(j); } +} +function _fillMappingDropdown(slot){ + var sel=document.getElementById('fmap-'+slot); + if(!sel) return; + var wantKey=_profileKey(sel.dataset.vendor, sel.dataset.name); + _loadOrcaFilaments(function(profiles){ + sel.innerHTML=''; + var userProfs=profiles.filter(function(p){return p.is_user;}); + var systemProfs=profiles.filter(function(p){return !p.is_user;}); + function _opt(g,p){ + var o=document.createElement('option'); + o.value=_profileKey(p.vendor,p.name); + o.dataset.vendor=p.vendor; o.dataset.name=p.name; o.dataset.id=p.id||''; + o.textContent=(p.is_user?'★ ':'')+p.name+(p.vendor?' — '+p.vendor:''); + if(o.value===wantKey)o.selected=true; + g.appendChild(o); + } + if(userProfs.length){ + var gUser=document.createElement('optgroup'); + gUser.label='★ '+(tr('orca_profile_user_label')||'Eigene Profile'); + userProfs.forEach(function(p){_opt(gUser,p);}); + sel.appendChild(gUser); + } + _loadVisibleVendors(function(vis){ + var filtered=systemProfs; + if(vis&&vis.length){ + var allow={};vis.forEach(function(v){allow[v]=1;});allow['Generic']=1; + filtered=systemProfs.filter(function(p){return allow[p.vendor];}); + } + var byVendor={}; + filtered.forEach(function(p){(byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p);}); + Object.keys(byVendor).sort().forEach(function(v){ + var g=document.createElement('optgroup');g.label=v; + byVendor[v].forEach(function(p){_opt(g,p);}); + sel.appendChild(g); + }); + }); + }); } function saveFilamentMapping(){ // Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend). - // Leere Felder = Mapping entfernen. + // Leere Auswahl ("") = Mapping entfernen. var chain=Promise.resolve(); for(var i=0;i<4;i++){ (function(slot){ - var vendor=((document.getElementById('fmap-'+slot+'-vendor')||{}).value||'').trim(); - var name=((document.getElementById('fmap-'+slot+'-name')||{}).value||'').trim(); + var sel=document.getElementById('fmap-'+slot); + var opt=sel?sel.options[sel.selectedIndex]:null; + var vendor=(opt&&opt.dataset.vendor)||''; + var name=(opt&&opt.dataset.name)||''; chain=chain.then(function(){ return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'), {method:'POST',headers:{'Content-Type':'application/json'}, @@ -1193,6 +1257,7 @@ function doProfileImportUpload(files){ var _slotEditIndex=-1; var _slotEditLoaded=false; var _MAT_PRESETS=['PLA','PETG','ABS','ASA','TPU','PA','PC','HIPS']; +var _BASE_MATERIAL_TYPES=['PLA','PETG','ABS','ASA','TPU','TPE','PA','PC','HIPS','PEI','PEEK']; function updateSlotEditFeedButton(){ var btn=document.getElementById('btn-slot-edit-feed'); if(!btn)return; @@ -1329,23 +1394,39 @@ function slotEditFeed(){ } function startReadyFile(filename){ var fn=filename||S.file_ready; + function _doStartReadyFile(){ + var btn=document.getElementById('file-ready-btn'); + if(btn){btn.disabled=true;btn.textContent='…';} + post('/printer/print/start',{filename:fn}) + .then(function(r){return r.json();}) + .then(function(){ + document.getElementById('file-ready-banner').style.display='none'; + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }) + .catch(function(e){ + clog(tr('log_error')+' '+e,'msg-err'); + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }); + } + function _gateAndStart(fileObj){ + if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){ + maybeGateWebUpload(fileObj, function(){ startReadyFile(fn); }); + return; + } + _doStartReadyFile(); + } var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); - if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); }); + if(currentFile){ + _gateAndStart(currentFile); return; } - var btn=document.getElementById('file-ready-btn'); - if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:fn}) - .then(function(r){return r.json();}) - .then(function(r){ - document.getElementById('file-ready-banner').style.display='none'; - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }) - .catch(function(e){ - clog(tr('log_error')+' '+e,'msg-err'); - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }); + fetch(_apiUrl('/kx/files')).then(function(r){return r.json();}).then(function(d){ + storeFiles=d.result||[]; + var refreshed=(storeFiles||[]).find(function(f){return f.filename===fn;})||null; + _gateAndStart(refreshed); + }).catch(function(){ + _doStartReadyFile(); + }); } function cancelReadyFile(){ post('/api/file_ready/clear',{}) @@ -1361,8 +1442,17 @@ function startIdleFileWithSlots(){ } function clearIdleFile(){ _lastLoadedFile=null; + _idleCleared=true; // verhindert Nachladen von s.filename im nächsten poll() (Issue #57) + _fdAutoOpenedFile=null; // nächster Upload derselben Datei soll Dialog wieder öffnen + _fdUserCancelled=false; + _fdDialogOpen=false; + sessionStorage.removeItem('fdAutoOpenedFile'); + sessionStorage.removeItem('fdUserCancelled'); + sessionStorage.removeItem('webVerifyCancelledFileId'); + S.file_ready=''; S.filename=''; S.thumbnail=''; // sofort lokal leeren, kein Warten auf nächsten Poll var ib=document.getElementById('d-idle-btns');if(ib)ib.style.display='none'; var fn=document.getElementById('d-fname');if(fn){fn.textContent='–';fn.title='';} + var thumb=document.getElementById('d-thumbnail');if(thumb){thumb.style.display='none';thumb.src='';} post('/api/file_ready/clear',{}).catch(function(){}); } function selectMatPreset(m){ @@ -1461,6 +1551,11 @@ function saveSettings(){ btn.disabled=true;btn.textContent='…'; var webUploadWarning=(document.getElementById('s-web-upload-warning')||{}).checked?1:0; S.web_upload_warning=webUploadWarning; + // Start-Print-Behavior-Wechsel könnte den Auto-Open sonst dauerhaft blockieren + // (alter _fdUserCancelled bei gleicher file_ready) → Dialog-State zurücksetzen (Issue #57). + _fdUserCancelled=false;_fdAutoOpenedFile=null; + sessionStorage.removeItem('fdUserCancelled');sessionStorage.removeItem('fdAutoOpenedFile'); + sessionStorage.removeItem('webVerifyCancelledFileId'); post('/api/settings',{ printer_name: document.getElementById('s-printer-name').value, printer_ip: document.getElementById('s-printer-ip').value, @@ -2132,6 +2227,7 @@ var _filamentDialogMode='store'; // 'store' oder 'banner' var _pendingWebVerifyFileId=null; var _pendingWebVerifyFilename=''; var _pendingWebVerifyAction=null; +var _pendingWebVerifyAutoOpen=false; // GCode-Store-Dateiliste. MUSS deklariert sein – sonst ReferenceError, wenn // "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen // wurde (Issue #29 / Theme-Auslagerung PR #27). @@ -2199,7 +2295,8 @@ function clearWebUploadWarningFlag(fileId, onDone){ }); } -function maybeGateWebUpload(fileObj, onContinue){ +function maybeGateWebUpload(fileObj, onContinue, opts){ + opts=opts||{}; if(!fileObj || !fileObj.web_unverified){ if(onContinue) onContinue(); return; @@ -2208,15 +2305,20 @@ function maybeGateWebUpload(fileObj, onContinue){ if(onContinue) onContinue(); return; } + var cancelledId=sessionStorage.getItem('webVerifyCancelledFileId')||''; + if(opts.autoOpen && cancelledId && cancelledId===String(fileObj.id||'')){ + return; + } openWebVerifyDialog(fileObj.id, fileObj.filename, function(){ clearWebUploadWarningFlag(fileObj.id, onContinue); - }); + }, !!opts.autoOpen); } -function openWebVerifyDialog(fileId, filename, onConfirm){ +function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen){ _pendingWebVerifyFileId=fileId; _pendingWebVerifyFilename=filename; _pendingWebVerifyAction=onConfirm||null; + _pendingWebVerifyAutoOpen=!!autoOpen; var status=document.getElementById('store-web-verify-status'); if(status){status.textContent='';} openStoreWebVerifyDialog(); @@ -2230,9 +2332,13 @@ function openStoreWebVerifyDialog(){ function closeStoreWebVerifyDialog(){ var modal=document.getElementById('store-web-verify-dialog'); if(modal){modal.classList.remove('open');} + if(_pendingWebVerifyAutoOpen && _pendingWebVerifyFileId){ + sessionStorage.setItem('webVerifyCancelledFileId', String(_pendingWebVerifyFileId)); + } _pendingWebVerifyFileId=null; _pendingWebVerifyFilename=''; _pendingWebVerifyAction=null; + _pendingWebVerifyAutoOpen=false; } function confirmStoreWebVerify(){ @@ -2252,9 +2358,11 @@ function confirmStoreWebVerify(){ .then(function(){ var fileObj=(storeFiles||[]).find(function(f){return f.id===fileId;}); if(fileObj){fileObj.web_unverified=false;} + sessionStorage.removeItem('webVerifyCancelledFileId'); _pendingWebVerifyFileId=null; _pendingWebVerifyFilename=''; _pendingWebVerifyAction=null; + _pendingWebVerifyAutoOpen=false; closeStoreWebVerifyDialog(); loadStore(); if(typeof action==='function') action(); @@ -2270,7 +2378,7 @@ function startReadyFileWithSlots(filename,_autoOpen){ var fn=filename||S.file_ready; var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;}); if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){ - maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn); }); + maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen}); return; } _filamentDialogMode='banner'; @@ -2291,23 +2399,31 @@ function startReadyFileWithSlots(filename,_autoOpen){ }); } + function _proceedWithFileObj(fileObj){ + if(fileObj && fileObj.web_unverified && webUploadWarningEnabled()){ + // Verify-Gate war beim ersten Lookup noch nicht aktiv (storeFiles leer) — jetzt prüfen. + if(_autoOpen){_fdDialogOpen=false;} + maybeGateWebUpload(fileObj, function(){ startReadyFileWithSlots(fn,_autoOpen); }, {autoOpen:!!_autoOpen}); + return; + } + if(fileObj){ + _storeFileId=fileObj.id; + _setGcodeFilamentsFromFileObj(fileObj); + } + openWithSlots(); + } + var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); if(fileObj){ - _storeFileId=fileObj.id; - _setGcodeFilamentsFromFileObj(fileObj); - openWithSlots(); + _proceedWithFileObj(fileObj); return; } // Fallback: refresh file list, then resolve current file by filename. fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){ storeFiles=d.result||[]; - var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;}); - if(refreshed){ - _storeFileId=refreshed.id; - _setGcodeFilamentsFromFileObj(refreshed); - } - openWithSlots(); + var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;})||null; + _proceedWithFileObj(refreshed); }).catch(function(){ openWithSlots(); }); @@ -2332,6 +2448,17 @@ function _normalizeMaterialKey(material){ var key=(material||'').toUpperCase().replace(/[^A-Z0-9+]/g,''); // Orca often uses PLA for PLA+, while AMS may report PLA+. if(key==='PLA+'||key==='PLAPLUS') return 'PLA'; + // Handle modifier+base patterns in either order: "Matte PLA", "Silk PETG", + // "PLA Silk", "PLA Matte". OrcaSlicer always writes the base type in GCode + // (filament_type = PLA), but users label slots with the full product-style name. + var trimmed=(material||'').trim(); + if(trimmed.indexOf(' ')>=0){ + var words=trimmed.toUpperCase().split(/\s+/); + for(var i=0;i=0) return w; + } + } return key; } function _materialsCompatible(a,b){ @@ -2387,19 +2514,30 @@ function openFilamentDialog(slots){ if(objBody)objBody.style.display='none'; // immer eingeklappt starten if(objArrow)objArrow.style.transform=''; if(_storeFileId){ - fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects')) - .then(function(r){return r.json()}) - .then(function(d){ - var names=(d.result&&d.result.names)||[]; - _printObjectsSvg=(d.result&&d.result.svg_b64)||''; - if(names.length>=2){ - _printObjects=names.map(function(n){return {name:n,skip:false};}); - renderObjectChecklist(); renderObjectSvg(); - var cnt=document.getElementById('fd-objects-count'); - if(cnt)cnt.textContent='('+names.length+')'; - if(objSection)objSection.style.display='block'; - } - }).catch(function(){}); + // Bei frischem Orca-/Web-Upload liefert der Drucker die Objektliste + // (objects_skip_parts) erst per fileDetails nach → ist im Store kurz leer. + // Daher mehrfach nachfragen, bis Objekte da sind (Issue #57 Skip-Parität). + var _objFid=_storeFileId; + var _objTries=0; + (function _loadObjects(){ + if(_objFid!==_storeFileId) return; // Dialog wechselte Datei → abbrechen + fetch(_apiUrl('/kx/files/'+encodeURIComponent(_objFid)+'/objects')) + .then(function(r){return r.json()}) + .then(function(d){ + var names=(d.result&&d.result.names)||[]; + var svg=(d.result&&d.result.svg_b64)||''; + if(names.length>=2){ + _printObjectsSvg=svg; + _printObjects=names.map(function(n){return {name:n,skip:false};}); + renderObjectChecklist(); renderObjectSvg(); + var cnt=document.getElementById('fd-objects-count'); + if(cnt)cnt.textContent='('+names.length+')'; + if(objSection)objSection.style.display='block'; + } else if(_objTries++ < 6){ + setTimeout(_loadObjects, 1000); // bis ~6s auf fileDetails warten + } + }).catch(function(){}); + })(); } // GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten @@ -2491,7 +2629,11 @@ function closeFilamentDialog(){ var dlg=document.getElementById('filament-dialog'); if(dlg)dlg.classList.remove('open'); _fdDialogOpen=false; - if(_fdAutoOpenedFile) _fdUserCancelled=true; + if(_fdAutoOpenedFile){ + _fdUserCancelled=true; + sessionStorage.setItem('fdUserCancelled','1'); + sessionStorage.setItem('fdAutoOpenedFile',_fdAutoOpenedFile); + } } function confirmFilamentPrint(){ @@ -2535,19 +2677,41 @@ function confirmFilamentPrint(){ var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0); closeFilamentDialog(); if(_filamentDialogMode==='banner'){ - // Banner-Modus: normaler print/start mit Slot-Override + // Banner-Modus: /kx/print bevorzugen wenn _storeFileId bekannt (gleicher Pfad wie File-Browser). var btn=document.getElementById('file-ready-btn'); if(btn){btn.disabled=true;btn.textContent='…';} - post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling}) - .then(function(r){return r.json();}) - .then(function(){ - document.getElementById('file-ready-banner').style.display='none'; - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} - }) - .catch(function(e){ - clog(tr('log_error')+' '+e,'msg-err'); - if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + var startPromise; + if(_storeFileId){ + startPromise=fetch(_apiUrl('/kx/print'),{ + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({ + file_id:_storeFileId, + filament_assignments:assignments, + excluded_objects:excludedObjects, + auto_leveling:fdAutoLeveling + }) }); + }else{ + startPromise=post('/printer/print/start',{ + filename:S.file_ready||_storeFilename, + filament_assignments:assignments, + excluded_objects:excludedObjects, + auto_leveling:fdAutoLeveling + }); + } + startPromise.then(function(r){ + if(!r.ok){return r.text().then(function(t){throw new Error(t||('HTTP '+r.status));});} + return r.json(); + }).then(function(d){ + if(d&&d.error) throw new Error(d.error); + if(d&&d.result&&d.result!=='ok') throw new Error(String(d.result)); + document.getElementById('file-ready-banner').style.display='none'; + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }).catch(function(e){ + clog(tr('log_error')+' '+e,'msg-err'); + if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);} + }); } else { // Store-Modus: POST /kx/print fetch(_apiUrl('/kx/print'),{ @@ -2618,35 +2782,45 @@ function renderObjectSvg(){ // ── Mid-Print Skip ── var _skipObjects=[]; // [{name, skipped, willSkip}] var _skipSvg=''; +var _skipPollTimer=null; +function _applySkipDialogState(s){ + s=s||{}; + _skipSvg=s.svg_b64||''; + var skipped=s.skipped||[]; + // Pending-Auswahl (willSkip) beim Refresh erhalten. + var prevWillSkip={}; + (_skipObjects||[]).forEach(function(o){if(o&&o.name)prevWillSkip[o.name]=!!o.willSkip;}); + _skipObjects=(s.objects||[]).map(function(n){ + var isSkipped=(skipped.indexOf(n)>=0); + return {name:n, skipped:isSkipped, willSkip:isSkipped?false:!!prevWillSkip[n]}; + }); + renderSkipList(); renderSkipSvg(); +} function openSkipDialog(){ document.getElementById('skip-status').textContent=''; document.getElementById('skip-confirm').disabled=false; _refreshSkipDialog(); + if(_skipPollTimer)clearInterval(_skipPollTimer); + _skipPollTimer=setInterval(function(){ + var dlg=document.getElementById('skip-dialog'); + if(!(dlg&&dlg.classList.contains('open')))return; + _refreshSkipDialog(); + },2000); document.getElementById('skip-dialog').classList.add('open'); } function _refreshSkipDialog(){ - // Erst aktueller State (mit DB-Objects + svg), dann query_obj für frischen skipped - fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ - var s=d.result||{}; - _skipSvg=s.svg_b64||''; - _skipObjects=(s.objects||[]).map(function(n){ - return {name:n, skipped:(s.skipped||[]).indexOf(n)>=0, willSkip:false}; + // query-Endpoint wartet intern auf frischen skip/report (bis 1.5s). + fetch(_apiUrl('/kx/skip/query'),{method:'POST'}) + .then(function(r){return r.json();}) + .then(function(d){_applySkipDialogState(d.result||{});}) + .catch(function(){ + fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json();}).then(function(d){ + _applySkipDialogState(d.result||{}); + }).catch(function(){}); }); - renderSkipList(); renderSkipSvg(); - }); - // Frisch nachfragen (skipped-Liste aktualisieren) - fetch(_apiUrl('/kx/skip/query'),{method:'POST'}).then(function(r){return r.json()}).then(function(){ - setTimeout(function(){ - fetch(_apiUrl('/kx/skip/state')).then(function(r){return r.json()}).then(function(d){ - var s=d.result||{}; - var skipped=s.skipped||[]; - _skipObjects.forEach(function(o){ o.skipped=skipped.indexOf(o.name)>=0; if(o.skipped)o.willSkip=false; }); - renderSkipList(); renderSkipSvg(); - }); - }, 500); - }).catch(function(){}); } function closeSkipDialog(){ + if(_skipPollTimer){clearInterval(_skipPollTimer);_skipPollTimer=null;} document.getElementById('skip-dialog').classList.remove('open'); } function _shortLabel(name){ diff --git a/web/themes/default/index.html b/web/themes/default/index.html index a783654..843d553 100644 --- a/web/themes/default/index.html +++ b/web/themes/default/index.html @@ -39,6 +39,7 @@ +
@@ -521,6 +522,7 @@ + diff --git a/web/translations/it.json b/web/translations/it.json new file mode 100644 index 0000000..56ed376 --- /dev/null +++ b/web/translations/it.json @@ -0,0 +1,291 @@ +{ + "header_status_standby": "Pronto", + "header_status_printing": "In stampa", + "header_status_complete": "Completato", + "header_status_error": "Errore", + "kobra_free": "Pronto", + "kobra_busy": "Occupato", + "kobra_printing": "In stampa", + "kobra_preheating": "Preriscaldamento", + "kobra_auto_leveling": "Livellamento automatico", + "kobra_checking": "Verifica", + "kobra_updated": "Aggiornamento", + "kobra_init": "Inizializzazione", + "kobra_pausing": "Pausa in corso...", + "kobra_paused": "In pausa", + "kobra_resuming": "Ripresa...", + "kobra_resumed": "Ripreso", + "kobra_stopping": "Arresto...", + "kobra_stoped": "Arrestato", + "kobra_finished": "Finito", + "kobra_failed": "Errore", + "kobra_canceled": "Annullato", + "kobra_offline": "Offline", + "nav_dashboard": "Dashboard", + "nav_print": "Stampa", + "nav_temps": "Temperature", + "nav_motion": "Movimento", + "nav_ams": "AMS", + "nav_extras": "Luce / Ventola", + "nav_console": "Console", + "card_progress": "Avanzamento", + "card_temps": "Temperature", + "card_light_fan": "Ventola", + "card_speed": "Velocità di stampa", + "card_cam": "Camera", + "lbl_elapsed": "Trascorso:", + "lbl_remaining": "Rimanente:", + "lbl_slicer_time": "Stima slicer:", + "lbl_layers": "Layer", + "lbl_zpos": "Z (mm)", + "speed_silent": "🐢 Silenzioso", + "speed_normal": "⚡ Normale", + "speed_sport": "🚀 Sport", + "lbl_light": "💡 Luce", + "lbl_feed": "Carica", + "lbl_unload": "Rimuovi", + "card_ace_dry": "Essiccazione ACE", + "ace_dry_dryer": "Essiccatore", + "ace_dry_status_off": "Stato: Spento", + "ace_dry_status_on": "Stato: Attivo", + "ace_dry_status_remaining": "Rimanente", + "ace_dry_humidity": "Umidità", + "ace_dry_current_temp": "Temperatura", + "ace_dry_chart": "Cronologia (Temp/Umidità)", + "ace_dry_temp": "Temperatura (°C)", + "ace_dry_duration": "Durata (min)", + "ace_dry_start": "▶ Avvia", + "ace_dry_stop": "■ Ferma", + "ace_dry_auto_refill": "Ricarica automatica", + "ace_dry_enable": "Abilita essiccazione", + "ace_dry_temp_line": "Temperatura di essiccazione", + "ace_dry_time_line": "Tempo di essiccazione", + "ace_dry_ui_pending": "(Solo interfaccia, backend a seguire)", + "ace_dry_dialog_title": "Impostazioni Temp/Tempo essiccatore", + "ace_dry_dialog_temp": "Temperatura (30-80°C)", + "ace_dry_dialog_time": "Tempo rim. (h:m:s)", + "ace_dry_dialog_confirm": "Conferma", + "ace_dry_dialog_cancel": "Annulla", + "ace_dry_dialog_save_restart": "Salva e riavvia", + "ace_dry_dialog_custom_name": "Nome personalizzato", + "ace_dry_dialog_reset_default": "Ripristina predefiniti", + "ace_dry_preset_pla": "PLA", + "ace_dry_preset_pla_plus": "PLA+", + "ace_dry_preset_petg": "PETG", + "ace_dry_preset_tpu": "TPU", + "ace_dry_preset_abs_asa": "ABS / ASA", + "ace_dry_preset_pa_pc": "PA / PC", + "ace_dry_preset_custom": "Personalizzato", + "cam_placeholder": "📷 Camera non avviata", + "cam_stream_unavailable": "Flusso video non disponibile", + "btn_cam_start": "▶ Camera", + "btn_cam_stop": "◼ Camera", + "btn_pause": "⏸ Pausa", + "btn_resume": "▶ Riprendi", + "btn_cancel": "✕ Stop", + "label_nozzle": "Ugello", + "label_bed": "Piatto", + "label_fan": "🌀 Ventola", + "label_light": "💡 Luce", + "label_on_off": "On / Off", + "label_speed": "Velocità", + "panel_print_title": "Controllo stampa", + "panel_print_btn_pause": "⏸ Pausa", + "panel_print_btn_resume": "▶ Riprendi", + "panel_print_btn_cancel": "✕ Annulla", + "panel_print_temps_live": "Temperature (In tempo reale)", + "label_set": "Imposta", + "label_off": "Off", + "panel_temps_nozzle": "Ugello", + "panel_temps_bed": "Piatto riscaldato", + "panel_temps_chart": "Cronologia (ultime 60 letture)", + "label_target_c": "Target:", + "panel_motion_xy": "Assi XY", + "panel_motion_z": "Asse Z", + "label_step": "Ampiezza passo:", + "btn_home_z": "Home Z", + "btn_home_xy": "Home XY", + "btn_home_all": "Home generale", + "btn_disable_motors": "Spegni motori", + "panel_ams_title": "Filamento", + "card_ams": "Filamento", + "ams_no_data": "Nessun dato ricevuto dall' AMS", + "label_slot": "Slot", + "ams_empty": "Vuoto", + "panel_extras_light": "Luce", + "panel_extras_fan": "Ventola", + "panel_extras_camera": "Camera", + "btn_cam_start2": "▶ Avvia", + "btn_cam_stop2": "◼ Ferma", + "panel_console_title": "Registro eventi", + "log_light_on": "Luce accesa", + "log_light_off": "Luce spenta", + "log_fan": "Ventola →", + "log_nozzle": "Ugello →", + "log_bed": "Piatto →", + "log_axis": "Asse", + "log_home": "Home", + "log_home_all": "Home generale", + "log_cam_start": "Camera avviata:", + "log_cam_stop": "Camera arrestata", + "log_poll_error": "Errore di sincronizzazione:", + "log_error": "Errore:", + "confirm_cancel": "Annullare davvero la stampa?", + "settings_title": "Impostazioni", + "settings_connection": "Connessione", + "settings_print": "Impostazioni di stampa", + "settings_poll": "Intervallo di sincronizzazione (secondi)", + "nav_settings": "Impostazioni", + "settings_cat_display": "Aspetto", + "settings_cat_filament": "Filamento", + "settings_cat_language": "Lingua", + "settings_cat_theme": "Alterna chiaro / scuro", + "settings_filament_mapping": "Mappatura profilo filamento (per slot)", + "settings_filament_mapping_save": "Salva mappatura", + "settings_visible_vendors": "Produttori visibili (menu del profilo)", + "settings_visible_vendors_hint": "Solo questi produttori appariranno nel menu del profilo dello slot. Se non selezioni nulla = mostra tutti. I profili \"Generici\" e i tuoi personali sono sempre visibili.", + "settings_visible_vendors_save": "Salva selezione", + "progress_action_print": "Stampa", + "progress_action_slots": "Mappa slot", + "progress_action_clear": "Cancella", + "settings_version": "Versione", + "settings_save": "Salva e riavvia", + "settings_printer_name": "Nome stampante", + "settings_printer_ip": "IP stampante", + "settings_mqtt_port": "Porta MQTT", + "settings_username": "Nome utente MQTT", + "settings_password": "Password MQTT", + "settings_device_id": "ID dispositivo", + "settings_mode_id": "ID modalità", + "hint_ip_no_port": "Solo indirizzo IP, senza porta (es. 192.168.1.102)", + "settings_default_slot": "Slot predefinito (colore singolo)", + "settings_slot_auto": "Auto (tutti gli slot caricati)", + "settings_auto_leveling": "Livellamento automatico predefinito", + "fd_options_title": "Opzioni di stampa", + "print_auto_leveling": "Livellamento automatico", + "settings_file_ready_mode": "Comportamento all'avvio stampa", + "settings_file_ready_banner": "Barra di stampa", + "settings_file_ready_dialog": "Finestra di dialogo di stampa", + "settings_camera_on_print": "Attiva la camera all'avvio della stampa", + "settings_web_upload_warning": "Mostra un avviso quando si stampano caricamenti web", + "update_check": "Controlla aggiornamenti", + "update_checking": "Verifica in corso...", + "update_available": "disponibile", + "update_none": "Già aggiornato", + "update_apply": "Installa ora", + "update_applying": "Download in corso...", + "update_restarting": "Riavvio in corso...", + "update_error": "Errore", + "btn_connect": "⚡ Connetti", + "btn_disconnect": "✕ Disconnetti", + "lbl_conn_error": "Errore di connessione:", + "slot_edit_title": "Modifica slot", + "slot_edit_color": "Colore", + "slot_edit_material": "Materiale", + "slot_edit_load": "⬇ Carica", + "slot_edit_unload": "⬆ Rimuovi", + "slot_edit_save": "💾 Salva", + "slot_edit_custom": "es. PLA, PETG, ABS…", + "slot_edit_ok": "Slot AMS", + "slot_edit_profile": "Profilo OrcaSlicer", + "slot_edit_profile_hint": "Inviato durante la sincronizzazione con OrcaSlicer come marchio specifico invece di un semplice \"Generico\"", + "slot_edit_profile_default": "— Generico (predefinito) —", + "orca_profile_section": "Profili OrcaSlicer", + "orca_profile_hint": "Importa i tuoi profili di filamento OrcaSlicer (apri la cartella utente tramite Aiuto → Mostra cartella di configurazione)", + "orca_profile_import_btn": "Importa profili", + "orca_profile_import_link": "★ Importa i tuoi profili…", + "orca_profile_import_title": "Importa i tuoi profili OrcaSlicer", + "orca_profile_help_html": "Carica un file ZIP della tua cartella filamenti di OrcaSlicer o file singoli .json.
In OrcaSlicer: Aiuto → Mostra cartella di configurazione → user/<id>/filament/", + "orca_profile_dropmsg": "Trascina qui o fai clic", + "orca_profile_list_label": "Attualmente importati", + "orca_profile_user_label": "Profili personali", + "orca_profile_user_empty": "– nessuno –", + "orca_profile_uploading": "Caricamento in corso…", + "orca_profile_done": "Importato", + "orca_profile_skipped": "saltato", + "log_dir_all": "Tutti", + "log_dir_rx": "RX", + "log_dir_tx": "TX", + "log_dir_label": "Dir:", + "log_lvl_label": "Livello:", + "log_lvl_err": "⛔ Errori", + "log_lvl_warn": "⚠ Avvisi", + "log_topic_label": "Argomento:", + "log_topic_ams": "AMS", + "log_topic_print": "Stampa", + "log_topic_info": "Info", + "log_topic_status": "Stato", + "log_download": "⬇ Scarica", + "log_auto": "⬇ Auto", + "log_clear": "✕ Cancella", + "log_filter_placeholder": "Filtra…", + "file_ready_btn": "▶ Avvia stampa", + "file_slots_btn": "🎨 Seleziona slot", + "file_cancel_btn": "✕ Annulla", + "nav_printers": "Stampanti", + "skip_title": "✂ Salta oggetti", + "skip_hint": "Deseleziona gli oggetti che non vuoi più stampare:", + "skip_btn_label": "Oggetti", + "skip_no_objects": "Nessun oggetto in questa stampa.", + "skip_already": "saltato", + "skip_cancel": "Annulla", + "skip_confirm": "Salta", + "skip_select_at_least_one": "Seleziona almeno un oggetto.", + "skip_sending": "Invio in corso …", + "skip_success": "Gli oggetti verranno saltati.", + "fd_objects_hint": "Salta oggetti (opzionale):", + "fd_objects_toggle": "Salta oggetti", + "fd_slots_hint": "Assegna il canale GCode allo slot AMS:", + "fd_cancel": "Annulla", + "fd_print": "▶ Stampa", + "fd_no_slots_msg": "Nessuno slot AMS caricato.{br}Avviare comunque la stampa?", + "fd_slot": "Slot", + "fd_no_matching_material": "Nessun materiale corrispondente", + "fd_used": "USATO", + "add_printer": "Aggiungi stampante", + "apd_lbl_ip": "IP stampante", + "apd_lbl_name": "Nome (opzionale)", + "apd_placeholder_name": "es. Kobra X Soggiorno", + "apd_cancel": "Annulla", + "apd_confirm": "Aggiungi", + "apd_fetching": "Recupero dati dalla stampante…", + "apd_success": "Stampante aggiunta, riavvio del bridge in corso…", + "apd_err_ip": "Inserisci un indirizzo IP", + "printers_remove": "Rimuovi stampante", + "printers_remove_confirm": "Rimuovere la stampante \"{name}\"? Il bridge si riavvierà.", + "printers_active": "● attiva", + "printers_switch": "Cambia →", + "printers_current": "Stampante corrente", + "printers_loading": "Caricamento in corso…", + "printers_none": "Nessuna stampante configurata.", + "printers_empty_hint": "Nessuna stampante ancora configurata.", + "nav_browser": "Browser", + "panel_browser_title": "Browser dei file", + "store_search_placeholder": "🔍 Cerca…", + "store_empty": "Nessun file caricato.", + "store_refresh": "↻ Aggiorna", + "store_print": "▶ Stampa", + "store_download": "⬇ Scarica", + "store_delete_confirm": "Eliminare il file?", + "store_print_confirm": "Stampare il file?", + "store_web_verify_title": "Verifica file", + "store_web_verify_msg": "Verifica che questo file sia stato creato per Anycubic Kobra X.", + "store_web_verify_confirm": "Conferma", + "store_web_verify_abort": "Interrompi", + "store_no_results": "Nessun file trovato.", + "store_never": "mai stampato", + "store_estimate": "Stima", + "store_upload_label_prefix": "Trascina il GCode qui o ", + "store_upload_label_browse": "sfoglia", + "store_upload_busy": "⏳ Caricamento in corso…", + "store_upload_success": "✓ {file}", + "store_upload_error": "✗ {error}", + "store_upload_only_gcode": "✗ Sono consentiti solo file GCode (.gcode, .3mf, .bgcode)", + "sf_all": "Tutti", + "sf_ok": "✓ Completato", + "sf_err": "✗ Fallito", + "sf_new": "Nuovo", + "ss_date": "↓ Data", + "ss_name": "Nome A–Z", + "ss_dur": "⏱ Tempo di stampa" +} \ No newline at end of file