Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 636889bdbc | |||
| 3f6ea269e6 | |||
| 3fff6e25f0 | |||
| 0f5a8cbc72 | |||
| a40f14af8e | |||
| 466b8c518d | |||
| 1c5396b37d | |||
| c23deebde5 | |||
| 76738e5961 | |||
| 9c82073540 | |||
| 031e34d8ea | |||
|
|
fc89dfffa5 |
108
CHANGELOG.de.md
108
CHANGELOG.de.md
@@ -1,5 +1,109 @@
|
||||
# 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
|
||||
@@ -11,8 +115,8 @@
|
||||
(`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 aktiv geladene Slot kann
|
||||
während des Drucks nicht umgeschrieben werden — vorher ausziehen.
|
||||
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
|
||||
|
||||
110
CHANGELOG.md
110
CHANGELOG.md
@@ -1,5 +1,111 @@
|
||||
# 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
|
||||
@@ -10,8 +116,8 @@
|
||||
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 currently loaded slot can not
|
||||
be overwritten during a print — unload it first.
|
||||
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 →
|
||||
|
||||
@@ -2,6 +2,8 @@ FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
@@ -13,6 +15,7 @@ COPY data/ ./static/
|
||||
COPY config_loader.py .
|
||||
COPY env_loader.py .
|
||||
COPY kobrax_client.py .
|
||||
COPY orca_filaments.py .
|
||||
COPY VERSION .
|
||||
COPY anycubic_slicer.crt .
|
||||
COPY anycubic_slicer.key .
|
||||
|
||||
49
README.de.md
49
README.de.md
@@ -8,6 +8,11 @@
|
||||
|
||||
Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
||||
|
||||
<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>
|
||||
@@ -32,12 +37,17 @@ Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
||||
|---|---|
|
||||
| 🖨️ | **Druckersteuerung** — Start, Pause, Resume, Abbruch, Temperaturen, Druckgeschwindigkeit |
|
||||
| 📊 | **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 |
|
||||
| 🧩 | **Multi-Printer** — mehrere Drucker in **einer** Bridge-Instanz, Umschalten per Dropdown |
|
||||
| ➕ | **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 |
|
||||
| 🌐 | **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
|
||||
|
||||
Für die beste Erfahrung mit der KX-Bridge bieten wir einen **gepatchten
|
||||
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.
|
||||
Für sauberen AMS-Filament-Sync gibt es einen **gepatchten OrcaSlicer-Build**:
|
||||
|
||||
→ **[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
|
||||
hauptsächlich das AMS-Handling. Es ist ein Build von
|
||||
[OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); der Quellcode
|
||||
ist über die verlinkten PRs verfügbar.
|
||||
**Upstream-PRs im KX-Build:**
|
||||
|
||||
- **[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)
|
||||
- **[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](docs/filament-preset-bridge-guide.md) in die Bridge gezogen hast.
|
||||
|
||||
Stock-Upstream-OrcaSlicer funktioniert für Slicing und Drucken weiterhin — nur das Per-Slot-Vendor-Matching beim AMS-Sync fällt dann weg. Material und Farbe pro Slot kannst du auch ohne den KX-Build über die Bridge ans Drucker-Display schreiben (das läuft über MQTT, nicht über den Slicer).
|
||||
|
||||
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)**
|
||||
von [@gangoke](https://github.com/gangoke) — bindet Sensoren, Drucksteuerung,
|
||||
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
|
||||
> supportet werden. Bei Fragen oder Problemen bitte das verlinkte Repository nutzen.
|
||||
|
||||
79
README.es.md
79
README.es.md
@@ -8,6 +8,10 @@
|
||||
|
||||
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>
|
||||
@@ -32,12 +36,17 @@ Un puente compatible con Moonraker que se comunica directamente con la impresora
|
||||
|---|---|
|
||||
| 🖨️ | **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 de filamento, reasignación por canal, emulación MMU para sincronización de filamento en OrcaSlicer |
|
||||
| 🎨 | **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), interfaz EN/ES |
|
||||
| 🧠 | **OrcaSlicer** — protocolo Moonraker completo (HTTP + WebSocket); usa el [build OrcaSlicer-KX](#-slicer-recomendado) para emparejamiento correcto de vendor por ranura |
|
||||
|
||||
---
|
||||
|
||||
@@ -57,17 +66,43 @@ docker compose up -d
|
||||
|
||||
**Binario Linux (sin Docker):**
|
||||
```bash
|
||||
chmod +x kx-bridge && ./kx-bridge
|
||||
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
|
||||
```
|
||||
|
||||
**EXE Windows (sin Docker):**
|
||||
```
|
||||
kx-bridge.exe
|
||||
```
|
||||
> `config\` y `data\` se crean junto al EXE — instalación portátil.
|
||||
|
||||
> 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.
|
||||
> ⚠️ **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
|
||||
@@ -103,16 +138,28 @@ Impresora → Tipo de conexión **Moonraker** → Host: `http://IP-DEL-PUENTE:71
|
||||
|
||||
## 🎨 Slicer recomendado
|
||||
|
||||
Para la mejor experiencia con KX-Bridge ofrecemos una **versión modificada de OrcaSlicer** que
|
||||
incluye tres PRs abiertos de SoftFever/OrcaSlicer: el perfil de la impresora Anycubic Kobra
|
||||
X, una corrección de GCode multicolor y — lo más importante — una corrección de sincronización de
|
||||
filamento Moonraker/Happy-Hare que mantiene las posiciones de las ranuras AMS intactas incluso con una ranura vacía.
|
||||
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)
|
||||
|
||||
OrcaSlicer estándar también funciona; la versión modificada mejora principalmente el manejo de AMS.
|
||||
Es una versión basada en [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0);
|
||||
el código fuente está disponible a través de los PRs enlazados.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -121,6 +168,12 @@ el código fuente está disponible a través de los PRs enlazados.
|
||||
- **[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.
|
||||
|
||||
45
README.md
45
README.md
@@ -8,6 +8,10 @@
|
||||
|
||||
A Moonraker-compatible bridge that talks directly to the printer.
|
||||
|
||||
<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>
|
||||
@@ -32,12 +36,17 @@ A Moonraker-compatible bridge that talks directly to the printer.
|
||||
|---|---|
|
||||
| 🖨️ | **Printer control** — start, pause, resume, cancel, temperatures, print speed |
|
||||
| 📊 | **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 |
|
||||
| 🧩 | **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 |
|
||||
| 🔁 | **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 |
|
||||
| 🌐 | **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
|
||||
|
||||
For the best KX-Bridge experience we offer a **patched OrcaSlicer build** that
|
||||
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.
|
||||
For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
|
||||
|
||||
→ **[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.
|
||||
It's a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0);
|
||||
source is available via the linked PRs.
|
||||
**Upstream PRs bundled in the KX build:**
|
||||
|
||||
- **[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](docs/filament-preset-bridge-guide.md) flow.
|
||||
|
||||
Stock upstream OrcaSlicer still works for slicing and printing — you just lose the per-slot brand matching on AMS sync. Slot material + colour can still be pushed bridge → printer either way (that goes over MQTT, not via the slicer).
|
||||
|
||||
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)**
|
||||
by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls,
|
||||
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.
|
||||
> For questions or issues, please use the linked repository.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
344
docs/filament-preset-bridge-guide.md
Normal file
344
docs/filament-preset-bridge-guide.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# Eigene Filament-Presets anlegen, prüfen und mit KX-Bridge verknüpfen
|
||||
|
||||
> **Gilt für:** OrcaSlicer-KX v2.4.0-alpha-kx2 oder neuer
|
||||
|
||||
---
|
||||
|
||||
## Was ist die `filament_id` und warum ist sie wichtig?
|
||||
|
||||
Jedes Filament-Preset in OrcaSlicer hat eine interne `filament_id`. Diese ID wird von der KX-Bridge genutzt, um beim AMS-Sync das richtige Preset zuzuordnen.
|
||||
|
||||
- System-Presets (z.B. "Polymaker PolyTerra PLA") haben eine feste ID wie `GFL99` oder `OGFL04`.
|
||||
- **Eigene (User-)Presets** bekommen in OrcaSlicer-KX automatisch eine eindeutige ID, die mit `P` beginnt (z.B. `P3a7f2c1`).
|
||||
|
||||
Ohne eindeutige ID zeigt OrcaSlicer beim Sync immer "Generic PLA" — auch wenn das Preset existiert.
|
||||
|
||||
---
|
||||
|
||||
## 1. Eigenes Filament-Preset anlegen
|
||||
|
||||
1. OrcaSlicer-KX starten
|
||||
2. Rechts oben im **Filament-Dropdown** ein passendes Basis-Preset wählen (z.B. "Generic PLA" oder ein Hersteller-Preset)
|
||||
3. Einstellungen nach Wunsch anpassen (Temperaturen, Kühlung, etc.)
|
||||
4. Auf das **Speichern-Symbol** (Diskette) klicken → **"Save as new preset"**
|
||||
5. Namen eingeben — z.B. `SUNLU PLA+ 2.0`
|
||||
> Der Name muss später exakt so in der Bridge eingetragen werden.
|
||||
6. Drucker auswählen: **Anycubic Kobra X 0.4 nozzle** — wichtig für die Kompatibilität!
|
||||
7. **Speichern** klicken
|
||||
8. OrcaSlicer **einmal neu starten** — erst dann wird die `filament_id` dauerhaft gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## 2. Eindeutige ID prüfen
|
||||
|
||||
Nach dem Neustart prüfen, ob die ID korrekt gesetzt wurde:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Die Datei öffnen und nach `filament_id` suchen:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Korrekt: ID beginnt mit `P` gefolgt von 7 Hex-Zeichen
|
||||
❌ Fehlt oder leer: OrcaSlicer-KX zu alt — Update auf v2.4.0-alpha-kx2 oder neuer
|
||||
|
||||
---
|
||||
|
||||
## 3. Preset auf einen anderen PC übertragen (Import)
|
||||
|
||||
### Exportieren (Quell-PC)
|
||||
|
||||
Die Preset-Datei einfach kopieren:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Importieren (Ziel-PC)
|
||||
|
||||
**Methode A — Datei direkt kopieren:**
|
||||
1. Die `.json`-Datei in das gleiche Verzeichnis auf dem Ziel-PC kopieren
|
||||
2. OrcaSlicer neu starten → Preset erscheint im Dropdown
|
||||
|
||||
**Methode B — OrcaSlicer Import-Funktion:**
|
||||
1. In OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Die `.json`-Datei auswählen
|
||||
3. OrcaSlicer neu starten
|
||||
|
||||
> **Wichtig:** Die `filament_id` in der Datei bleibt erhalten — das Preset wird auf dem Ziel-PC genauso erkannt wie auf dem Quell-PC.
|
||||
|
||||
---
|
||||
|
||||
## 4. Preset in KX-Bridge verknüpfen
|
||||
|
||||
1. KX-Bridge UI öffnen
|
||||
2. **Filament-Verwaltung** → AMS-Slot auswählen
|
||||
3. Im Feld **Filament-Name** exakt den OrcaSlicer-Preset-Namen eintragen:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Speichern
|
||||
|
||||
Die Bridge sendet beim Sync `filament_name: "SUNLU PLA+ 2.0"` → OrcaSlicer findet das Preset anhand von Name und `filament_id` → zeigt es korrekt an.
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
| Was | Warum |
|
||||
|-----|-------|
|
||||
| Name in OrcaSlicer und Bridge müssen **exakt** übereinstimmen | Groß-/Kleinschreibung und Sonderzeichen werden verglichen |
|
||||
| Preset muss für **Anycubic Kobra X 0.4 nozzle** kompatibel sein | Beim Speichern den richtigen Drucker auswählen |
|
||||
| Nach dem ersten Speichern OrcaSlicer **neu starten** | Erst dann wird die `filament_id` persistent geschrieben |
|
||||
| **OrcaSlicer-KX v2.4.0-alpha-kx2** oder neuer verwenden | Ältere Versionen generieren keine eindeutige `filament_id` für User-Presets |
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# How to Create, Verify and Import Custom Filament Presets for KX-Bridge
|
||||
|
||||
> **Requires:** OrcaSlicer-KX v2.4.0-alpha-kx2 or newer
|
||||
|
||||
---
|
||||
|
||||
## What is the `filament_id` and why does it matter?
|
||||
|
||||
Every filament preset in OrcaSlicer has an internal `filament_id`. The KX-Bridge uses this ID to match the correct preset during AMS sync.
|
||||
|
||||
- System presets (e.g. "Polymaker PolyTerra PLA") have a fixed ID like `GFL99` or `OGFL04`.
|
||||
- **Custom (user) presets** automatically receive a unique ID starting with `P` (e.g. `P3a7f2c1`) in OrcaSlicer-KX.
|
||||
|
||||
Without a unique ID, OrcaSlicer will always show "Generic PLA" during sync — even if the preset exists.
|
||||
|
||||
---
|
||||
|
||||
## 1. Create a Custom Filament Preset
|
||||
|
||||
1. Launch OrcaSlicer-KX
|
||||
2. Select a suitable base preset from the **filament dropdown** (e.g. "Generic PLA" or a vendor preset)
|
||||
3. Adjust settings as needed (temperatures, cooling, etc.)
|
||||
4. Click the **save icon** (floppy disk) → **"Save as new preset"**
|
||||
5. Enter a name — e.g. `SUNLU PLA+ 2.0`
|
||||
> This name must be entered in the bridge exactly as typed here.
|
||||
6. Select printer: **Anycubic Kobra X 0.4 nozzle** — required for compatibility!
|
||||
7. Click **Save**
|
||||
8. **Restart OrcaSlicer once** — the `filament_id` is only written permanently after a restart.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verify the Unique ID
|
||||
|
||||
After restarting, check that the ID was set correctly:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Open the file and look for `filament_id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Correct: ID starts with `P` followed by 7 hex characters
|
||||
❌ Missing or empty: Your OrcaSlicer-KX version is too old — update to v2.4.0-alpha-kx2 or newer
|
||||
|
||||
---
|
||||
|
||||
## 3. Transfer a Preset to Another PC (Import)
|
||||
|
||||
### Export (source PC)
|
||||
|
||||
Simply copy the preset file:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Import (target PC)
|
||||
|
||||
**Method A — Copy file directly:**
|
||||
1. Copy the `.json` file to the same directory on the target PC
|
||||
2. Restart OrcaSlicer → preset appears in the dropdown
|
||||
|
||||
**Method B — OrcaSlicer import function:**
|
||||
1. In OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Select the `.json` file
|
||||
3. Restart OrcaSlicer
|
||||
|
||||
> **Note:** The `filament_id` inside the file is preserved — the preset will be recognized on the target PC exactly as on the source PC.
|
||||
|
||||
---
|
||||
|
||||
## 4. Link the Preset in KX-Bridge
|
||||
|
||||
1. Open the KX-Bridge UI
|
||||
2. Go to **Filament Management** → select the AMS slot
|
||||
3. In the **Filament Name** field, enter the OrcaSlicer preset name exactly:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Save
|
||||
|
||||
The bridge sends `filament_name: "SUNLU PLA+ 2.0"` during sync → OrcaSlicer matches by name and `filament_id` → displays the preset correctly.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| What | Why |
|
||||
|------|-----|
|
||||
| Name in OrcaSlicer and Bridge must match **exactly** | Case and special characters are compared |
|
||||
| Preset must be compatible with **Anycubic Kobra X 0.4 nozzle** | Select the correct printer when saving |
|
||||
| **Restart OrcaSlicer** after saving for the first time | The `filament_id` is only written persistently after a restart |
|
||||
| Use **OrcaSlicer-KX v2.4.0-alpha-kx2** or newer | Older versions do not generate a unique `filament_id` for user presets |
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# Cómo crear, verificar e importar perfiles de filamento personalizados para KX-Bridge
|
||||
|
||||
> **Requiere:** OrcaSlicer-KX v2.4.0-alpha-kx2 o superior
|
||||
|
||||
---
|
||||
|
||||
## ¿Qué es el `filament_id` y por qué es importante?
|
||||
|
||||
Cada perfil de filamento en OrcaSlicer tiene un `filament_id` interno. KX-Bridge usa este ID para asignar el perfil correcto durante la sincronización AMS.
|
||||
|
||||
- Los perfiles del sistema (p. ej. "Polymaker PolyTerra PLA") tienen un ID fijo como `GFL99` o `OGFL04`.
|
||||
- Los **perfiles personalizados (usuario)** reciben automáticamente un ID único que empieza por `P` (p. ej. `P3a7f2c1`) en OrcaSlicer-KX.
|
||||
|
||||
Sin un ID único, OrcaSlicer mostrará siempre "Generic PLA" durante la sincronización, aunque el perfil exista.
|
||||
|
||||
---
|
||||
|
||||
## 1. Crear un perfil de filamento personalizado
|
||||
|
||||
1. Iniciar OrcaSlicer-KX
|
||||
2. Seleccionar un perfil base adecuado en el **menú desplegable de filamento** (p. ej. "Generic PLA" o un perfil de fabricante)
|
||||
3. Ajustar la configuración según sea necesario (temperaturas, refrigeración, etc.)
|
||||
4. Hacer clic en el **icono de guardar** (disquete) → **"Save as new preset"**
|
||||
5. Introducir un nombre — p. ej. `SUNLU PLA+ 2.0`
|
||||
> Este nombre debe introducirse en la bridge exactamente igual.
|
||||
6. Seleccionar impresora: **Anycubic Kobra X 0.4 nozzle** — ¡necesario para la compatibilidad!
|
||||
7. Hacer clic en **Guardar**
|
||||
8. **Reiniciar OrcaSlicer una vez** — el `filament_id` solo se escribe de forma permanente tras un reinicio.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verificar el ID único
|
||||
|
||||
Tras reiniciar, comprobar que el ID se ha establecido correctamente:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Abrir el archivo y buscar `filament_id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Correcto: el ID empieza por `P` seguido de 7 caracteres hexadecimales
|
||||
❌ Falta o está vacío: la versión de OrcaSlicer-KX es demasiado antigua — actualizar a v2.4.0-alpha-kx2 o superior
|
||||
|
||||
---
|
||||
|
||||
## 3. Transferir un perfil a otro PC (importar)
|
||||
|
||||
### Exportar (PC de origen)
|
||||
|
||||
Simplemente copiar el archivo del perfil:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Importar (PC de destino)
|
||||
|
||||
**Método A — Copiar el archivo directamente:**
|
||||
1. Copiar el archivo `.json` al mismo directorio en el PC de destino
|
||||
2. Reiniciar OrcaSlicer → el perfil aparece en el menú desplegable
|
||||
|
||||
**Método B — Función de importación de OrcaSlicer:**
|
||||
1. En OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Seleccionar el archivo `.json`
|
||||
3. Reiniciar OrcaSlicer
|
||||
|
||||
> **Nota:** El `filament_id` dentro del archivo se conserva — el perfil se reconocerá en el PC de destino exactamente igual que en el de origen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Vincular el perfil en KX-Bridge
|
||||
|
||||
1. Abrir la interfaz de KX-Bridge
|
||||
2. Ir a **Gestión de filamentos** → seleccionar la ranura AMS
|
||||
3. En el campo **Nombre de filamento**, introducir el nombre exacto del perfil de OrcaSlicer:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Guardar
|
||||
|
||||
La bridge envía `filament_name: "SUNLU PLA+ 2.0"` durante la sincronización → OrcaSlicer busca por nombre y `filament_id` → muestra el perfil correctamente.
|
||||
|
||||
---
|
||||
|
||||
## Referencia rápida
|
||||
|
||||
| Qué | Por qué |
|
||||
|-----|---------|
|
||||
| El nombre en OrcaSlicer y en Bridge debe coincidir **exactamente** | Se comparan mayúsculas, minúsculas y caracteres especiales |
|
||||
| El perfil debe ser compatible con **Anycubic Kobra X 0.4 nozzle** | Seleccionar la impresora correcta al guardar |
|
||||
| **Reiniciar OrcaSlicer** tras guardar por primera vez | El `filament_id` solo se escribe de forma permanente tras un reinicio |
|
||||
| Usar **OrcaSlicer-KX v2.4.0-alpha-kx2** o superior | Las versiones anteriores no generan un `filament_id` único para perfiles de usuario |
|
||||
@@ -154,6 +154,13 @@ class KobraXClient:
|
||||
# -- Connection ----------------------------------------------------------
|
||||
|
||||
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.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
@@ -222,7 +222,7 @@ def _parse_gcode_estimated_time(data: bytes) -> int:
|
||||
elif unit == "m": secs += int(val) * 60
|
||||
elif unit == "s": secs += int(val)
|
||||
if secs:
|
||||
log.info(f"Slicer-Schätzzeit: {secs}s ({m.group(1).strip()})")
|
||||
log.info(f"Slicer estimate: {secs}s ({m.group(1).strip()})")
|
||||
return secs
|
||||
|
||||
|
||||
@@ -798,6 +798,7 @@ class KobraXBridge:
|
||||
self._serve_dir_path: str = self._store._gcode_dir
|
||||
self._current_job_id: str = ""
|
||||
self._camera_autostarted: bool = False
|
||||
self._camera_user_stopped: bool = False # User hat Kamera während Druck manuell gestoppt
|
||||
self.camera_cache: CameraCache = CameraCache()
|
||||
|
||||
self._thumbnail_b64: str = ""
|
||||
@@ -809,7 +810,7 @@ class KobraXBridge:
|
||||
# Theme-Name prüfen (keine Sonderzeichen oder Umlaute)
|
||||
raw_theme = (getattr(args, "ui_theme", None) or "default").strip()
|
||||
if not _UI_THEME_NAME_RE.match(raw_theme):
|
||||
log.warning("Ungültiger UI-Theme-Name %r – nutze default", raw_theme)
|
||||
log.warning("Invalid UI theme name %r – using default", raw_theme)
|
||||
raw_theme = "default"
|
||||
self._ui_theme = raw_theme
|
||||
self._index_tpl_cache: str | None = None
|
||||
@@ -914,7 +915,9 @@ class KobraXBridge:
|
||||
# Zentral hier, damit es alle Druck-Startwege abdeckt (OrcaSlicer + UI).
|
||||
# _camera_autostarted verhindert Mehrfach-Trigger pro Druck.
|
||||
if kobra_state == "printing":
|
||||
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
|
||||
if (getattr(self._args, "camera_on_print", 0)
|
||||
and not self._camera_autostarted
|
||||
and not self._camera_user_stopped):
|
||||
self._camera_autostarted = True
|
||||
try:
|
||||
self.client.start_camera()
|
||||
@@ -923,6 +926,7 @@ class KobraXBridge:
|
||||
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||
self._camera_autostarted = False
|
||||
self._camera_user_stopped = False # für nächsten Druck freigeben
|
||||
|
||||
# Job-History: Druckstart erkennen
|
||||
if kobra_state == "printing" and not self._current_job_id:
|
||||
@@ -934,7 +938,7 @@ class KobraXBridge:
|
||||
gcode_file_id=gf["id"],
|
||||
printer_id=self._printer_id,
|
||||
)
|
||||
log.info(f"Job gestartet: {self._current_job_id} für {filename}")
|
||||
log.info(f"Job started: {self._current_job_id} for {filename}")
|
||||
|
||||
# Job-History: Druckende erkennen
|
||||
if kobra_state in ("finished",) and self._current_job_id:
|
||||
@@ -1001,7 +1005,9 @@ class KobraXBridge:
|
||||
# Kamera-Autostart auch hier (OrcaSlicer meldet Start oft via info/report).
|
||||
# _camera_autostarted-Guard verhindert Doppel-Start mit _on_print.
|
||||
if kobra_state == "printing":
|
||||
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
|
||||
if (getattr(self._args, "camera_on_print", 0)
|
||||
and not self._camera_autostarted
|
||||
and not self._camera_user_stopped):
|
||||
self._camera_autostarted = True
|
||||
try:
|
||||
self.client.start_camera()
|
||||
@@ -1010,6 +1016,7 @@ class KobraXBridge:
|
||||
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||
self._camera_autostarted = False
|
||||
self._camera_user_stopped = False # für nächsten Druck freigeben
|
||||
if project:
|
||||
if "filename" in project:
|
||||
self._state["filename"] = project["filename"]
|
||||
@@ -1075,7 +1082,7 @@ class KobraXBridge:
|
||||
if filename:
|
||||
try:
|
||||
self._store.update_file_objects(filename, objs, svg)
|
||||
log.info(f"Skip-Objekte für {filename}: {len(objs)} ({'mit SVG' if svg else 'ohne SVG'})")
|
||||
log.info(f"Skip objects for {filename}: {len(objs)} ({'with SVG' if svg else 'no SVG'})")
|
||||
except Exception as e:
|
||||
log.warning(f"update_file_objects fehlgeschlagen: {e}")
|
||||
self._push_status_update()
|
||||
@@ -1494,12 +1501,30 @@ class KobraXBridge:
|
||||
self._push_status_update()
|
||||
|
||||
# OrcaSlicer filament preset IDs (MoonrakerPrinterAgent.cpp mapping)
|
||||
# Default-Mapping pro Material-Typ wenn der User keinen Slot-Profil-
|
||||
# Override gesetzt hat. Für den Kobra X bevorzugen wir Anycubic-eigene
|
||||
# Filament-IDs aus den `@Anycubic Kobra X 0.4 nozzle`-Profilen — die
|
||||
# sind druckerspezifisch is_compatible und werden von OrcaSlicer direkt
|
||||
# gematched. Library-Fallbacks (OGF*) nur für Material-Typen ohne
|
||||
# Kobra-X-spezifisches Anycubic-Profil — deren @System-Profile haben
|
||||
# `compatible_printers: []` (= mit allen Druckern kompatibel).
|
||||
_TRAY_INFO_IDX = {
|
||||
"PLA": "OGFL99", "PLA-CF": "OGFL98", "PLA SILK": "OGFL96",
|
||||
"PETG": "OGFG99", "PETG-CF": "OGFG98",
|
||||
"ABS": "OGFB99", "ASA": "OGFB98",
|
||||
"TPU": "OGFT99", "PA": "OGFP99", "PA-CF": "OGFP98",
|
||||
"PC": "OGFC99", "HIPS": "OGFH99", "PVA": "OGFV99",
|
||||
# Anycubic-eigene Kobra-X-Profile
|
||||
"PLA": "GFPLA",
|
||||
"PLA+": "GFPLA+",
|
||||
"PLA SILK": "GFPLA Silk",
|
||||
"PETG": "GFPETG",
|
||||
"ABS": "GFABS",
|
||||
"ASA": "GFASA",
|
||||
"TPU": "GFTPU 95A",
|
||||
"PVA": "GFPVA",
|
||||
# Kein Anycubic-Kobra-X-Profil → Library-Fallback
|
||||
"PLA-CF": "OGFL98",
|
||||
"PETG-CF": "OGFG98",
|
||||
"PA": "OGFN99",
|
||||
"PA-CF": "OGFN98",
|
||||
"PC": "OGFC99",
|
||||
"HIPS": "OGFS98",
|
||||
}
|
||||
|
||||
def _build_lane_data(self) -> dict:
|
||||
@@ -1556,9 +1581,13 @@ class KobraXBridge:
|
||||
fila_name = user_profile.get("name", "")
|
||||
tray_info_idx = user_profile.get("id") or self._TRAY_INFO_IDX.get(material, "OGFL99")
|
||||
else:
|
||||
vendor = ""
|
||||
fila_name = ""
|
||||
tray_info_idx = self._TRAY_INFO_IDX.get(material, "OGFL99")
|
||||
# Default: Library-Generic-Profil (siehe _default_filament_name) —
|
||||
# ist mit allen Druckern kompatibel und garantiert sichtbar.
|
||||
# Der User wählt pro Slot bewusst eine konkrete Marke wenn er
|
||||
# eine will; Default bleibt neutral.
|
||||
fila_name = self._default_filament_name(material)
|
||||
vendor = "Generic" if fila_name.startswith("Generic ") else ""
|
||||
tray_info_idx = self._lookup_filament_id(vendor, fila_name) or self._TRAY_INFO_IDX.get(material, "OGFL99")
|
||||
tray_array.append({
|
||||
"id": str(slot_id),
|
||||
"tag_uid": "0000000000000000",
|
||||
@@ -1566,17 +1595,20 @@ class KobraXBridge:
|
||||
"tray_type": material,
|
||||
"tray_color": color_hex,
|
||||
"tray_sub_brands": vendor,
|
||||
# OrcaSlicer-Empfangs-Patch PR #13719 erwartet `name` +
|
||||
# `vendor_name` pro Lane (Stufen-Matching: Vendor+Name → Name →
|
||||
# filament_id_by_type). Wir senden beide Schreibweisen mit
|
||||
# damit ältere Patch-Varianten + zukünftige Upstream-PRs beide
|
||||
# bedient sind.
|
||||
"name": fila_name,
|
||||
"vendor_name": vendor,
|
||||
# Aliase für ältere Patch-Varianten (Variante 2,
|
||||
# MoonrakerPrinterAgent.cpp): filament_id direkt (exakt),
|
||||
# sonst preset-Name per find_preset() auflösen.
|
||||
"filament_id": tray_info_idx,
|
||||
"filament_vendor": vendor,
|
||||
# Für den OrcaSlicer-Empfangs-Patch (Variante 2,
|
||||
# MoonrakerPrinterAgent.cpp): `filament_id` direkt
|
||||
# übernehmen (exakt), sonst `preset`-Name per
|
||||
# find_preset() auflösen. tray_info_idx ist im Orca-
|
||||
# Datenmodell nicht eindeutig (z.B. OGFL99 für 136
|
||||
# Profile), aber der Bare-Name aus orca_filaments.json
|
||||
# ist eindeutig — find_preset() parsed @-Suffixe weg.
|
||||
"filament_id": tray_info_idx,
|
||||
"preset": fila_name,
|
||||
"filament_name": fila_name, # ältere Aliase
|
||||
"filament_name": fila_name,
|
||||
"preset": fila_name,
|
||||
})
|
||||
else:
|
||||
tray_array.append({
|
||||
@@ -1695,6 +1727,7 @@ class KobraXBridge:
|
||||
"TPU": 220, "PA": 260, "PC": 270, "HIPS": 220}
|
||||
num_gates = len(slots)
|
||||
gate_status, gate_material, gate_color, gate_temperature, gate_color_rgb = [], [], [], [], []
|
||||
gate_filament_name = []
|
||||
for _global_index, slot in slots:
|
||||
occupied = slot.get("status") == 5
|
||||
gate_status.append(1 if occupied else 0)
|
||||
@@ -1706,6 +1739,17 @@ class KobraXBridge:
|
||||
gate_color.append("{:02X}{:02X}{:02X}".format(*c[:3]) if occupied else "")
|
||||
gate_color_rgb.append([round(c[0]/255, 3), round(c[1]/255, 3), round(c[2]/255, 3)] if occupied else [0.0, 0.0, 0.0])
|
||||
gate_temperature.append(_TEMP.get(material, 210) if occupied else 0)
|
||||
# gate_filament_name aus User-Override oder Material-Default für den
|
||||
# HH-Pfad in OrcaSlicer (fetch_hh_filament_info). Wenn Orca den
|
||||
# HH-Pfad wählt (MMU-Erkennung), wertet PR #13719 dieses Feld als
|
||||
# Preset-Namen aus → 'Anycubic PLA' matched das druckerspezifische
|
||||
# Preset, leerer String führte vorher auf Generic PLA.
|
||||
if occupied:
|
||||
user_profile = self._filament_profiles.get(_global_index) or {}
|
||||
fila_name = user_profile.get("name") or self._default_filament_name(material)
|
||||
gate_filament_name.append(fila_name)
|
||||
else:
|
||||
gate_filament_name.append("")
|
||||
|
||||
loaded_index_map = {global_index: idx for idx, (global_index, _) in enumerate(slots)}
|
||||
active_gate = loaded_index_map.get(int(self._ams_loaded_slot), -1)
|
||||
@@ -1717,13 +1761,36 @@ class KobraXBridge:
|
||||
"gate_color": gate_color,
|
||||
"gate_temperature": gate_temperature,
|
||||
"gate_color_rgb": gate_color_rgb,
|
||||
"gate_filament_name": [""] * num_gates,
|
||||
"gate_filament_name": gate_filament_name,
|
||||
"gate_spool_id": [-1] * num_gates,
|
||||
"ttg_map": list(range(num_gates)),
|
||||
"tool": active_gate,
|
||||
"gate": active_gate,
|
||||
}
|
||||
|
||||
def _default_filament_name(self, material: str) -> str:
|
||||
"""Default-Name für `gate_filament_name`/`name` in lane_data wenn kein
|
||||
User-Override gesetzt ist. Bewusste Designentscheidung: **immer
|
||||
Generic <Typ>** als Default — das Library-Profil ist `compatible_printers:[]`
|
||||
(= mit jedem Drucker kompatibel) und damit garantiert sichtbar.
|
||||
|
||||
OrcaSlicer matcht dann das neutrale Generic-Preset und der User
|
||||
kann pro Slot eine konkrete Marke setzen wenn er das will."""
|
||||
if not material:
|
||||
return ""
|
||||
mat = material.upper().strip()
|
||||
profs = self._load_orca_filaments()
|
||||
def _match_type(p: dict) -> bool:
|
||||
pt = (p.get("type") or "").upper()
|
||||
return pt == mat or pt.startswith(mat + "-") or pt.startswith(mat + " ")
|
||||
# Library-Generic-Profil (immer is_visible+is_compatible)
|
||||
for p in profs:
|
||||
if p.get("vendor") == "Generic" and p.get("name", "").startswith("Generic ") and _match_type(p):
|
||||
return p.get("name", "")
|
||||
# Falls die Library-Generic für diesen exotischen Material-Typ fehlt,
|
||||
# liefern wir nichts — OrcaSlicer fällt auf filament_id_by_type zurück.
|
||||
return ""
|
||||
|
||||
def _build_printer_objects(self) -> dict:
|
||||
s = self._state
|
||||
return {
|
||||
@@ -1824,6 +1891,41 @@ class KobraXBridge:
|
||||
"gcode_macro TIMELAPSE_TAKE_FRAME": {
|
||||
"is_paused": False,
|
||||
},
|
||||
# configfile stub — Mobileraker und andere Clients crashen ohne
|
||||
# dieses Objekt (Missing field: configFile). Werte aus der
|
||||
# entschlüsselten avata_main.conf (ACCFG1.0 — Kobra X Firmware).
|
||||
"configfile": {
|
||||
"config": {},
|
||||
"settings": {
|
||||
"printer": {
|
||||
"kinematics": "cartesian",
|
||||
"max_velocity": 450,
|
||||
"max_accel": 10000,
|
||||
"max_z_velocity": 12,
|
||||
"max_z_accel": 100,
|
||||
"square_corner_velocity": 20.0,
|
||||
},
|
||||
"extruder": {
|
||||
"nozzle_diameter": 0.4,
|
||||
"min_temp": 0,
|
||||
"max_temp": 320,
|
||||
"min_extrude_temp": 10,
|
||||
},
|
||||
"heater_bed": {
|
||||
"min_temp": 0,
|
||||
"max_temp": 120,
|
||||
},
|
||||
"stepper_x": {"position_min": -18.5, "position_max": 280},
|
||||
"stepper_y": {"position_min": -6.5, "position_max": 272.5},
|
||||
"stepper_z": {"position_min": -4, "position_max": 262},
|
||||
"virtual_sdcard": {"path": "/data/gcodes"},
|
||||
"pause_resume": {},
|
||||
"display_status": {},
|
||||
},
|
||||
"warnings": [],
|
||||
"save_config_pending": False,
|
||||
"save_config_pending_items": {},
|
||||
},
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -1946,6 +2048,128 @@ class KobraXBridge:
|
||||
profiles = [p for p in profiles if p.get("vendor", "") == vendor_filter]
|
||||
return self._json_cors({"result": profiles})
|
||||
|
||||
async def handle_kx_filament_profiles_user_list(self, request):
|
||||
"""GET /kx/filament/profiles/user — nur die User-importierten Profile,
|
||||
für den Settings-Tab (Verwaltung mit Lösch-Buttons)."""
|
||||
path = self._orca_filaments_user_path()
|
||||
if not os.path.isfile(path):
|
||||
return self._json_cors({"result": []})
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
user_profiles = json.load(f) or []
|
||||
except Exception:
|
||||
user_profiles = []
|
||||
return self._json_cors({"result": user_profiles})
|
||||
|
||||
async def handle_kx_filament_profiles_import(self, request):
|
||||
"""POST /kx/filament/profiles/user — multipart-Upload mit einer
|
||||
ZIP-Datei oder mehreren `.json`-Files aus
|
||||
~/.config/OrcaSlicer/user/<id>/filament/.
|
||||
|
||||
Bestehende User-Profile mit gleichem (vendor, name)-Key werden
|
||||
überschrieben. Geparste Profile haben dasselbe Schema wie
|
||||
orca_filaments.json (id, name, vendor, type, color)."""
|
||||
import io, zipfile
|
||||
from orca_filaments import parse_profile_bytes
|
||||
added: list[dict] = []
|
||||
skipped: int = 0
|
||||
# System-Index für Inherits-Resolve: User-Profile referenzieren
|
||||
# System-Parents via "inherits" (z.B. "Generic PLA @System"). Damit
|
||||
# können wir filament_id/vendor/type/color aus dem System-Parent
|
||||
# ziehen wenn das User-Profil sie selbst nicht setzt.
|
||||
sys_idx = [p for p in self._load_orca_filaments() if not p.get("is_user")]
|
||||
try:
|
||||
reader = await request.multipart()
|
||||
except Exception:
|
||||
return self._json_cors({"error": "expected multipart"}, status=400)
|
||||
async for part in reader:
|
||||
if part.name not in ("file", "files", "upload"):
|
||||
continue
|
||||
blob = await part.read()
|
||||
fn = (part.filename or "").lower()
|
||||
if fn.endswith(".zip"):
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(blob)) as zf:
|
||||
for inner in zf.namelist():
|
||||
if not inner.lower().endswith(".json"):
|
||||
continue
|
||||
try:
|
||||
with zf.open(inner) as zf_in:
|
||||
p = parse_profile_bytes(zf_in.read(), source_name=inner, system_index=sys_idx)
|
||||
except Exception:
|
||||
skipped += 1
|
||||
continue
|
||||
if p:
|
||||
added.append(p)
|
||||
else:
|
||||
skipped += 1
|
||||
except zipfile.BadZipFile:
|
||||
return self._json_cors({"error": "bad zip"}, status=400)
|
||||
elif fn.endswith(".json"):
|
||||
p = parse_profile_bytes(blob, source_name=fn, system_index=sys_idx)
|
||||
if p:
|
||||
added.append(p)
|
||||
else:
|
||||
skipped += 1
|
||||
|
||||
if not added:
|
||||
return self._json_cors({"result": "ok", "added": 0, "skipped": skipped})
|
||||
|
||||
# Merge mit existierender User-JSON (gleicher (vendor,name) → ersetzen)
|
||||
path = self._orca_filaments_user_path()
|
||||
existing: list[dict] = []
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
existing = json.load(f) or []
|
||||
except Exception:
|
||||
existing = []
|
||||
by_key = {(p.get("vendor"), p.get("name")): p for p in existing}
|
||||
for p in added:
|
||||
by_key[(p.get("vendor"), p.get("name"))] = p
|
||||
merged = sorted(by_key.values(), key=lambda x: (x.get("vendor",""), x.get("name","")))
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(merged, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
except Exception as e:
|
||||
return self._json_cors({"error": f"write failed: {e}"}, status=500)
|
||||
self._invalidate_filaments_cache()
|
||||
return self._json_cors({"result": "ok",
|
||||
"added": len(added),
|
||||
"skipped": skipped,
|
||||
"total_user": len(merged)})
|
||||
|
||||
async def handle_kx_filament_profiles_user_delete(self, request):
|
||||
"""DELETE /kx/filament/profiles/user — löscht entweder einen einzelnen
|
||||
Eintrag (?vendor=…&name=…) oder alle wenn keine Query angegeben."""
|
||||
vendor = request.rel_url.query.get("vendor", "").strip()
|
||||
name = request.rel_url.query.get("name", "").strip()
|
||||
path = self._orca_filaments_user_path()
|
||||
if not os.path.isfile(path):
|
||||
return self._json_cors({"result": "ok", "removed": 0})
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
existing = json.load(f) or []
|
||||
except Exception:
|
||||
existing = []
|
||||
before = len(existing)
|
||||
if vendor and name:
|
||||
existing = [p for p in existing
|
||||
if not (p.get("vendor") == vendor and p.get("name") == name)]
|
||||
else:
|
||||
existing = []
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(existing, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
except Exception as e:
|
||||
return self._json_cors({"error": str(e)}, status=500)
|
||||
self._invalidate_filaments_cache()
|
||||
return self._json_cors({"result": "ok",
|
||||
"removed": before - len(existing),
|
||||
"total_user": len(existing)})
|
||||
|
||||
def _find_orca_filaments_json(self) -> str | None:
|
||||
"""Findet die statische JSON-Datei. Liegt analog zu web/ unter _WEB_BASE/data/
|
||||
— in allen 3 Deployment-Modi:
|
||||
@@ -2022,21 +2246,45 @@ class KobraXBridge:
|
||||
"id": entry.get("id", "")})
|
||||
|
||||
def _load_orca_filaments(self) -> list[dict]:
|
||||
"""Lädt orca_filaments.json einmalig in den Cache. Bei wiederholten
|
||||
Aufrufen wird die Liste aus dem RAM geliefert."""
|
||||
"""Lädt System- + User-Profile aus dem Cache. System-Profile kommen
|
||||
aus bridge/data/orca_filaments.json (Image-embedded), User-Profile
|
||||
aus <KX_DATA_DIR>/orca_filaments.user.json (Volume-persistent —
|
||||
überlebt Image-Updates). User-Profile bekommen ein `is_user: True`-
|
||||
Flag damit das Frontend sie markieren kann."""
|
||||
if getattr(self, "_orca_filaments_cache", None) is not None:
|
||||
return self._orca_filaments_cache
|
||||
self._orca_filaments_cache = []
|
||||
data_path = self._find_orca_filaments_json()
|
||||
if not data_path or not os.path.isfile(data_path):
|
||||
return self._orca_filaments_cache
|
||||
try:
|
||||
with open(data_path, encoding="utf-8") as f:
|
||||
self._orca_filaments_cache = json.load(f) or []
|
||||
except Exception as e:
|
||||
log.warning(f"orca_filaments.json read error: {e}")
|
||||
merged: list[dict] = []
|
||||
# System
|
||||
sys_path = self._find_orca_filaments_json()
|
||||
if sys_path and os.path.isfile(sys_path):
|
||||
try:
|
||||
with open(sys_path, encoding="utf-8") as f:
|
||||
merged.extend(json.load(f) or [])
|
||||
except Exception as e:
|
||||
log.warning(f"orca_filaments.json read error: {e}")
|
||||
# User
|
||||
usr_path = self._orca_filaments_user_path()
|
||||
if usr_path and os.path.isfile(usr_path):
|
||||
try:
|
||||
with open(usr_path, encoding="utf-8") as f:
|
||||
for p in (json.load(f) or []):
|
||||
p["is_user"] = True
|
||||
merged.append(p)
|
||||
except Exception as e:
|
||||
log.warning(f"orca_filaments.user.json read error: {e}")
|
||||
self._orca_filaments_cache = merged
|
||||
return self._orca_filaments_cache
|
||||
|
||||
def _orca_filaments_user_path(self) -> str:
|
||||
"""Pfad zur User-Profile-JSON. Liegt im Volume-Mount (KX_DATA_DIR),
|
||||
damit Image-Updates die Daten nicht zerstören."""
|
||||
data_dir = os.environ.get("KX_DATA_DIR") or os.path.join(_WEB_BASE, "data")
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
return os.path.join(data_dir, "orca_filaments.user.json")
|
||||
|
||||
def _invalidate_filaments_cache(self):
|
||||
self._orca_filaments_cache = None
|
||||
|
||||
def _lookup_filament_id(self, vendor: str, name: str) -> str:
|
||||
"""Sucht in orca_filaments.json die filament_id zu einem (vendor,name)-
|
||||
Tupel. Liefert '' wenn nicht gefunden."""
|
||||
@@ -2449,7 +2697,20 @@ class KobraXBridge:
|
||||
return web.json_response({"result": {"count": len(result_jobs), "jobs": result_jobs}})
|
||||
|
||||
async def handle_webcams_list(self, request):
|
||||
"""Moonraker /server/webcams/list — Obico holt die Webcam-URLs hier."""
|
||||
"""Moonraker /server/webcams/list — Obico holt die Webcam-URLs hier.
|
||||
|
||||
Wenn der Client von einem anderen Host kommt (z.B. moonraker-obico auf
|
||||
separatem Server), braucht er absolute URLs damit er den Stream erreicht.
|
||||
Host-Header mit localhost/127.0.0.1 wird durch die echte LAN-IP ersetzt."""
|
||||
host_hdr = request.headers.get("Host", "") if request else ""
|
||||
host_name = (host_hdr or "").split(":")[0]
|
||||
port_part = f":{host_hdr.split(':')[1]}" if ":" in (host_hdr or "") else f":{self._args.port}"
|
||||
local_ip = getattr(self, "_local_ip", None) or host_name
|
||||
if host_name in ("localhost", "127.0.0.1", ""):
|
||||
host_name = local_ip
|
||||
base = f"http://{host_name}{port_part}"
|
||||
stream_url = f"{base}/api/camera/stream"
|
||||
snapshot_url = f"{base}/api/camera/snapshot"
|
||||
return web.json_response({
|
||||
"result": {
|
||||
"webcams": [
|
||||
@@ -2461,8 +2722,8 @@ class KobraXBridge:
|
||||
"icon": "mdiWebcam",
|
||||
"target_fps": 5,
|
||||
"target_fps_idle": 2,
|
||||
"stream_url": "/api/camera/stream",
|
||||
"snapshot_url": "/api/camera/snapshot",
|
||||
"stream_url": stream_url,
|
||||
"snapshot_url": snapshot_url,
|
||||
"flip_horizontal": False,
|
||||
"flip_vertical": False,
|
||||
"rotation": 0,
|
||||
@@ -2647,7 +2908,7 @@ class KobraXBridge:
|
||||
log.info(f"print/start → {filename} url={url} ams={len(ams_box_mapping)} slots mode={self._filament_mode}")
|
||||
result = self.client.publish("print", "start", payload, timeout=15.0)
|
||||
if result:
|
||||
log.info(f"Druckstart bestätigt: state={result.get('state')}")
|
||||
log.info(f"Print start confirmed: state={result.get('state')}")
|
||||
else:
|
||||
log.warning("Druckstart: keine Antwort vom Drucker")
|
||||
|
||||
@@ -2902,7 +3163,7 @@ class KobraXBridge:
|
||||
return web.json_response({"result": "disconnected"})
|
||||
|
||||
async def handle_api_restart(self, request):
|
||||
log.info("Neustart über API angefordert")
|
||||
log.info("Restart requested via API")
|
||||
response = web.json_response({"status": "restarting"})
|
||||
asyncio.get_event_loop().call_later(0.3, self._restart_bridge)
|
||||
return response
|
||||
@@ -3085,13 +3346,10 @@ class KobraXBridge:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _send():
|
||||
return self.client.publish("multiColorBox", "setDry", payload, timeout=5)
|
||||
|
||||
resp = await loop.run_in_executor(None, _send)
|
||||
if resp is None:
|
||||
return web.json_response({"error": "No response from printer"}, status=504)
|
||||
if int(resp.get("code", 200)) != 200:
|
||||
return web.json_response({"error": f"Printer rejected command: {resp}"}, status=502)
|
||||
return self.client.publish("multiColorBox", "setDry", payload, timeout=0)
|
||||
# Fire-and-forget: setDry ACK arrives via multiColorBox/report callback.
|
||||
# Waiting for a response on that busy push topic causes false "code:0" rejections.
|
||||
await loop.run_in_executor(None, _send)
|
||||
|
||||
self._state["ace_drying"] = ui_state
|
||||
self._state_dirty = True
|
||||
@@ -3185,6 +3443,9 @@ class KobraXBridge:
|
||||
await loop.run_in_executor(None, lambda: self.client.publish(
|
||||
"video", "stopCapture", None, timeout=0
|
||||
))
|
||||
# Verhindert dass der Autostart-Guard die Kamera während des
|
||||
# laufenden Drucks wieder einschaltet (State-Flicker-Problem).
|
||||
self._camera_user_stopped = True
|
||||
return web.json_response({"result": "ok"})
|
||||
|
||||
async def handle_api_camera_snapshot(self, request):
|
||||
@@ -3244,7 +3505,7 @@ class KobraXBridge:
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
log.warning("Kamera: ffmpeg nicht gefunden – Kamerastream nicht verfügbar")
|
||||
log.warning("Camera: ffmpeg not found – camera stream unavailable")
|
||||
return web.Response(status=503, text="ffmpeg not found")
|
||||
except Exception as e:
|
||||
log.warning(f"Kamera: ffmpeg konnte nicht gestartet werden: {e}")
|
||||
@@ -3339,7 +3600,7 @@ class KobraXBridge:
|
||||
if not os.path.isfile(serve_path):
|
||||
return web.Response(status=404, text="not found")
|
||||
size = os.path.getsize(serve_path)
|
||||
log.info(f"Drucker lädt Datei ab: {filename} ({size} bytes)")
|
||||
log.info(f"Printer downloading file: {filename} ({size} bytes)")
|
||||
return web.FileResponse(serve_path, headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
})
|
||||
@@ -3377,6 +3638,7 @@ class KobraXBridge:
|
||||
"remain_time": s["remain_time"],
|
||||
"curr_layer": s["curr_layer"],
|
||||
"total_layers": s["total_layers"],
|
||||
"z_mm": self._estimate_current_z(),
|
||||
"filename": s["filename"],
|
||||
"slicer_time": slicer_time,
|
||||
"camera_url": s["camera_url"],
|
||||
@@ -3654,7 +3916,7 @@ class KobraXBridge:
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write("# KX-Bridge Konfigurationsdatei\n\n")
|
||||
cfg.write(f)
|
||||
log.info(f"Drucker '{name or creds['model']}' als {sec} hinzugefügt (Port {new_port})")
|
||||
log.info(f"Printer '{name or creds['model']}' added as {sec} (port {new_port})")
|
||||
response = self._json_cors({"status": "restarting", "section": sec, "http_port": new_port})
|
||||
asyncio.get_event_loop().call_later(0.5, self._restart_bridge)
|
||||
return response
|
||||
@@ -3729,13 +3991,13 @@ class KobraXBridge:
|
||||
# die alten Werte statt der geänderten config.ini.
|
||||
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
|
||||
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
|
||||
"BRIDGE_PRINTER_NAME"):
|
||||
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "BRIDGE_PRINTER_NAME"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
|
||||
if in_docker:
|
||||
# Docker/systemd: Prozess beenden reicht – der Supervisor startet neu (frische environ)
|
||||
log.info("Container-Umgebung erkannt – beende Prozess für Supervisor-Restart")
|
||||
log.info("Container environment detected – exiting for supervisor restart")
|
||||
os._exit(0)
|
||||
|
||||
frozen = getattr(sys, "frozen", False)
|
||||
@@ -3770,7 +4032,9 @@ class KobraXBridge:
|
||||
GITEA_RAW_BASE = "https://gitea.it-drui.de/viewit/KX-Bridge-Release/raw/tag"
|
||||
|
||||
def _read_version(self) -> str:
|
||||
for base in (pathlib.Path(_BASE), pathlib.Path(_BASE).parent):
|
||||
# PyInstaller-Onefile entpackt VERSION (per kx-bridge.spec datas) nach
|
||||
# sys._MEIPASS — daher _WEB_BASE statt _BASE benutzen.
|
||||
for base in (pathlib.Path(_WEB_BASE), pathlib.Path(_BASE), pathlib.Path(_BASE).parent):
|
||||
p = base / "VERSION"
|
||||
if p.is_file():
|
||||
return p.read_text(encoding="utf-8").strip()
|
||||
@@ -3912,7 +4176,7 @@ class KobraXBridge:
|
||||
if fname == "kobrax_moonraker_bridge.py":
|
||||
return web.json_response(
|
||||
{"error": f"Download {fname}: HTTP {resp.status}"}, status=502)
|
||||
log.warning(f"Update: {fname} nicht im Release ({resp.status}) – übersprungen")
|
||||
log.warning(f"Update: {fname} not found in release ({resp.status}) – skipped")
|
||||
continue
|
||||
downloaded.append((app_dir / fname, await resp.read()))
|
||||
# Phase 2: atomar ersetzen (erst nach komplettem, erfolgreichem Download)
|
||||
@@ -4189,11 +4453,14 @@ class KobraXBridge:
|
||||
# Obico registriert obico_remote_event-Callback. Wir akzeptieren leer.
|
||||
result = "ok"
|
||||
elif method == "server.webcams.list":
|
||||
# WS-Variante des HTTP-Endpoints
|
||||
# WS-Variante: absolute URL mit echter LAN-IP statt localhost
|
||||
_lip = getattr(self, "_local_ip", None) or "127.0.0.1"
|
||||
_base = f"http://{_lip}:{self._args.port}"
|
||||
result = {"webcams": [{
|
||||
"name": "KX-Bridge", "location": "printer", "service": "mjpegstreamer",
|
||||
"enabled": True, "stream_url": "/api/camera/stream",
|
||||
"snapshot_url": "/api/camera/snapshot",
|
||||
"enabled": True,
|
||||
"stream_url": f"{_base}/api/camera/stream",
|
||||
"snapshot_url": f"{_base}/api/camera/snapshot",
|
||||
"flip_horizontal": False, "flip_vertical": False, "rotation": 0,
|
||||
"target_fps": 5, "aspect_ratio": "16:9",
|
||||
}]}
|
||||
@@ -4243,7 +4510,7 @@ class KobraXBridge:
|
||||
log.debug(f"Unbekannte RPC-Methode: {method}")
|
||||
result = {}
|
||||
except Exception as e:
|
||||
log.error(f"RPC-Fehler für {method}: {e}")
|
||||
log.error(f"RPC error for {method}: {e}")
|
||||
error = {"code": -32603, "message": str(e)}
|
||||
|
||||
if rpc_id is not None:
|
||||
@@ -4442,6 +4709,12 @@ def build_app(bridge: KobraXBridge) -> web.Application:
|
||||
r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots)
|
||||
r.add_get("/kx/filament/profiles", bridge.handle_kx_filament_profiles)
|
||||
r.add_post("/kx/filament/slots/{idx}/profile", bridge.handle_kx_filament_slot_profile)
|
||||
# Custom-Profile-Import (Issue #41) — User lädt eigene Orca-Filament-
|
||||
# Profile als ZIP/JSON hoch (z.B. aus ~/.config/OrcaSlicer/user/<id>/filament/),
|
||||
# weil die Bridge typischerweise nicht auf demselben Host wie OrcaSlicer läuft.
|
||||
r.add_get("/kx/filament/profiles/user", bridge.handle_kx_filament_profiles_user_list)
|
||||
r.add_post("/kx/filament/profiles/user", bridge.handle_kx_filament_profiles_import)
|
||||
r.add_delete("/kx/filament/profiles/user", bridge.handle_kx_filament_profiles_user_delete)
|
||||
r.add_get("/kx/history", bridge.handle_kx_history)
|
||||
r.add_get("/kx/ui/{name:.*}", bridge.handle_kx_ui_asset)
|
||||
r.add_get("/kx/files/{id}/objects", bridge.handle_kx_file_objects)
|
||||
@@ -4551,7 +4824,7 @@ async def run_bridge(args):
|
||||
site = web.TCPSite(runner, args.host, per_args.port)
|
||||
await site.start()
|
||||
runners.append((runner, client, pid))
|
||||
log.info(f"[Drucker {pid}] Bridge läuft auf http://{args.host}:{per_args.port}")
|
||||
log.info(f"[Printer {pid}] Bridge running on http://{args.host}:{per_args.port}")
|
||||
|
||||
import socket as _socket
|
||||
try:
|
||||
@@ -4560,6 +4833,9 @@ async def run_bridge(args):
|
||||
_local_ip = _s.getsockname()[0]
|
||||
except Exception:
|
||||
_local_ip = args.host
|
||||
# An alle Bridge-Instanzen weitergeben — wird für absolute Webcam-URLs genutzt
|
||||
for _b in all_bridges.values():
|
||||
_b._local_ip = _local_ip
|
||||
log.info(f"OrcaSlicer → Klipper → Host: {_local_ip} Ports: " +
|
||||
", ".join(str(getattr(b._args, 'port', 0)) for b in all_bridges.values()))
|
||||
log.info("Ctrl-C zum Beenden")
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
|
||||
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 = []
|
||||
hiddenimports = []
|
||||
|
||||
|
||||
147
orca_filaments.py
Normal file
147
orca_filaments.py
Normal 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)
|
||||
@@ -1,11 +1,12 @@
|
||||
// ── State ──
|
||||
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
|
||||
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
|
||||
curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–',
|
||||
curr_layer:0,total_layers:0,z_mm:0,printer_name:'Kobra X',firmware_version:'–',
|
||||
camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,
|
||||
ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]},web_upload_warning:1};
|
||||
var tempHistory={n:[],b:[]};
|
||||
var camOn=false;
|
||||
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
|
||||
var currentStep=1;
|
||||
var currentPanel='dashboard';
|
||||
var aceAutoRefillPrefs=(function(){
|
||||
@@ -101,6 +102,7 @@ function tr(key,fallback){
|
||||
function _langToggleLabel(lang){
|
||||
if(lang==='de')return 'Deutsch';
|
||||
if(lang==='en')return 'English';
|
||||
if(lang==='fr')return 'Français';
|
||||
if(lang==='zh-cn')return '简体中文';
|
||||
return 'Espanol';
|
||||
}
|
||||
@@ -108,10 +110,10 @@ function _langToggleLabel(lang){
|
||||
function _mapSupportedLang(lang){
|
||||
if(!lang)return '';
|
||||
var l=String(lang).toLowerCase().replace(/_/g,'-').trim();
|
||||
if(l==='de'||l==='en'||l==='es'||l==='zh-cn')return l;
|
||||
if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='zh-cn')return l;
|
||||
|
||||
var base=l.split('-')[0];
|
||||
if(base==='de'||base==='en'||base==='es')return base;
|
||||
if(base==='de'||base==='en'||base==='es'||base==='fr')return base;
|
||||
|
||||
if(base==='zh'){
|
||||
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
|
||||
@@ -282,6 +284,7 @@ function applyLang(){
|
||||
setText('d-lbl-remain',T.lbl_remaining);
|
||||
setText('d-slicer-label',T.lbl_slicer_time);
|
||||
setText('d-lbl-layers',T.lbl_layers);
|
||||
setText('d-lbl-zpos',T.lbl_zpos);
|
||||
setText('d-lbl-light',T.lbl_light);
|
||||
setText('d-lbl-nozzle',T.label_nozzle);
|
||||
setText('d-lbl-bed',T.label_bed);
|
||||
@@ -312,6 +315,17 @@ function applyLang(){
|
||||
setText('modal-sec-print',T.settings_print);
|
||||
setText('modal-sec-poll',T.settings_poll);
|
||||
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('lbl-printer-name',T.settings_printer_name);
|
||||
setText('lbl-printer-ip',T.settings_printer_ip);
|
||||
@@ -648,6 +662,7 @@ function applyState(){
|
||||
|
||||
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–';
|
||||
var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers;
|
||||
var dzpos=document.getElementById('d-zpos');if(dzpos)dzpos.textContent=s.z_mm>0?s.z_mm.toFixed(2)+' mm':'–';
|
||||
|
||||
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration);
|
||||
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–';
|
||||
@@ -811,10 +826,14 @@ function applyState(){
|
||||
var co=document.getElementById('cam-overlay');
|
||||
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
|
||||
|
||||
// auto-start camera during print
|
||||
if(s.print_state==='printing'&&!camOn&&s.camera_url){
|
||||
// auto-start camera during print (unless user explicitly stopped it)
|
||||
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){
|
||||
camStart();
|
||||
}
|
||||
// reset user-stopped flag when print ends so next print auto-starts again
|
||||
if(s.print_state!=='printing'){
|
||||
camUserStopped=false;
|
||||
}
|
||||
|
||||
updateConnBtn();
|
||||
}
|
||||
@@ -903,11 +922,110 @@ function openSettings(){
|
||||
var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none';
|
||||
_updateTag='';_updateUrl='';
|
||||
document.getElementById('settings-modal').classList.add('open');
|
||||
// Custom-Profile-Liste laden (Issue #41 — Verwaltung von User-importierten
|
||||
// OrcaSlicer-Filament-Profilen)
|
||||
refreshUserProfileList();
|
||||
}
|
||||
function closeSettings(){
|
||||
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 ──
|
||||
var _slotEditIndex=-1;
|
||||
var _slotEditLoaded=false;
|
||||
@@ -949,21 +1067,31 @@ function _fillSlotProfileDropdown(material, currentVendor, currentName){
|
||||
return matU==='' || pt===matU || pt.startsWith(matU+'-') || pt.startsWith(matU+' ');
|
||||
});
|
||||
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={};
|
||||
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){
|
||||
var g=document.createElement('optgroup'); g.label=v;
|
||||
byVendor[v].forEach(function(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.name;
|
||||
if(o.value===wantKey) o.selected=true;
|
||||
g.appendChild(o);
|
||||
});
|
||||
byVendor[v].forEach(function(p){ _appendOption(g, p); });
|
||||
sel.appendChild(g);
|
||||
});
|
||||
});
|
||||
@@ -1112,6 +1240,10 @@ function saveSlotEdit(){
|
||||
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');});
|
||||
@@ -1313,7 +1445,8 @@ function setBed(){
|
||||
function setLight(){
|
||||
var on=document.getElementById('d-light-toggle').checked;
|
||||
post('/api/light',{on:on,brightness:80})
|
||||
.then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')})
|
||||
.then(function(){clog('Licht '+(on?'an, 80%':'aus'),'msg-ok')})
|
||||
|
||||
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
|
||||
}
|
||||
|
||||
@@ -1394,11 +1527,13 @@ function camStart(){
|
||||
|
||||
function camStop(){
|
||||
var img=document.getElementById('cam-img');
|
||||
img.onerror=null; // deregister error handler before clearing src to avoid spurious error toast
|
||||
post('/api/camera/stop',{}).catch(function(){});
|
||||
img.src='';
|
||||
img.style.display='none';
|
||||
document.getElementById('cam-placeholder').style.display='flex';
|
||||
camOn=false;
|
||||
camUserStopped=true; // suppress auto-restart for remainder of this print
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
|
||||
clog(tr('log_cam_stop'),'msg-ok');
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Espanol</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -121,6 +122,19 @@
|
||||
</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 class="modal-section" id="modal-sec-version">Version</div>
|
||||
<div class="update-row">
|
||||
@@ -170,12 +184,42 @@
|
||||
<option value="" id="slot-profile-default-opt"></option>
|
||||
</select>
|
||||
<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>
|
||||
<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>
|
||||
</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/<id>/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">
|
||||
<nav class="sidebar">
|
||||
<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 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="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0">
|
||||
<div class="time-label" id="d-lbl-layers"></div>
|
||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
|
||||
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center">
|
||||
<div class="time-label" id="d-lbl-layers"></div>
|
||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||
</div>
|
||||
<div class="time-block" style="padding:4px 10px;min-width:72px;text-align:center">
|
||||
<div class="time-label" id="d-lbl-zpos">Z</div>
|
||||
<div class="time-val" style="font-size:13px" id="d-zpos">–</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-grid">
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"lbl_remaining": "Restzeit:",
|
||||
"lbl_slicer_time": "Slicer-Schätzung:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Leise",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -164,6 +165,19 @@
|
||||
"slot_edit_profile": "OrcaSlicer-Profil",
|
||||
"slot_edit_profile_hint": "Sendet beim OrcaSlicer-Sync die konkrete Marke statt nur „Generic\"",
|
||||
"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/<id>/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_lvl_label": "Level:",
|
||||
"file_ready_btn": "▶ Druck starten",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"lbl_remaining": "Remaining:",
|
||||
"lbl_slicer_time": "Slicer estimate:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silent",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -164,6 +165,19 @@
|
||||
"slot_edit_profile": "OrcaSlicer profile",
|
||||
"slot_edit_profile_hint": "Sent on OrcaSlicer sync as the specific brand instead of just \"Generic\"",
|
||||
"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/<id>/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_lvl_label": "Level:",
|
||||
"file_ready_btn": "▶ Start Print",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"lbl_remaining": "Restante:",
|
||||
"lbl_slicer_time": "Estimación del slicer:",
|
||||
"lbl_layers": "Capa",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silencioso",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -164,6 +165,19 @@
|
||||
"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_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/<id>/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_lvl_label": "Nivel:",
|
||||
"file_ready_btn": "▶ Iniciar impresión",
|
||||
|
||||
249
web/translations/fr.json
Normal file
249
web/translations/fr.json
Normal 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/<id>/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": "A–Z Nom",
|
||||
"ss_dur": "⏱ Durée d'impression"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"lbl_remaining": "剩余时间:",
|
||||
"lbl_slicer_time": "切片预估:",
|
||||
"lbl_layers": "层",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 静音",
|
||||
"speed_normal": "⚡ 标准",
|
||||
"speed_sport": "🚀 运动",
|
||||
@@ -164,6 +165,19 @@
|
||||
"slot_edit_profile": "OrcaSlicer 配置",
|
||||
"slot_edit_profile_hint": "在 OrcaSlicer 同步时发送具体品牌,而不仅仅是“Generic”",
|
||||
"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/<id>/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_lvl_label": "级别:",
|
||||
"file_ready_btn": "▶ 开始打印",
|
||||
|
||||
Reference in New Issue
Block a user