Compare commits

15 Commits

Author SHA1 Message Date
3f6ea269e6 build: sources for v0.9.20 2026-06-08 23:14:50 +02:00
3fff6e25f0 docs(readme): explicit warning that standalone binaries need anycubic_slicer.crt + .key next to the executable (from anycubic-certs.zip) 2026-06-04 20:34:41 +02:00
0f5a8cbc72 sync: kobrax_moonraker_bridge.py mit Dev-Repo (VERSION-Lookup-Fix) 2026-06-04 12:17:46 +02:00
a40f14af8e build: sources for v0.9.19.1 2026-06-04 11:40:02 +02:00
466b8c518d build: sources for v0.9.19.1 2026-06-03 13:12:30 +02:00
1c5396b37d fix(spec): VERSION ins Onefile einbetten — Windows-EXE zeigte vunknown 2026-06-03 13:08:14 +02:00
c23deebde5 docs(changelog): remove internal test-profile name 'Bert - PLA' 2026-06-02 14:34:55 +02:00
76738e5961 release: v0.9.19 2026-06-02 13:59:53 +02:00
9c82073540 build: sources for v0.9.19 2026-06-02 13:31:47 +02:00
031e34d8ea merge: PR #42 — Dryer toggle false error (@gangoke)
ACE-Dryer setDry geht jetzt fire-and-forget (timeout=0, kein Response-
Check). Drucker führt den Befehl korrekt aus, aber liefert code:0
statt code:200 — was eine 502-Fehlermeldung in der Bridge-UI auslöste
obwohl der Trockner-Toggle eigentlich funktioniert hat.

Pattern identisch zu setAutoFeed (Z.3161).
2026-06-01 14:28:42 +02:00
Gangoke
fc89dfffa5 fire and forget setDry 2026-05-31 17:27:01 -10:00
ac695ecf36 build: sources for v0.9.18 2026-05-31 19:53:36 +02:00
23b8a69065 merge: PR #40 — Spanish Translation Fixes (@pezfisk)
Native-speaker review der spanischen Übersetzung — fehlende Akzente
(impresión, cámara, después, animación, …) und sprachliche
Korrekturen (Pause→Pausa, Start→Iniciar, Layer→Capa, Stream→Stream).
Zusätzlich neue README.es.md plus Links in README.md + README.de.md.
2026-05-31 18:42:06 +02:00
22dc58258c Spanish translation 2026-05-31 15:47:19 +02:00
e4b4d091f3 Spanish translation 2026-05-31 15:42:28 +02:00
20 changed files with 2286 additions and 5871 deletions

View File

