Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0217e6cdbf | |||
| 95d9358d2a | |||
| 340364a7bc | |||
| d2ec428a32 | |||
| c036a25a66 | |||
| fdcc38dd96 | |||
| 3e33d6ff93 | |||
| d3f3763181 | |||
| 79c702ea0b | |||
| d81f0a36c3 | |||
| 8e02e96ee8 |
@@ -1,90 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.23] – 2026-06-16
|
||||
|
||||
### Neu
|
||||
- **Druckdialog nach Upload automatisch öffnen.** Eine neue Einstellung
|
||||
`print_start_dialog` (Einstellungen → Drucker → „Druckstart-Verhalten") steuert,
|
||||
was nach einem Upload bei leerlaufendem Drucker passiert: „Print-Dialog" öffnet
|
||||
den Slot-Zuordnungs-Dialog automatisch, „Print-Leiste" behält das bisherige
|
||||
Banner. Basiert auf PR #56 von @gangoke.
|
||||
- **Auto-Leveling-Schalter pro Druck.** Der Druckdialog hat jetzt eine eigene
|
||||
Auto-Leveling-Checkbox, die den globalen Standard für einen einzelnen Druck
|
||||
überschreibt.
|
||||
|
||||
### Behoben
|
||||
- **Objekt-Skip wurde beim Druckstart still ignoriert (PR #56, @gangoke).** Der
|
||||
Skip-Befehl wurde gesendet, *bevor* der Drucker im `printing`-Status war, und
|
||||
daher verworfen. Der Skip wird nun in einer Retry-Schleife erneut angewendet,
|
||||
sobald der Druck bestätigt läuft — mit einer Pending-Sperre, damit die UI den
|
||||
Skip-Status nicht vorzeitig zurücksetzt.
|
||||
- **Upload während eines laufenden Drucks überschrieb die Vorschau des laufenden
|
||||
Auftrags.** Ein neuer Upload während des Drucks ersetzt nicht mehr Thumbnail /
|
||||
file_ready des Auftrags auf dem Druckbett.
|
||||
|
||||
## [0.9.22] – 2026-06-16
|
||||
|
||||
### Neu
|
||||
- **Neu strukturiertes Einstellungs-Panel.** Das Einstellungs-Modal wurde durch
|
||||
ein dauerhaftes Master-Detail-Panel mit fünf Kategorien ersetzt: Verbindung,
|
||||
Drucker, Darstellung, Filament und System. Das Poll-Intervall ist nun live
|
||||
einstellbar.
|
||||
- **Vendor-Sichtbarkeitsfilter (Issue #41).** Eine neue Checkliste in den
|
||||
Filament-Einstellungen beschränkt das Slot-Profil-Dropdown auf bestimmte
|
||||
Hersteller. „Generic" und eigene importierte Profile sind immer sichtbar.
|
||||
- **Idle-Datei-Aktionen in der Fortschritts-Karte (Issue #55).** Nach einem
|
||||
Upload bei leerlaufendem Drucker erscheinen drei Schnellaktionen direkt in der
|
||||
Fortschritts-Karte: ▶ Drucken, ⚙ Slots zuordnen und ✕ Leeren.
|
||||
|
||||
### Behoben
|
||||
- **Mobileraker-Kompatibilität (Issue #48).** Absturz in `ConfigExtruder.fromJson`
|
||||
(leeres `configfile.config`), Hänger beim Refresh (Metadata-Endlosschleife) und
|
||||
fehlende ETA/Restzeit behoben.
|
||||
|
||||
## [0.9.21] – 2026-06-14
|
||||
|
||||
### Behoben
|
||||
- **Kamera-Stream auf Android (Chrome / Firefox) nicht sichtbar.** Android-Browser
|
||||
unterstützen `multipart/x-mixed-replace` (MJPEG) nicht. Die UI erkennt Android
|
||||
jetzt automatisch und fällt auf Snapshot-Polling mit 5 fps zurück
|
||||
(`/api/camera/snapshot` alle 200 ms) — keine Server-Änderung nötig.
|
||||
|
||||
### Geändert
|
||||
- Docker-Image auf **Debian 12 (Bookworm)** gepinnt (`python:3.11-slim-bookworm`),
|
||||
um Kompatibilitätsprobleme mit glibc 2.41 zu vermeiden, die das aktuell von
|
||||
`python:3.11-slim` gezogene Debian 13 Basis-Image mitbringt.
|
||||
- MQTT- und HTTP-Verbindungen erzwingen jetzt **IPv4** (`AF_INET`), um
|
||||
Verbindungsfehler auf Hosts zu verhindern, bei denen der Drucker nur über IPv4
|
||||
erreichbar ist, das OS aber IPv6 bevorzugt.
|
||||
- Extruder-Stub in der Moonraker-`configfile`-Antwort enthält jetzt `sensor_type`
|
||||
und `filament_diameter` — behebt einen Mobileraker-Absturz
|
||||
(`Null is not a subtype of Object`, Issue #48).
|
||||
|
||||
## [0.9.20] – 2026-06-08
|
||||
|
||||
### Neu
|
||||
- **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
|
||||
|
||||
133
CHANGELOG.md
133
CHANGELOG.md
@@ -1,129 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.23] – 2026-06-16
|
||||
|
||||
### New
|
||||
- **Auto-open print dialog after upload.** A new `print_start_dialog` setting
|
||||
(Settings → Printer → "Start Print Behavior") controls what happens after a
|
||||
file is uploaded while the printer is idle: `Print Dialog` opens the
|
||||
slot-assignment dialog automatically, `Print Bar` keeps the previous banner
|
||||
behaviour. Based on PR #56 by @gangoke.
|
||||
- **Per-print auto-leveling toggle.** The print dialog now has its own
|
||||
auto-leveling checkbox that overrides the global default for a single print.
|
||||
|
||||
### Fixed
|
||||
- **Object skip was silently ignored at print start (PR #56, @gangoke).** The
|
||||
skip command was sent *before* the printer entered the `printing` state, so it
|
||||
was dropped. The skip is now re-applied in a retry loop once the print is
|
||||
confirmed running, with a pending-lock so the UI doesn't reset the skip state
|
||||
prematurely.
|
||||
- **Upload during an active print overwrote the running job's preview.**
|
||||
Uploading a new file while printing no longer replaces the thumbnail /
|
||||
file_ready of the job currently on the bed.
|
||||
|
||||
## [0.9.22] – 2026-06-16
|
||||
|
||||
### New
|
||||
- **Restructured Settings panel.** The settings modal has been replaced by a
|
||||
persistent Master-Detail panel with five categories: Connection, Printer,
|
||||
Appearance, Filament, and System. Poll interval is now adjustable live.
|
||||
- **Vendor visibility filter (issue #41).** A new checklist in the Filament
|
||||
settings lets you restrict the slot profile dropdown to specific manufacturers.
|
||||
"Generic" and your own imported profiles are always visible. The list updates
|
||||
automatically after a profile import.
|
||||
- **Idle file actions in the progress card (issue #55).** After uploading a file
|
||||
while the printer is idle, three quick-action buttons appear directly in the
|
||||
progress card: ▶ Print, ⚙ Map Slots, and ✕ Clear — matching the file browser
|
||||
workflow without navigating away.
|
||||
|
||||
### Fixed
|
||||
- **Mobileraker: app crashed with `Null is not a subtype of Object` in
|
||||
`ConfigExtruder.fromJson` (issue #48).** `configfile.config` was returned as
|
||||
an empty object `{}`. Mobileraker parses both `configfile.settings` and
|
||||
`configfile.config` through the same strict Dart parser — both are now
|
||||
populated with the same extruder/bed/stepper stub.
|
||||
- **Mobileraker: app hung indefinitely on refresh (issue #48).** The WebSocket
|
||||
`server.files.metadata` handler called a non-existent store method
|
||||
(`get_file_by_filename`), always returning empty metadata. Mobileraker retried
|
||||
this thousands of times per second. Both the HTTP and WS paths now share a
|
||||
single `_build_file_metadata()` method.
|
||||
- **Mobileraker: ETA / remaining time not shown (issue #48).** A side effect of
|
||||
the metadata loop fix — once `currentFile` resolves, Mobileraker can calculate
|
||||
ETA from `estimated_time`.
|
||||
- **Mobileraker: `notify_status_update` triggered repeated `ConfigFile.parse`
|
||||
(issue #48).** Static objects (`configfile`, `webhooks`, `heaters`, `history`)
|
||||
were included in every live status push. They are now filtered out; only live
|
||||
telemetry is broadcast.
|
||||
- `motion_report` (`live_position`, `live_velocity`) added to printer objects
|
||||
for Mobileraker motion display.
|
||||
- Saving filament slot profiles no longer silently drops the `visible_vendors`
|
||||
setting from `config.ini`.
|
||||
|
||||
## [0.9.21] – 2026-06-14
|
||||
|
||||
### Fixed
|
||||
- **Camera stream not visible on Android (Chrome / Firefox).** Android
|
||||
browsers do not support `multipart/x-mixed-replace` (MJPEG). The UI
|
||||
now detects Android and falls back to snapshot-polling at 5 fps
|
||||
(`/api/camera/snapshot` every 200 ms) — no server-side change needed.
|
||||
|
||||
### Changed
|
||||
- Docker image now pinned to **Debian 12 (Bookworm)** (`python:3.11-slim-bookworm`)
|
||||
to avoid glibc 2.41 compatibility issues introduced by the Debian 13
|
||||
base image that `python:3.11-slim` recently started pulling.
|
||||
- MQTT and HTTP connections now **force IPv4** (`AF_INET`) to prevent
|
||||
connection failures on hosts where the printer is only reachable via
|
||||
IPv4 but the OS prefers IPv6.
|
||||
- Extruder stub in the Moonraker `configfile` response now includes
|
||||
`sensor_type` and `filament_diameter` — fixes a Mobileraker crash
|
||||
(`Null is not a subtype of Object`, issue #48).
|
||||
|
||||
## [0.9.20] – 2026-06-08
|
||||
|
||||
### New
|
||||
- **French language support (PR #45 by @Nathacks)**
|
||||
- **Z height display in the print UI (PR #49 by @Nathacks).** Shows
|
||||
current Z position in mm below the layer counter.
|
||||
|
||||
### Fixed
|
||||
- **Camera auto-start ignored "Enable camera on print start" setting
|
||||
after a bridge restart (issue #50).** The setting was cached in the
|
||||
process environment — after saving it in the UI, the old value
|
||||
survived the restart and the new value from `config.ini` was never
|
||||
read.
|
||||
- **Camera restarted automatically after manual stop during a print
|
||||
(issue #50).** A new `_camera_user_stopped` flag suppresses
|
||||
auto-restart for the current print session. It resets when the
|
||||
print ends.
|
||||
- **Spurious "stream unavailable" error toast when stopping the camera
|
||||
manually.** The image error handler was still registered when
|
||||
`img.src` was cleared.
|
||||
- **JS error (`ReferenceError: br is not defined`) when toggling the
|
||||
light.** Variable was referenced from the wrong scope.
|
||||
- Webcam URLs are now absolute so that Mobileraker/Obico clients can
|
||||
reach them.
|
||||
|
||||
## [0.9.19.1] – 2026-06-04
|
||||
|
||||
### Fixed
|
||||
- Standalone binaries (Linux/Windows) reported `vunknown` as their
|
||||
version. The `VERSION` file is now embedded into the PyInstaller
|
||||
onefile bundle.
|
||||
- When the TLS certificates (`anycubic_slicer.crt`/`.key`) were
|
||||
missing, the bridge only logged the raw `[Errno 2] No such file
|
||||
or directory`. It now states clearly where the files need to be
|
||||
placed and that `anycubic-certs.zip` from the Gitea release is the
|
||||
source.
|
||||
- Standalone-Binaries (Linux/Windows) zeigten `vunknown` als Version.
|
||||
Die `VERSION`-Datei ist jetzt ins PyInstaller-Onefile eingebettet.
|
||||
- Bei fehlenden TLS-Zertifikaten (`anycubic_slicer.crt`/`.key`) gab
|
||||
es nur den rohen Fehler `[Errno 2] No such file or directory`. Die
|
||||
Bridge meldet jetzt klar, wo die Dateien hingelegt werden müssen
|
||||
und dass `anycubic-certs.zip` aus dem Gitea-Release stammt.
|
||||
|
||||
### Changed
|
||||
- Filament profile list re-curated: 209 entries instead of 399.
|
||||
Profiles that only exist inside printer-specific vendor bundles
|
||||
(e.g. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) were dropped —
|
||||
OrcaSlicer wouldn't have found them in a default Kobra X setup
|
||||
anyway, because the matching vendor bundle is only loaded when
|
||||
the corresponding printer vendor is active. For those filaments
|
||||
the custom profile import (issue #41) remains the way.
|
||||
- Filament-Profil-Liste neu kuratiert: 209 statt 399 Einträge.
|
||||
Profile die nur für drucker-spezifische Vendor-Bundles existieren
|
||||
(z.B. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) sind rausgeflogen
|
||||
— OrcaSlicer hätte sie im Standard-Kobra-X-Setup beim Sync
|
||||
ohnehin nicht gefunden, weil die jeweiligen Vendor-Bundles nur
|
||||
bei aktivem Drucker-Vendor geladen werden. Für solche Filamente
|
||||
bleibt der Custom-Profile-Import (Issue #41) der Weg.
|
||||
|
||||
## [0.9.19] – 2026-06-02
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
FROM python:3.11-slim-bookworm
|
||||
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
|
||||
|
||||
|
||||
37
README.de.md
37
README.de.md
@@ -67,17 +67,44 @@ docker compose up -d
|
||||
|
||||
**Linux-Binary (kein Docker):**
|
||||
```bash
|
||||
chmod +x kx-bridge && ./kx-bridge
|
||||
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
|
||||
```
|
||||
|
||||
**Windows-EXE (kein Docker):**
|
||||
```
|
||||
kx-bridge.exe
|
||||
```
|
||||
> `config\` und `data\` werden neben der EXE angelegt — portabel.
|
||||
|
||||
> Bei Linux- und Windows-Binary liegen `config/` und `data/` (Einstellungen, SQLite,
|
||||
> GCode-Store) jeweils neben dem Programm. Einfach den ganzen Ordner kopieren = umziehen.
|
||||
> ⚠️ **TLS-Zertifikate für Standalone-Binary nötig**
|
||||
>
|
||||
> Die Bridge spricht per mTLS mit dem Drucker-MQTT und braucht zwei
|
||||
> Zertifikat-Dateien **direkt neben dem Binary**:
|
||||
>
|
||||
> - `anycubic_slicer.crt`
|
||||
> - `anycubic_slicer.key`
|
||||
>
|
||||
> Beide liegen im **`anycubic-certs.zip`** auf derselben Release-Seite.
|
||||
> Lade die ZIP herunter und entpacke die beiden Dateien in dasselbe
|
||||
> Verzeichnis wie `kx-bridge-linux-amd64` bzw. `kx-bridge.exe`. Ohne
|
||||
> die Zertifikate siehst du `Verbindung fehlgeschlagen: TLS-Zertifikate
|
||||
> fehlen …` (0.9.19.1+) oder `[Errno 2] No such file or directory`
|
||||
> (ältere Builds).
|
||||
>
|
||||
> So muss es aussehen:
|
||||
> ```
|
||||
> ~/kx-bridge/
|
||||
> ├── kx-bridge-linux-amd64 (oder kx-bridge.exe)
|
||||
> ├── anycubic_slicer.crt ← aus anycubic-certs.zip
|
||||
> ├── anycubic_slicer.key ← aus anycubic-certs.zip
|
||||
> └── config/ (wird beim ersten Start angelegt)
|
||||
> ```
|
||||
>
|
||||
> Docker-User müssen das nicht machen — die Zertifikate sind im Image
|
||||
> enthalten.
|
||||
|
||||
> Bei Linux- und Windows-Binary liegen `config/` und `data/` (Einstellungen,
|
||||
> SQLite, GCode-Store) jeweils neben dem Programm. Einfach den ganzen Ordner
|
||||
> kopieren = umziehen.
|
||||
|
||||
**Python direkt:**
|
||||
```bash
|
||||
@@ -130,7 +157,7 @@ Für sauberen AMS-Filament-Sync gibt es einen **gepatchten OrcaSlicer-Build**:
|
||||
- Vendor-Match wenn `tray_info_idx` gesetzt ist, das Preset aber inkompatibel
|
||||
- Zwei-Pass-Suche: erst kompatible Presets, dann alle sichtbaren
|
||||
|
||||
**Warum das zusammen wichtig ist:** ohne #13719 landen die AMS-Slots in OrcaSlicer alle auf `Generic PLA` / `Generic PETG`, obwohl die Bridge die konkrete Marke schon mitsendet (`name + vendor_name + gate_filament_name`). Mit dem KX-Build matched OrcaSlicer deine echten User-Presets — auch die, die du via [Eigene OrcaSlicer-Profile importieren](#-features) in die Bridge gezogen hast.
|
||||
**Warum das zusammen wichtig ist:** ohne #13719 landen die AMS-Slots in OrcaSlicer alle auf `Generic PLA` / `Generic PETG`, obwohl die Bridge die konkrete Marke schon mitsendet (`name + vendor_name + gate_filament_name`). Mit dem KX-Build matched OrcaSlicer deine echten User-Presets — auch die, die du via [Eigene OrcaSlicer-Profile importieren](https://gitea.it-drui.de/viewit/KX-Bridge-Release/src/branch/master/docs/filament-preset-bridge-guide.md) in die Bridge gezogen hast.
|
||||
|
||||
Stock-Upstream-OrcaSlicer funktioniert für Slicing und Drucken weiterhin — nur das Per-Slot-Vendor-Matching beim AMS-Sync fällt dann weg. Material und Farbe pro Slot kannst du auch ohne den KX-Build über die Bridge ans Drucker-Display schreiben (das läuft über MQTT, nicht über den Slicer).
|
||||
|
||||
|
||||
56
README.md
56
README.md
@@ -30,7 +30,7 @@ officially tested or supported. Feedback welcome.</sub>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
## Features
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
@@ -50,7 +50,7 @@ officially tested or supported. Feedback welcome.</sub>
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
## Quick Start
|
||||
|
||||
### 1. Prepare the printer
|
||||
|
||||
@@ -66,17 +66,43 @@ docker compose up -d
|
||||
|
||||
**Linux binary (no Docker):**
|
||||
```bash
|
||||
chmod +x kx-bridge && ./kx-bridge
|
||||
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
|
||||
```
|
||||
|
||||
**Windows EXE (no Docker):**
|
||||
```
|
||||
kx-bridge.exe
|
||||
```
|
||||
> `config\` and `data\` are created next to the EXE — portable.
|
||||
|
||||
> With the Linux and Windows binaries, `config/` and `data/` (settings, SQLite, GCode store)
|
||||
> live next to the program. Copy the whole folder = move the installation.
|
||||
> ⚠️ **TLS certificates required for the standalone binary**
|
||||
>
|
||||
> The bridge talks to the printer's MQTT over mTLS and needs two
|
||||
> certificate files **right next to the binary**:
|
||||
>
|
||||
> - `anycubic_slicer.crt`
|
||||
> - `anycubic_slicer.key`
|
||||
>
|
||||
> Both ship inside **`anycubic-certs.zip`** on the same release page.
|
||||
> Download it and extract the two files into the same directory as
|
||||
> `kx-bridge-linux-amd64` / `kx-bridge.exe`. Without them you'll see
|
||||
> `Verbindung fehlgeschlagen: TLS-Zertifikate fehlen …` (0.9.19.1+)
|
||||
> or `[Errno 2] No such file or directory` (older builds).
|
||||
>
|
||||
> Working layout:
|
||||
> ```
|
||||
> ~/kx-bridge/
|
||||
> ├── kx-bridge-linux-amd64 (or kx-bridge.exe)
|
||||
> ├── anycubic_slicer.crt ← from anycubic-certs.zip
|
||||
> ├── anycubic_slicer.key ← from anycubic-certs.zip
|
||||
> └── config/ (auto-created on first run)
|
||||
> ```
|
||||
>
|
||||
> Docker users don't need to do this — the certs are baked into the
|
||||
> image.
|
||||
|
||||
> With the Linux and Windows binaries, `config/` and `data/` (settings,
|
||||
> SQLite, GCode store) live next to the program. Copy the whole folder
|
||||
> = move the installation.
|
||||
|
||||
**Python directly:**
|
||||
```bash
|
||||
@@ -104,13 +130,13 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
||||
|
||||
---
|
||||
|
||||
## 📺 Video Tutorial
|
||||
## Video Tutorial
|
||||
|
||||
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Recommended Slicer
|
||||
## Recommended Slicer
|
||||
|
||||
For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
|
||||
|
||||
@@ -129,7 +155,7 @@ For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
|
||||
- Vendor match when `tray_info_idx` is set but its preset is incompatible
|
||||
- Two-pass lookup: first compatible presets, then all visible ones
|
||||
|
||||
**Why this matters:** without #13719 the AMS slots in OrcaSlicer all fall back to `Generic PLA` / `Generic PETG` even though the bridge already sends the concrete brand (`name + vendor_name + gate_filament_name`). With the KX build OrcaSlicer matches your actual user presets — including profiles you imported into the bridge via the [Import your own OrcaSlicer profiles](#-features) flow.
|
||||
**Why this matters:** without #13719 the AMS slots in OrcaSlicer all fall back to `Generic PLA` / `Generic PETG` even though the bridge already sends the concrete brand (`name + vendor_name + gate_filament_name`). With the KX build OrcaSlicer matches your actual user presets — including profiles you imported into the bridge via the [Import your own OrcaSlicer profiles](https://gitea.it-drui.de/viewit/KX-Bridge-Release/src/branch/master/docs/filament-preset-bridge-guide.md) flow.
|
||||
|
||||
Stock upstream OrcaSlicer still works for slicing and printing — you just lose the per-slot brand matching on AMS sync. Slot material + colour can still be pushed bridge → printer either way (that goes over MQTT, not via the slicer).
|
||||
|
||||
@@ -137,7 +163,7 @@ OrcaSlicer-KX is a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer
|
||||
|
||||
---
|
||||
|
||||
## 🏠 Community & Integrations
|
||||
## Community & Integrations
|
||||
|
||||
- **[Home Assistant integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||
by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls,
|
||||
@@ -154,7 +180,7 @@ OrcaSlicer-KX is a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Getting credentials manually
|
||||
## Getting credentials manually
|
||||
|
||||
Normally not needed — *"+ Add printer"* does this automatically. If you do need it:
|
||||
|
||||
@@ -171,7 +197,7 @@ Alternatively (if the IP is unknown): open AnycubicSlicerNext, connect the print
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Useful commands
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
docker compose logs -f # show logs
|
||||
@@ -182,7 +208,7 @@ docker compose up -d --build # rebuild locally (instead of pulling)
|
||||
|
||||
---
|
||||
|
||||
## 🩹 Troubleshooting
|
||||
## Troubleshooting
|
||||
|
||||
<details>
|
||||
<summary><b>"Wrong MQTT credentials" on start</b></summary>
|
||||
@@ -216,7 +242,7 @@ Migration runs automatically on first start after the upgrade — no action requ
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security
|
||||
## Security
|
||||
|
||||
- The bridge is reachable on the local network at `http://<host-IP>:7125` — **do not** expose it to the internet
|
||||
- `config/config.ini` contains printer credentials — do not share publicly
|
||||
@@ -224,7 +250,7 @@ Migration runs automatically on first start after the upgrade — no action requ
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
## License
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ def _load_config_file(path: pathlib.Path):
|
||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
|
||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||
}
|
||||
for env_key, (section, option) in mapping.items():
|
||||
@@ -74,18 +73,6 @@ def _load_config_file(path: pathlib.Path):
|
||||
pass
|
||||
|
||||
|
||||
# Backward compatibility: old key FILE_READY_DIALOG → PRINT_START_DIALOG
|
||||
if "PRINT_START_DIALOG" not in os.environ:
|
||||
try:
|
||||
legacy = cfg.get(CONFIG_SECTION_PRINT, "file_ready_dialog")
|
||||
if legacy:
|
||||
os.environ["PRINT_START_DIALOG"] = legacy
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
pass
|
||||
if "PRINT_START_DIALOG" not in os.environ and "FILE_READY_DIALOG" in os.environ:
|
||||
os.environ["PRINT_START_DIALOG"] = os.environ["FILE_READY_DIALOG"]
|
||||
|
||||
|
||||
def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
||||
"""Einmalige Migration: .env → config.ini anlegen."""
|
||||
env_vals: dict[str, str] = {}
|
||||
@@ -240,17 +227,10 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
|
||||
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
|
||||
preserved_vendors = None
|
||||
if cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
|
||||
if cfg.has_section("filament_profiles"):
|
||||
cfg.remove_section("filament_profiles")
|
||||
if profiles or preserved_vendors:
|
||||
if profiles:
|
||||
cfg["filament_profiles"] = {}
|
||||
if preserved_vendors:
|
||||
cfg["filament_profiles"]["visible_vendors"] = preserved_vendors
|
||||
for slot_idx in sorted(profiles.keys()):
|
||||
entry = profiles[slot_idx] or {}
|
||||
if entry.get("vendor"):
|
||||
@@ -264,43 +244,6 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def list_visible_vendors() -> list[str]:
|
||||
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
|
||||
|
||||
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
|
||||
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return []
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
return []
|
||||
raw = cfg.get("filament_profiles", "visible_vendors")
|
||||
return [v.strip() for v in raw.split(",") if v.strip()]
|
||||
|
||||
|
||||
def save_visible_vendors(vendors: list[str]) -> bool:
|
||||
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
|
||||
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder."""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_section("filament_profiles"):
|
||||
cfg.add_section("filament_profiles")
|
||||
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
|
||||
if clean:
|
||||
cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean)
|
||||
elif cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
cfg.remove_option("filament_profiles", "visible_vendors")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
cfg.write(f)
|
||||
return True
|
||||
|
||||
|
||||
def get(key: str, default: str = "") -> str:
|
||||
return os.environ.get(key, default)
|
||||
|
||||
@@ -316,4 +259,3 @@ DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# KX-Bridge — Portainer Stack
|
||||
#
|
||||
# Paste this into Portainer → Stacks → Add stack → Web editor
|
||||
#
|
||||
# No configuration needed upfront — just deploy, open http://HOST-IP:7125
|
||||
# and add your printer via the UI (IP only, credentials are fetched automatically).
|
||||
#
|
||||
# All data (config, GCode store, database) is stored in named Docker volumes
|
||||
# managed by Portainer.
|
||||
|
||||
services:
|
||||
kx-bridge:
|
||||
image: gitea.it-drui.de/viewit/kx-bridge:latest
|
||||
volumes:
|
||||
- kx-bridge-config:/app/config
|
||||
- kx-bridge-data:/app/data
|
||||
ports:
|
||||
# Port 7125 = first printer. Add 7126, 7127, … for each additional printer.
|
||||
- "7125:7125"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
kx-bridge-config:
|
||||
kx-bridge-data:
|
||||
@@ -48,4 +48,3 @@ MODE_ID = get("MODE_ID", "")
|
||||
DEVICE_ID = get("DEVICE_ID", "")
|
||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
|
||||
@@ -167,8 +167,7 @@ class KobraXClient:
|
||||
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
|
||||
ctx.load_cert_chain(CERT_FILE, KEY_FILE)
|
||||
|
||||
_ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM)
|
||||
raw = socket.create_connection(_ai[0][4], timeout=5)
|
||||
raw = socket.create_connection((self.host, self.port), timeout=5)
|
||||
self._sock = ctx.wrap_socket(raw)
|
||||
log.info("TLS connected cipher=%s", self._sock.cipher()[0])
|
||||
|
||||
@@ -597,8 +596,7 @@ class KobraXClient:
|
||||
# langsamerem WLAN am Drucker dauert das Schieben sonst >30 s und
|
||||
# würde den Connect-Timeout fälschlich auslösen. Read-Timeout danach
|
||||
# generös (Drucker verarbeitet die Datei bevor er antwortet).
|
||||
_ai = socket.getaddrinfo(self.host, 18910, socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock = socket.create_connection(_ai[0][4], timeout=10)
|
||||
sock = socket.create_connection((self.host, 18910), timeout=10)
|
||||
sock.settimeout(None) # blocking während Send
|
||||
sock.sendall(headers + body)
|
||||
sock.settimeout(180)
|
||||
|
||||
@@ -34,7 +34,6 @@ except ImportError:
|
||||
import env_loader
|
||||
import asyncio
|
||||
import hashlib
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -223,7 +222,7 @@ def _parse_gcode_estimated_time(data: bytes) -> int:
|
||||
elif unit == "m": secs += int(val) * 60
|
||||
elif unit == "s": secs += int(val)
|
||||
if secs:
|
||||
log.info(f"Slicer estimate: {secs}s ({m.group(1).strip()})")
|
||||
log.info(f"Slicer-Schätzzeit: {secs}s ({m.group(1).strip()})")
|
||||
return secs
|
||||
|
||||
|
||||
@@ -752,13 +751,6 @@ class KobraXBridge:
|
||||
self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles()
|
||||
except Exception:
|
||||
self._filament_profiles = {}
|
||||
# Vendor-Sichtbarkeitsfilter fürs Slot-Profil-Dropdown (Issue #41 Option A).
|
||||
# Leere Liste = alle Vendoren sichtbar (rückwärtskompatibel).
|
||||
try:
|
||||
import config_loader as _cl
|
||||
self._visible_vendors: list[str] = _cl.list_visible_vendors()
|
||||
except Exception:
|
||||
self._visible_vendors = []
|
||||
self._last_state: dict = {}
|
||||
self._state = {
|
||||
"nozzle_temp": 0.0,
|
||||
@@ -791,7 +783,6 @@ class KobraXBridge:
|
||||
"print_speed_mode": 2,
|
||||
"connection_error": "",
|
||||
"file_ready": "",
|
||||
"print_start_dialog": getattr(args, "print_start_dialog", 1),
|
||||
"filament_mode": "toolhead",
|
||||
"ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None},
|
||||
}
|
||||
@@ -807,7 +798,6 @@ class KobraXBridge:
|
||||
self._serve_dir_path: str = self._store._gcode_dir
|
||||
self._current_job_id: str = ""
|
||||
self._camera_autostarted: bool = False
|
||||
self._camera_user_stopped: bool = False # User hat Kamera während Druck manuell gestoppt
|
||||
self.camera_cache: CameraCache = CameraCache()
|
||||
|
||||
self._thumbnail_b64: str = ""
|
||||
@@ -815,14 +805,11 @@ class KobraXBridge:
|
||||
|
||||
# Part-Skip: zuletzt vom Drucker gemeldete Skip-Liste (v0.9.10)
|
||||
self._skip_state: dict = {"objects": [], "skipped": [], "ts": 0}
|
||||
# Pre-Print-Skip: pending until printer enters printing state
|
||||
self._pending_preprint_skip: list[str] = []
|
||||
self._pending_preprint_skip_deadline: float = 0.0
|
||||
|
||||
# Theme-Name prüfen (keine Sonderzeichen oder Umlaute)
|
||||
raw_theme = (getattr(args, "ui_theme", None) or "default").strip()
|
||||
if not _UI_THEME_NAME_RE.match(raw_theme):
|
||||
log.warning("Invalid UI theme name %r – using default", raw_theme)
|
||||
log.warning("Ungültiger UI-Theme-Name %r – nutze default", raw_theme)
|
||||
raw_theme = "default"
|
||||
self._ui_theme = raw_theme
|
||||
self._index_tpl_cache: str | None = None
|
||||
@@ -927,9 +914,7 @@ class KobraXBridge:
|
||||
# Zentral hier, damit es alle Druck-Startwege abdeckt (OrcaSlicer + UI).
|
||||
# _camera_autostarted verhindert Mehrfach-Trigger pro Druck.
|
||||
if kobra_state == "printing":
|
||||
if (getattr(self._args, "camera_on_print", 0)
|
||||
and not self._camera_autostarted
|
||||
and not self._camera_user_stopped):
|
||||
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
|
||||
self._camera_autostarted = True
|
||||
try:
|
||||
self.client.start_camera()
|
||||
@@ -938,7 +923,6 @@ class KobraXBridge:
|
||||
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||
self._camera_autostarted = False
|
||||
self._camera_user_stopped = False # für nächsten Druck freigeben
|
||||
|
||||
# Job-History: Druckstart erkennen
|
||||
if kobra_state == "printing" and not self._current_job_id:
|
||||
@@ -950,7 +934,7 @@ class KobraXBridge:
|
||||
gcode_file_id=gf["id"],
|
||||
printer_id=self._printer_id,
|
||||
)
|
||||
log.info(f"Job started: {self._current_job_id} for {filename}")
|
||||
log.info(f"Job gestartet: {self._current_job_id} für {filename}")
|
||||
|
||||
# Job-History: Druckende erkennen
|
||||
if kobra_state in ("finished",) and self._current_job_id:
|
||||
@@ -1017,9 +1001,7 @@ class KobraXBridge:
|
||||
# Kamera-Autostart auch hier (OrcaSlicer meldet Start oft via info/report).
|
||||
# _camera_autostarted-Guard verhindert Doppel-Start mit _on_print.
|
||||
if kobra_state == "printing":
|
||||
if (getattr(self._args, "camera_on_print", 0)
|
||||
and not self._camera_autostarted
|
||||
and not self._camera_user_stopped):
|
||||
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
|
||||
self._camera_autostarted = True
|
||||
try:
|
||||
self.client.start_camera()
|
||||
@@ -1028,7 +1010,6 @@ class KobraXBridge:
|
||||
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||
self._camera_autostarted = False
|
||||
self._camera_user_stopped = False # für nächsten Druck freigeben
|
||||
if project:
|
||||
if "filename" in project:
|
||||
self._state["filename"] = project["filename"]
|
||||
@@ -1071,16 +1052,7 @@ class KobraXBridge:
|
||||
"""
|
||||
d = payload.get("data") or {}
|
||||
skipped = d.get("objects_skip_parts") or d.get("skipped") or d.get("skipped_parts") or []
|
||||
# Während ein Pre-Print-Skip noch pending ist, leere Früh-Reports ignorieren
|
||||
# damit die UI nicht sofort zurückspringt bevor der Drucker den Skip bestätigt.
|
||||
now = time.time()
|
||||
if (not skipped and self._pending_preprint_skip
|
||||
and now <= self._pending_preprint_skip_deadline):
|
||||
return
|
||||
# Pending-Lock aufheben sobald Drucker die gewünschten Objekte bestätigt
|
||||
if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip):
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
# Liste immer (auch leer) übernehmen – sonst bleibt sie auf alten Stand
|
||||
self._skip_state = {
|
||||
"skipped": list(skipped),
|
||||
"ts": int(time.time()),
|
||||
@@ -1092,54 +1064,22 @@ class KobraXBridge:
|
||||
d = payload.get("data") or {}
|
||||
details = d.get("file_details") or {}
|
||||
thumb = details.get("thumbnail") or details.get("png_image") or ""
|
||||
file_name = d.get("filename") or details.get("filename") or self._last_uploaded_file
|
||||
active_print = self._state.get("print_state") in ("printing", "paused")
|
||||
current_print_file = self._state.get("filename") or ""
|
||||
# Uploads während eines laufenden Drucks dürfen die aktive
|
||||
# Fortschritts-Vorschau nicht überschreiben.
|
||||
if thumb and (not active_print or (file_name and file_name == current_print_file)):
|
||||
if thumb:
|
||||
self._thumbnail_b64 = thumb
|
||||
log.info(f"Vorschaubild empfangen: {len(thumb)} Zeichen base64")
|
||||
# Part-Skip: Objekt-Liste + optionales SVG (v0.9.10)
|
||||
objs = details.get("objects_skip_parts") or []
|
||||
svg = details.get("svg_image") or ""
|
||||
if objs:
|
||||
filename = file_name
|
||||
filename = d.get("filename") or details.get("filename") or self._last_uploaded_file
|
||||
if filename:
|
||||
try:
|
||||
self._store.update_file_objects(filename, objs, svg)
|
||||
log.info(f"Skip objects for {filename}: {len(objs)} ({'with SVG' if svg else 'no SVG'})")
|
||||
log.info(f"Skip-Objekte für {filename}: {len(objs)} ({'mit SVG' if svg else 'ohne SVG'})")
|
||||
except Exception as e:
|
||||
log.warning(f"update_file_objects fehlgeschlagen: {e}")
|
||||
self._push_status_update()
|
||||
|
||||
def _apply_preprint_skip_after_start(self, names: list[str], retries: int = 20, delay_s: float = 0.75):
|
||||
"""Sendet Skip-Befehl erst nachdem Drucker in printing-State gewechselt hat.
|
||||
|
||||
Vorher sendet der Drucker den Befehl ins Leere (kein aktiver Druck).
|
||||
"""
|
||||
wanted = [str(n) for n in (names or []) if isinstance(n, str) and n]
|
||||
if not wanted:
|
||||
return False
|
||||
for i in range(max(1, int(retries))):
|
||||
try:
|
||||
if self._state.get("kobra_state") != "printing":
|
||||
time.sleep(max(0.1, float(delay_s)))
|
||||
continue
|
||||
resp = self.client.skip_objects(wanted)
|
||||
if resp is not None:
|
||||
log.info(f"Pre-Print skip applied ({len(wanted)} objects) on attempt {i+1}/{retries}")
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
return True
|
||||
except Exception as e:
|
||||
log.debug(f"Pre-Print skip attempt {i+1}/{retries} failed: {e}")
|
||||
time.sleep(max(0.1, float(delay_s)))
|
||||
log.warning(f"Pre-Print skip could not be confirmed after {retries} attempts")
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _detect_filament_mode(boxes: list, head_tools_model: int = -1) -> str:
|
||||
"""Detect active filament topology mode.
|
||||
@@ -1747,22 +1687,13 @@ class KobraXBridge:
|
||||
# WebSocket push
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Statische Objekte, die sich zur Laufzeit nie ändern. Sie werden einmalig
|
||||
# per objects.query/subscribe ausgeliefert, aber NICHT in jedem
|
||||
# notify_status_update mitgeschickt — sonst läuft Mobilerakers
|
||||
# ConfigFile.parse (teuer + strikt) bei jedem Status-Tick erneut und die App
|
||||
# hängt/crasht beim Refresh (Issue #48).
|
||||
_STATIC_STATUS_OBJECTS = ("configfile", "webhooks", "heaters", "history")
|
||||
|
||||
def _push_status_update(self):
|
||||
if not self.ws_clients:
|
||||
return
|
||||
objs = self._build_printer_objects()
|
||||
live = {k: v for k, v in objs.items() if k not in self._STATIC_STATUS_OBJECTS}
|
||||
msg = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notify_status_update",
|
||||
"params": [live, time.time()],
|
||||
"params": [self._build_printer_objects(), time.time()],
|
||||
}
|
||||
text = json.dumps(msg)
|
||||
dead = set()
|
||||
@@ -1920,17 +1851,6 @@ class KobraXBridge:
|
||||
"homing_origin": [0, 0, 0, 0],
|
||||
"position": [0, 0, self._estimate_current_z(), 0],
|
||||
},
|
||||
# motion_report: Mobileraker liest die Live-Geschwindigkeit hier
|
||||
# (live_velocity). Der Kobra-X-MQTT liefert KEINE echte mm/s, nur
|
||||
# einen print_speed_mode (1-4). live_velocity bleibt daher 0 — das
|
||||
# Objekt muss aber existieren, sonst zeigt Mobileraker nichts an
|
||||
# (motion_report war zuvor null). live_position spiegelt die
|
||||
# geschätzte Z-Höhe (wie gcode_move).
|
||||
"motion_report": {
|
||||
"live_position": [0, 0, self._estimate_current_z(), 0],
|
||||
"live_velocity": 0.0,
|
||||
"live_extruder_velocity": 0.0,
|
||||
},
|
||||
"fan": {
|
||||
"speed": (int(s.get("fan_speed") or 0)) / 100.0,
|
||||
"rpm": None,
|
||||
@@ -1964,81 +1884,6 @@ class KobraXBridge:
|
||||
"gcode_macro TIMELAPSE_TAKE_FRAME": {
|
||||
"is_paused": False,
|
||||
},
|
||||
# configfile stub — Mobileraker und andere Clients crashen ohne
|
||||
# dieses Objekt (Missing field: configFile). Werte aus der
|
||||
# entschlüsselten avata_main.conf (ACCFG1.0 — Kobra X Firmware).
|
||||
# Mobileraker (Issue #48) parst BEIDE Zweige config + settings durch
|
||||
# denselben ConfigFile.parse → ConfigExtruder.fromJson; ein leeres
|
||||
# config:{} ließ den nicht-nullbaren Dart-Parser crashen. Daher wird
|
||||
# config identisch zu settings gespiegelt.
|
||||
"configfile": self._klipper_configfile_stub(),
|
||||
}
|
||||
|
||||
def _klipper_configfile_stub(self) -> dict:
|
||||
"""Minimaler Klipper-configfile-Stub für Mobileraker/OctoApp (Issue #48).
|
||||
|
||||
Mobileraker parst BEIDE Zweige `config` und `settings` durch denselben
|
||||
ConfigFile.parse → ConfigExtruder.fromJson. Ein leeres `config: {}`
|
||||
ließ den nicht-nullbaren Dart-Parser crashen, daher wird `config`
|
||||
identisch zu `settings` gespiegelt. Werte aus der entschlüsselten
|
||||
avata_main.conf (ACCFG1.0 — Kobra X Firmware).
|
||||
"""
|
||||
settings = {
|
||||
"printer": {
|
||||
"kinematics": "cartesian",
|
||||
"max_velocity": 450,
|
||||
"max_accel": 10000,
|
||||
"max_z_velocity": 12,
|
||||
"max_z_accel": 100,
|
||||
"square_corner_velocity": 20.0,
|
||||
},
|
||||
"extruder": {
|
||||
"nozzle_diameter": 0.4,
|
||||
"filament_diameter": 1.75,
|
||||
"sensor_type": "ATC Semitec 104GT-2",
|
||||
"min_temp": 0,
|
||||
"max_temp": 320,
|
||||
"min_extrude_temp": 10,
|
||||
# Mobileraker ConfigExtruder erwartet diese Felder non-nullable
|
||||
# (max_extrude_only_distance, max_power) bzw. als Key präsent
|
||||
# (max_extrude_only_velocity/accel dürfen null sein). Fehlen =
|
||||
# Crash in ConfigExtruder.fromJson (Issue #48).
|
||||
"max_extrude_only_distance": 100.0,
|
||||
"max_power": 1.0,
|
||||
"max_extrude_only_velocity": None,
|
||||
"max_extrude_only_accel": None,
|
||||
},
|
||||
"heater_bed": {
|
||||
# Mobileraker ConfigHeaterBed: heater_pin, sensor_type, control
|
||||
# sind non-nullable. Werte sind Platzhalter (Bridge kennt die
|
||||
# echten Pins nicht — Anycubic-Firmware, kein Klipper-printer.cfg).
|
||||
"heater_pin": "PA0",
|
||||
"sensor_type": "ATC Semitec 104GT-2",
|
||||
"control": "pid",
|
||||
"min_temp": 0,
|
||||
"max_temp": 120,
|
||||
},
|
||||
# stepper_* mit non-nullable Pflichtfeldern (step_pin, dir_pin,
|
||||
# rotation_distance) füllen, sonst crasht ConfigStepper.fromJson.
|
||||
"stepper_x": {"step_pin": "PA1", "dir_pin": "PA2", "rotation_distance": 40,
|
||||
"position_min": -18.5, "position_max": 280},
|
||||
"stepper_y": {"step_pin": "PA3", "dir_pin": "PA4", "rotation_distance": 40,
|
||||
"position_min": -6.5, "position_max": 272.5},
|
||||
"stepper_z": {"step_pin": "PA5", "dir_pin": "PA6", "rotation_distance": 8,
|
||||
"position_min": -4, "position_max": 262},
|
||||
"virtual_sdcard": {"path": "/data/gcodes"},
|
||||
"pause_resume": {},
|
||||
"display_status": {},
|
||||
}
|
||||
# config + settings müssen dieselben Felder enthalten — Mobileraker
|
||||
# parst beide. deepcopy, damit kein Client durch geteilte Referenz
|
||||
# versehentlich beide Zweige mutiert.
|
||||
return {
|
||||
"config": copy.deepcopy(settings),
|
||||
"settings": settings,
|
||||
"warnings": [],
|
||||
"save_config_pending": False,
|
||||
"save_config_pending_items": {},
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2358,31 +2203,6 @@ class KobraXBridge:
|
||||
"name": entry.get("name", ""),
|
||||
"id": entry.get("id", "")})
|
||||
|
||||
async def handle_kx_visible_vendors(self, request):
|
||||
"""GET/POST /kx/filament/visible_vendors — Vendor-Sichtbarkeitsfilter
|
||||
fürs Slot-Profil-Dropdown (Issue #41 Option A).
|
||||
|
||||
GET → {"result": ["Polymaker", "eSUN", ...]}
|
||||
POST {"vendors": [...]} → speichert in config.ini [filament_profiles]
|
||||
visible_vendors. Leere Liste = alle sichtbar. KEIN Bridge-Neustart
|
||||
nötig (nur Anzeigefilter)."""
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
data = {}
|
||||
vendors = data.get("vendors") or []
|
||||
if not isinstance(vendors, list):
|
||||
return self._json_cors({"error": "vendors must be a list"}, status=400)
|
||||
self._visible_vendors = [str(v).strip() for v in vendors if str(v).strip()]
|
||||
try:
|
||||
import config_loader as _cl
|
||||
_cl.save_visible_vendors(self._visible_vendors)
|
||||
except Exception as e:
|
||||
log.warning(f"save_visible_vendors failed: {e}")
|
||||
return self._json_cors({"error": str(e)}, status=500)
|
||||
return self._json_cors({"result": self._visible_vendors})
|
||||
|
||||
def _load_orca_filaments(self) -> list[dict]:
|
||||
"""Lädt System- + User-Profile aus dem Cache. System-Profile kommen
|
||||
aus bridge/data/orca_filaments.json (Image-embedded), User-Profile
|
||||
@@ -2583,7 +2403,7 @@ class KobraXBridge:
|
||||
ams_box_mapping = self._build_auto_ams_box_mapping()
|
||||
|
||||
use_ams = len(ams_box_mapping) > 0
|
||||
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
|
||||
auto_leveling = getattr(self._args, "auto_leveling", 1)
|
||||
filename = gcode_file["filename"]
|
||||
file_path = gcode_file["path"]
|
||||
|
||||
@@ -2615,15 +2435,6 @@ class KobraXBridge:
|
||||
},
|
||||
}
|
||||
|
||||
# Pre-Print-Skip sofort im UI-Status spiegeln
|
||||
self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())}
|
||||
if excluded_objects:
|
||||
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
|
||||
self._pending_preprint_skip_deadline = time.time() + 12.0
|
||||
else:
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
|
||||
log.info(f"KX-Store Druckstart: {filename} ams={len(ams_box_mapping)} slots assignments={bool(assignments)} excluded={len(excluded_objects)}")
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
@@ -2632,9 +2443,6 @@ class KobraXBridge:
|
||||
if result is None:
|
||||
return self._json_cors({"error": "Keine Antwort vom Drucker"}, status=504)
|
||||
|
||||
if excluded_objects:
|
||||
loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects))
|
||||
|
||||
# Job in History starten
|
||||
self._current_job_id = self._store.start_job(
|
||||
gcode_file_id=gcode_file["id"],
|
||||
@@ -2739,16 +2547,18 @@ class KobraXBridge:
|
||||
})
|
||||
return web.json_response({"result": files})
|
||||
|
||||
def _build_file_metadata(self, filename: str) -> dict:
|
||||
"""Baut die Moonraker-file-metadata für eine Datei. Gemeinsame Quelle
|
||||
für HTTP /server/files/metadata UND den WS-RPC server.files.metadata
|
||||
(vorher hatte der WS-Pfad eigene, kaputte Logik mit einer nicht
|
||||
existierenden Store-Methode → leere Antwort → Mobileraker fragte in
|
||||
Endlosschleife, App hing beim Refresh, Issue #48).
|
||||
async def handle_files_metadata(self, request):
|
||||
"""Moonraker /server/files/metadata — moonraker-obico-Plugin holt das
|
||||
einmal pro Druck und liest daraus `object_height` (für `currentZ`-
|
||||
Anzeige im Obico-UI: `mmProgress` braucht maxZ), `layer_count`,
|
||||
`layer_height` und `first_layer_height` (für die Layer-Berechnung).
|
||||
|
||||
Liefert Mobileraker-kompatible Pflichtfelder: `filename`, `size`,
|
||||
`modified` sind in GCodeFile non-nullable; `print_start_time` und die
|
||||
Slicer-Felder optional."""
|
||||
Quelle: aktueller `_state` + GCode-Store-Eintrag wenn vorhanden.
|
||||
Wenn Layer-Heights weder im State noch im Store sind, Fallback auf die
|
||||
OrcaSlicer-Default-Filename-Heuristik (`_layer_height_from_filename`)."""
|
||||
filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "")
|
||||
if not filename:
|
||||
return web.json_response({"result": {}})
|
||||
s = self._state
|
||||
layer_h = float(s.get("layer_height") or 0.0)
|
||||
first_h = float(s.get("first_layer_height") or 0.0)
|
||||
@@ -2772,27 +2582,16 @@ class KobraXBridge:
|
||||
if layer_h and not first_h:
|
||||
first_h = layer_h
|
||||
object_height = round(first_h + max(0, total_layers - 1) * layer_h, 3) if (layer_h and total_layers) else 0.0
|
||||
return {
|
||||
return web.json_response({"result": {
|
||||
"filename": filename,
|
||||
# GCodeFile (Mobileraker) verlangt size als non-nullable int.
|
||||
"size": size_bytes or 1,
|
||||
"size": size_bytes,
|
||||
"modified": time.time(),
|
||||
"estimated_time": est_time or None,
|
||||
"layer_height": layer_h or None,
|
||||
"first_layer_height": first_h or None,
|
||||
"layer_count": total_layers or None,
|
||||
"object_height": object_height or None,
|
||||
"thumbnails": [],
|
||||
}
|
||||
|
||||
async def handle_files_metadata(self, request):
|
||||
"""Moonraker /server/files/metadata — moonraker-obico + Mobileraker
|
||||
holen Datei-Metadaten (Slicer-Zeit, Layer, object_height).
|
||||
Logik in _build_file_metadata (gemeinsam mit WS-RPC)."""
|
||||
filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "")
|
||||
if not filename:
|
||||
return web.json_response({"result": {}})
|
||||
return web.json_response({"result": self._build_file_metadata(filename)})
|
||||
}})
|
||||
|
||||
# ── Moonraker-Stubs für moonraker-obico ──────────────────────────────────
|
||||
async def handle_access_api_key(self, request):
|
||||
@@ -2856,20 +2655,7 @@ class KobraXBridge:
|
||||
return web.json_response({"result": {"count": len(result_jobs), "jobs": result_jobs}})
|
||||
|
||||
async def handle_webcams_list(self, request):
|
||||
"""Moonraker /server/webcams/list — Obico holt die Webcam-URLs hier.
|
||||
|
||||
Wenn der Client von einem anderen Host kommt (z.B. moonraker-obico auf
|
||||
separatem Server), braucht er absolute URLs damit er den Stream erreicht.
|
||||
Host-Header mit localhost/127.0.0.1 wird durch die echte LAN-IP ersetzt."""
|
||||
host_hdr = request.headers.get("Host", "") if request else ""
|
||||
host_name = (host_hdr or "").split(":")[0]
|
||||
port_part = f":{host_hdr.split(':')[1]}" if ":" in (host_hdr or "") else f":{self._args.port}"
|
||||
local_ip = getattr(self, "_local_ip", None) or host_name
|
||||
if host_name in ("localhost", "127.0.0.1", ""):
|
||||
host_name = local_ip
|
||||
base = f"http://{host_name}{port_part}"
|
||||
stream_url = f"{base}/api/camera/stream"
|
||||
snapshot_url = f"{base}/api/camera/snapshot"
|
||||
"""Moonraker /server/webcams/list — Obico holt die Webcam-URLs hier."""
|
||||
return web.json_response({
|
||||
"result": {
|
||||
"webcams": [
|
||||
@@ -2881,8 +2667,8 @@ class KobraXBridge:
|
||||
"icon": "mdiWebcam",
|
||||
"target_fps": 5,
|
||||
"target_fps_idle": 2,
|
||||
"stream_url": stream_url,
|
||||
"snapshot_url": snapshot_url,
|
||||
"stream_url": "/api/camera/stream",
|
||||
"snapshot_url": "/api/camera/snapshot",
|
||||
"flip_horizontal": False,
|
||||
"flip_vertical": False,
|
||||
"rotation": 0,
|
||||
@@ -3067,7 +2853,7 @@ class KobraXBridge:
|
||||
log.info(f"print/start → {filename} url={url} ams={len(ams_box_mapping)} slots mode={self._filament_mode}")
|
||||
result = self.client.publish("print", "start", payload, timeout=15.0)
|
||||
if result:
|
||||
log.info(f"Print start confirmed: state={result.get('state')}")
|
||||
log.info(f"Druckstart bestätigt: state={result.get('state')}")
|
||||
else:
|
||||
log.warning("Druckstart: keine Antwort vom Drucker")
|
||||
|
||||
@@ -3322,7 +3108,7 @@ class KobraXBridge:
|
||||
return web.json_response({"result": "disconnected"})
|
||||
|
||||
async def handle_api_restart(self, request):
|
||||
log.info("Restart requested via API")
|
||||
log.info("Neustart über API angefordert")
|
||||
response = web.json_response({"status": "restarting"})
|
||||
asyncio.get_event_loop().call_later(0.3, self._restart_bridge)
|
||||
return response
|
||||
@@ -3602,9 +3388,6 @@ class KobraXBridge:
|
||||
await loop.run_in_executor(None, lambda: self.client.publish(
|
||||
"video", "stopCapture", None, timeout=0
|
||||
))
|
||||
# Verhindert dass der Autostart-Guard die Kamera während des
|
||||
# laufenden Drucks wieder einschaltet (State-Flicker-Problem).
|
||||
self._camera_user_stopped = True
|
||||
return web.json_response({"result": "ok"})
|
||||
|
||||
async def handle_api_camera_snapshot(self, request):
|
||||
@@ -3664,7 +3447,7 @@ class KobraXBridge:
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
log.warning("Camera: ffmpeg not found – camera stream unavailable")
|
||||
log.warning("Kamera: ffmpeg nicht gefunden – Kamerastream nicht verfügbar")
|
||||
return web.Response(status=503, text="ffmpeg not found")
|
||||
except Exception as e:
|
||||
log.warning(f"Kamera: ffmpeg konnte nicht gestartet werden: {e}")
|
||||
@@ -3759,7 +3542,7 @@ class KobraXBridge:
|
||||
if not os.path.isfile(serve_path):
|
||||
return web.Response(status=404, text="not found")
|
||||
size = os.path.getsize(serve_path)
|
||||
log.info(f"Printer downloading file: {filename} ({size} bytes)")
|
||||
log.info(f"Drucker lädt Datei ab: {filename} ({size} bytes)")
|
||||
return web.FileResponse(serve_path, headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
})
|
||||
@@ -3797,7 +3580,6 @@ class KobraXBridge:
|
||||
"remain_time": s["remain_time"],
|
||||
"curr_layer": s["curr_layer"],
|
||||
"total_layers": s["total_layers"],
|
||||
"z_mm": self._estimate_current_z(),
|
||||
"filename": s["filename"],
|
||||
"slicer_time": slicer_time,
|
||||
"camera_url": s["camera_url"],
|
||||
@@ -3816,7 +3598,6 @@ class KobraXBridge:
|
||||
"thumbnail": thumbnail,
|
||||
"connection_error": s["connection_error"],
|
||||
"file_ready": s["file_ready"],
|
||||
"print_start_dialog": s.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1)),
|
||||
"version": self._read_version(),
|
||||
})
|
||||
|
||||
@@ -3955,10 +3736,6 @@ class KobraXBridge:
|
||||
"auto_leveling": getattr(self._args, "auto_leveling", 1),
|
||||
"camera_on_print": getattr(self._args, "camera_on_print", 0),
|
||||
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
|
||||
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
|
||||
"poll_interval": getattr(self._args, "poll_interval", 3),
|
||||
"filament_profiles": {str(k): v for k, v in self._filament_profiles.items()},
|
||||
"visible_vendors": self._visible_vendors,
|
||||
"ace_dry_presets": self._ace_dry_presets,
|
||||
})
|
||||
|
||||
@@ -3989,14 +3766,7 @@ class KobraXBridge:
|
||||
cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1))))
|
||||
cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0))))))
|
||||
cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1))))))
|
||||
cfg.set("print", "print_start_dialog", str(int(bool(data.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1))))))
|
||||
if "poll_interval" in data:
|
||||
try:
|
||||
pi = max(1, min(60, int(data["poll_interval"])))
|
||||
except (TypeError, ValueError):
|
||||
pi = 3
|
||||
cfg.set("bridge", "poll_interval", str(pi))
|
||||
elif not cfg.has_option("bridge", "poll_interval"):
|
||||
if not cfg.has_option("bridge", "poll_interval"):
|
||||
cfg.set("bridge", "poll_interval", "3")
|
||||
printer_name = str(data.get("printer_name", "")).strip()
|
||||
if printer_name:
|
||||
@@ -4087,7 +3857,7 @@ class KobraXBridge:
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write("# KX-Bridge Konfigurationsdatei\n\n")
|
||||
cfg.write(f)
|
||||
log.info(f"Printer '{name or creds['model']}' added as {sec} (port {new_port})")
|
||||
log.info(f"Drucker '{name or creds['model']}' als {sec} hinzugefügt (Port {new_port})")
|
||||
response = self._json_cors({"status": "restarting", "section": sec, "http_port": new_port})
|
||||
asyncio.get_event_loop().call_later(0.5, self._restart_bridge)
|
||||
return response
|
||||
@@ -4162,14 +3932,13 @@ class KobraXBridge:
|
||||
# die alten Werte statt der geänderten config.ini.
|
||||
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
|
||||
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
|
||||
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "PRINT_START_DIALOG",
|
||||
"FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME"):
|
||||
"BRIDGE_PRINTER_NAME"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
|
||||
if in_docker:
|
||||
# Docker/systemd: Prozess beenden reicht – der Supervisor startet neu (frische environ)
|
||||
log.info("Container environment detected – exiting for supervisor restart")
|
||||
log.info("Container-Umgebung erkannt – beende Prozess für Supervisor-Restart")
|
||||
os._exit(0)
|
||||
|
||||
frozen = getattr(sys, "frozen", False)
|
||||
@@ -4348,7 +4117,7 @@ class KobraXBridge:
|
||||
if fname == "kobrax_moonraker_bridge.py":
|
||||
return web.json_response(
|
||||
{"error": f"Download {fname}: HTTP {resp.status}"}, status=502)
|
||||
log.warning(f"Update: {fname} not found in release ({resp.status}) – skipped")
|
||||
log.warning(f"Update: {fname} nicht im Release ({resp.status}) – übersprungen")
|
||||
continue
|
||||
downloaded.append((app_dir / fname, await resp.read()))
|
||||
# Phase 2: atomar ersetzen (erst nach komplettem, erfolgreichem Download)
|
||||
@@ -4625,14 +4394,11 @@ class KobraXBridge:
|
||||
# Obico registriert obico_remote_event-Callback. Wir akzeptieren leer.
|
||||
result = "ok"
|
||||
elif method == "server.webcams.list":
|
||||
# WS-Variante: absolute URL mit echter LAN-IP statt localhost
|
||||
_lip = getattr(self, "_local_ip", None) or "127.0.0.1"
|
||||
_base = f"http://{_lip}:{self._args.port}"
|
||||
# WS-Variante des HTTP-Endpoints
|
||||
result = {"webcams": [{
|
||||
"name": "KX-Bridge", "location": "printer", "service": "mjpegstreamer",
|
||||
"enabled": True,
|
||||
"stream_url": f"{_base}/api/camera/stream",
|
||||
"snapshot_url": f"{_base}/api/camera/snapshot",
|
||||
"enabled": True, "stream_url": "/api/camera/stream",
|
||||
"snapshot_url": "/api/camera/snapshot",
|
||||
"flip_horizontal": False, "flip_vertical": False, "rotation": 0,
|
||||
"target_fps": 5, "aspect_ratio": "16:9",
|
||||
}]}
|
||||
@@ -4661,18 +4427,28 @@ class KobraXBridge:
|
||||
elif method == "machine.update.status":
|
||||
result = {"busy": False, "version_info": {}}
|
||||
elif method == "server.files.metadata":
|
||||
# Obico + Mobileraker fragen Metadaten zu einer Datei. Dieselbe
|
||||
# Logik wie der HTTP-Endpoint (vorher eigener kaputter Pfad mit
|
||||
# nicht existierender Store-Methode → leere Antwort →
|
||||
# Mobileraker-Endlosschleife, Issue #48).
|
||||
# Obico fragt nach Metadaten zu einer Datei (filename in params)
|
||||
fname = (params or {}).get("filename") if isinstance(params, dict) else None
|
||||
fname = fname or self._state.get("filename", "")
|
||||
result = self._build_file_metadata(fname) if fname else {}
|
||||
meta = {}
|
||||
if fname:
|
||||
try:
|
||||
rec = self._store.get_file_by_filename(fname) if hasattr(self._store, "get_file_by_filename") else None
|
||||
except Exception:
|
||||
rec = None
|
||||
if rec:
|
||||
meta = {
|
||||
"filename": rec.get("filename"),
|
||||
"size": rec.get("size_bytes") or 0,
|
||||
"modified": time.time(),
|
||||
"estimated_time": rec.get("est_print_time_sec") or 0,
|
||||
"thumbnails": [],
|
||||
}
|
||||
result = meta
|
||||
else:
|
||||
log.debug(f"Unbekannte RPC-Methode: {method}")
|
||||
result = {}
|
||||
except Exception as e:
|
||||
log.error(f"RPC error for {method}: {e}")
|
||||
log.error(f"RPC-Fehler für {method}: {e}")
|
||||
error = {"code": -32603, "message": str(e)}
|
||||
|
||||
if rpc_id is not None:
|
||||
@@ -4871,8 +4647,6 @@ def build_app(bridge: KobraXBridge) -> web.Application:
|
||||
r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots)
|
||||
r.add_get("/kx/filament/profiles", bridge.handle_kx_filament_profiles)
|
||||
r.add_post("/kx/filament/slots/{idx}/profile", bridge.handle_kx_filament_slot_profile)
|
||||
r.add_get("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors)
|
||||
r.add_post("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors)
|
||||
# Custom-Profile-Import (Issue #41) — User lädt eigene Orca-Filament-
|
||||
# Profile als ZIP/JSON hoch (z.B. aus ~/.config/OrcaSlicer/user/<id>/filament/),
|
||||
# weil die Bridge typischerweise nicht auf demselben Host wie OrcaSlicer läuft.
|
||||
@@ -4988,7 +4762,7 @@ async def run_bridge(args):
|
||||
site = web.TCPSite(runner, args.host, per_args.port)
|
||||
await site.start()
|
||||
runners.append((runner, client, pid))
|
||||
log.info(f"[Printer {pid}] Bridge running on http://{args.host}:{per_args.port}")
|
||||
log.info(f"[Drucker {pid}] Bridge läuft auf http://{args.host}:{per_args.port}")
|
||||
|
||||
import socket as _socket
|
||||
try:
|
||||
@@ -4997,9 +4771,6 @@ async def run_bridge(args):
|
||||
_local_ip = _s.getsockname()[0]
|
||||
except Exception:
|
||||
_local_ip = args.host
|
||||
# An alle Bridge-Instanzen weitergeben — wird für absolute Webcam-URLs genutzt
|
||||
for _b in all_bridges.values():
|
||||
_b._local_ip = _local_ip
|
||||
log.info(f"OrcaSlicer → Klipper → Host: {_local_ip} Ports: " +
|
||||
", ".join(str(getattr(b._args, 'port', 0)) for b in all_bridges.values()))
|
||||
log.info("Ctrl-C zum Beenden")
|
||||
@@ -5048,8 +4819,6 @@ def main():
|
||||
parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING)
|
||||
parser.add_argument("--camera-on-print", type=int, default=env_loader.CAMERA_ON_PRINT)
|
||||
parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
|
||||
parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG)
|
||||
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
|
||||
|
||||
parser.add_argument("--host", default="0.0.0.0",
|
||||
help="Bind-Adresse für den Bridge-Server")
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
// ── State ──
|
||||
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
|
||||
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
|
||||
curr_layer:0,total_layers:0,z_mm:0,printer_name:'Kobra X',firmware_version:'–',
|
||||
curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–',
|
||||
camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,
|
||||
ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]},web_upload_warning:1};
|
||||
var tempHistory={n:[],b:[]};
|
||||
var camOn=false;
|
||||
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
|
||||
var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support)
|
||||
var _lastLoadedFile=null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55)
|
||||
var _fdDialogOpen=false; // Dialog ist gerade offen
|
||||
var _fdAutoOpenedFile=null; // Dateiname für den der Dialog auto-geöffnet wurde
|
||||
var _fdUserCancelled=false; // User hat den Auto-Open-Dialog abgebrochen
|
||||
var currentStep=1;
|
||||
var currentPanel='dashboard';
|
||||
var aceAutoRefillPrefs=(function(){
|
||||
@@ -107,7 +101,6 @@ function tr(key,fallback){
|
||||
function _langToggleLabel(lang){
|
||||
if(lang==='de')return 'Deutsch';
|
||||
if(lang==='en')return 'English';
|
||||
if(lang==='fr')return 'Français';
|
||||
if(lang==='zh-cn')return '简体中文';
|
||||
return 'Espanol';
|
||||
}
|
||||
@@ -171,8 +164,6 @@ async function setLanguage(lang){
|
||||
localStorage.setItem('lang',l);
|
||||
var langSel=document.getElementById('lang-select');
|
||||
if(langSel)langSel.value=l;
|
||||
var sLangSel=document.getElementById('s-lang-select');
|
||||
if(sLangSel)sLangSel.value=l;
|
||||
document.documentElement.setAttribute('lang',l);
|
||||
applyLang();
|
||||
}
|
||||
@@ -291,7 +282,6 @@ function applyLang(){
|
||||
setText('d-lbl-remain',T.lbl_remaining);
|
||||
setText('d-slicer-label',T.lbl_slicer_time);
|
||||
setText('d-lbl-layers',T.lbl_layers);
|
||||
setText('d-lbl-zpos',T.lbl_zpos);
|
||||
setText('d-lbl-light',T.lbl_light);
|
||||
setText('d-lbl-nozzle',T.label_nozzle);
|
||||
setText('d-lbl-bed',T.label_bed);
|
||||
@@ -316,26 +306,12 @@ function applyLang(){
|
||||
document.querySelectorAll('.temp-input').forEach(e=>e.setAttribute('placeholder',T.label_target_c.replace(':','')));
|
||||
// Console
|
||||
setText('ptitle-console',T.panel_console_title);
|
||||
// Settings-Panel
|
||||
// Settings modal
|
||||
setText('modal-title-settings',T.settings_title);
|
||||
setText('modal-sec-connection',T.settings_connection);
|
||||
setText('modal-sec-print',T.settings_print);
|
||||
setText('modal-sec-poll',T.settings_poll);
|
||||
setText('modal-sec-version',T.settings_version);
|
||||
// Nav + Kategorie-Labels (mit Fallback falls i18n-Key noch fehlt)
|
||||
setText('nav-settings',T.nav_settings||'Einstellungen');
|
||||
setText('setcat-lbl-connection',T.settings_connection||'Verbindung');
|
||||
setText('setcat-lbl-printer',T.settings_print||'Drucker');
|
||||
setText('setcat-lbl-display',T.settings_cat_display||'Darstellung');
|
||||
setText('setcat-lbl-display2',T.settings_cat_display||'Darstellung');
|
||||
setText('setcat-lbl-filament',T.settings_cat_filament||'Filament');
|
||||
setText('setcat-lbl-system',T.settings_version||'System');
|
||||
setText('lbl-set-lang',T.settings_cat_language||'Sprache');
|
||||
setText('lbl-set-theme',T.settings_cat_theme||'Hell / Dunkel umschalten');
|
||||
setText('lbl-poll-interval',T.settings_poll||'Poll-Intervall (Sekunden)');
|
||||
setText('lbl-filament-mapping',T.settings_filament_mapping||'Filament-Profil-Mapping (pro Slot)');
|
||||
setText('lbl-filament-mapping-save',T.settings_filament_mapping_save||'Mapping speichern');
|
||||
setText('lbl-visible-vendors',T.settings_visible_vendors||'Sichtbare Hersteller (Profil-Dropdown)');
|
||||
setText('visible-vendors-hint',T.settings_visible_vendors_hint||'Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.');
|
||||
setText('lbl-visible-vendors-save',T.settings_visible_vendors_save||'Auswahl speichern');
|
||||
// Custom-Profile-Import (Issue #41)
|
||||
setText('modal-sec-orca-profiles',T.orca_profile_section);
|
||||
setText('orca-profiles-hint',T.orca_profile_hint);
|
||||
@@ -358,20 +334,11 @@ function applyLang(){
|
||||
setText('lbl-default-slot',T.settings_default_slot);
|
||||
setText('opt-slot-auto',T.settings_slot_auto);
|
||||
setText('lbl-auto-leveling',T.settings_auto_leveling);
|
||||
setText('lbl-file-ready-mode',T.settings_file_ready_mode);
|
||||
setText('opt-file-ready-dialog',T.settings_file_ready_dialog);
|
||||
setText('opt-file-ready-banner',T.settings_file_ready_banner);
|
||||
setText('lbl-camera-on-print',T.settings_camera_on_print);
|
||||
setText('lbl-web-upload-warning',T.settings_web_upload_warning);
|
||||
setText('fd-options-title',T.fd_options_title);
|
||||
setText('fd-lbl-auto-leveling',T.print_auto_leveling);
|
||||
|
||||
setText('lbl-update-check',T.update_check);
|
||||
setText('lbl-update-apply',T.update_apply);
|
||||
// Progress-Karten-Aktionen für geladene/idle Datei (Issue #55)
|
||||
setText('d-idle-print-lbl',T.progress_action_print||'Drucken');
|
||||
setText('d-idle-slots-lbl',T.progress_action_slots||'Slots zuordnen');
|
||||
setText('d-idle-clear-lbl',T.progress_action_clear||'Leeren');
|
||||
// Speed buttons
|
||||
setText('d-spd-lbl-1',T.speed_silent.replace(/^\S+\s/,''));
|
||||
setText('d-spd-lbl-2',T.speed_normal.replace(/^\S+\s/,''));
|
||||
@@ -491,15 +458,6 @@ function showPanel(id){
|
||||
var nb=document.getElementById('nb-'+id);if(nb)nb.classList.add('active');
|
||||
var bnb=document.getElementById('bnb-'+id);if(bnb)bnb.classList.add('active');
|
||||
currentPanel=id;
|
||||
if(id==='settings')openSettings();
|
||||
}
|
||||
|
||||
// Settings-Kategorie umschalten (Master-Detail)
|
||||
function showSettingsCat(name){
|
||||
document.querySelectorAll('.set-group').forEach(g=>g.classList.remove('active'));
|
||||
document.querySelectorAll('.set-cat').forEach(b=>b.classList.remove('active'));
|
||||
var g=document.getElementById('setgrp-'+name);if(g)g.classList.add('active');
|
||||
var c=document.getElementById('setcat-'+name);if(c)c.classList.add('active');
|
||||
}
|
||||
|
||||
// ── Console log ──
|
||||
@@ -657,21 +615,11 @@ function applyState(){
|
||||
// connection error banner – nur wenn überhaupt ein Drucker konfiguriert ist
|
||||
var banner=document.getElementById('conn-error-banner');
|
||||
if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+tr('lbl_conn_error')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}}
|
||||
var bannerVisible=false;
|
||||
var frb=document.getElementById('file-ready-banner');
|
||||
if(frb){
|
||||
var shouldAutoOpen=(s.print_start_dialog===undefined?true:!!s.print_start_dialog);
|
||||
if(s.file_ready&&s.print_state==='standby'){
|
||||
document.getElementById('file-ready-name').textContent=s.file_ready;
|
||||
// Neue Datei → Abbruch-Sperre aufheben
|
||||
if(_fdAutoOpenedFile&&_fdAutoOpenedFile!==s.file_ready) _fdUserCancelled=false;
|
||||
if(shouldAutoOpen&&!_fdDialogOpen&&!_fdUserCancelled&&_fdAutoOpenedFile!==s.file_ready){
|
||||
_fdAutoOpenedFile=s.file_ready;
|
||||
startReadyFileWithSlots(s.file_ready,true);
|
||||
} else {
|
||||
frb.style.display='flex';
|
||||
bannerVisible=true;
|
||||
}
|
||||
frb.style.display='flex';
|
||||
}else{frb.style.display='none';}
|
||||
}
|
||||
// skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird
|
||||
@@ -683,25 +631,6 @@ function applyState(){
|
||||
var ctrlBtns=document.getElementById('d-ctrl-btns');
|
||||
if(ctrlBtns) ctrlBtns.style.display=printing?'':'none';
|
||||
updatePauseResumeBtn();
|
||||
// Zuletzt geladene Datei merken (Issue #55): solange sie über den State
|
||||
// sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename
|
||||
// (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen.
|
||||
if(s.file_ready) _lastLoadedFile=s.file_ready;
|
||||
else if(s.filename) _lastLoadedFile=s.filename;
|
||||
// Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine
|
||||
// Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion
|
||||
// anbietet.
|
||||
var idleBtns=document.getElementById('d-idle-btns');
|
||||
if(idleBtns){
|
||||
var showIdle=(!printing && _lastLoadedFile && !bannerVisible);
|
||||
idleBtns.style.display=showIdle?'':'none';
|
||||
if(showIdle){
|
||||
var dfn=document.getElementById('d-fname');
|
||||
if(dfn && (!dfn.textContent || dfn.textContent==='–')){
|
||||
dfn.textContent=_lastLoadedFile;dfn.title=_lastLoadedFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// header
|
||||
var b=document.getElementById('h-badge');
|
||||
@@ -730,7 +659,6 @@ function applyState(){
|
||||
|
||||
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–';
|
||||
var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers;
|
||||
var dzpos=document.getElementById('d-zpos');if(dzpos)dzpos.textContent=s.z_mm>0?s.z_mm.toFixed(2)+' mm':'–';
|
||||
|
||||
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration);
|
||||
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–';
|
||||
@@ -894,14 +822,10 @@ function applyState(){
|
||||
var co=document.getElementById('cam-overlay');
|
||||
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
|
||||
|
||||
// auto-start camera during print (unless user explicitly stopped it)
|
||||
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){
|
||||
// auto-start camera during print
|
||||
if(s.print_state==='printing'&&!camOn&&s.camera_url){
|
||||
camStart();
|
||||
}
|
||||
// reset user-stopped flag when print ends so next print auto-starts again
|
||||
if(s.print_state!=='printing'){
|
||||
camUserStopped=false;
|
||||
}
|
||||
|
||||
updateConnBtn();
|
||||
}
|
||||
@@ -963,6 +887,10 @@ function drawChart(id,_,series){
|
||||
var _updateTag='';
|
||||
var _updateUrl='';
|
||||
function openSettings(){
|
||||
// Titel mit aktivem Drucker-Namen aktualisieren
|
||||
var pname=_activePrinter&&_activePrinter.name?_activePrinter.name:null;
|
||||
var title=document.getElementById('modal-title-settings');
|
||||
if(title)title.textContent=T.settings_title+(pname?' – '+pname:'');
|
||||
fetch(_apiUrl('/api/settings')).then(function(r){return r.json()}).then(function(d){
|
||||
document.getElementById('s-printer-name').value=d.printer_name||'';
|
||||
document.getElementById('s-printer-ip').value=d.printer_ip||'';
|
||||
@@ -974,120 +902,24 @@ function openSettings(){
|
||||
document.getElementById('s-default-slot').value=d.default_ams_slot||'auto';
|
||||
document.getElementById('s-auto-leveling').checked=(d.auto_leveling===undefined?true:!!d.auto_leveling);
|
||||
var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print;
|
||||
var frm=document.getElementById('s-file-ready-mode');if(frm)frm.value=(d.print_start_dialog===undefined?'1':String(d.print_start_dialog?1:0));
|
||||
var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning);
|
||||
// Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage
|
||||
var pi=document.getElementById('s-poll-interval');
|
||||
if(pi){
|
||||
var sec=d.poll_interval||Math.round((parseInt(localStorage.getItem('pollInterval')||'2000'))/1000)||3;
|
||||
pi.value=sec;
|
||||
}
|
||||
renderFilamentMapping(d.filament_profiles||{});
|
||||
});
|
||||
// Sprach-Select im Settings-Panel mit aktueller Sprache spiegeln
|
||||
var ls=document.getElementById('s-lang-select');
|
||||
if(ls)ls.value=(localStorage.getItem('lang')||document.documentElement.lang||'de');
|
||||
var v=localStorage.getItem('pollInterval')||'2000';
|
||||
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
|
||||
var pb=document.getElementById('poll-'+Math.round(parseInt(v)/1000));
|
||||
if(pb)pb.classList.add('active');
|
||||
document.getElementById('s-version-label').textContent='v'+('__VERSION__'||'?');
|
||||
document.getElementById('update-status').textContent='';
|
||||
document.getElementById('btn-update-apply').style.display='none';
|
||||
var cl=document.getElementById('update-changelog');if(cl)cl.style.display='none';
|
||||
_updateTag='';_updateUrl='';
|
||||
// Custom-Profile-Liste laden (Issue #41)
|
||||
document.getElementById('settings-modal').classList.add('open');
|
||||
// Custom-Profile-Liste laden (Issue #41 — Verwaltung von User-importierten
|
||||
// OrcaSlicer-Filament-Profilen)
|
||||
refreshUserProfileList();
|
||||
// Vendor-Sichtbarkeitsfilter (Issue #41 Option A)
|
||||
loadVendorChecklist();
|
||||
}
|
||||
function closeSettings(){
|
||||
// Panel-Variante: zurück zum Dashboard
|
||||
showPanel('dashboard');
|
||||
}
|
||||
|
||||
// Poll-Intervall-Feld → Live-Poll sofort übernehmen (Persistenz erst beim Speichern)
|
||||
function onPollIntervalInput(){
|
||||
var pi=document.getElementById('s-poll-interval');
|
||||
if(!pi)return;
|
||||
var sec=parseInt(pi.value,10);
|
||||
if(sec>=1&&sec<=60)setPoll(sec*1000);
|
||||
}
|
||||
|
||||
// ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ──
|
||||
function renderFilamentMapping(map){
|
||||
var el=document.getElementById('filament-mapping-list');
|
||||
if(!el)return;
|
||||
var rows='';
|
||||
for(var i=0;i<4;i++){
|
||||
var m=map[i]||map[String(i)]||{};
|
||||
var idHint=m.id?' <span style="color:var(--txt2);font-size:11px">('+m.id+')</span>':'';
|
||||
rows+='<div class="modal-field" style="margin-bottom:8px">'
|
||||
+'<label>Slot '+(i+1)+idHint+'</label>'
|
||||
+'<div style="display:flex;gap:6px;flex-wrap:wrap">'
|
||||
+'<input type="text" id="fmap-'+i+'-vendor" placeholder="Vendor (z.B. Polymaker)" value="'+(m.vendor||'')+'" style="flex:1;min-width:120px">'
|
||||
+'<input type="text" id="fmap-'+i+'-name" placeholder="Name (z.B. PolyTerra PLA)" value="'+(m.name||'')+'" style="flex:1;min-width:140px">'
|
||||
+'</div></div>';
|
||||
}
|
||||
el.innerHTML=rows;
|
||||
}
|
||||
function saveFilamentMapping(){
|
||||
// Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend).
|
||||
// Leere Felder = Mapping entfernen.
|
||||
var chain=Promise.resolve();
|
||||
for(var i=0;i<4;i++){
|
||||
(function(slot){
|
||||
var vendor=((document.getElementById('fmap-'+slot+'-vendor')||{}).value||'').trim();
|
||||
var name=((document.getElementById('fmap-'+slot+'-name')||{}).value||'').trim();
|
||||
chain=chain.then(function(){
|
||||
return fetch(_apiUrl('/kx/filament/slots/'+slot+'/profile'),
|
||||
{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({vendor:vendor,name:name})});
|
||||
});
|
||||
})(i);
|
||||
}
|
||||
chain.then(function(){
|
||||
clog(tr('log_filament_mapping_saved')||'Filament-Mapping gespeichert','msg-ok');
|
||||
openSettings(); // neu laden → ID-Hints aktualisieren
|
||||
}).catch(function(e){clog('Mapping-Fehler: '+e,'msg-err');});
|
||||
}
|
||||
|
||||
// ── Vendor-Sichtbarkeitsfilter (Issue #41 Option A) ──
|
||||
var _vendorChecklistSel={}; // {vendor:true} — laufende Auswahl im UI
|
||||
function loadVendorChecklist(){
|
||||
// aktuelle Auswahl aus Backend, dann alle verfügbaren Vendoren rendern
|
||||
_visibleVendors=null; // Cache invalidieren
|
||||
_loadVisibleVendors(function(vis){
|
||||
_vendorChecklistSel={};
|
||||
(vis||[]).forEach(function(v){_vendorChecklistSel[v]=true;});
|
||||
renderVendorChecklist();
|
||||
});
|
||||
}
|
||||
function renderVendorChecklist(){
|
||||
var el=document.getElementById('visible-vendors-list');
|
||||
if(!el)return;
|
||||
_loadOrcaFilaments(function(profiles){
|
||||
// alle System-Vendoren (ohne Generic — der ist immer sichtbar) sammeln
|
||||
var set={};
|
||||
profiles.forEach(function(p){ if(!p.is_user && p.vendor && p.vendor!=='Generic') set[p.vendor]=1; });
|
||||
var vendors=Object.keys(set).sort();
|
||||
var q=((document.getElementById('vendor-filter-search')||{}).value||'').toLowerCase();
|
||||
if(q)vendors=vendors.filter(function(v){return v.toLowerCase().indexOf(q)>=0;});
|
||||
el.innerHTML=vendors.map(function(v){
|
||||
var ck=_vendorChecklistSel[v]?'checked':'';
|
||||
var safe=v.replace(/"/g,'"');
|
||||
return '<label style="display:flex;align-items:center;gap:8px;padding:3px 0;cursor:pointer;font-size:13px">'
|
||||
+'<input type="checkbox" data-vendor="'+safe+'" '+ck+' onchange="_vendorCheck(this)" style="width:auto;margin:0"> '+v+'</label>';
|
||||
}).join('')||'<i style="color:var(--txt2)">–</i>';
|
||||
});
|
||||
}
|
||||
function _vendorCheck(cb){
|
||||
var v=cb.getAttribute('data-vendor');
|
||||
if(cb.checked)_vendorChecklistSel[v]=true; else delete _vendorChecklistSel[v];
|
||||
}
|
||||
function saveVisibleVendors(){
|
||||
var vendors=Object.keys(_vendorChecklistSel);
|
||||
fetch(_apiUrl('/kx/filament/visible_vendors'),{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({vendors:vendors})}).then(function(r){return r.json();}).then(function(){
|
||||
_visibleVendors=vendors.slice(); // Dropdown-Cache aktualisieren
|
||||
clog(tr('log_visible_vendors_saved')||'Hersteller-Auswahl gespeichert','msg-ok');
|
||||
}).catch(function(e){clog('Vendor-Filter-Fehler: '+e,'msg-err');});
|
||||
document.getElementById('settings-modal').classList.remove('open');
|
||||
}
|
||||
|
||||
// ── Custom Filament Profile Import (Issue #41) ──
|
||||
@@ -1161,9 +993,6 @@ function doProfileImportUpload(files){
|
||||
_orcaFilamentCache=null;
|
||||
refreshImportDialogList();
|
||||
refreshUserProfileList();
|
||||
// Vendor-Checkliste neu aufbauen — ein Import kann einen bisher
|
||||
// unbekannten System-Vendor mitbringen (Issue #41).
|
||||
if(document.getElementById('visible-vendors-list')) renderVendorChecklist();
|
||||
// Wenn Slot-Edit offen ist, Dropdown gleich neu befüllen
|
||||
var mat=document.getElementById('slot-edit-mat');
|
||||
if(mat && document.getElementById('slot-edit-modal').classList.contains('open')){
|
||||
@@ -1204,14 +1033,6 @@ function updateSlotEditFeedButton(){
|
||||
btn.textContent=_slotEditLoaded?tr('slot_edit_unload'):tr('slot_edit_load');
|
||||
}
|
||||
var _orcaFilamentCache=null; // [{id,name,vendor,type,color}, …]
|
||||
var _visibleVendors=null; // Vendor-Sichtbarkeitsfilter (Issue #41); [] = alle
|
||||
function _loadVisibleVendors(cb){
|
||||
if(_visibleVendors!==null){ cb(_visibleVendors); return; }
|
||||
fetch(_apiUrl('/kx/filament/visible_vendors')).then(function(r){return r.json();}).then(function(d){
|
||||
_visibleVendors=d.result||[];
|
||||
cb(_visibleVendors);
|
||||
}).catch(function(){ _visibleVendors=[]; cb([]); });
|
||||
}
|
||||
function _loadOrcaFilaments(cb){
|
||||
if(_orcaFilamentCache){ cb(_orcaFilamentCache); return; }
|
||||
fetch(_apiUrl('/kx/filament/profiles')).then(function(r){return r.json();}).then(function(d){
|
||||
@@ -1257,23 +1078,13 @@ function _fillSlotProfileDropdown(material, currentVendor, currentName){
|
||||
userProfs.forEach(function(p){ _appendOption(gUser, p); });
|
||||
sel.appendChild(gUser);
|
||||
}
|
||||
// Vendor-Sichtbarkeitsfilter (Issue #41 Option A): nur gewählte Vendoren +
|
||||
// Generic. Leere Liste = alle (rückwärtskompatibel). Eigene Profile (is_user)
|
||||
// sind oben bereits unkonditional drin.
|
||||
_loadVisibleVendors(function(vis){
|
||||
var filtered=systemProfs;
|
||||
if(vis&&vis.length){
|
||||
var allow={};vis.forEach(function(v){allow[v]=1;});
|
||||
allow['Generic']=1;
|
||||
filtered=systemProfs.filter(function(p){return allow[p.vendor];});
|
||||
}
|
||||
var byVendor={};
|
||||
filtered.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); });
|
||||
Object.keys(byVendor).sort().forEach(function(v){
|
||||
var g=document.createElement('optgroup'); g.label=v;
|
||||
byVendor[v].forEach(function(p){ _appendOption(g, p); });
|
||||
sel.appendChild(g);
|
||||
});
|
||||
// System-Profile nach Vendor gruppieren
|
||||
var byVendor={};
|
||||
systemProfs.forEach(function(p){ (byVendor[p.vendor]=byVendor[p.vendor]||[]).push(p); });
|
||||
Object.keys(byVendor).sort().forEach(function(v){
|
||||
var g=document.createElement('optgroup'); g.label=v;
|
||||
byVendor[v].forEach(function(p){ _appendOption(g, p); });
|
||||
sel.appendChild(g);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1327,16 +1138,15 @@ function slotEditFeed(){
|
||||
})
|
||||
.catch(function(){});
|
||||
}
|
||||
function startReadyFile(filename){
|
||||
var fn=filename||S.file_ready;
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
|
||||
function startReadyFile(){
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===S.file_ready;});
|
||||
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFile(fn); });
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFile(); });
|
||||
return;
|
||||
}
|
||||
var btn=document.getElementById('file-ready-btn');
|
||||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||||
post('/printer/print/start',{filename:fn})
|
||||
post('/printer/print/start',{filename:S.file_ready})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(r){
|
||||
document.getElementById('file-ready-banner').style.display='none';
|
||||
@@ -1351,20 +1161,6 @@ function cancelReadyFile(){
|
||||
post('/api/file_ready/clear',{})
|
||||
.then(function(){document.getElementById('file-ready-banner').style.display='none';});
|
||||
}
|
||||
|
||||
// ── Aktionen für geladene/idle Datei in der Progress-Karte (Issue #55) ──
|
||||
function startIdleFile(){
|
||||
if(_lastLoadedFile) startReadyFile(_lastLoadedFile);
|
||||
}
|
||||
function startIdleFileWithSlots(){
|
||||
if(_lastLoadedFile) startReadyFileWithSlots(_lastLoadedFile);
|
||||
}
|
||||
function clearIdleFile(){
|
||||
_lastLoadedFile=null;
|
||||
var ib=document.getElementById('d-idle-btns');if(ib)ib.style.display='none';
|
||||
var fn=document.getElementById('d-fname');if(fn){fn.textContent='–';fn.title='';}
|
||||
post('/api/file_ready/clear',{}).catch(function(){});
|
||||
}
|
||||
function selectMatPreset(m){
|
||||
document.getElementById('slot-edit-mat').value=m;
|
||||
highlightMatBtn(m);
|
||||
@@ -1452,6 +1248,9 @@ document.addEventListener('DOMContentLoaded',function(){
|
||||
});
|
||||
});
|
||||
function setPoll(ms){
|
||||
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
|
||||
var id='poll-'+Math.round(ms/1000);
|
||||
var pb=document.getElementById(id);if(pb)pb.classList.add('active');
|
||||
localStorage.setItem('pollInterval',ms);
|
||||
clearInterval(pollTimer);
|
||||
pollTimer=setInterval(poll,ms);
|
||||
@@ -1472,9 +1271,7 @@ function saveSettings(){
|
||||
default_ams_slot: document.getElementById('s-default-slot').value,
|
||||
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
|
||||
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
|
||||
print_start_dialog: parseInt((document.getElementById('s-file-ready-mode')||{}).value||'1',10),
|
||||
web_upload_warning:webUploadWarning,
|
||||
poll_interval: Math.min(60,Math.max(1,parseInt((document.getElementById('s-poll-interval')||{}).value,10)||3)),
|
||||
}).then(function(){
|
||||
btn.textContent=T.update_restarting;
|
||||
setTimeout(function(){
|
||||
@@ -1640,8 +1437,7 @@ function setBed(){
|
||||
function setLight(){
|
||||
var on=document.getElementById('d-light-toggle').checked;
|
||||
post('/api/light',{on:on,brightness:80})
|
||||
.then(function(){clog('Licht '+(on?'an, 80%':'aus'),'msg-ok')})
|
||||
|
||||
.then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')})
|
||||
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
|
||||
}
|
||||
|
||||
@@ -1696,24 +1492,23 @@ function camStart(){
|
||||
img.style.display='none';
|
||||
sp.style.display='block';
|
||||
post('/api/camera/start',{}).then(function(){
|
||||
img.onerror=function(){
|
||||
sp.style.display='none';
|
||||
img.style.display='none';
|
||||
ph.style.display='flex';
|
||||
camOn=false;
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
|
||||
clog(tr('log_error')+' '+tr('cam_stream_unavailable'),'msg-err');
|
||||
};
|
||||
img.src='/api/camera/stream?t='+Date.now();
|
||||
camOn=true;
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_stop');
|
||||
clog(tr('log_cam_start'),'msg-ok');
|
||||
setTimeout(function(){ sp.style.display='none'; img.style.display='block'; },1200);
|
||||
if(/Android/i.test(navigator.userAgent)){
|
||||
// Android browsers don't support multipart/x-mixed-replace (MJPEG) — poll snapshots instead
|
||||
_camPollInterval=setInterval(function(){ img.src='/api/camera/snapshot?t='+Date.now(); },200);
|
||||
} else {
|
||||
img.onerror=function(){
|
||||
sp.style.display='none';
|
||||
img.style.display='none';
|
||||
ph.style.display='flex';
|
||||
camOn=false;
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
|
||||
clog(tr('log_error')+' '+tr('cam_stream_unavailable'),'msg-err');
|
||||
};
|
||||
img.src='/api/camera/stream?t='+Date.now();
|
||||
}
|
||||
// MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden
|
||||
setTimeout(function(){
|
||||
sp.style.display='none';
|
||||
img.style.display='block';
|
||||
},1200);
|
||||
}).catch(function(e){
|
||||
sp.style.display='none';
|
||||
ph.style.display='flex';
|
||||
@@ -1723,14 +1518,11 @@ function camStart(){
|
||||
|
||||
function camStop(){
|
||||
var img=document.getElementById('cam-img');
|
||||
if(_camPollInterval){clearInterval(_camPollInterval);_camPollInterval=null;}
|
||||
img.onerror=null; // deregister error handler before clearing src to avoid spurious error toast
|
||||
post('/api/camera/stop',{}).catch(function(){});
|
||||
img.src='';
|
||||
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');
|
||||
}
|
||||
@@ -2257,30 +2049,22 @@ function confirmStoreWebVerify(){
|
||||
});
|
||||
}
|
||||
|
||||
function startReadyFileWithSlots(filename,_autoOpen){
|
||||
if(!_autoOpen) _fdAutoOpenedFile=null; // manueller Aufruf → Auto-Open-Sperre aufheben
|
||||
var fn=filename||S.file_ready;
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===fn;});
|
||||
function startReadyFileWithSlots(){
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===S.file_ready;});
|
||||
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(fn); });
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(); });
|
||||
return;
|
||||
}
|
||||
_filamentDialogMode='banner';
|
||||
_storeFilename=fn||'';
|
||||
_storeFilename=S.file_ready||'';
|
||||
// Banner must never reuse stale store-file context.
|
||||
_storeFileId=null;
|
||||
_gcodeFilaments=[];
|
||||
|
||||
var _autoOpenFile=_autoOpen?fn:null;
|
||||
if(_autoOpen) _fdDialogOpen=true; // bereits während Fetch sperren
|
||||
function openWithSlots(){
|
||||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
|
||||
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
|
||||
openFilamentDialog(d.result||[]);
|
||||
}).catch(function(){
|
||||
if(_autoOpenFile && _fdUserCancelled){_fdDialogOpen=false;return;}
|
||||
openFilamentDialog([]);
|
||||
});
|
||||
}).catch(function(){openFilamentDialog([]);});
|
||||
}
|
||||
|
||||
var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
|
||||
@@ -2350,9 +2134,6 @@ function openFilamentDialog(slots){
|
||||
var title=document.getElementById('fd-title');
|
||||
var body=document.getElementById('fd-slots');
|
||||
if(title)title.textContent='▶ '+_storeFilename;
|
||||
// Auto-Leveling-Checkbox mit globalem Default vorbelegen
|
||||
var fdAl=document.getElementById('fd-auto-leveling');
|
||||
if(fdAl) fdAl.checked=(S.auto_leveling===undefined?true:!!S.auto_leveling);
|
||||
// Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID)
|
||||
_printObjects=[];
|
||||
_printObjectsSvg='';
|
||||
@@ -2460,8 +2241,6 @@ function openFilamentDialog(slots){
|
||||
function closeFilamentDialog(){
|
||||
var dlg=document.getElementById('filament-dialog');
|
||||
if(dlg)dlg.classList.remove('open');
|
||||
_fdDialogOpen=false;
|
||||
if(_fdAutoOpenedFile) _fdUserCancelled=true;
|
||||
}
|
||||
|
||||
function confirmFilamentPrint(){
|
||||
@@ -2501,14 +2280,12 @@ function confirmFilamentPrint(){
|
||||
}
|
||||
// Pre-Print Skip: Namen der abgehakten Objekte sammeln
|
||||
var excludedObjects=_printObjects.filter(function(o){return o.skip;}).map(function(o){return o.name;});
|
||||
var fdAlEl=document.getElementById('fd-auto-leveling');
|
||||
var fdAutoLeveling=fdAlEl?( fdAlEl.checked?1:0):(S.auto_leveling===undefined?1:S.auto_leveling?1:0);
|
||||
closeFilamentDialog();
|
||||
if(_filamentDialogMode==='banner'){
|
||||
// Banner-Modus: normaler print/start mit Slot-Override
|
||||
var btn=document.getElementById('file-ready-btn');
|
||||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||||
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling})
|
||||
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(){
|
||||
document.getElementById('file-ready-banner').style.display='none';
|
||||
@@ -2523,7 +2300,7 @@ function confirmFilamentPrint(){
|
||||
fetch(_apiUrl('/kx/print'),{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:fdAutoLeveling})
|
||||
body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects})
|
||||
}).then(function(r){return r.json()}).then(function(d){
|
||||
if(d.result==='ok'){clog('Druckstart: '+_storeFilename,'msg-ok');showPanel('dashboard');}
|
||||
else{clog('Druckfehler: '+(d.error||'?'),'msg-err');}
|
||||
|
||||
@@ -42,12 +42,115 @@
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="theme-btn" onclick="showPanel('settings')" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
||||
</header>
|
||||
|
||||
<!-- ═══ SETTINGS MODAL ═══ -->
|
||||
<!-- Settings-Modal entfernt — jetzt #panel-settings (Master-Detail im Main-Bereich) -->
|
||||
<div class="modal-overlay" id="settings-modal" onclick="if(event.target===this)closeSettings()">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="modal-title-settings">Einstellungen</span>
|
||||
<button class="modal-close" onclick="closeSettings()">✕</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-field" style="margin-bottom:12px">
|
||||
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
|
||||
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
|
||||
</div>
|
||||
<div class="modal-section" id="modal-sec-connection">Verbindung</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-printer-ip">Drucker-IP</label>
|
||||
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
|
||||
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mqtt-port">MQTT-Port</label>
|
||||
<input type="number" id="s-mqtt-port" placeholder="9883">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-username">MQTT-Benutzername</label>
|
||||
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-password">MQTT-Passwort</label>
|
||||
<input type="password" id="s-password" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-device-id">Device-ID</label>
|
||||
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mode-id">Mode-ID</label>
|
||||
<input type="text" id="s-mode-id" placeholder="20030">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-print">Druckeinstellungen</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
|
||||
<select id="s-default-slot">
|
||||
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
|
||||
<option value="0" id="opt-slot-0">Slot 1</option>
|
||||
<option value="1" id="opt-slot-1">Slot 2</option>
|
||||
<option value="2" id="opt-slot-2">Slot 3</option>
|
||||
<option value="3" id="opt-slot-3">Slot 4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
||||
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
|
||||
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-poll">Poll-Intervall</div>
|
||||
<div class="poll-btns">
|
||||
<button class="poll-btn" onclick="setPoll(1000)" id="poll-1">1s</button>
|
||||
<button class="poll-btn active" onclick="setPoll(2000)" id="poll-2">2s</button>
|
||||
<button class="poll-btn" onclick="setPoll(5000)" id="poll-5">5s</button>
|
||||
</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">
|
||||
<span id="s-version-label" style="font-size:13px;color:var(--txt)">–</span>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
|
||||
</div>
|
||||
<div class="update-status" id="update-status" style="margin-top:6px"></div>
|
||||
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
|
||||
<span id="lbl-update-apply">Jetzt installieren</span>
|
||||
</button>
|
||||
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
|
||||
</div>
|
||||
|
||||
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings">Speichern & Neustart</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ AMS SLOT EDIT DIALOG ═══ -->
|
||||
<div class="modal-overlay" id="slot-edit-modal" onclick="if(event.target===this)closeSlotEdit()">
|
||||
@@ -127,8 +230,6 @@
|
||||
<span class="nav-icon">🗂</span><span class="nav-text">Browser</span></button>
|
||||
<button class="nav-btn" onclick="showPanel('console');clearLogBadge()" id="nb-console">
|
||||
<span class="nav-icon">≡</span><span class="nav-text">Konsole</span><span id="log-badge" style="display:none;margin-left:4px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 5px;font-weight:700"></span></button>
|
||||
<button class="nav-btn" onclick="showPanel('settings')" id="nb-settings">
|
||||
<span class="nav-icon">⚙</span><span class="nav-text" id="nav-settings">Einstellungen</span></button>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
@@ -166,15 +267,9 @@
|
||||
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
|
||||
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
|
||||
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center">
|
||||
<div class="time-label" id="d-lbl-layers"></div>
|
||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||
</div>
|
||||
<div class="time-block" style="padding:4px 10px;min-width:72px;text-align:center">
|
||||
<div class="time-label" id="d-lbl-zpos">Z</div>
|
||||
<div class="time-val" style="font-size:13px" id="d-zpos">–</div>
|
||||
</div>
|
||||
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0">
|
||||
<div class="time-label" id="d-lbl-layers"></div>
|
||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-grid">
|
||||
@@ -197,12 +292,6 @@
|
||||
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none">✂ <span id="d-btn-skip-label">Objekte</span></button>
|
||||
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
|
||||
</div>
|
||||
<!-- Aktionen für eine geladene, aber nicht laufende Datei (Issue #55) -->
|
||||
<div class="ctrl-btns" id="d-idle-btns" style="margin-top:12px;display:none">
|
||||
<button class="btn btn-accent btn-sm" id="d-idle-print" onclick="startIdleFile()">▶ <span id="d-idle-print-lbl">Drucken</span></button>
|
||||
<button class="btn btn-sm" id="d-idle-slots" onclick="startIdleFileWithSlots()" style="background:var(--raised);color:var(--txt)">⚙ <span id="d-idle-slots-lbl">Slots zuordnen</span></button>
|
||||
<button class="btn btn-cancel btn-sm" id="d-idle-clear" onclick="clearIdleFile()">✕ <span id="d-idle-clear-lbl">Leeren</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temperatursteuerung + Verlauf -->
|
||||
@@ -425,169 +514,6 @@
|
||||
<div class="console" id="console-log" style="height:calc(100vh - 260px);min-height:160px" onscroll="onLogScroll()"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ EINSTELLUNGEN ═══ -->
|
||||
<div class="panel" id="panel-settings">
|
||||
<div class="settings-wrap">
|
||||
<div class="settings-cats">
|
||||
<button class="set-cat active" id="setcat-connection" onclick="showSettingsCat('connection')"><span>🔌</span> <span id="setcat-lbl-connection">Verbindung</span></button>
|
||||
<button class="set-cat" id="setcat-printer" onclick="showSettingsCat('printer')"><span>🖨</span> <span id="setcat-lbl-printer">Drucker</span></button>
|
||||
<button class="set-cat" id="setcat-display" onclick="showSettingsCat('display')"><span>🎨</span> <span id="setcat-lbl-display">Darstellung</span></button>
|
||||
<button class="set-cat" id="setcat-filament" onclick="showSettingsCat('filament')"><span>🧵</span> <span id="setcat-lbl-filament">Filament</span></button>
|
||||
<button class="set-cat" id="setcat-system" onclick="showSettingsCat('system')"><span>⚙</span> <span id="setcat-lbl-system">System</span></button>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<!-- Verbindung -->
|
||||
<div class="set-group active" id="setgrp-connection">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🔌</span> <span id="modal-sec-connection">Verbindung</span></div>
|
||||
<div class="modal-field" style="margin-bottom:12px">
|
||||
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
|
||||
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-printer-ip">Drucker-IP</label>
|
||||
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
|
||||
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mqtt-port">MQTT-Port</label>
|
||||
<input type="number" id="s-mqtt-port" placeholder="9883">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-username">MQTT-Benutzername</label>
|
||||
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-password">MQTT-Passwort</label>
|
||||
<input type="password" id="s-password" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-device-id">Device-ID</label>
|
||||
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mode-id">Mode-ID</label>
|
||||
<input type="text" id="s-mode-id" placeholder="20030">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drucker -->
|
||||
<div class="set-group" id="setgrp-printer">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🖨</span> <span id="modal-sec-print">Druckeinstellungen</span></div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
|
||||
<select id="s-default-slot">
|
||||
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
|
||||
<option value="0" id="opt-slot-0">Slot 1</option>
|
||||
<option value="1" id="opt-slot-1">Slot 2</option>
|
||||
<option value="2" id="opt-slot-2">Slot 3</option>
|
||||
<option value="3" id="opt-slot-3">Slot 4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
||||
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-file-ready-mode">Nach Upload: Druckstart-Verhalten</label>
|
||||
<select id="s-file-ready-mode">
|
||||
<option value="1" id="opt-file-ready-dialog">Print-Dialog</option>
|
||||
<option value="0" id="opt-file-ready-banner">Print-Leiste</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
|
||||
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Darstellung -->
|
||||
<div class="set-group" id="setgrp-display">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🎨</span> <span id="setcat-lbl-display2">Darstellung</span></div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-set-lang">Sprache</label>
|
||||
<select id="s-lang-select" onchange="setLanguage(this.value)">
|
||||
<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>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="toggleTheme()"><span id="lbl-set-theme">Hell / Dunkel umschalten</span></button>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-poll-interval">Poll-Intervall (Sekunden)</label>
|
||||
<input type="number" id="s-poll-interval" min="1" max="60" step="1" placeholder="3" oninput="onPollIntervalInput()">
|
||||
<small style="color:var(--txt2)" id="lbl-poll-hint">Wie oft die Bridge den Drucker-Status abfragt</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filament -->
|
||||
<div class="set-group" id="setgrp-filament">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🧵</span> <span id="modal-sec-orca-profiles">OrcaSlicer-Profile</span></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 class="card">
|
||||
<div class="card-title"><span>🎯</span> <span id="lbl-filament-mapping">Filament-Profil-Mapping (pro Slot)</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="filament-mapping-hint">
|
||||
Festes Orca-Profil pro AMS-Slot. Beim Slicer-Sync sendet die Bridge dieses Profil statt „Generic".
|
||||
</div>
|
||||
<div id="filament-mapping-list"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveFilamentMapping()"><span id="lbl-filament-mapping-save">Mapping speichern</span></button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span>👁</span> <span id="lbl-visible-vendors">Sichtbare Hersteller (Profil-Dropdown)</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="visible-vendors-hint">
|
||||
Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.
|
||||
</div>
|
||||
<input type="text" id="vendor-filter-search" placeholder="Hersteller suchen…" oninput="renderVendorChecklist()"
|
||||
style="width:100%;padding:6px 10px;margin-bottom:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px">
|
||||
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System -->
|
||||
<div class="set-group" id="setgrp-system">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>⚙</span> <span id="modal-sec-version">Version</span></div>
|
||||
<div class="update-row">
|
||||
<span id="s-version-label" style="font-size:13px;color:var(--txt)">–</span>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
|
||||
</div>
|
||||
<div class="update-status" id="update-status" style="margin-top:6px"></div>
|
||||
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
|
||||
<span id="lbl-update-apply">Jetzt installieren</span>
|
||||
</button>
|
||||
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings" style="margin-top:14px">Speichern & Neustart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -596,7 +522,6 @@
|
||||
<button class="bnav-btn" onclick="showPanel('printers');loadPrinterTab()" id="bnb-printers"><span class="bnav-icon">🖨</span>Drucker</button>
|
||||
<button class="bnav-btn" onclick="showPanel('store');loadStore()" id="bnb-store"><span class="bnav-icon">🗂</span>Browser</button>
|
||||
<button class="bnav-btn" onclick="showPanel('console');clearLogBadge()" id="bnb-console"><span class="bnav-icon">≡</span>Log<span id="log-badge-bot" style="display:none;margin-left:3px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 4px;font-weight:700"></span></button>
|
||||
<button class="bnav-btn" onclick="showPanel('settings')" id="bnb-settings"><span class="bnav-icon">⚙</span>Setup</button>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -630,13 +555,6 @@
|
||||
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
|
||||
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
|
||||
</div>
|
||||
<div style="margin-bottom:14px;padding:10px 12px;background:var(--raised);border-radius:8px;border:1px solid var(--border)">
|
||||
<div style="font-size:11px;font-weight:600;color:var(--txt2);margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em" id="fd-options-title">Druckoptionen</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<input type="checkbox" id="fd-auto-leveling" style="width:auto;margin:0">
|
||||
<label for="fd-auto-leveling" style="margin:0;cursor:pointer;font-size:13px" id="fd-lbl-auto-leveling">Auto-Leveling</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button id="fd-cancel" onclick="closeFilamentDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="fd-print" onclick="confirmFilamentPrint()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">▶ Drucken</button>
|
||||
|
||||
@@ -212,24 +212,6 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
|
||||
.panel{display:none}
|
||||
.panel.active{display:block}
|
||||
|
||||
/* ── SETTINGS (Master-Detail) ── */
|
||||
.settings-wrap{display:grid;grid-template-columns:200px 1fr;gap:16px;align-items:start}
|
||||
.settings-cats{display:flex;flex-direction:column;gap:4px;position:sticky;top:12px}
|
||||
.set-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;
|
||||
border:1px solid transparent;background:var(--raised);color:var(--txt2);cursor:pointer;
|
||||
font-size:13px;text-align:left;transition:background .15s,color .15s}
|
||||
.set-cat:hover{color:var(--txt)}
|
||||
.set-cat.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||
.set-group{display:none}
|
||||
.set-group.active{display:block}
|
||||
.set-group .card{margin-bottom:14px}
|
||||
@media(max-width:768px){
|
||||
.settings-wrap{grid-template-columns:1fr}
|
||||
.settings-cats{flex-direction:row;flex-wrap:wrap;position:static;overflow-x:auto}
|
||||
.set-cat{flex:1;min-width:auto;justify-content:center;padding:8px 6px;font-size:12px}
|
||||
.set-cat .nav-text{display:inline}
|
||||
}
|
||||
|
||||
/* ── FILE BROWSER UPLOAD ZONE ── */
|
||||
#store-upload-zone{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"lbl_remaining": "Restzeit:",
|
||||
"lbl_slicer_time": "Slicer-Schätzung:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Leise",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -127,21 +126,8 @@
|
||||
"settings_title": "Einstellungen",
|
||||
"settings_connection": "Verbindung",
|
||||
"settings_print": "Druckeinstellungen",
|
||||
"settings_poll": "Poll-Intervall (Sekunden)",
|
||||
"settings_poll": "Poll-Intervall",
|
||||
"settings_version": "Version",
|
||||
"nav_settings": "Einstellungen",
|
||||
"settings_cat_display": "Darstellung",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Sprache",
|
||||
"settings_cat_theme": "Hell / Dunkel umschalten",
|
||||
"settings_filament_mapping": "Filament-Profil-Mapping (pro Slot)",
|
||||
"settings_filament_mapping_save": "Mapping speichern",
|
||||
"settings_visible_vendors": "Sichtbare Hersteller (Profil-Dropdown)",
|
||||
"settings_visible_vendors_hint": "Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic\" und eigene Profile sind immer sichtbar.",
|
||||
"settings_visible_vendors_save": "Auswahl speichern",
|
||||
"progress_action_print": "Drucken",
|
||||
"progress_action_slots": "Slots zuordnen",
|
||||
"progress_action_clear": "Leeren",
|
||||
"settings_save": "Speichern & Neustart",
|
||||
"settings_printer_name": "Drucker-Name",
|
||||
"settings_printer_ip": "Drucker-IP",
|
||||
@@ -257,33 +243,5 @@
|
||||
"sf_new": "Neu",
|
||||
"ss_date": "↓ Datum",
|
||||
"ss_name": "A–Z Name",
|
||||
"ss_dur": "⏱ Druckzeit",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Custom",
|
||||
"fd_options_title": "Optionen",
|
||||
"print_auto_leveling": "Auto-Leveling für diesen Druck",
|
||||
"settings_file_ready_mode": "Druckdialog starten",
|
||||
"settings_file_ready_banner": "Druckleiste",
|
||||
"settings_file_ready_dialog": "Druckdialog",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Richtung:",
|
||||
"log_lvl_err": "⛔ Fehler",
|
||||
"log_lvl_warn": "⚠ Warnung",
|
||||
"log_topic_label": "Thema:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Druck",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Status",
|
||||
"log_download": "⬇ Download",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Leeren",
|
||||
"log_filter_placeholder": "Filtern…",
|
||||
"skip_cancel": "Abbrechen",
|
||||
"skip_confirm": "Überspringen"
|
||||
"ss_dur": "⏱ Druckzeit"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"lbl_remaining": "Remaining:",
|
||||
"lbl_slicer_time": "Slicer estimate:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silent",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -69,13 +68,6 @@
|
||||
"ace_dry_dialog_save_restart": "Save & Restart",
|
||||
"ace_dry_dialog_custom_name": "Custom Name",
|
||||
"ace_dry_dialog_reset_default": "Reset to Default",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Custom",
|
||||
"cam_placeholder": "📷 Camera not started",
|
||||
"cam_stream_unavailable": "Stream unavailable",
|
||||
"btn_cam_start": "▶ Camera",
|
||||
@@ -134,20 +126,7 @@
|
||||
"settings_title": "Settings",
|
||||
"settings_connection": "Connection",
|
||||
"settings_print": "Print Settings",
|
||||
"settings_poll": "Poll Interval (seconds)",
|
||||
"nav_settings": "Settings",
|
||||
"settings_cat_display": "Appearance",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Language",
|
||||
"settings_cat_theme": "Toggle light / dark",
|
||||
"settings_filament_mapping": "Filament profile mapping (per slot)",
|
||||
"settings_filament_mapping_save": "Save mapping",
|
||||
"settings_visible_vendors": "Visible vendors (profile dropdown)",
|
||||
"settings_visible_vendors_hint": "Only these vendors appear in the slot profile dropdown. Nothing selected = show all. \"Generic\" and your own profiles are always visible.",
|
||||
"settings_visible_vendors_save": "Save selection",
|
||||
"progress_action_print": "Print",
|
||||
"progress_action_slots": "Map slots",
|
||||
"progress_action_clear": "Clear",
|
||||
"settings_poll": "Poll Interval",
|
||||
"settings_version": "Version",
|
||||
"settings_save": "Save & Restart",
|
||||
"settings_printer_name": "Printer Name",
|
||||
@@ -160,12 +139,7 @@
|
||||
"hint_ip_no_port": "IP address only, no port (e.g. 192.168.1.102)",
|
||||
"settings_default_slot": "Default Slot (single color)",
|
||||
"settings_slot_auto": "Auto (all loaded slots)",
|
||||
"settings_auto_leveling": "Auto-Leveling Default",
|
||||
"fd_options_title": "Print Options",
|
||||
"print_auto_leveling": "Auto-Leveling",
|
||||
"settings_file_ready_mode": "Start Print Behavior",
|
||||
"settings_file_ready_banner": "Print Bar",
|
||||
"settings_file_ready_dialog": "Print Dialog",
|
||||
"settings_auto_leveling": "Auto-Leveling before print",
|
||||
"settings_camera_on_print": "Turn camera on at print start",
|
||||
"settings_web_upload_warning": "Show warning when printing web uploads",
|
||||
"update_check": "Check for Updates",
|
||||
@@ -204,21 +178,7 @@
|
||||
"orca_profile_done": "Imported",
|
||||
"orca_profile_skipped": "skipped",
|
||||
"log_dir_all": "All",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dir:",
|
||||
"log_lvl_label": "Level:",
|
||||
"log_lvl_err": "⛔ Errors",
|
||||
"log_lvl_warn": "⚠ Warn",
|
||||
"log_topic_label": "Topic:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Print",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Status",
|
||||
"log_download": "⬇ Download",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Clear",
|
||||
"log_filter_placeholder": "Filter…",
|
||||
"file_ready_btn": "▶ Start Print",
|
||||
"file_slots_btn": "🎨 Select Slots",
|
||||
"file_cancel_btn": "✕ Cancel",
|
||||
@@ -228,8 +188,6 @@
|
||||
"skip_btn_label": "Objects",
|
||||
"skip_no_objects": "No objects in this print.",
|
||||
"skip_already": "skipped",
|
||||
"skip_cancel": "Cancel",
|
||||
"skip_confirm": "Skip",
|
||||
"skip_select_at_least_one": "Please pick at least one object.",
|
||||
"skip_sending": "Sending …",
|
||||
"skip_success": "Objects will be skipped.",
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"lbl_remaining": "Restante:",
|
||||
"lbl_slicer_time": "Estimación del slicer:",
|
||||
"lbl_layers": "Capa",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silencioso",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -127,20 +126,7 @@
|
||||
"settings_title": "Configuración",
|
||||
"settings_connection": "Conexión",
|
||||
"settings_print": "Ajustes de impresión",
|
||||
"settings_poll": "Intervalo de sondeo (segundos)",
|
||||
"nav_settings": "Ajustes",
|
||||
"settings_cat_display": "Apariencia",
|
||||
"settings_cat_filament": "Filamento",
|
||||
"settings_cat_language": "Idioma",
|
||||
"settings_cat_theme": "Alternar claro / oscuro",
|
||||
"settings_filament_mapping": "Asignación de perfil de filamento (por ranura)",
|
||||
"settings_filament_mapping_save": "Guardar asignación",
|
||||
"settings_visible_vendors": "Fabricantes visibles (lista de perfiles)",
|
||||
"settings_visible_vendors_hint": "Solo estos fabricantes aparecen en la lista de perfiles de ranura. Nada seleccionado = mostrar todos. «Generic» y tus propios perfiles siempre son visibles.",
|
||||
"settings_visible_vendors_save": "Guardar selección",
|
||||
"progress_action_print": "Imprimir",
|
||||
"progress_action_slots": "Asignar ranuras",
|
||||
"progress_action_clear": "Vaciar",
|
||||
"settings_poll": "Intervalo de sondeo",
|
||||
"settings_version": "Versión",
|
||||
"settings_save": "Guardar y reiniciar",
|
||||
"settings_printer_name": "Nombre de impresora",
|
||||
@@ -257,33 +243,5 @@
|
||||
"sf_new": "Nuevo",
|
||||
"ss_date": "↓ Fecha",
|
||||
"ss_name": "A–Z Nombre",
|
||||
"ss_dur": "⏱ Tiempo de impresión",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Personalizado",
|
||||
"fd_options_title": "Opciones",
|
||||
"print_auto_leveling": "Autonivelado para esta impresión",
|
||||
"settings_file_ready_mode": "Iniciar diálogo de impresión",
|
||||
"settings_file_ready_banner": "Barra de impresión",
|
||||
"settings_file_ready_dialog": "Diálogo de impresión",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dirección:",
|
||||
"log_lvl_err": "⛔ Errores",
|
||||
"log_lvl_warn": "⚠ Avisos",
|
||||
"log_topic_label": "Tema:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Impresión",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Estado",
|
||||
"log_download": "⬇ Descargar",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Limpiar",
|
||||
"log_filter_placeholder": "Filtrar…",
|
||||
"skip_cancel": "Cancelar",
|
||||
"skip_confirm": "Omitir"
|
||||
"ss_dur": "⏱ Tiempo de impresión"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"lbl_remaining": "Restant :",
|
||||
"lbl_slicer_time": "Estimation slicer :",
|
||||
"lbl_layers": "Couche",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silencieux",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -127,20 +126,7 @@
|
||||
"settings_title": "Paramètres",
|
||||
"settings_connection": "Connexion",
|
||||
"settings_print": "Paramètres d'impression",
|
||||
"settings_poll": "Intervalle de sondage (secondes)",
|
||||
"nav_settings": "Paramètres",
|
||||
"settings_cat_display": "Apparence",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Langue",
|
||||
"settings_cat_theme": "Basculer clair / sombre",
|
||||
"settings_filament_mapping": "Mappage du profil de filament (par emplacement)",
|
||||
"settings_filament_mapping_save": "Enregistrer le mappage",
|
||||
"settings_visible_vendors": "Fabricants visibles (liste des profils)",
|
||||
"settings_visible_vendors_hint": "Seuls ces fabricants apparaissent dans la liste des profils d'emplacement. Rien de sélectionné = tout afficher. « Generic » et vos propres profils sont toujours visibles.",
|
||||
"settings_visible_vendors_save": "Enregistrer la sélection",
|
||||
"progress_action_print": "Imprimer",
|
||||
"progress_action_slots": "Affecter les emplacements",
|
||||
"progress_action_clear": "Vider",
|
||||
"settings_poll": "Intervalle de sondage",
|
||||
"settings_version": "Version",
|
||||
"settings_save": "Enregistrer et redémarrer",
|
||||
"settings_printer_name": "Nom de l'imprimante",
|
||||
@@ -257,33 +243,5 @@
|
||||
"sf_new": "Nouveau",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_name": "A–Z Nom",
|
||||
"ss_dur": "⏱ Durée d'impression",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Personnalisé",
|
||||
"fd_options_title": "Options",
|
||||
"print_auto_leveling": "Mise à niveau auto pour cette impression",
|
||||
"settings_file_ready_mode": "Démarrer le dialogue d'impression",
|
||||
"settings_file_ready_banner": "Barre d'impression",
|
||||
"settings_file_ready_dialog": "Dialogue d'impression",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Sens :",
|
||||
"log_lvl_err": "⛔ Erreurs",
|
||||
"log_lvl_warn": "⚠ Avert.",
|
||||
"log_topic_label": "Sujet :",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Impression",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Statut",
|
||||
"log_download": "⬇ Télécharger",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Effacer",
|
||||
"log_filter_placeholder": "Filtrer…",
|
||||
"skip_cancel": "Annuler",
|
||||
"skip_confirm": "Ignorer"
|
||||
"ss_dur": "⏱ Durée d'impression"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"lbl_remaining": "剩余时间:",
|
||||
"lbl_slicer_time": "切片预估:",
|
||||
"lbl_layers": "层",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 静音",
|
||||
"speed_normal": "⚡ 标准",
|
||||
"speed_sport": "🚀 运动",
|
||||
@@ -127,20 +126,7 @@
|
||||
"settings_title": "设置",
|
||||
"settings_connection": "连接",
|
||||
"settings_print": "打印设置",
|
||||
"settings_poll": "轮询间隔(秒)",
|
||||
"nav_settings": "设置",
|
||||
"settings_cat_display": "外观",
|
||||
"settings_cat_filament": "耗材",
|
||||
"settings_cat_language": "语言",
|
||||
"settings_cat_theme": "切换浅色 / 深色",
|
||||
"settings_filament_mapping": "耗材配置映射(每槽位)",
|
||||
"settings_filament_mapping_save": "保存映射",
|
||||
"settings_visible_vendors": "可见厂商(配置下拉框)",
|
||||
"settings_visible_vendors_hint": "仅这些厂商会出现在槽位配置下拉框中。未选择 = 显示全部。“Generic”和您自己的配置始终可见。",
|
||||
"settings_visible_vendors_save": "保存选择",
|
||||
"progress_action_print": "打印",
|
||||
"progress_action_slots": "分配槽位",
|
||||
"progress_action_clear": "清除",
|
||||
"settings_poll": "轮询间隔",
|
||||
"settings_version": "版本",
|
||||
"settings_save": "保存并重启",
|
||||
"settings_printer_name": "打印机名称",
|
||||
@@ -257,33 +243,5 @@
|
||||
"sf_new": "新",
|
||||
"ss_date": "↓ 日期",
|
||||
"ss_name": "A–Z 名称",
|
||||
"ss_dur": "⏱ 打印时间",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "自定义",
|
||||
"fd_options_title": "选项",
|
||||
"print_auto_leveling": "本次打印自动调平",
|
||||
"settings_file_ready_mode": "开始打印对话框",
|
||||
"settings_file_ready_banner": "打印栏",
|
||||
"settings_file_ready_dialog": "打印对话框",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "方向:",
|
||||
"log_lvl_err": "⛔ 错误",
|
||||
"log_lvl_warn": "⚠ 警告",
|
||||
"log_topic_label": "主题:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "打印",
|
||||
"log_topic_info": "信息",
|
||||
"log_topic_status": "状态",
|
||||
"log_download": "⬇ 下载",
|
||||
"log_auto": "⬇ 自动",
|
||||
"log_clear": "✕ 清空",
|
||||
"log_filter_placeholder": "筛选…",
|
||||
"skip_cancel": "取消",
|
||||
"skip_confirm": "跳过"
|
||||
"ss_dur": "⏱ 打印时间"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user