@@ -1,5 +1,178 @@
# Changelog # 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
- 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.
### Geändert
- Filament-Profil-Liste neu kuratiert: 209 statt 399 Einträge.
Profile die nur für drucker-spezifische Vendor-Bundles existieren
(z.B. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) sind rausgeflogen
— OrcaSlicer hätte sie im Standard-Kobra-X-Setup beim Sync
ohnehin nicht gefunden, weil die jeweiligen Vendor-Bundles nur
bei aktivem Drucker-Vendor geladen werden. Für solche Filamente
bleibt der Custom-Profile-Import (Issue #41) der Weg.
## [0.9.19] 2026-06-02
### Neu
- **🎯 Filament-Sync mit OrcaSlicer matched jetzt das richtige Preset**
statt immer auf „Generic PLA" zu landen. Voraussetzung: ein
OrcaSlicer-Build mit dem
[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)
Empfangs-Patch (im OrcaSlicer-KX-Build dabei). Die Bridge sendet pro
AMS-Slot jetzt `name` + `vendor_name` im Lane-Pfad UND
`gate_filament_name` im Happy-Hare-MMU-Pfad (OrcaSlicer wechselt bei
AMS-Setups automatisch auf den HH-Pfad).
- **Eigene OrcaSlicer-Profile in die Bridge importieren (Issue #41).**
Settings-Tab → „OrcaSlicer-Profile" oder direkt im Slot-Edit-Dialog
(„★ Eigene Profile importieren…") lädst du deine `.json`-Files aus
`~/.config/OrcaSlicer/user/<id>/filament/` hoch — einzeln oder als
ZIP. Erscheint dann im Slot-Dropdown unter „★ Eigene Profile" und
wird beim Sync an Orca als User-Match weitergegeben. Funktioniert
über HTTP, also auch wenn die Bridge im Docker auf Raspi/NAS läuft
und OrcaSlicer auf dem Desktop. Auch reine Override-Profile mit nur
`inherits: "Generic PLA @System"` + ein paar Tweaks werden korrekt
erkannt — die Bridge resolved die vererbten Felder aus dem
System-Parent.
### Fixes
- **AMS-Sync landete hartnäckig auf „Generic PLA":** das Orca-
Datenmodell hat 68 duplikate `filament_id`-Werte (`OGFL99` allein
136 mal), und die Bridge wählte oft eine ID die für den Kobra X
nicht `is_compatible` war (z.B. `GFL92` aus dem Kobra-2-Profil →
Orca verwarf es). Generator priorisiert jetzt Kobra-X-Varianten und
filtert Phantom-Profile (Cross-Vendor-Overrides) raus —
`orca_filaments.json` von 1035 → 400 saubere Profile.
- **Slot ohne expliziten Override sendet jetzt `Generic <Typ>`** statt
einer impliziten Vendor-Annahme. Library-Generic-Profile haben
`compatible_printers: []` (= alle Drucker), sind also immer sichtbar
und matchen verlässlich.
- **Slot-Karte zeigt den Hersteller direkt nach dem Speichern** —
ohne Browser-Reload. `poll()` war async, das Re-Render kam erst
beim nächsten Tick.
- **ACE-Trockner-Toggle warf 502-Fehler obwohl der Trockner ein-/aus-
ging (PR #42 von @gangoke):** `setDry` jetzt fire-and-forget wie
`setAutoFeed`. Der Drucker antwortet auf diesem Push-Topic mit
`code: 0` statt `code: 200`, das hat die Bridge fälschlich als
Fehler interpretiert.
### Datenmodell / API
- `orca_filaments.json` regeneriert: nur echte Vendor-Profile und
OrcaFilamentLibrary-Profile bleiben drin. Bambu/Polymaker/SUNLU-
Library-Profile dabei, Qidi-Cross-Bundles raus.
- Neue Endpoints: `POST /kx/filament/profiles/user` (ZIP/JSON-Upload),
`GET /kx/filament/profiles/user` (Liste der User-Imports),
`DELETE /kx/filament/profiles/user[?vendor=…&name=…]`. Persistenz
in `<KX_DATA_DIR>/orca_filaments.user.json` (Volume — überlebt
Image-Updates).
### Build
- Neues Modul `bridge/orca_filaments.py` (gemeinsame Parser-Helfer
für Generator und Import-Endpoint).
- Dockerfile + release.sh um `orca_filaments.py` erweitert.
## [0.9.18] 2026-05-31
### Neu
- **🎉 Filament-Material und -Farbe pro AMS-Slot aus der Bridge an den
Drucker senden:** Im Slot-Edit-Dialog gewählte Werte gehen jetzt
tatsächlich an den Drucker und werden persistent übernommen — am
Drucker-Display siehst du sofort dieselbe Belegung wie in der Bridge-UI.
In 0.9.17 wurde der Befehl über das falsche MQTT-Topic
(`slicer/printer/…`) gesendet, der Drucker hat ihn stillschweigend
ignoriert. Jetzt geht er über `web/printer/…` wie der Anycubic
Slicer Next es macht (verifiziert via Live-MQTT-Sniff +
Workbench-Vue-Source). **Achtung:** der Drucker muss im Idle-Zustand
sein, und leere Slots lassen sich nicht beschriften.
- **Mehrsprachiges UI spanische Übersetzung von Muttersprachler
überarbeitet (PR #40 von @pezfisk):** fehlende Akzente
(impresión, cámara, después, animación, …), Begriffe vereinheitlicht
(Pause → Pausa, Start → Iniciar, Layer → Capa). Plus neues
`README.es.md` und Cross-Links in den drei READMEs.
- **Z-Höhen-Anzeige in Obico** funktioniert jetzt. Der Drucker liefert
keine echte Z-Position via MQTT (per Live-Sniff bestätigt), die Bridge
schätzt sie aus `curr_layer × layer_height + first_layer_height`.
Layer-Heights kommen aus dem GCode-Header beim Upload, persistiert
im GCode-Store; Fallback aus dem OrcaSlicer-Default-Filename
(`…_0.2_…gcode`). `/server/files/metadata` liefert zusätzlich
`object_height` (Gesamt-Z), damit Obicos `mmProgress`-Widget
`aktuelles Z / Gesamt-Z` anzeigt.
- **Slot-Karte zeigt den OrcaSlicer-Profil-Vendor** unter dem Material
(z.B. „PLA / Polymaker"), mit Profil-Namen + interner ID als
Tooltip. So ist auf einen Blick erkennbar welcher Slot-Override
aktiv ist.
### Fixes
- **Slot-Profil-Auswahl im AMS-Dialog (Issue #39 von @harrygeier):**
drei separate Bugs in 0.9.17 sorgten dafür dass die gewählte Marke
nach dem Speichern verschwand und beim erneuten Öffnen ein falsches
Material angezeigt wurde.
- `multiColorBox/setInfo` über das falsche MQTT-Topic — siehe oben.
- Speichern lief in zwei parallelen Requests (Profil-Override +
Material/Farbe) → Race-Bedingung. Läuft jetzt sequenziell und
reloaded den lokalen State bevor der Dialog geschlossen wird.
- OrcaSlicer-Filament-IDs sind nicht eindeutig — `orca_filaments.json`
hat 68 duplikate IDs, `OGFL99` allein ist 136 Vendor-Profilen
zugeordnet (Erkenntnis von @gangoke). Der primäre Selector ist
jetzt `(vendor, name)` — über alle 1002 Profile eindeutig.
- **MQTT-Reconnect (Issue #33 von @icebear):** wurde der Drucker über
Nacht ausgeschaltet, schlug die Bridge nach 5 Reconnect-Versuchen
(~60 s gesamt) endgültig fehl — Filament-Sync ging morgens noch
(weil das HTTP ist), aber Print-Start scheiterte mit
„connection refused", User musste die Bridge selbst neu starten.
Reader-Thread reconnectet jetzt **endlos** (Backoff cappt bei 60 s)
bis der Drucker wieder antwortet, mit DEBUG-Logging nach den ersten
5 Versuchen damit das Log nicht über Nacht zugemüllt wird.
- **„Unknown child pid"-Warnungen im Log:** beim Killen der Kamera-
`ffmpeg`-Prozesse fehlte das `wait()` — Children blieben als
Zombies und asyncio meldete sie alle ~20 s. Gefixt im CameraCache
+ `/api/camera/stream`.
### UI-Aufräumen
- **Pause-Button als Toggle:** druckt der Drucker → `⏸ Pause`, ist
pausiert → `▶ Weiter`. Der separate „Weiter"-Button entfällt.
- **Pause + Stopp komplett ausgeblendet wenn Drucker idle** — bei
Standby waren beide Buttons vorher dauerhaft sichtbar, was beim
Idle-Drucker verwirrend wirkte.
### Build
- **GCode-Store-Migration:** neue Spalten `layer_height` +
`first_layer_height` in `gcode_files` (automatisch beim ersten
Start von 0.9.18 angelegt).
## [0.9.17] 2026-05-30 ## [0.9.17] 2026-05-30
### Neu ### Neu

View File

@@ -1,5 +1,180 @@
# Changelog # Changelog
## [0.9.20] 2026-06-08
### New
- **French language support (PR #45 by @Nathacks)**
- **Z height display in the print UI (PR #49 by @Nathacks).** Shows
current Z position in mm below the layer counter.
### Fixed
- **Camera auto-start ignored "Enable camera on print start" setting
after a bridge restart (issue #50).** The setting was cached in the
process environment — after saving it in the UI, the old value
survived the restart and the new value from `config.ini` was never
read.
- **Camera restarted automatically after manual stop during a print
(issue #50).** A new `_camera_user_stopped` flag suppresses
auto-restart for the current print session. It resets when the
print ends.
- **Spurious "stream unavailable" error toast when stopping the camera
manually.** The image error handler was still registered when
`img.src` was cleared.
- **JS error (`ReferenceError: br is not defined`) when toggling the
light.** Variable was referenced from the wrong scope.
- Webcam URLs are now absolute so that Mobileraker/Obico clients can
reach them.
## [0.9.19.1] 2026-06-04
### Fixed
- Standalone binaries (Linux/Windows) reported `vunknown` as their
version. The `VERSION` file is now embedded into the PyInstaller
onefile bundle.
- When the TLS certificates (`anycubic_slicer.crt`/`.key`) were
missing, the bridge only logged the raw `[Errno 2] No such file
or directory`. It now states clearly where the files need to be
placed and that `anycubic-certs.zip` from the Gitea release is the
source.
### Changed
- Filament profile list re-curated: 209 entries instead of 399.
Profiles that only exist inside printer-specific vendor bundles
(e.g. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) were dropped —
OrcaSlicer wouldn't have found them in a default Kobra X setup
anyway, because the matching vendor bundle is only loaded when
the corresponding printer vendor is active. For those filaments
the custom profile import (issue #41) remains the way.
## [0.9.19] 2026-06-02
### New
- **🎯 Filament sync with OrcaSlicer now picks the right preset**
instead of always falling back to "Generic PLA". Requires an
OrcaSlicer build with the
[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)
receive-side patch (included in the OrcaSlicer-KX build). The bridge
sends `name` + `vendor_name` per AMS slot on the lane path AND
`gate_filament_name` per gate on the Happy-Hare MMU path (OrcaSlicer
switches to the HH path automatically for AMS setups).
- **Import your own OrcaSlicer profiles into the bridge (issue #41).**
Settings → "OrcaSlicer Profiles" or directly in the slot-edit dialog
("★ Import own profiles…") lets you upload `.json` files from
`~/.config/OrcaSlicer/user/<id>/filament/` — single files or as a
ZIP. They show up in the slot dropdown under "★ Own profiles" and
are passed through to Orca on sync as user matches. Works over HTTP
so the bridge can run in Docker on a Raspi/NAS while OrcaSlicer
lives on a desktop. Override-only profiles with just
`inherits: "Generic PLA @System"` + a few tweaks are detected
correctly — the bridge resolves the inherited fields from the
system parent.
### Fixes
- **AMS sync stuck on "Generic PLA":** the Orca data model has 68
duplicate `filament_id` values (`OGFL99` alone shared by 136
profiles), and the bridge often picked an ID that was not
`is_compatible` with the Kobra X (e.g. `GFL92` from the Kobra-2
profile → Orca rejected it). The generator now prioritises Kobra-X
variants and filters out phantom profiles (cross-vendor overrides) —
`orca_filaments.json` dropped from 1035 to 400 clean profiles.
- **Slot without an explicit override now sends `Generic <type>`**
instead of an implicit vendor guess. Library generic profiles have
`compatible_printers: []` (= all printers), so they are always
visible and match reliably.
- **Slot card shows the vendor right after save** without a browser
reload. `poll()` was async, the re-render only happened on the next
tick.
- **ACE dryer toggle threw a 502 even though the dryer worked
(PR #42 by @gangoke):** `setDry` is now fire-and-forget like
`setAutoFeed`. The printer answers on that push topic with
`code: 0` instead of `code: 200`, which the bridge wrongly treated
as an error.
### Data model / API
- `orca_filaments.json` regenerated: only real vendor profiles and
OrcaFilamentLibrary profiles. Bambu/Polymaker/SUNLU library profiles
in, Qidi cross-bundles out.
- New endpoints: `POST /kx/filament/profiles/user` (ZIP/JSON upload),
`GET /kx/filament/profiles/user` (list user imports),
`DELETE /kx/filament/profiles/user[?vendor=…&name=…]`. Persisted in
`<KX_DATA_DIR>/orca_filaments.user.json` (volume — survives image
updates).
### Build
- New module `bridge/orca_filaments.py` (shared parser helpers used by
the generator and the import endpoint).
- Dockerfile + release.sh updated to include `orca_filaments.py`.
## [0.9.18] 2026-05-31
### New
- **🎉 Push filament material and colour from the bridge to the
printer:** The values you pick in the slot-edit dialog now actually
reach the printer and stick — the printer display shows the same
slot setup as the bridge UI right away. In 0.9.17 the command was
sent over the wrong MQTT topic (`slicer/printer/…`) and the printer
silently dropped it. It now goes via `web/printer/…` like the
Anycubic Slicer Next does (verified by live MQTT sniff +
Workbench-Vue source). **Note:** the printer must be idle, and
empty slots can not be labelled.
- **Spanish translation reviewed by a native speaker (PR #40 by
@pezfisk):** missing accents (impresión, cámara, después,
animación, …) and term consistency (Pause → Pausa, Start →
Iniciar, Layer → Capa). New `README.es.md` and cross-links between
the three READMEs.
- **Z-height now shows up in Obico.** The printer does not report a
real Z position over MQTT (live-sniff confirmed), so the bridge
estimates it from `current_layer × layer_height + first_layer_height`.
Layer heights are parsed from the gcode header at upload time and
persisted in the gcode store; fallback for prints started directly
from the slicer is the OrcaSlicer default filename pattern
(`…_0.2_…gcode`). `/server/files/metadata` also serves
`object_height` (total Z) so Obicos `mmProgress` widget can render
`current Z / total Z`.
- **Slot card shows the OrcaSlicer profile vendor** under the
material (e.g. `PLA / Polymaker`), with the profile name + internal
ID as tooltip. Lets you see at a glance which slot override is
active.
### Fixes
- **Slot profile picker in the AMS dialog (issue #39 by
@harrygeier):** three separate bugs in 0.9.17 caused the chosen
brand to disappear after save and a different material to show up
on re-open.
- `multiColorBox/setInfo` was sent on the wrong MQTT topic — see
above.
- Save fired two parallel requests (profile override + material/
colour) → race. Now sequential, and the local state is reloaded
before the dialog closes.
- OrcaSlicer filament IDs are not unique — `orca_filaments.json`
has 68 duplicate IDs, `OGFL99` alone is shared by 136 vendor
profiles (caught by @gangoke). The primary selector is now
`(vendor, name)` — unique across all 1002 profiles.
- **MQTT reconnect (issue #33 by @icebear):** if the printer was
powered off overnight the bridge gave up after 5 reconnect attempts
(~60 s total) — filament sync still worked in the morning (its
HTTP), but starting a print failed with `connection refused` and
the user had to restart the bridge itself. The reader thread now
reconnects **forever** (backoff caps at 60 s) until the printer
responds again, with logs dropping to DEBUG after the first 5
attempts so an overnight outage does not spam the log.
- **`Unknown child pid` warnings in the log:** the camera ffmpeg
helpers were killed without awaiting their `wait()` — children
lingered as zombies and asyncio reported them every ~20 s. Fixed
in CameraCache + `/api/camera/stream`.
### UI polish
- **Pause button is now a toggle:** while printing → `⏸ Pause`,
while paused → `▶ Resume`. The separate resume button is gone.
- **Pause + stop hidden when the printer is idle** — both used to be
visible at all times, which was confusing on a standby printer.
### Build
- **gcode store migration:** new columns `layer_height` +
`first_layer_height` on `gcode_files` (added automatically on first
start of 0.9.18).
## [0.9.17] 2026-05-30 ## [0.9.17] 2026-05-30
### New ### New

View File

@@ -2,6 +2,8 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
COPY requirements.txt . COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
@@ -13,6 +15,7 @@ COPY data/ ./static/
COPY config_loader.py . COPY config_loader.py .
COPY env_loader.py . COPY env_loader.py .
COPY kobrax_client.py . COPY kobrax_client.py .
COPY orca_filaments.py .
COPY VERSION . COPY VERSION .
COPY anycubic_slicer.crt . COPY anycubic_slicer.crt .
COPY anycubic_slicer.key . COPY anycubic_slicer.key .

View File

@@ -8,7 +8,12 @@
Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht. Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
<sub>🇬🇧 <a href="README.md">English version</a></sub> <sub>🧪 Ein Community-Bericht auf Reddit deutet darauf hin, dass die Bridge auch
mit dem **Kobra S1** und **Kobra S1 Max** funktioniert — die Protokolle wirken
kompatibel, beides ist aber weder offiziell getestet noch unterstützt.
Feedback willkommen.</sub>
<sub>🇬🇧 <a href="README.md">English version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
<br> <br>
@@ -32,12 +37,17 @@ Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|---|---| |---|---|
| 🖨️ | **Druckersteuerung** — Start, Pause, Resume, Abbruch, Temperaturen, Druckgeschwindigkeit | | 🖨️ | **Druckersteuerung** — Start, Pause, Resume, Abbruch, Temperaturen, Druckgeschwindigkeit |
| 📊 | **Live-Status** — Temperatur, Fortschritt, Layer, Restzeit, Kamera-Stream | | 📊 | **Live-Status** — Temperatur, Fortschritt, Layer, Restzeit, Kamera-Stream |
| 🎨 | **AMS / Multicolor**Filament-Slots, Per-Kanal-Remapping, MMU-Emulation für OrcaSlicer Filament-Sync | | 🎨 | **AMS / Multicolor**Slots mit **Profil-Picker pro Slot** (eigene Marke aus OrcaSlicer-Profilen pro Slot zuweisen); Bridge schreibt Material und Farbe ans Drucker-Display zurück |
| 📦 | **Eigene OrcaSlicer-Profile importieren** — ZIP aus `~/.config/OrcaSlicer/user/<id>/filament/` in die Bridge ziehen; tauchen im Slot-Dropdown unter ★ Eigene Profile auf |
| 📷 | **Obico-Integration (experimentell)** — Time-Lapse und WebRTC-Livestream gegen einen selbst gehosteten [Obico-Server](https://github.com/TheSpaghettiDetective/obico-server) via moonraker-obico |
| 📐 | **H.264-Direkt-Stream + Z-Höhe** — sparsamer Kamera-Pfad für Obico, aktuelle Z aus der Layer-Höhe abgeleitet (Mm-Progress-Widget) |
| 🗂️ | **GCode-Browser** — hochgeladene Dateien mit Thumbnail, Druckhistorie, Suche & Filter | | 🗂️ | **GCode-Browser** — hochgeladene Dateien mit Thumbnail, Druckhistorie, Suche & Filter |
| 🧩 | **Multi-Printer** — mehrere Drucker in **einer** Bridge-Instanz, Umschalten per Dropdown | | 🧩 | **Multi-Printer** — mehrere Drucker in **einer** Bridge-Instanz, Umschalten per Dropdown |
| | **Drucker hinzufügen per Klick** — nur die IP eingeben, Zugangsdaten werden automatisch importiert | | | **Drucker hinzufügen per Klick** — nur die IP eingeben, Zugangsdaten werden automatisch importiert |
| 🔁 | **Robuster MQTT-Reconnect** — Bridge überlebt nächtlichen Drucker-Reboot ohne manuellen Neustart |
| 🌐 | **Mehrsprachiges UI** — DE / EN / ES / 中文, Browser-Sprache automatisch erkannt |
| 🔄 | **Self-Update** — neue Versionen direkt im Browser installieren | | 🔄 | **Self-Update** — neue Versionen direkt im Browser installieren |
| 🌐 | **OrcaSlicer** — volles Moonraker-Protokoll (HTTP + WebSocket), DE/EN UI | | 🧠 | **OrcaSlicer** — volles Moonraker-Protokoll (HTTP + WebSocket); für korrekten Vendor-Match pro Slot den [OrcaSlicer-KX-Build](#-empfohlener-slicer) nutzen |
--- ---
@@ -103,18 +113,28 @@ Drucker → Verbindungstyp **Moonraker** → Host: `http://BRIDGE-IP:7125`
## 🎨 Empfohlener Slicer ## 🎨 Empfohlener Slicer
Für die beste Erfahrung mit der KX-Bridge bieten wir einen **gepatchten Für sauberen AMS-Filament-Sync gibt es einen **gepatchten OrcaSlicer-Build**:
OrcaSlicer-Build**, der drei offene SoftFever/OrcaSlicer-PRs bündelt: das
Anycubic-Kobra-X-Druckerprofil, einen Multicolor-G-Code-Fix und — am wichtigsten
— einen Moonraker/Happy-Hare-Filament-Sync-Fix, der die AMS-Slot-Positionen auch
bei einem leeren Slot in der Mitte korrekt beibehält.
**[OrcaSlicer-KX Releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP) **[OrcaSlicer-KX Releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
Standard-OrcaSlicer funktioniert auch; der gepatchte Build verbessert **Upstream-PRs im KX-Build:**
hauptsächlich das AMS-Handling. Es ist ein Build von
[OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); der Quellcode - **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Moonraker / Happy-Hare AMS-Sync-Fix (Slot-Positionen bleiben auch bei leerem Slot in der Mitte korrekt)
ist über die verlinkten PRs verfügbar. - **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Vendor- + Name-Matching für Moonraker (liest `name` + `vendor_name` pro Slot und matched gegen deine Filament-Presets), von [@LordGuenni](https://github.com/LordGuenni)
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — Eindeutige `filament_id` für User-Presets (neu erstellte eigene Profile bekommen eine frische ID statt das `OGFL99` vom Generic-Parent zu erben), von [@mrnoisytiger](https://github.com/mrnoisytiger)
**Plus vier KX-eigene Verbesserungen on top:**
- Bridge-Filament-Hint (`tray_info_idx` + Vendor) respektieren
- Vendor-Match auch wenn das gewählte Base-Preset **nicht is_compatible** mit dem aktiven Drucker ist (so matchen Profile aus anderen Drucker-Setups trotzdem über die Marke)
- 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.
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).
OrcaSlicer-KX ist ein Build von [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); der Quellcode der Upstream-PRs ist auf GitHub, die KX-spezifischen Patches im OrcaSlicer-KX-Repo.
--- ---
@@ -123,6 +143,13 @@ ist über die verlinkten PRs verfügbar.
- **[Home-Assistant-Integration](https://github.com/gangoke/kobrax-lan-hass-component)** - **[Home-Assistant-Integration](https://github.com/gangoke/kobrax-lan-hass-component)**
von [@gangoke](https://github.com/gangoke) — bindet Sensoren, Drucksteuerung, von [@gangoke](https://github.com/gangoke) — bindet Sensoren, Drucksteuerung,
Licht, Kamera und das GCode-Vorschaubild als native Home-Assistant-Entitäten ein. Licht, Kamera und das GCode-Vorschaubild als native Home-Assistant-Entitäten ein.
- **[Obico (selbst gehostet)](https://github.com/TheSpaghettiDetective/obico-server)** —
die Bridge bietet eine Moonraker-kompatible API, die
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
akzeptiert; damit hast du Time-Lapse und WebRTC-Live-Stream gegen deinen
eigenen Obico-Server. Die KI-Spaghetti-Erkennung ist beim Kobra X
experimentell — der Top-Down-Kamerawinkel weicht von dem ab, auf den
das Modell trainiert wurde.
> Dies sind **Community-Projekte**, die nicht von KX-Bridge betreut oder > Dies sind **Community-Projekte**, die nicht von KX-Bridge betreut oder
> supportet werden. Bei Fragen oder Problemen bitte das verlinkte Repository nutzen. > supportet werden. Bei Fragen oder Problemen bitte das verlinkte Repository nutzen.

277
README.es.md Normal file
View File

@@ -0,0 +1,277 @@
<div align="center">
<img src="knlogo.png" alt="KX-Bridge" width="160"/>
# KX-Bridge
**Controla tu Anycubic Kobra X con OrcaSlicer — sin Klipper, sin Raspberry Pi.**
Un puente compatible con Moonraker que se comunica directamente con la impresora.
<sub>🧪 Un usuario en Reddit ha reportado que el puente también funciona con la
**Kobra S1** y la **Kobra S1 Max** — los protocolos parecen compatibles, pero
ninguna está oficialmente probada ni soportada. Se agradece el feedback.</sub>
<sub>🇬🇧 <a href="README.md">English version</a> · 🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
<br>
[![Ko-fi](https://img.shields.io/badge/Ko--fi-Apoya%20este%20proyecto-FF5E5B?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/viewitde)
&nbsp;
[![Releases](https://img.shields.io/badge/Descargar-Lanzamientos-2EA043?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Downloads](https://img.shields.io/badge/Descargas-800%2B-8957E5?style=for-the-badge&logo=gitea&logoColor=white)](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
&nbsp;
[![Video](https://img.shields.io/badge/YouTube-Tutorial-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=1Ql4wfH27fM)
<sub>¿Te gusta KX-Bridge? Un café en <a href="https://ko-fi.com/viewitde">Ko-fi</a> mantiene el proyecto vivo. ☕</sub>
</div>
---
## ✨ Características
| | |
|---|---|
| 🖨️ | **Control de impresora** — iniciar, pausar, reanudar, cancelar, temperaturas, velocidad de impresión |
| 📊 | **Estado en tiempo real** — temperatura, progreso, capas, tiempo restante, transmisión de cámara |
| 🎨 | **AMS / multicolor** — ranuras con **selector de perfil por ranura** (asigna tu propia marca de los perfiles de OrcaSlicer a cada ranura); el puente escribe material y color al display de la impresora |
| 📦 | **Importa tus propios perfiles de OrcaSlicer** — arrastra un ZIP de `~/.config/OrcaSlicer/user/<id>/filament/` al puente; aparecen en el desplegable de la ranura bajo ★ Perfiles propios |
| 📷 | **Integración con Obico (experimental)** — Time-Lapse y stream en vivo WebRTC contra un [servidor Obico](https://github.com/TheSpaghettiDetective/obico-server) autoalojado vía moonraker-obico |
| 📐 | **Stream H.264 directo + altura Z** — ruta de cámara de bajo consumo de CPU para Obico, Z actual derivada de la altura de capa (widget de progreso) |
| 🗂️ | **Explorador de GCode** — archivos subidos con vistas previas, historial de impresión, búsqueda y filtros |
| 🧩 | **Multi-impresora** — múltiples impresoras en **una** instancia del puente, cambia mediante un menú desplegable |
| | **Añade una impresora con un clic** — solo introduce la IP, las credenciales se importan automáticamente |
| 🔁 | **Reconexión MQTT robusta** — el puente sobrevive a reinicios nocturnos de la impresora sin reinicio manual |
| 🌐 | **Interfaz multilingüe** — DE / EN / ES / 中文, detecta automáticamente el idioma del navegador |
| 🔄 | **Actualización automática** — instala nuevas versiones directamente desde el navegador |
| 🧠 | **OrcaSlicer** — protocolo Moonraker completo (HTTP + WebSocket); usa el [build OrcaSlicer-KX](#-slicer-recomendado) para emparejamiento correcto de vendor por ranura |
---
## 🚀 Inicio rápido
### 1. Prepara la impresora
Activa el modo LAN en la Kobra X:
**Pantalla de la impresora → Ajustes → Activar modo LAN**
### 2. Inicia el puente
**Docker (recomendado):**
```bash
docker compose up -d
```
**Binario Linux (sin Docker):**
```bash
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
```
**EXE Windows (sin Docker):**
```
kx-bridge.exe
```
> ⚠️ **Certificados TLS necesarios para el binario standalone**
>
> El bridge habla con el MQTT de la impresora vía mTLS y necesita dos
> ficheros de certificado **junto al binario**:
>
> - `anycubic_slicer.crt`
> - `anycubic_slicer.key`
>
> Ambos vienen en **`anycubic-certs.zip`** en la misma página de release.
> Descárgalo y extrae los dos ficheros en el mismo directorio que
> `kx-bridge-linux-amd64` o `kx-bridge.exe`. Sin ellos verás
> `Verbindung fehlgeschlagen: TLS-Zertifikate fehlen …` (0.9.19.1+) o
> `[Errno 2] No such file or directory` (versiones anteriores).
>
> Estructura correcta:
> ```
> ~/kx-bridge/
> ├── kx-bridge-linux-amd64 (o kx-bridge.exe)
> ├── anycubic_slicer.crt ← de anycubic-certs.zip
> ├── anycubic_slicer.key ← de anycubic-certs.zip
> └── config/ (se crea en el primer arranque)
> ```
>
> Los usuarios de Docker no necesitan hacer esto — los certificados
> están incluidos en la imagen.
> Con los binarios de Linux y Windows, `config/` y `data/` (configuración,
> SQLite, almacén de GCode) viven junto al programa. Copia toda la carpeta
> para mover la instalación.
**Python directamente:**
```bash
pip install -r bridge/requirements.txt
python bridge/kobrax_moonraker_bridge.py
```
### 3. Configura la impresora
Abre la interfaz web: **`http://IP-DEL-PUENTE:7125`**
En el primer inicio, la pestaña **Impresoras** muestra *"+ Añadir impresora"* — solo introduce la dirección IP
de la impresora, el resto (usuario, contraseña, ID de dispositivo) se obtiene de la impresora y se desencripta
automáticamente. Listo.
> ¿Más de una impresora? Simplemente haz clic en *"+ Añadir impresora"* de nuevo — cada una recibe su propio puerto
> (7125, 7126, …) y se puede seleccionar desde el menú desplegable del encabezado.
### 4. Conecta OrcaSlicer
Impresora → Tipo de conexión **Moonraker** → Host: `http://IP-DEL-PUENTE:7125`
> ⚠️ El tipo de conexión debe ser **Moonraker** (no "Bambu" ni "Klipper").
> Introduce la URL completa incluyendo `http://` y el puerto `:7125` en el campo de host.
---
## 📺 Vídeo tutorial
[![Configuración y uso de KX-Bridge](https://img.youtube.com/vi/1Ql4wfH27fM/hqdefault.jpg)](https://www.youtube.com/watch?v=1Ql4wfH27fM)
---
## 🎨 Slicer recomendado
Para una sincronización de filamento AMS correcta ofrecemos una **versión modificada de OrcaSlicer**:
**[Lanzamientos de OrcaSlicer-KX](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
**PRs upstream incluidos en el build KX:**
- **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Corrección de sincronización Moonraker / Happy-Hare AMS (las posiciones de las ranuras se mantienen correctas incluso con ranuras vacías)
- **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Coincidencia de Vendor + Nombre para Moonraker (lee `name` + `vendor_name` por ranura y los empareja con los presets de filamento del usuario), por [@LordGuenni](https://github.com/LordGuenni)
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — `filament_id` único para presets de usuario (los perfiles nuevos reciben un ID nuevo en vez de heredar `OGFL99` del padre genérico), por [@mrnoisytiger](https://github.com/mrnoisytiger)
**Más cuatro mejoras específicas de KX encima:**
- Respetar el hint de filamento del puente (`tray_info_idx` + vendor)
- Coincidencia por vendor incluso cuando el preset base no es **is_compatible** con la impresora activa (así un perfil copiado de otra máquina sigue coincidiendo por vendor)
- Coincidencia por vendor cuando `tray_info_idx` está definido pero su preset es incompatible
- Búsqueda de dos pasadas: primero presets compatibles, luego todos los visibles
**Por qué importa:** sin #13719 todas las ranuras AMS caen en `Generic PLA` / `Generic PETG` aunque el puente ya envíe la marca concreta (`name + vendor_name + gate_filament_name`). Con el build KX, OrcaSlicer coincide con tus presets de usuario reales — incluyendo los perfiles que importaste al puente vía [Importa tus propios perfiles de OrcaSlicer](#-características).
OrcaSlicer upstream también funciona para rebanar e imprimir — solo pierdes la coincidencia de vendor por ranura en la sincronización AMS. El material y color por ranura se pueden empujar puente → impresora con cualquier slicer (eso va por MQTT, no por el slicer).
OrcaSlicer-KX es un build de [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); el código fuente de cada PR upstream está en GitHub, los parches específicos de KX en el repo OrcaSlicer-KX.
---
## 🏠 Comunidad e integraciones
- **[Integración con Home Assistant](https://github.com/gangoke/kobrax-lan-hass-component)**
por [@gangoke](https://github.com/gangoke) — expone sensores, controles de impresión,
luz, cámara y la vista previa del GCode como entidades nativas de Home Assistant.
- **[Obico (autoalojado)](https://github.com/TheSpaghettiDetective/obico-server)** —
el puente expone una API compatible con Moonraker que
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
acepta, así obtienes Time-Lapse y streaming en vivo WebRTC contra tu propio
servidor Obico. La detección de fallos por IA es experimental en la Kobra X
(el ángulo cenital de la cámara difiere del que el modelo fue entrenado).
> Estos son **proyectos de la comunidad**, no mantenidos ni soportados por KX-Bridge.
> Para preguntas o problemas, utiliza el repositorio enlazado.
---
## 🔧 Obtener credenciales manualmente
Normalmente no es necesario — *"+ Añadir impresora"* lo hace automáticamente. Si lo necesitas:
```bash
fetch_credentials --ip 192.168.x.x --write-config
```
Obtiene las credenciales directamente de la impresora vía HTTP y las escribe en `config/config.ini`.
Solo se requiere la IP de la impresora, no hace falta un slicer.
Alternativamente (si se desconoce la IP): abre AnycubicSlicerNext, conecta la impresora, luego ejecuta
`extract_credentials` → muestra usuario, contraseña, ID de dispositivo y la IP de la impresora.
> **Descargas:** [Lanzamientos](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases) → `fetch_credentials` / `extract_credentials` (Linux y Windows)
---
## ⚙️ Comandos útiles
```bash
docker compose logs -f # mostrar registros
docker compose down # detener el puente
docker compose pull && docker compose up -d # actualizar a la imagen publicada más reciente
docker compose up -d --build # recompilar localmente (en lugar de descargar)
```
---
## 🩹 Solución de problemas
<details>
<summary><b>"Credenciales MQTT incorrectas" al iniciar</b></summary>
- Vuelve a añadir la impresora mediante *"+ Añadir impresora"*, o ejecuta
`fetch_credentials --ip <ip> --write-config` y reinicia el puente
- Introduce solo la dirección IP, sin puerto (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`)
</details>
<details>
<summary><b>Impresora no encontrada / modo LAN no activado</b></summary>
- En la pantalla de la impresora: Ajustes → Activar modo LAN
- La impresora y el puente deben estar en la misma red
</details>
<details>
<summary><b>Docker: Permiso denegado</b></summary>
```bash
sudo usermod -aG docker $USER # luego cierra la sesión y vuelve a iniciarla
```
</details>
<details>
<summary><b>Actualizar desde 0.9.1 o anterior</b></summary>
A partir de 0.9.2, KX-Bridge almacena la configuración en `config/config.ini` en lugar de `.env`.
La migración se ejecuta automáticamente en el primer inicio después de la actualización — no requiere acción.
</details>
---
## 🔒 Seguridad
- El puente es accesible en la red local en `http://<IP-del-host>:7125`**no** lo expongas a internet
- `config/config.ini` contiene las credenciales de la impresora — no las compartas públicamente
- Las credenciales **no** otorgan acceso a los servicios en la nube de Anycubic
---
## 📄 Licencia
[![License: GPL v3](https://img.shields.io/badge/License-GPL_v3-blue.svg)](LICENSE)
KX-Bridge se publica bajo la **GNU General Public License v3.0**. Consulta
[LICENSE](LICENSE) para el texto completo. Las bifurcaciones y modificaciones deben
permanecer bajo GPLv3 si se redistribuyen.
La implementación del protocolo MQTT es el resultado de una ingeniería inversa
independiente con fines de interoperabilidad (§69e UrhG / Directiva de Software de la UE
Art. 6). El material de terceros en el repositorio (certificados TLS de Anycubic)
**no** está cubierto por GPLv3 y se incluye únicamente para permitir la
autenticación contra impresoras que el usuario final ya posee. Consulta
[NOTICE.md](NOTICE.md) para más detalles y el aviso legal.
Este proyecto es independiente y no está afiliado con Anycubic.
<div align="center">
<br>
**Si KX-Bridge te ayuda, el proyecto agradece tu apoyo:**
[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/viewitde)
</div>

View File

@@ -8,7 +8,11 @@
A Moonraker-compatible bridge that talks directly to the printer. A Moonraker-compatible bridge that talks directly to the printer.
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a></sub> <sub>🧪 A community report on Reddit suggests the bridge also works with the
**Kobra S1** and **Kobra S1 Max** — protocols look compatible, but neither is
officially tested or supported. Feedback welcome.</sub>
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
<br> <br>
@@ -32,12 +36,17 @@ A Moonraker-compatible bridge that talks directly to the printer.
|---|---| |---|---|
| 🖨️ | **Printer control** — start, pause, resume, cancel, temperatures, print speed | | 🖨️ | **Printer control** — start, pause, resume, cancel, temperatures, print speed |
| 📊 | **Live status** — temperature, progress, layers, remaining time, camera stream | | 📊 | **Live status** — temperature, progress, layers, remaining time, camera stream |
| 🎨 | **AMS / multicolor**filament slots, per-channel remapping, MMU emulation for OrcaSlicer filament sync | | 🎨 | **AMS / multicolor**slots with per-slot **profile picker** (assign your own brand from OrcaSlicer profiles per slot); bridge writes material & colour back to the printer display |
| 📦 | **Import your own OrcaSlicer profiles** — drag a ZIP from `~/.config/OrcaSlicer/user/<id>/filament/` into the bridge; they show up in the slot dropdown under ★ Own profiles |
| 📷 | **Obico integration (experimental)** — Time-Lapse and WebRTC live stream against a self-hosted [Obico server](https://github.com/TheSpaghettiDetective/obico-server) via moonraker-obico |
| 📐 | **Direct H.264 stream + Z-height** — low-CPU camera path for Obico, current Z derived from layer-height for the print-progress widget |
| 🗂️ | **GCode browser** — uploaded files with thumbnails, print history, search & filter | | 🗂️ | **GCode browser** — uploaded files with thumbnails, print history, search & filter |
| 🧩 | **Multi-printer** — multiple printers in **one** bridge instance, switch via dropdown | | 🧩 | **Multi-printer** — multiple printers in **one** bridge instance, switch via dropdown |
| | **Add a printer with one click** — just enter the IP, credentials are imported automatically | | | **Add a printer with one click** — just enter the IP, credentials are imported automatically |
| 🔁 | **Robust MQTT reconnect** — bridge survives overnight printer reboots without manual restart |
| 🌐 | **Multi-language UI** — DE / EN / ES / 中文, auto-detect browser locale |
| 🔄 | **Self-update** — install new versions directly in the browser | | 🔄 | **Self-update** — install new versions directly in the browser |
| 🌐 | **OrcaSlicer** — full Moonraker protocol (HTTP + WebSocket), EN/DE UI | | 🧠 | **OrcaSlicer** — full Moonraker protocol (HTTP + WebSocket); pair with the [OrcaSlicer-KX build](#-recommended-slicer) for proper per-slot vendor matching |
--- ---
@@ -103,16 +112,28 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125`
## 🎨 Recommended Slicer ## 🎨 Recommended Slicer
For the best KX-Bridge experience we offer a **patched OrcaSlicer build** that For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
bundles three open SoftFever/OrcaSlicer PRs: the Anycubic Kobra X printer
profile, a multicolor G-code fix and — most importantly — a Moonraker/Happy-Hare
filament-sync fix that keeps AMS slot positions intact even with an empty slot.
**[OrcaSlicer-KX releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP) **[OrcaSlicer-KX releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
Standard OrcaSlicer also works; the patched build mainly improves AMS handling. **Upstream PRs bundled in the KX build:**
It's a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0);
source is available via the linked PRs. - **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Moonraker / Happy-Hare AMS sync fix (slot positions stay correct even with empty slots)
- **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Vendor + Name matching for Moonraker (reads `name` + `vendor_name` per slot and matches against the user's filament presets), by [@LordGuenni](https://github.com/LordGuenni)
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — Unique `filament_id` for user presets (so newly created custom profiles get a fresh ID instead of inheriting `OGFL99` from the generic parent), by [@mrnoisytiger](https://github.com/mrnoisytiger)
**Plus four KX-specific matching improvements on top:**
- Respect the bridge filament hint (`tray_info_idx` + vendor)
- Vendor match also when the chosen base preset is **not is_compatible** with the active printer (so a profile copied from a different machine still matches by vendor)
- 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.
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).
OrcaSlicer-KX is a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); source for each upstream PR is on GitHub, KX-specific patches live in the OrcaSlicer-KX repo.
--- ---
@@ -121,6 +142,12 @@ source is available via the linked PRs.
- **[Home Assistant integration](https://github.com/gangoke/kobrax-lan-hass-component)** - **[Home Assistant integration](https://github.com/gangoke/kobrax-lan-hass-component)**
by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls, by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls,
light, camera and the GCode thumbnail as native Home Assistant entities. light, camera and the GCode thumbnail as native Home Assistant entities.
- **[Obico (self-hosted)](https://github.com/TheSpaghettiDetective/obico-server)** —
the bridge exposes a Moonraker-compatible surface that
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
accepts, so you get Time-Lapse and WebRTC live streaming against your own
Obico server. The AI failure-detection side is experimental on the Kobra X
(top-down camera angle differs from what the model was trained on).
> These are **community projects**, not maintained or supported by KX-Bridge. > These are **community projects**, not maintained or supported by KX-Bridge.
> For questions or issues, please use the linked repository. > For questions or issues, please use the linked repository.

View File

@@ -1 +1 @@
0.9.17 0.9.20

View File

@@ -169,16 +169,23 @@ def list_printers() -> list[dict]:
def list_filament_profiles() -> dict[int, dict]: def list_filament_profiles() -> dict[int, dict]:
"""Liest die [filament_profiles]-Sektion aus config.ini. """Liest die [filament_profiles]-Sektion aus config.ini.
Format pro AMS-Slot (slot_N_id + slot_N_vendor): Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
[filament_profiles] aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
slot_0_id = OGFL01 (als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
slot_0_vendor = Polymaker derselben filament_id wie 'OGFL99', d.h. die ID ist nicht eindeutig):
slot_1_id = OGFG23
slot_1_vendor = Polymaker
Gibt einen Dict {slot_index: {"id": ..., "vendor": ...}} zurück. [filament_profiles]
Leere/fehlende Slots werden NICHT aufgenommen — das Default-Mapping slot_0_vendor = Polymaker
slot_0_name = PolyTerra PLA
slot_0_id = OGFL01
Gibt einen Dict {slot_index: {"id": ..., "vendor": ..., "name": ...}}
zurück. Leere/fehlende Slots werden NICHT aufgenommen — das Default-Mapping
(per filament_type) in der Bridge bleibt dann aktiv. (per filament_type) in der Bridge bleibt dann aktiv.
Backwards-Kompat: alte Configs mit nur (vendor, id) bleiben lesbar; `name`
fehlt dann und der Aufrufer kann optional aus der orca_filaments.json
rekonstruieren.
""" """
path = _find_config_file() path = _find_config_file()
if not path: if not path:
@@ -189,7 +196,7 @@ def list_filament_profiles() -> dict[int, dict]:
return {} return {}
result: dict[int, dict] = {} result: dict[int, dict] = {}
for key, value in cfg.items("filament_profiles"): for key, value in cfg.items("filament_profiles"):
# Erwartet: slot_<idx>_id oder slot_<idx>_vendor # Erwartet: slot_<idx>_id oder slot_<idx>_vendor oder slot_<idx>_name
if not key.startswith("slot_"): if not key.startswith("slot_"):
continue continue
parts = key.split("_", 2) parts = key.split("_", 2)
@@ -200,13 +207,11 @@ def list_filament_profiles() -> dict[int, dict]:
except ValueError: except ValueError:
continue continue
field = parts[2] field = parts[2]
if field not in ("id", "vendor"): if field not in ("id", "vendor", "name"):
continue continue
if not value.strip(): if not value.strip():
continue continue
result.setdefault(slot_idx, {})[field] = value.strip() result.setdefault(slot_idx, {})[field] = value.strip()
# Leere Einträge (nur vendor ohne id oder umgekehrt) trotzdem behalten —
# der Aufrufer prüft selbst was er nutzt.
return result return result
@@ -214,24 +219,26 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
"""Schreibt die übergebenen Slot-Profile in die [filament_profiles]- """Schreibt die übergebenen Slot-Profile in die [filament_profiles]-
Sektion der config.ini. Existierende Einträge werden komplett ersetzt. Sektion der config.ini. Existierende Einträge werden komplett ersetzt.
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker"}} profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
""" """
path = _find_config_file() path = _find_config_file()
if not path: if not path:
return False return False
cfg = configparser.ConfigParser() cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8") cfg.read(path, encoding="utf-8")
# Sektion neu aufbauen — entfernt damit auch alte/verwaiste Slots
if cfg.has_section("filament_profiles"): if cfg.has_section("filament_profiles"):
cfg.remove_section("filament_profiles") cfg.remove_section("filament_profiles")
if profiles: if profiles:
cfg["filament_profiles"] = {} cfg["filament_profiles"] = {}
for slot_idx in sorted(profiles.keys()): for slot_idx in sorted(profiles.keys()):
entry = profiles[slot_idx] or {} entry = profiles[slot_idx] or {}
if entry.get("id"):
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
if entry.get("vendor"): if entry.get("vendor"):
cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"] cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"]
if entry.get("name"):
cfg["filament_profiles"][f"slot_{slot_idx}_name"] = entry["name"]
if entry.get("id"):
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
with open(path, "w", encoding="utf-8") as f: with open(path, "w", encoding="utf-8") as f:
cfg.write(f) cfg.write(f)
return True return True

File diff suppressed because it is too large Load Diff

View File

@@ -154,6 +154,13 @@ class KobraXClient:
# -- Connection ---------------------------------------------------------- # -- Connection ----------------------------------------------------------
def _do_connect(self): def _do_connect(self):
if not os.path.exists(CERT_FILE) or not os.path.exists(KEY_FILE):
raise FileNotFoundError(
f"TLS-Zertifikate fehlen: anycubic_slicer.crt + anycubic_slicer.key "
f"müssen neben der kx-bridge Binary liegen ({_SCRIPT_DIR}/). "
f"Lade anycubic-certs.zip vom Gitea-Release herunter und entpacke "
f"die Dateien dorthin."
)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE ctx.verify_mode = ssl.CERT_NONE
@@ -179,10 +186,24 @@ class KobraXClient:
def connect(self): def connect(self):
self._do_connect() self._do_connect()
self._running = True self._running = True
t = threading.Thread(target=self._read_loop, daemon=True) self._ensure_reader()
t.start()
time.sleep(0.3) time.sleep(0.3)
def _ensure_reader(self):
"""Stellt sicher dass der Reader-Thread lebt. Wenn der Reader nach einer
früheren disconnect/reconnect-Sequenz oder einem unbehandelten Fehler
gestorben ist, würden empfangene Replies sonst nie ankommen — publish()
würde dann zwar senden, aber auf Antworten ewig warten."""
if not self._running:
return # gewollter disconnect
t = getattr(self, "_reader_thread", None)
if t is not None and t.is_alive():
return
self._reader_thread = threading.Thread(
target=self._read_loop, daemon=True, name="kobrax-mqtt-reader",
)
self._reader_thread.start()
def disconnect(self): def disconnect(self):
self._running = False self._running = False
try: try:
@@ -191,20 +212,34 @@ class KobraXClient:
pass pass
def _reconnect(self): def _reconnect(self):
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
antwortet oder disconnect() gerufen wurde. Backoff cappt bei 60 s. Die
ersten 5 Versuche loggen als WARNING (akute Verbindungsstörung), danach
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
ausgeschaltet) zu vermeiden."""
log.warning("Verbindung verloren reconnect…") log.warning("Verbindung verloren reconnect…")
try: try:
self._sock.close() self._sock.close()
except Exception: except Exception:
pass pass
for delay in [2, 4, 8, 15, 30]: delays = [2, 4, 8, 15, 30, 60]
attempt = 0
while self._running:
delay = delays[min(attempt, len(delays) - 1)]
try: try:
self._do_connect() self._do_connect()
log.info("Reconnect erfolgreich") log.info("Reconnect erfolgreich (nach %d Versuchen)", attempt + 1)
return True return True
except Exception as e: except Exception as e:
log.warning("Reconnect fehlgeschlagen (%s), warte %ss…", e, delay) attempt += 1
time.sleep(delay) lvl = log.warning if attempt <= 5 else log.debug
return False lvl("Reconnect fehlgeschlagen (%s, Versuch %d), warte %ss…", e, attempt, delay)
# Geteiltes Sleep damit disconnect() den Loop schneller bricht.
slept = 0.0
while slept < delay and self._running:
time.sleep(min(0.5, delay - slept))
slept += 0.5
return False # nur wenn disconnect() gerufen wurde
def _subscribe(self, topic: str): def _subscribe(self, topic: str):
with self._lock: with self._lock:
@@ -348,6 +383,9 @@ class KobraXClient:
# -- Publish + request/response ------------------------------------------ # -- Publish + request/response ------------------------------------------
def publish(self, msg_type: str, action: str, data=None, timeout: float = 5.0) -> dict | None: def publish(self, msg_type: str, action: str, data=None, timeout: float = 5.0) -> dict | None:
# Falls Reader-Thread aus historischen Gründen tot ist, wiederbeleben —
# sonst würden Replies nie ankommen und event.wait() läuft ins Timeout.
self._ensure_reader()
msgid = str(uuid.uuid4()) msgid = str(uuid.uuid4())
payload = json.dumps({ payload = json.dumps({
"type": msg_type, "type": msg_type,
@@ -413,6 +451,7 @@ class KobraXClient:
def publish_web(self, msg_type: str, action: str, data=None) -> None: def publish_web(self, msg_type: str, action: str, data=None) -> None:
"""Fire-and-forget publish on the web/printer topic (used for runtime updates during print).""" """Fire-and-forget publish on the web/printer topic (used for runtime updates during print)."""
self._ensure_reader()
msgid = str(uuid.uuid4()) msgid = str(uuid.uuid4())
payload = json.dumps({ payload = json.dumps({
"type": msg_type, "type": msg_type,
@@ -429,7 +468,14 @@ class KobraXClient:
with self._lock: with self._lock:
self._sock.sendall(_build_publish(topic, payload)) self._sock.sendall(_build_publish(topic, payload))
except Exception as e: except Exception as e:
log.error("web send error: %s", e) log.error("web send error: %s, reconnecting…", e)
# Reconnect triggern (analog zu publish()); ohne Retry weil
# fire-and-forget — der nächste Aufruf wird auf den frischen Socket
# treffen.
try:
self._reconnect()
except Exception:
pass
# -- High-level commands ------------------------------------------------- # -- High-level commands -------------------------------------------------

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge). # ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
from PyInstaller.utils.hooks import collect_all from PyInstaller.utils.hooks import collect_all
datas = [("web", "web"), ("data", "static")] # bridge/data/ → static/ im _MEIPASS datas = [("web", "web"), ("data", "static"), ("VERSION", ".")] # bridge/data/ → static/ im _MEIPASS
binaries = [] binaries = []
hiddenimports = [] hiddenimports = []

147
orca_filaments.py Normal file
View File

@@ -0,0 +1,147 @@
"""OrcaSlicer Filament-Profil Parser.
Geteilt zwischen dem Generator (tools/gen_orca_filament_list.py) und dem
Custom-Profile-Import-Endpoint (bridge/kobrax_moonraker_bridge.py).
Liest Orca-Filament-JSON-Dateien (System- oder User-Profile) und gibt
sie als normalisierte Liste mit (id, name, vendor, type, color) zurück.
"""
from __future__ import annotations
import json
import re
def first_str(value, default: str = "") -> str:
"""Orca-Profile speichern manche Felder als ['wert']. Liefert erstes
Element als String."""
if isinstance(value, list):
return str(value[0]) if value else default
if isinstance(value, str):
return value
return default
def clean_name(raw: str) -> str:
"""Strippt printer-/varianten-spezifische Suffixe:
'PolyTerra PLA @base''PolyTerra PLA'
'Anycubic PLA @Anycubic Kobra X 0.4 nozzle''Anycubic PLA'
'Anker Generic PLA 0.4 nozzle''Anker Generic PLA'
"""
name = re.sub(r"\s*@.*$", "", raw).strip()
name = re.sub(r"\s+\d+(\.\d+)?\s*nozzle\s*$", "", name, flags=re.IGNORECASE).strip()
return name or raw
def parse_profile(data: dict, by_name: dict | None = None,
path_vendor: str | None = None,
source_path: str = "",
system_index: list | None = None) -> dict | None:
"""Parsed ein einzelnes Orca-Filament-Profil zum Bridge-Schema.
`by_name` ist optional ein {name: [profile, …]}-Index für Inherits-Resolve
aus dem rohen Source-Tree (Generator). Bei Single-File-Import (User-Datei
aus OrcaSlicer-User-Dir) reichen wir stattdessen `system_index` rein —
die fertige System-Profile-Liste aus orca_filaments.json. Damit können
wir filament_id/vendor/type/color über die `inherits`-Kette aus dem
System-Parent ableiten, auch wenn das User-Profil diese Felder nicht
selbst setzt (typisch: User-Override-Profile haben nur Tweaks).
Liefert {id, name, vendor, type, color} oder None wenn das Profil
keine filament_id hat (z.B. abstrakte @base-Templates).
"""
if not isinstance(data, dict):
return None
# User-Profile aus dem OrcaSlicer-User-Dir setzen oft KEIN "type"-Feld —
# das kommt vom System-Parent. Wir akzeptieren das wenn entweder "type"
# explizit "filament" ist ODER ein "inherits" auf ein anderes Profil zeigt.
if data.get("type") not in (None, "filament") and not data.get("inherits"):
return None
if data.get("type") == "filament" and data.get("inherits") is None and not data.get("filament_id"):
# type=filament aber kein parent + keine ID → wertloses Stub
return None
inst = data.get("instantiation", "true")
if isinstance(inst, str) and inst.lower() == "false":
return None
# Build system-name-Index für den fallback-Lookup wenn system_index gesetzt.
sys_by_name: dict[str, dict] = {}
if system_index:
for p in system_index:
if isinstance(p, dict) and p.get("name"):
sys_by_name[p["name"]] = p
def _resolve(key: str, depth: int = 5):
cur_list = [data]
for _ in range(depth):
for cur in cur_list:
v = cur.get(key)
if v not in ("", [], None, [""]) and v is not None:
return v
# Erst raw-Inherits via by_name (Generator-Pfad)
if by_name:
next_list: list[dict] = []
for cur in cur_list:
parent_name = cur.get("inherits")
if parent_name and parent_name in by_name:
next_list.extend(by_name[parent_name])
if next_list:
cur_list = next_list
continue
break
return None
def _resolve_via_system_index(key: str):
"""Inherits-Kette über system_index (clean_name-Match)."""
parent_raw = data.get("inherits")
if not parent_raw or not sys_by_name:
return None
parent_clean = clean_name(parent_raw)
sys_p = sys_by_name.get(parent_clean)
if not sys_p:
return None
# System-JSON benutzt schon das normalisierte Schema
mapping = {
"filament_id": "id",
"filament_vendor": "vendor",
"filament_type": "type",
"default_filament_colour": "color",
}
return sys_p.get(mapping.get(key, key))
def _resolve_full(key: str):
v = _resolve(key)
if v not in ("", [], None, [""]) and v is not None:
return v
return _resolve_via_system_index(key)
fid = _resolve_full("filament_id")
if not fid or not isinstance(fid, str):
return None
name_raw = data.get("name", fid)
name = clean_name(name_raw)
vendor = first_str(_resolve_full("filament_vendor")) or (path_vendor or "Generic")
ftype = first_str(_resolve_full("filament_type"), "")
color = first_str(_resolve_full("default_filament_colour"), "")
return {
"id": fid,
"name": name,
"vendor": vendor,
"type": ftype,
"color": color,
}
def parse_profile_bytes(blob: bytes, source_name: str = "",
system_index: list | None = None) -> dict | None:
"""Liest ein einzelnes Profil aus JSON-Bytes. Für File-Upload-Pfad.
`system_index` ist optional die fertige Liste aus orca_filaments.json —
wird für die Inherits-Resolve von User-Profilen genutzt die das volle
Schema vom System-Parent erben."""
try:
data = json.loads(blob.decode("utf-8", errors="replace"))
except Exception:
return None
return parse_profile(data, source_path=source_name, system_index=system_index)

View File

@@ -1,11 +1,12 @@
// ── State ── // ── State ──
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0, var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time: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, 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}; 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 tempHistory={n:[],b:[]};
var camOn=false; var camOn=false;
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
var currentStep=1; var currentStep=1;
var currentPanel='dashboard'; var currentPanel='dashboard';
var aceAutoRefillPrefs=(function(){ var aceAutoRefillPrefs=(function(){
@@ -101,6 +102,7 @@ function tr(key,fallback){
function _langToggleLabel(lang){ function _langToggleLabel(lang){
if(lang==='de')return 'Deutsch'; if(lang==='de')return 'Deutsch';
if(lang==='en')return 'English'; if(lang==='en')return 'English';
if(lang==='fr')return 'Français';
if(lang==='zh-cn')return '简体中文'; if(lang==='zh-cn')return '简体中文';
return 'Espanol'; return 'Espanol';
} }
@@ -108,10 +110,10 @@ function _langToggleLabel(lang){
function _mapSupportedLang(lang){ function _mapSupportedLang(lang){
if(!lang)return ''; if(!lang)return '';
var l=String(lang).toLowerCase().replace(/_/g,'-').trim(); 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]; 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(base==='zh'){
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn'; if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
@@ -282,12 +284,13 @@ function applyLang(){
setText('d-lbl-remain',T.lbl_remaining); setText('d-lbl-remain',T.lbl_remaining);
setText('d-slicer-label',T.lbl_slicer_time); setText('d-slicer-label',T.lbl_slicer_time);
setText('d-lbl-layers',T.lbl_layers); setText('d-lbl-layers',T.lbl_layers);
setText('d-lbl-zpos',T.lbl_zpos);
setText('d-lbl-light',T.lbl_light); setText('d-lbl-light',T.lbl_light);
setText('d-lbl-nozzle',T.label_nozzle); setText('d-lbl-nozzle',T.label_nozzle);
setText('d-lbl-bed',T.label_bed); setText('d-lbl-bed',T.label_bed);
// Dashboard buttons // Dashboard buttons — Pause-Button wird zur Toggle-Action; Resume-Beschriftung
setText('d-btn-pause',T.btn_pause); // wird in updatePauseResumeBtn() je nach Druckerzustand gesetzt.
setText('d-btn-resume',T.btn_resume); updatePauseResumeBtn();
setText('d-btn-cancel',T.btn_cancel); setText('d-btn-cancel',T.btn_cancel);
setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start); setText('cam-toggle-btn',camOn?T.btn_cam_stop:T.btn_cam_start);
setText('cam-placeholder-txt',T.cam_placeholder); setText('cam-placeholder-txt',T.cam_placeholder);
@@ -312,6 +315,17 @@ function applyLang(){
setText('modal-sec-print',T.settings_print); setText('modal-sec-print',T.settings_print);
setText('modal-sec-poll',T.settings_poll); setText('modal-sec-poll',T.settings_poll);
setText('modal-sec-version',T.settings_version); setText('modal-sec-version',T.settings_version);
// Custom-Profile-Import (Issue #41)
setText('modal-sec-orca-profiles',T.orca_profile_section);
setText('orca-profiles-hint',T.orca_profile_hint);
setText('lbl-orca-profiles-import',T.orca_profile_import_btn);
setText('lbl-slot-profile-import',T.orca_profile_import_link);
setText('profile-import-title',T.orca_profile_import_title);
setText('profile-import-dropmsg',T.orca_profile_dropmsg);
setText('profile-import-list-label',T.orca_profile_list_label);
// Hilfe-Text mit Inline-HTML — innerHTML statt setText
var helpEl=document.getElementById('profile-import-help');
if(helpEl && T.orca_profile_help_html) helpEl.innerHTML=T.orca_profile_help_html;
setText('btn-save-settings',T.settings_save); setText('btn-save-settings',T.settings_save);
setText('lbl-printer-name',T.settings_printer_name); setText('lbl-printer-name',T.settings_printer_name);
setText('lbl-printer-ip',T.settings_printer_ip); setText('lbl-printer-ip',T.settings_printer_ip);
@@ -612,11 +626,14 @@ function applyState(){
}else{frb.style.display='none';} }else{frb.style.display='none';}
} }
// skip-button (mid-print) nur sichtbar wenn aktuell gedruckt wird // skip-button (mid-print) nur sichtbar wenn aktuell gedruckt wird
var printing=(s.print_state==='printing'||s.print_state==='paused');
var skipBtn=document.getElementById('d-btn-skip'); var skipBtn=document.getElementById('d-btn-skip');
if(skipBtn){ if(skipBtn) skipBtn.style.display=printing?'':'none';
var printing=(s.print_state==='printing'||s.print_state==='paused'); // Pause/Stopp-Buttons nur bei aktivem Druck zeigen (sonst verwirrend wenn
skipBtn.style.display=printing?'':'none'; // der Drucker idle ist). Pause-Button rendert sich passend zum State um.
} var ctrlBtns=document.getElementById('d-ctrl-btns');
if(ctrlBtns) ctrlBtns.style.display=printing?'':'none';
updatePauseResumeBtn();
// header // header
var b=document.getElementById('h-badge'); var b=document.getElementById('h-badge');
@@ -645,6 +662,7 @@ function applyState(){
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:''; 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 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 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):''; var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'';
@@ -778,10 +796,17 @@ function applyState(){
var activity=(slot.activity||''); var activity=(slot.activity||'');
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':''); var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'');
var slotLabel=T.label_slot+' '+(globalIdx+1); var slotLabel=T.label_slot+' '+(globalIdx+1);
var profile=(window._slotProfileMap||{})[globalIdx];
var vendorBadge='';
if(!empty && profile && profile.vendor){
var tt=(profile.name||'')+(profile.id?' ('+profile.id+')':'');
vendorBadge='<div class="slot-label" style="font-size:9px;color:var(--accent);font-weight:600;margin-top:1px" title="'+tt+'">'+profile.vendor+'</div>';
}
html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'') html+='<div class="ams-slot'+(active?' active':'')+(loaded?' loaded':'')+(activity?' '+activity:'')+(empty?' empty':'')
+'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">' +'" style="--slot-color:'+col+';opacity:'+(empty?0.4:1)+';cursor:pointer" onclick="openSlotEdit('+i+')">'
+'<div class="slot-circle" style="background:'+col+'"></div>' +'<div class="slot-circle" style="background:'+col+'"></div>'
+'<div class="slot-material">'+(empty?'':(slot.type||slot.material_type||''))+'</div>' +'<div class="slot-material">'+(empty?'':(slot.type||slot.material_type||''))+'</div>'
+vendorBadge
+'<div class="slot-label">'+slotLabel+'</div>' +'<div class="slot-label">'+slotLabel+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>' +'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>' +'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
@@ -801,10 +826,14 @@ function applyState(){
var co=document.getElementById('cam-overlay'); var co=document.getElementById('cam-overlay');
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none'; if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
// auto-start camera during print // auto-start camera during print (unless user explicitly stopped it)
if(s.print_state==='printing'&&!camOn&&s.camera_url){ if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){
camStart(); camStart();
} }
// reset user-stopped flag when print ends so next print auto-starts again
if(s.print_state!=='printing'){
camUserStopped=false;
}
updateConnBtn(); updateConnBtn();
} }
@@ -893,11 +922,110 @@ function openSettings(){
var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none'; var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none';
_updateTag='';_updateUrl=''; _updateTag='';_updateUrl='';
document.getElementById('settings-modal').classList.add('open'); document.getElementById('settings-modal').classList.add('open');
// Custom-Profile-Liste laden (Issue #41 — Verwaltung von User-importierten
// OrcaSlicer-Filament-Profilen)
refreshUserProfileList();
} }
function closeSettings(){ function closeSettings(){
document.getElementById('settings-modal').classList.remove('open'); document.getElementById('settings-modal').classList.remove('open');
} }
// ── Custom Filament Profile Import (Issue #41) ──
function refreshUserProfileList(){
var listEl=document.getElementById('orca-profiles-list');
if(!listEl) return;
fetch(_apiUrl('/kx/filament/profiles/user')).then(function(r){return r.json();}).then(function(d){
var profs=(d && d.result)||[];
if(!profs.length){
listEl.innerHTML='<i style="color:var(--txt2)">'+(tr('orca_profile_user_empty')||' keine ')+'</i>';
return;
}
listEl.innerHTML=profs.map(function(p){
var label=p.vendor+' / '+p.name+' ('+p.type+')';
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;border-bottom:1px solid var(--border)">'
+'<span>★ '+label+'</span>'
+'<button onclick="deleteUserProfile(\''+encodeURIComponent(p.vendor)+'\',\''+encodeURIComponent(p.name)+'\')" '
+'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="löschen">🗑</button>'
+'</div>';
}).join('');
}).catch(function(){});
}
function deleteUserProfile(vendor, name){
fetch(_apiUrl('/kx/filament/profiles/user?vendor='+vendor+'&name='+name), {method:'DELETE'})
.then(function(r){return r.json();})
.then(function(){
_orcaFilamentCache=null;
refreshUserProfileList();
// Falls Import-Dialog offen ist, dort auch refreshen
refreshImportDialogList();
});
}
function openProfileImport(){
document.getElementById('profile-import-status').textContent='';
refreshImportDialogList();
document.getElementById('profile-import-modal').classList.add('open');
}
function closeProfileImport(){
document.getElementById('profile-import-modal').classList.remove('open');
}
function refreshImportDialogList(){
var el=document.getElementById('profile-import-list');
if(!el) return;
fetch(_apiUrl('/kx/filament/profiles/user')).then(function(r){return r.json();}).then(function(d){
var profs=(d && d.result)||[];
if(!profs.length){
el.innerHTML='<i style="color:var(--txt2)">'+(tr('orca_profile_user_empty')||' keine ')+'</i>';
return;
}
el.innerHTML=profs.map(function(p){
var label=p.vendor+' / '+p.name+' ('+p.type+')';
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 6px;border-bottom:1px solid var(--border)">'
+'<span>★ '+label+'</span>'
+'<button onclick="deleteUserProfile(\''+encodeURIComponent(p.vendor)+'\',\''+encodeURIComponent(p.name)+'\')" '
+'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="löschen">🗑</button>'
+'</div>';
}).join('');
}).catch(function(){});
}
function doProfileImportUpload(files){
if(!files || !files.length) return;
var status=document.getElementById('profile-import-status');
status.textContent=(tr('orca_profile_uploading')||'Lade hoch…');
status.style.color='var(--txt2)';
var done=0, totalAdded=0, totalSkipped=0;
function _one(idx){
if(idx>=files.length){
status.textContent=(tr('orca_profile_done')||'Importiert')+': '+totalAdded
+(totalSkipped?' / '+totalSkipped+' '+(tr('orca_profile_skipped')||'übersprungen'):'');
status.style.color='var(--ok)';
_orcaFilamentCache=null;
refreshImportDialogList();
refreshUserProfileList();
// 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')){
_fillSlotProfileDropdown(mat.value, '', '');
}
return;
}
var fd=new FormData();
fd.append('file', files[idx]);
fetch(_apiUrl('/kx/filament/profiles/user'), {method:'POST', body:fd})
.then(function(r){return r.json();})
.then(function(d){
totalAdded += (d.added||0);
totalSkipped += (d.skipped||0);
done++;
_one(idx+1);
})
.catch(function(e){
status.textContent='Fehler: '+e;
status.style.color='var(--err)';
});
}
_one(0);
}
// ── AMS Slot Edit ── // ── AMS Slot Edit ──
var _slotEditIndex=-1; var _slotEditIndex=-1;
var _slotEditLoaded=false; var _slotEditLoaded=false;
@@ -920,9 +1048,16 @@ function _loadOrcaFilaments(cb){
cb(_orcaFilamentCache); cb(_orcaFilamentCache);
}).catch(function(){ cb([]); }); }).catch(function(){ cb([]); });
} }
function _fillSlotProfileDropdown(material, currentId){ function _profileKey(vendor, name){
// Eindeutiger Selector: (vendor, name). IDs aus orca_filaments.json sind
// NICHT eindeutig (z.B. 136 Profile mit OGFL99). Wir kodieren beide in den
// <option>-Value-String mit | als Trenner.
return (vendor||'')+'|'+(name||'');
}
function _fillSlotProfileDropdown(material, currentVendor, currentName){
var sel=document.getElementById('slot-edit-profile'); var sel=document.getElementById('slot-edit-profile');
if(!sel) return; if(!sel) return;
var wantKey=_profileKey(currentVendor, currentName);
_loadOrcaFilaments(function(profiles){ _loadOrcaFilaments(function(profiles){
// Type-Filter: nur Profile vom passenden material zeigen (z.B. PLA → alle PLA-Varianten) // Type-Filter: nur Profile vom passenden material zeigen (z.B. PLA → alle PLA-Varianten)
var matU=(material||'').toUpperCase().trim(); var matU=(material||'').toUpperCase().trim();
@@ -932,18 +1067,31 @@ function _fillSlotProfileDropdown(material, currentId){
return matU==='' || pt===matU || pt.startsWith(matU+'-') || pt.startsWith(matU+' '); return matU==='' || pt===matU || pt.startsWith(matU+'-') || pt.startsWith(matU+' ');
}); });
sel.innerHTML='<option value="">'+tr('slot_edit_profile_default')+'</option>'; sel.innerHTML='<option value="">'+tr('slot_edit_profile_default')+'</option>';
// Gruppieren nach Vendor // User-Profile (is_user) zuerst — eigene Optgroup '★ Eigene' an erster Stelle.
var userProfs=matched.filter(function(p){return p.is_user;});
var systemProfs=matched.filter(function(p){return !p.is_user;});
function _appendOption(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;
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){ _appendOption(gUser, p); });
sel.appendChild(gUser);
}
// System-Profile nach Vendor gruppieren
var byVendor={}; var byVendor={};
matched.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); }); systemProfs.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); });
Object.keys(byVendor).sort().forEach(function(v){ Object.keys(byVendor).sort().forEach(function(v){
var g=document.createElement('optgroup'); g.label=v; var g=document.createElement('optgroup'); g.label=v;
byVendor[v].forEach(function(p){ byVendor[v].forEach(function(p){ _appendOption(g, p); });
var o=document.createElement('option');
o.value=p.id; o.dataset.vendor=p.vendor;
o.textContent=p.name;
if(p.id===currentId) o.selected=true;
g.appendChild(o);
});
sel.appendChild(g); sel.appendChild(g);
}); });
}); });
@@ -968,15 +1116,18 @@ function openSlotEdit(i){
+(m===mat?'background:var(--accent);color:#fff':'background:var(--raised);color:var(--txt2)')+'">'+m+'</button>'; +(m===mat?'background:var(--accent);color:#fff':'background:var(--raised);color:var(--txt2)')+'">'+m+'</button>';
}).join(''); }).join('');
// OrcaSlicer-Profil-Dropdown: aktuellen User-Override für diesen Slot // OrcaSlicer-Profil-Dropdown: aktuellen User-Override für diesen Slot
// aus /kx/filament/slots holen (enthält filament_id+filament_vendor). // aus /kx/filament/slots holen (enthält vendor+name+id).
// Mit dem material-Filter (PLA→PLA*) wird die Liste auf passende Profile reduziert.
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){ fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){
var arr=d.result||[]; var arr=d.result||[];
var entry=arr.find(function(x){return x.slot_index===globalIdx;})||{}; var entry=arr.find(function(x){return x.slot_index===globalIdx;})||{};
window._slotProfileMap=window._slotProfileMap||{}; window._slotProfileMap=window._slotProfileMap||{};
window._slotProfileMap[globalIdx]={id:entry.filament_id||'',vendor:entry.filament_vendor||''}; window._slotProfileMap[globalIdx]={
_fillSlotProfileDropdown(mat, entry.filament_id||''); id: entry.filament_id||'',
}).catch(function(){ _fillSlotProfileDropdown(mat,''); }); vendor:entry.filament_vendor||'',
name: entry.filament_name||'',
};
_fillSlotProfileDropdown(mat, entry.filament_vendor||'', entry.filament_name||'');
}).catch(function(){ _fillSlotProfileDropdown(mat,'',''); });
updateSlotEditFeedButton(); updateSlotEditFeedButton();
document.getElementById('slot-edit-modal').classList.add('open'); document.getElementById('slot-edit-modal').classList.add('open');
} }
@@ -1023,7 +1174,7 @@ function selectMatPreset(m){
highlightMatBtn(m); highlightMatBtn(m);
// Filament-Profile-Dropdown an neues Material anpassen // Filament-Profile-Dropdown an neues Material anpassen
// (vorherige Selektion zurücksetzen — andere Material-Profile passen nicht) // (vorherige Selektion zurücksetzen — andere Material-Profile passen nicht)
_fillSlotProfileDropdown(m, ''); _fillSlotProfileDropdown(m, '', '');
} }
function highlightMatBtn(val){ function highlightMatBtn(val){
document.querySelectorAll('.mat-preset-btn').forEach(function(b){ document.querySelectorAll('.mat-preset-btn').forEach(function(b){
@@ -1032,7 +1183,7 @@ function highlightMatBtn(val){
b.style.color=on?'#fff':'var(--txt2)'; b.style.color=on?'#fff':'var(--txt2)';
}); });
// Auch bei manueller Eingabe ins Material-Textfeld: Dropdown refreshen. // Auch bei manueller Eingabe ins Material-Textfeld: Dropdown refreshen.
if(val) _fillSlotProfileDropdown(val, ''); if(val) _fillSlotProfileDropdown(val, '', '');
} }
function hexToRgb(hex){ function hexToRgb(hex){
var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16); var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);
@@ -1043,31 +1194,59 @@ function saveSlotEdit(){
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA'; var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
var color=hexToRgb(hex); var color=hexToRgb(hex);
var slotIdx=_slotEditIndex; var slotIdx=_slotEditIndex;
// OrcaSlicer-Profil-Override: parallel persistieren (Profile bleiben auch
// erhalten wenn der User nur Farbe/Material ändert)
var profSel=document.getElementById('slot-edit-profile'); var profSel=document.getElementById('slot-edit-profile');
var newProfId=profSel?profSel.value:''; var sel=profSel && profSel.selectedOptions && profSel.selectedOptions[0];
var newProfVendor=''; // Primärer Selector: (vendor, name). id ist nur Hint (aus dem JSON-data-attr
if(profSel && profSel.selectedOptions && profSel.selectedOptions[0]){ // mitgegeben — Backend lookt sie nochmal selber nach um veraltete Hints zu
newProfVendor=profSel.selectedOptions[0].dataset.vendor||''; // korrigieren).
} var newProfVendor=sel?(sel.dataset.vendor||''):'';
var newProfName =sel?(sel.dataset.name ||''):'';
var newProfId =sel?(sel.dataset.id ||''):'';
// Sequenziell speichern: erst Profil-Override (config.ini), dann Material/Farbe
// (MQTT zum Drucker). Sonst können beide Pfade sich überholen und der Slot-State
// ist beim nächsten Re-Open inkonsistent.
fetch(_apiUrl('/kx/filament/slots/'+slotIdx+'/profile'),{ fetch(_apiUrl('/kx/filament/slots/'+slotIdx+'/profile'),{
method:'POST', method:'POST',
headers:{'Content-Type':'application/json'}, headers:{'Content-Type':'application/json'},
body:JSON.stringify({id:newProfId,vendor:newProfVendor}) body:JSON.stringify({vendor:newProfVendor, name:newProfName, id:newProfId})
}).then(function(r){return r.json();}).then(function(){ })
.then(function(r){return r.json();})
.then(function(){
window._slotProfileMap=window._slotProfileMap||{}; window._slotProfileMap=window._slotProfileMap||{};
if(newProfId){ window._slotProfileMap[slotIdx]={id:newProfId,vendor:newProfVendor}; } if(newProfVendor && newProfName){
else delete window._slotProfileMap[slotIdx]; window._slotProfileMap[slotIdx]={id:newProfId, vendor:newProfVendor, name:newProfName};
}).catch(function(e){clog('Profil-Speichern fehlgeschlagen: '+e,'msg-err');}); } else {
post('/api/ams/set_slot',{index:slotIdx,type:mat,color:color}) delete window._slotProfileMap[slotIdx];
.then(function(r){return r.json();}) }
.then(function(r){ return post('/api/ams/set_slot',{index:slotIdx,type:mat,color:color});
closeSlotEdit(); })
var profSuffix=newProfId?(' ['+newProfId+']'):''; .then(function(r){return r?r.json():null;})
clog(tr('slot_edit_ok')+' '+(slotIdx+1)+': '+mat+' '+hex+profSuffix,'msg-ok'); .then(function(){
}) // Slot-Map refreshen damit die Karte sofort den Vendor zeigt.
.catch(function(e){clog('Fehler: '+e,'msg-err');}); return fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();});
})
.then(function(d){
var arr=(d && d.result)||[];
window._slotProfileMap={};
arr.forEach(function(e){
if(e.filament_vendor && e.filament_name){
window._slotProfileMap[e.slot_index]={
id: e.filament_id||'',
vendor:e.filament_vendor,
name: e.filament_name,
};
}
});
closeSlotEdit();
var profSuffix=newProfName?(' ['+newProfVendor+' '+newProfName+']'):'';
clog(tr('slot_edit_ok')+' '+(slotIdx+1)+': '+mat+' '+hex+profSuffix,'msg-ok');
// Sofortiges Re-Render mit aktuellem _slotProfileMap (poll() ist async
// und re-rendert beim nächsten Tick — wir wollen aber dass die Vendor-
// Badge JETZT direkt sichtbar wird).
if(typeof applyState==='function') applyState();
if(typeof poll==='function') poll();
})
.catch(function(e){clog('Fehler: '+e,'msg-err');});
} }
document.addEventListener('DOMContentLoaded',function(){ document.addEventListener('DOMContentLoaded',function(){
document.getElementById('s-printer-ip').addEventListener('input',function(){ document.getElementById('s-printer-ip').addEventListener('input',function(){
@@ -1164,6 +1343,21 @@ var pollTimer;
(function(){ (function(){
var ms=parseInt(localStorage.getItem('pollInterval')||'2000'); var ms=parseInt(localStorage.getItem('pollInterval')||'2000');
initPrinters(); initPrinters();
// Slot-Profile-Map initial laden, sonst zeigen die Karten beim ersten
// Render keine Vendor-Badge obwohl in der config.ini ein Override steht.
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){
var arr=(d && d.result)||[];
window._slotProfileMap={};
arr.forEach(function(e){
if(e.filament_vendor && e.filament_name){
window._slotProfileMap[e.slot_index]={
id: e.filament_id||'',
vendor:e.filament_vendor,
name: e.filament_name,
};
}
});
}).catch(function(){});
poll();pollTimer=setInterval(poll,ms); poll();pollTimer=setInterval(poll,ms);
})(); })();
@@ -1172,7 +1366,28 @@ function printAction(a){
post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()}) post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()})
.catch(function(e){clog('Fehler: '+e,'msg-err')}); .catch(function(e){clog('Fehler: '+e,'msg-err')});
} }
function confirmCancel(){if(confirm('Druck wirklich abbrechen?'))printAction('cancel')} function togglePauseResume(){
// Druckt → pause; Pausiert → resume. Status kommt aus dem zuletzt gepollten
// print_state in S; bei Unklarheit (kein State) Pause als Default.
var state=(S && S.print_state)||'';
if(state==='paused') printAction('resume');
else printAction('pause');
}
function updatePauseResumeBtn(){
var btn=document.getElementById('d-btn-pause');
if(!btn) return;
var state=(S && S.print_state)||'';
if(state==='paused'){
btn.textContent=T.btn_resume||'▶ Weiter';
btn.classList.add('btn-resume');
btn.classList.remove('btn-pause');
} else {
btn.textContent=T.btn_pause||'⏸ Pause';
btn.classList.add('btn-pause');
btn.classList.remove('btn-resume');
}
}
function confirmCancel(){if(confirm(T.confirm_cancel||'Druck wirklich abbrechen?'))printAction('cancel')}
// ── Axis motion ── // ── Axis motion ──
// axis codes: 0=X, 1=Y, 2=Z // axis codes: 0=X, 1=Y, 2=Z
@@ -1230,7 +1445,8 @@ function setBed(){
function setLight(){ function setLight(){
var on=document.getElementById('d-light-toggle').checked; var on=document.getElementById('d-light-toggle').checked;
post('/api/light',{on:on,brightness:80}) 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')}); .catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
} }
@@ -1311,11 +1527,13 @@ function camStart(){
function camStop(){ function camStop(){
var img=document.getElementById('cam-img'); 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(){}); post('/api/camera/stop',{}).catch(function(){});
img.src=''; img.src='';
img.style.display='none'; img.style.display='none';
document.getElementById('cam-placeholder').style.display='flex'; document.getElementById('cam-placeholder').style.display='flex';
camOn=false; camOn=false;
camUserStopped=true; // suppress auto-restart for remainder of this print
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start'); document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
clog(tr('log_cam_stop'),'msg-ok'); clog(tr('log_cam_stop'),'msg-ok');
} }

View File

@@ -38,6 +38,7 @@
<option value="de">Deutsch</option> <option value="de">Deutsch</option>
<option value="en">English</option> <option value="en">English</option>
<option value="es">Espanol</option> <option value="es">Espanol</option>
<option value="fr">Français</option>
<option value="zh-cn">中文(简体)</option> <option value="zh-cn">中文(简体)</option>
</select> </select>
</div> </div>
@@ -121,6 +122,19 @@
</div> </div>
</div> </div>
<!-- OrcaSlicer-Profile (Custom-Profile-Import, Issue #41) -->
<div>
<div class="modal-section" id="modal-sec-orca-profiles">OrcaSlicer-Profile</div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
</div>
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
style="background:var(--raised);color:var(--txt)">
<span id="lbl-orca-profiles-import">Profile importieren</span>
</button>
</div>
<div> <div>
<div class="modal-section" id="modal-sec-version">Version</div> <div class="modal-section" id="modal-sec-version">Version</div>
<div class="update-row"> <div class="update-row">
@@ -170,12 +184,42 @@
<option value="" id="slot-profile-default-opt"></option> <option value="" id="slot-profile-default-opt"></option>
</select> </select>
<div style="font-size:11px;color:var(--txt2);margin-top:4px" id="slot-profile-hint"></div> <div style="font-size:11px;color:var(--txt2);margin-top:4px" id="slot-profile-hint"></div>
<a href="#" onclick="event.preventDefault();openProfileImport()"
style="display:inline-block;margin-top:6px;font-size:11px;color:var(--accent);text-decoration:none"
id="lbl-slot-profile-import">★ Eigene Profile importieren…</a>
</div> </div>
<button class="btn" id="btn-slot-edit-feed" style="width:100%;margin-bottom:8px" onclick="slotEditFeed()"></button> <button class="btn" id="btn-slot-edit-feed" style="width:100%;margin-bottom:8px" onclick="slotEditFeed()"></button>
<button class="modal-save" id="btn-slot-edit-save" onclick="saveSlotEdit()"></button> <button class="modal-save" id="btn-slot-edit-save" onclick="saveSlotEdit()"></button>
</div> </div>
</div> </div>
<!-- ═══ ORCA-PROFILE-IMPORT-DIALOG (Issue #41) ═══ -->
<div class="modal-overlay" id="profile-import-modal" onclick="if(event.target===this)closeProfileImport()">
<div class="modal-box" style="max-width:480px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<div style="font-size:16px;font-weight:600" id="profile-import-title">Eigene OrcaSlicer-Profile importieren</div>
<button onclick="closeProfileImport()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer">×</button>
</div>
<div style="font-size:12px;color:var(--txt2);margin-bottom:12px;line-height:1.5" id="profile-import-help">
Lade ein <b>ZIP</b> deines OrcaSlicer-Filament-Ordners oder einzelne <b>.json</b>-Files hoch.<br>
In OrcaSlicer: <i>Help → Show Configuration Folder → user/&lt;id&gt;/filament/</i>
</div>
<div id="profile-import-drop" style="border:2px dashed var(--border);border-radius:8px;padding:24px;text-align:center;cursor:pointer;margin-bottom:12px"
ondragover="event.preventDefault();this.style.borderColor='var(--accent)'"
ondragleave="this.style.borderColor='var(--border)'"
ondrop="event.preventDefault();this.style.borderColor='var(--border)';doProfileImportUpload(event.dataTransfer.files)"
onclick="document.getElementById('profile-import-file').click()">
<div style="font-size:32px;margin-bottom:8px"></div>
<div style="font-size:13px;color:var(--txt2)" id="profile-import-dropmsg">Hierher ziehen oder klicken</div>
<input type="file" id="profile-import-file" accept=".zip,.json" multiple
style="display:none" onchange="doProfileImportUpload(this.files);this.value=''">
</div>
<div id="profile-import-status" style="font-size:12px;margin-bottom:12px;min-height:18px"></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="profile-import-list-label">Aktuell importiert</div>
<div id="profile-import-list" style="max-height:240px;overflow-y:auto;font-size:12px"></div>
</div>
</div>
<div class="layout"> <div class="layout">
<nav class="sidebar"> <nav class="sidebar">
<button class="nav-btn active" onclick="showPanel('dashboard')" id="nb-dashboard"> <button class="nav-btn active" onclick="showPanel('dashboard')" id="nb-dashboard">
@@ -223,9 +267,15 @@
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div> <div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
<div style="display:flex;align-items:center;gap:10px;margin:8px 0"> <div style="display:flex;align-items:center;gap:10px;margin:8px 0">
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div> <div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0"> <div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
<div class="time-label" id="d-lbl-layers"></div> <div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center">
<div class="time-val" style="font-size:16px" id="d-layers"></div> <div class="time-label" id="d-lbl-layers"></div>
<div class="time-val" style="font-size:16px" id="d-layers"></div>
</div>
<div class="time-block" style="padding:4px 10px;min-width:72px;text-align:center">
<div class="time-label" id="d-lbl-zpos">Z</div>
<div class="time-val" style="font-size:13px" id="d-zpos"></div>
</div>
</div> </div>
</div> </div>
<div class="time-grid"> <div class="time-grid">
@@ -243,9 +293,8 @@
</div> </div>
</div> </div>
<div class="fname" id="d-fname" title="" style="margin-top:6px"></div> <div class="fname" id="d-fname" title="" style="margin-top:6px"></div>
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px"> <div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="printAction('pause')">⏸ Pause</button> <button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
<button class="btn btn-resume btn-sm" id="d-btn-resume" onclick="printAction('resume')">▶ Weiter</button>
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none"><span id="d-btn-skip-label">Objekte</span></button> <button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none"><span id="d-btn-skip-label">Objekte</span></button>
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button> <button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
</div> </div>

View File

@@ -37,6 +37,7 @@
"lbl_remaining": "Restzeit:", "lbl_remaining": "Restzeit:",
"lbl_slicer_time": "Slicer-Schätzung:", "lbl_slicer_time": "Slicer-Schätzung:",
"lbl_layers": "Layer", "lbl_layers": "Layer",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 Leise", "speed_silent": "🐢 Leise",
"speed_normal": "⚡ Normal", "speed_normal": "⚡ Normal",
"speed_sport": "🚀 Sport", "speed_sport": "🚀 Sport",
@@ -164,6 +165,19 @@
"slot_edit_profile": "OrcaSlicer-Profil", "slot_edit_profile": "OrcaSlicer-Profil",
"slot_edit_profile_hint": "Sendet beim OrcaSlicer-Sync die konkrete Marke statt nur „Generic\"", "slot_edit_profile_hint": "Sendet beim OrcaSlicer-Sync die konkrete Marke statt nur „Generic\"",
"slot_edit_profile_default": "— Generic (Default) —", "slot_edit_profile_default": "— Generic (Default) —",
"orca_profile_section": "OrcaSlicer-Profile",
"orca_profile_hint": "Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)",
"orca_profile_import_btn": "Profile importieren",
"orca_profile_import_link": "★ Eigene Profile importieren…",
"orca_profile_import_title": "Eigene OrcaSlicer-Profile importieren",
"orca_profile_help_html": "Lade ein <b>ZIP</b> deines OrcaSlicer-Filament-Ordners oder einzelne <b>.json</b>-Files hoch.<br>In OrcaSlicer: <i>Help → Show Configuration Folder → user/&lt;id&gt;/filament/</i>",
"orca_profile_dropmsg": "Hierher ziehen oder klicken",
"orca_profile_list_label": "Aktuell importiert",
"orca_profile_user_label": "Eigene Profile",
"orca_profile_user_empty": " keine ",
"orca_profile_uploading": "Lade hoch…",
"orca_profile_done": "Importiert",
"orca_profile_skipped": "übersprungen",
"log_dir_all": "Alle", "log_dir_all": "Alle",
"log_lvl_label": "Level:", "log_lvl_label": "Level:",
"file_ready_btn": "▶ Druck starten", "file_ready_btn": "▶ Druck starten",

View File

@@ -37,6 +37,7 @@
"lbl_remaining": "Remaining:", "lbl_remaining": "Remaining:",
"lbl_slicer_time": "Slicer estimate:", "lbl_slicer_time": "Slicer estimate:",
"lbl_layers": "Layer", "lbl_layers": "Layer",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 Silent", "speed_silent": "🐢 Silent",
"speed_normal": "⚡ Normal", "speed_normal": "⚡ Normal",
"speed_sport": "🚀 Sport", "speed_sport": "🚀 Sport",
@@ -164,6 +165,19 @@
"slot_edit_profile": "OrcaSlicer profile", "slot_edit_profile": "OrcaSlicer profile",
"slot_edit_profile_hint": "Sent on OrcaSlicer sync as the specific brand instead of just \"Generic\"", "slot_edit_profile_hint": "Sent on OrcaSlicer sync as the specific brand instead of just \"Generic\"",
"slot_edit_profile_default": "— Generic (default) —", "slot_edit_profile_default": "— Generic (default) —",
"orca_profile_section": "OrcaSlicer Profiles",
"orca_profile_hint": "Import your own OrcaSlicer filament profiles (open the user dir via Help → Show Configuration Folder)",
"orca_profile_import_btn": "Import profiles",
"orca_profile_import_link": "★ Import own profiles…",
"orca_profile_import_title": "Import your OrcaSlicer profiles",
"orca_profile_help_html": "Upload a <b>ZIP</b> of your OrcaSlicer filament folder or single <b>.json</b> files.<br>In OrcaSlicer: <i>Help → Show Configuration Folder → user/&lt;id&gt;/filament/</i>",
"orca_profile_dropmsg": "Drop here or click",
"orca_profile_list_label": "Currently imported",
"orca_profile_user_label": "Own profiles",
"orca_profile_user_empty": " none ",
"orca_profile_uploading": "Uploading…",
"orca_profile_done": "Imported",
"orca_profile_skipped": "skipped",
"log_dir_all": "All", "log_dir_all": "All",
"log_lvl_label": "Level:", "log_lvl_label": "Level:",
"file_ready_btn": "▶ Start Print", "file_ready_btn": "▶ Start Print",

View File

@@ -20,9 +20,9 @@
"kobra_finished": "Finalizado", "kobra_finished": "Finalizado",
"kobra_failed": "Error", "kobra_failed": "Error",
"kobra_canceled": "Cancelado", "kobra_canceled": "Cancelado",
"kobra_offline": "Offline", "kobra_offline": "Desconectada",
"nav_dashboard": "Panel", "nav_dashboard": "Panel",
"nav_print": "Impresion", "nav_print": "Impresión",
"nav_temps": "Temperaturas", "nav_temps": "Temperaturas",
"nav_motion": "Movimiento", "nav_motion": "Movimiento",
"nav_ams": "AMS", "nav_ams": "AMS",
@@ -31,12 +31,13 @@
"card_progress": "Progreso", "card_progress": "Progreso",
"card_temps": "Temperaturas", "card_temps": "Temperaturas",
"card_light_fan": "Ventilador", "card_light_fan": "Ventilador",
"card_speed": "Velocidad de impresion", "card_speed": "Velocidad de impresión",
"card_cam": "Camara", "card_cam": "Cámara",
"lbl_elapsed": "Transcurrido:", "lbl_elapsed": "Transcurrido:",
"lbl_remaining": "Restante:", "lbl_remaining": "Restante:",
"lbl_slicer_time": "Estimacion del slicer:", "lbl_slicer_time": "Estimación del slicer:",
"lbl_layers": "Layer", "lbl_layers": "Capa",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 Silencioso", "speed_silent": "🐢 Silencioso",
"speed_normal": "⚡ Normal", "speed_normal": "⚡ Normal",
"speed_sport": "🚀 Sport", "speed_sport": "🚀 Sport",
@@ -52,14 +53,14 @@
"ace_dry_current_temp": "Temperatura", "ace_dry_current_temp": "Temperatura",
"ace_dry_chart": "Historial (Temp/Humedad)", "ace_dry_chart": "Historial (Temp/Humedad)",
"ace_dry_temp": "Temperatura (°C)", "ace_dry_temp": "Temperatura (°C)",
"ace_dry_duration": "Duracion (min)", "ace_dry_duration": "Duración (min)",
"ace_dry_start": "▶ Start", "ace_dry_start": "▶ Iniciar",
"ace_dry_stop": "■ Stop", "ace_dry_stop": "■ Parar",
"ace_dry_auto_refill": "Relleno automatico", "ace_dry_auto_refill": "Relleno automático",
"ace_dry_enable": "Activar secado", "ace_dry_enable": "Activar secado",
"ace_dry_temp_line": "Temperatura de secado", "ace_dry_temp_line": "Temperatura de secado",
"ace_dry_time_line": "Tiempo de secado", "ace_dry_time_line": "Tiempo de secado",
"ace_dry_ui_pending": "(solo UI, backend despues)", "ace_dry_ui_pending": "(solo UI, backend después)",
"ace_dry_dialog_title": "Ajustes de temp/tiempo del secador", "ace_dry_dialog_title": "Ajustes de temp/tiempo del secador",
"ace_dry_dialog_temp": "Temperatura (30-80°C)", "ace_dry_dialog_temp": "Temperatura (30-80°C)",
"ace_dry_dialog_time": "Tiempo restante (h:m:s)", "ace_dry_dialog_time": "Tiempo restante (h:m:s)",
@@ -67,12 +68,12 @@
"ace_dry_dialog_cancel": "Cancelar", "ace_dry_dialog_cancel": "Cancelar",
"ace_dry_dialog_save_restart": "Guardar y reiniciar", "ace_dry_dialog_save_restart": "Guardar y reiniciar",
"ace_dry_dialog_custom_name": "Nombre personalizado", "ace_dry_dialog_custom_name": "Nombre personalizado",
"ace_dry_dialog_reset_default": "Restablecer por defecto", "ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
"cam_placeholder": "📷 Camara no iniciada", "cam_placeholder": "📷 Cámara no iniciada",
"cam_stream_unavailable": "Stream no disponible", "cam_stream_unavailable": "Stream no disponible",
"btn_cam_start": "▶ Camara", "btn_cam_start": "▶ Cámara",
"btn_cam_stop": "◼ Camara", "btn_cam_stop": "◼ Cámara",
"btn_pause": "⏸ Pause", "btn_pause": "⏸ Pausa",
"btn_resume": "▶ Reanudar", "btn_resume": "▶ Reanudar",
"btn_cancel": "✕ Detener", "btn_cancel": "✕ Detener",
"label_nozzle": "Boquilla", "label_nozzle": "Boquilla",
@@ -81,20 +82,20 @@
"label_light": "💡 Luz", "label_light": "💡 Luz",
"label_on_off": "Encendido / Apagado", "label_on_off": "Encendido / Apagado",
"label_speed": "Velocidad", "label_speed": "Velocidad",
"panel_print_title": "Control de impresion", "panel_print_title": "Control de impresión",
"panel_print_btn_pause": "⏸ Pause", "panel_print_btn_pause": "⏸ Pausa",
"panel_print_btn_resume": "▶ Reanudar", "panel_print_btn_resume": "▶ Reanudar",
"panel_print_btn_cancel": "✕ Cancelar", "panel_print_btn_cancel": "✕ Cancelar",
"panel_print_temps_live": "Temperaturas (en vivo)", "panel_print_temps_live": "Temperaturas (en vivo)",
"label_set": "Set", "label_set": "Set",
"label_off": "Off", "label_off": "Apagado",
"panel_temps_nozzle": "Boquilla", "panel_temps_nozzle": "Boquilla",
"panel_temps_bed": "Cama caliente", "panel_temps_bed": "Cama caliente",
"panel_temps_chart": "Historial (ultimas 60 lecturas)", "panel_temps_chart": "Historial (últimas 60 lecturas)",
"label_target_c": "Objetivo:", "label_target_c": "Objetivo:",
"panel_motion_xy": "Ejes XY", "panel_motion_xy": "Ejes XY",
"panel_motion_z": "Eje Z", "panel_motion_z": "Eje Z",
"label_step": "Tamano del paso:", "label_step": "Tamaño del paso:",
"btn_home_z": "Home Z", "btn_home_z": "Home Z",
"btn_home_xy": "Home XY", "btn_home_xy": "Home XY",
"btn_home_all": "Home All", "btn_home_all": "Home All",
@@ -103,11 +104,11 @@
"card_ams": "Filamento", "card_ams": "Filamento",
"ams_no_data": "No se recibieron datos de AMS", "ams_no_data": "No se recibieron datos de AMS",
"label_slot": "Ranura", "label_slot": "Ranura",
"ams_empty": "Vacio", "ams_empty": "Vacío",
"panel_extras_light": "Luz", "panel_extras_light": "Luz",
"panel_extras_fan": "Ventilador", "panel_extras_fan": "Ventilador",
"panel_extras_camera": "Camara", "panel_extras_camera": "Cámara",
"btn_cam_start2": "▶ Start", "btn_cam_start2": "▶ Iniciar",
"btn_cam_stop2": "◼ Detener", "btn_cam_stop2": "◼ Detener",
"panel_console_title": "Registro de eventos", "panel_console_title": "Registro de eventos",
"log_light_on": "Luz encendida", "log_light_on": "Luz encendida",
@@ -118,29 +119,29 @@
"log_axis": "Eje", "log_axis": "Eje",
"log_home": "Home", "log_home": "Home",
"log_home_all": "Home All", "log_home_all": "Home All",
"log_cam_start": "Camara iniciada:", "log_cam_start": "Cámara iniciada:",
"log_cam_stop": "Camara detenida", "log_cam_stop": "Cámara detenida",
"log_poll_error": "Error de sondeo:", "log_poll_error": "Error de sondeo:",
"log_error": "Error:", "log_error": "Error:",
"confirm_cancel": "Realmente cancelar la impresion?", "confirm_cancel": "¿Realmente cancelar la impresión?",
"settings_title": "Configuracion", "settings_title": "Configuración",
"settings_connection": "Conexion", "settings_connection": "Conexión",
"settings_print": "Ajustes de impresion", "settings_print": "Ajustes de impresión",
"settings_poll": "Intervalo de sondeo", "settings_poll": "Intervalo de sondeo",
"settings_version": "Version", "settings_version": "Versión",
"settings_save": "Guardar y reiniciar", "settings_save": "Guardar y reiniciar",
"settings_printer_name": "Nombre de impresora", "settings_printer_name": "Nombre de impresora",
"settings_printer_ip": "IP de impresora", "settings_printer_ip": "IP de impresora",
"settings_mqtt_port": "MQTT Port", "settings_mqtt_port": "MQTT Port",
"settings_username": "Usuario MQTT", "settings_username": "Usuario MQTT",
"settings_password": "Contrasena MQTT", "settings_password": "Contraseña MQTT",
"settings_device_id": "ID del dispositivo", "settings_device_id": "ID del dispositivo",
"settings_mode_id": "Mode ID", "settings_mode_id": "ID de modo",
"hint_ip_no_port": "Solo direccion IP, sin puerto (p. ej. 192.168.1.102)", "hint_ip_no_port": "Solo dirección IP, sin puerto (p. ej. 192.168.1.102)",
"settings_default_slot": "Ranura predeterminada (un color)", "settings_default_slot": "Ranura predeterminada (un color)",
"settings_slot_auto": "Auto (todos los slots cargados)", "settings_slot_auto": "Auto (todos los slots cargados)",
"settings_auto_leveling": "Autonivelado antes de imprimir", "settings_auto_leveling": "Autonivelado antes de imprimir",
"settings_camera_on_print": "Encender camara al iniciar impresion", "settings_camera_on_print": "Encender cámara al iniciar impresión",
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web", "settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
"update_check": "Buscar actualizaciones", "update_check": "Buscar actualizaciones",
"update_checking": "Comprobando...", "update_checking": "Comprobando...",
@@ -152,7 +153,7 @@
"update_error": "Error", "update_error": "Error",
"btn_connect": "⚡ Conectar", "btn_connect": "⚡ Conectar",
"btn_disconnect": "✕ Desconectar", "btn_disconnect": "✕ Desconectar",
"lbl_conn_error": "Error de conexion:", "lbl_conn_error": "Error de conexión:",
"slot_edit_title": "Editar slot", "slot_edit_title": "Editar slot",
"slot_edit_color": "Color", "slot_edit_color": "Color",
"slot_edit_material": "Material", "slot_edit_material": "Material",
@@ -164,62 +165,75 @@
"slot_edit_profile": "Perfil de OrcaSlicer", "slot_edit_profile": "Perfil de OrcaSlicer",
"slot_edit_profile_hint": "Envía al sincronizar con OrcaSlicer la marca concreta en lugar de solo \"Generic\"", "slot_edit_profile_hint": "Envía al sincronizar con OrcaSlicer la marca concreta en lugar de solo \"Generic\"",
"slot_edit_profile_default": "— Genérico (Predeterminado) —", "slot_edit_profile_default": "— Genérico (Predeterminado) —",
"orca_profile_section": "Perfiles de OrcaSlicer",
"orca_profile_hint": "Importa tus propios perfiles de filamento de OrcaSlicer (abre el directorio del usuario vía Help → Show Configuration Folder)",
"orca_profile_import_btn": "Importar perfiles",
"orca_profile_import_link": "★ Importar perfiles propios…",
"orca_profile_import_title": "Importar tus perfiles de OrcaSlicer",
"orca_profile_help_html": "Sube un <b>ZIP</b> de tu carpeta de filamentos de OrcaSlicer o archivos <b>.json</b> sueltos.<br>En OrcaSlicer: <i>Help → Show Configuration Folder → user/&lt;id&gt;/filament/</i>",
"orca_profile_dropmsg": "Suelta aquí o haz clic",
"orca_profile_list_label": "Actualmente importados",
"orca_profile_user_label": "Perfiles propios",
"orca_profile_user_empty": " ninguno ",
"orca_profile_uploading": "Subiendo…",
"orca_profile_done": "Importado",
"orca_profile_skipped": "omitido",
"log_dir_all": "Todos", "log_dir_all": "Todos",
"log_lvl_label": "Level:", "log_lvl_label": "Nivel:",
"file_ready_btn": "▶ Iniciar impresion", "file_ready_btn": "▶ Iniciar impresión",
"file_slots_btn": "🎨 Seleccionar ranuras", "file_slots_btn": "🎨 Seleccionar ranuras",
"file_cancel_btn": "✕ Cancelar", "file_cancel_btn": "✕ Cancelar",
"nav_printers": "Impresoras", "nav_printers": "Impresoras",
"skip_title": "✂ Omitir objetos", "skip_title": "✂ Omitir objetos",
"skip_hint": "Desmarca objetos que ya no quieras imprimir:", "skip_hint": "Deselecciona los objetos que ya no quieras imprimir:",
"skip_btn_label": "Objetos", "skip_btn_label": "Objetos",
"skip_no_objects": "No hay objetos en esta impresion.", "skip_no_objects": "No hay objetos en esta impresión.",
"skip_already": "omitido", "skip_already": "omitido",
"skip_select_at_least_one": "Elige al menos un objeto.", "skip_select_at_least_one": "Selecciona al menos un objeto.",
"skip_sending": "Enviando …", "skip_sending": "Enviando …",
"skip_success": "Se omitiran los objetos.", "skip_success": "Se omitirán los objetos.",
"fd_objects_hint": "Omitir objetos (opcional):", "fd_objects_hint": "Omitir objetos (opcional):",
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:", "fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
"fd_cancel": "Cancelar", "fd_cancel": "Cancelar",
"fd_print": "▶ Imprimir", "fd_print": "▶ Imprimir",
"fd_no_slots_msg": "No hay slots AMS cargados.{br}Iniciar impresion de todos modos?", "fd_no_slots_msg": "No hay slots AMS cargados.{br}¿Iniciar impresión de todos modos?",
"fd_slot": "Ranura", "fd_slot": "Ranura",
"fd_no_matching_material": "No hay material compatible", "fd_no_matching_material": "No hay material compatible",
"fd_used": "USADO", "fd_used": "USADO",
"add_printer": "Agregar impresora", "add_printer": "Añadir impresora",
"apd_lbl_ip": "IP de impresora", "apd_lbl_ip": "IP de impresora",
"apd_lbl_name": "Nombre (opcional)", "apd_lbl_name": "Nombre (opcional)",
"apd_placeholder_name": "p. ej. Kobra X Sala", "apd_placeholder_name": "p. ej. Kobra X Sala",
"apd_cancel": "Cancelar", "apd_cancel": "Cancelar",
"apd_confirm": "Agregar", "apd_confirm": "Añadir",
"apd_fetching": "Obteniendo datos de la impresora…", "apd_fetching": "Obteniendo datos de la impresora…",
"apd_success": "Impresora agregada, reiniciando bridge…", "apd_success": "Impresora añadida, reiniciando bridge…",
"apd_err_ip": "Introduce una direccion IP", "apd_err_ip": "Introduce una dirección IP",
"printers_remove": "Eliminar impresora", "printers_remove": "Eliminar impresora",
"printers_remove_confirm": "Eliminar impresora \"{name}\"? El bridge se reiniciara.", "printers_remove_confirm": "¿Eliminar impresora \"{name}\"? El bridge se reiniciará.",
"printers_active": "● activa", "printers_active": "● activa",
"printers_switch": "Cambiar →", "printers_switch": "Cambiar →",
"printers_current": "Impresora actual", "printers_current": "Impresora actual",
"printers_loading": "Cargando…", "printers_loading": "Cargando…",
"printers_none": "No hay impresoras configuradas.", "printers_none": "No hay impresoras configuradas.",
"printers_empty_hint": "Aun no hay impresora configurada.", "printers_empty_hint": "Aún no hay impresora configurada.",
"nav_browser": "Explorador", "nav_browser": "Explorador",
"panel_browser_title": "Explorador de archivos", "panel_browser_title": "Explorador de archivos",
"store_search_placeholder": "🔍 Buscar…", "store_search_placeholder": "🔍 Buscar…",
"store_empty": "Aun no hay archivos subidos.", "store_empty": "Aún no hay archivos subidos.",
"store_refresh": "↻ Actualizar", "store_refresh": "↻ Actualizar",
"store_print": "▶ Imprimir", "store_print": "▶ Imprimir",
"store_download": "⬇ Descargar", "store_download": "⬇ Descargar",
"store_delete_confirm": "Eliminar archivo?", "store_delete_confirm": "¿Eliminar archivo?",
"store_print_confirm": "Imprimir archivo?", "store_print_confirm": "¿Imprimir archivo?",
"store_web_verify_title": "Verificar archivo", "store_web_verify_title": "Verificar archivo",
"store_web_verify_msg": "Verifica que este archivo fue creado para Anycubic Kobra X.", "store_web_verify_msg": "Verifica que este archivo fue creado para Anycubic Kobra X.",
"store_web_verify_confirm": "Confirmar", "store_web_verify_confirm": "Confirmar",
"store_web_verify_abort": "Abortar", "store_web_verify_abort": "Abortar",
"store_no_results": "No se encontraron archivos.", "store_no_results": "No se encontraron archivos.",
"store_never": "nunca impreso", "store_never": "nunca impreso",
"store_estimate": "Estimacion", "store_estimate": "Estimación",
"store_upload_label_prefix": "Arrastra GCode aqui o ", "store_upload_label_prefix": "Arrastra el GCode aquí o ",
"store_upload_label_browse": "buscar", "store_upload_label_browse": "buscar",
"store_upload_busy": "⏳ Subiendo…", "store_upload_busy": "⏳ Subiendo…",
"store_upload_success": "✓ {file}", "store_upload_success": "✓ {file}",
@@ -230,5 +244,5 @@
"sf_new": "Nuevo", "sf_new": "Nuevo",
"ss_date": "↓ Fecha", "ss_date": "↓ Fecha",
"ss_name": "AZ Nombre", "ss_name": "AZ Nombre",
"ss_dur": "⏱ Tiempo de impresion" "ss_dur": "⏱ Tiempo de impresión"
} }

249
web/translations/fr.json Normal file
View File

@@ -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 <b>ZIP</b> de votre dossier filament OrcaSlicer ou des fichiers <b>.json</b> individuels.<br>Dans OrcaSlicer : <i>Aide → Afficher le dossier de configuration → user/&lt;id&gt;/filament/</i>",
"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": "AZ Nom",
"ss_dur": "⏱ Durée d'impression"
}

View File

@@ -37,6 +37,7 @@
"lbl_remaining": "剩余时间:", "lbl_remaining": "剩余时间:",
"lbl_slicer_time": "切片预估:", "lbl_slicer_time": "切片预估:",
"lbl_layers": "层", "lbl_layers": "层",
"lbl_zpos": "Z (mm)",
"speed_silent": "🐢 静音", "speed_silent": "🐢 静音",
"speed_normal": "⚡ 标准", "speed_normal": "⚡ 标准",
"speed_sport": "🚀 运动", "speed_sport": "🚀 运动",
@@ -164,6 +165,19 @@
"slot_edit_profile": "OrcaSlicer 配置", "slot_edit_profile": "OrcaSlicer 配置",
"slot_edit_profile_hint": "在 OrcaSlicer 同步时发送具体品牌而不仅仅是“Generic”", "slot_edit_profile_hint": "在 OrcaSlicer 同步时发送具体品牌而不仅仅是“Generic”",
"slot_edit_profile_default": "— 通用 (默认) —", "slot_edit_profile_default": "— 通用 (默认) —",
"orca_profile_section": "OrcaSlicer 配置",
"orca_profile_hint": "导入你自己的 OrcaSlicer 耗材配置(在 Help → Show Configuration Folder 打开用户目录)",
"orca_profile_import_btn": "导入配置",
"orca_profile_import_link": "★ 导入自己的配置…",
"orca_profile_import_title": "导入你的 OrcaSlicer 配置",
"orca_profile_help_html": "上传 OrcaSlicer 耗材文件夹的 <b>ZIP</b> 或单个 <b>.json</b> 文件。<br>在 OrcaSlicer 中: <i>Help → Show Configuration Folder → user/&lt;id&gt;/filament/</i>",
"orca_profile_dropmsg": "拖到此处或点击",
"orca_profile_list_label": "已导入",
"orca_profile_user_label": "自己的配置",
"orca_profile_user_empty": "",
"orca_profile_uploading": "上传中…",
"orca_profile_done": "已导入",
"orca_profile_skipped": "跳过",
"log_dir_all": "全部", "log_dir_all": "全部",
"log_lvl_label": "级别:", "log_lvl_label": "级别:",
"file_ready_btn": "▶ 开始打印", "file_ready_btn": "▶ 开始打印",