Compare commits
16 Commits
v0.9.19
...
start-prin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c91ba66801 | ||
|
|
9517493c39 | ||
|
|
5d691c6153 | ||
|
|
2a02c598b5 | ||
|
|
e64aebd145 | ||
|
|
778c4ce85a | ||
| 1f300589d1 | |||
| 930e3774af | |||
| 636889bdbc | |||
| 3f6ea269e6 | |||
| 3fff6e25f0 | |||
| 0f5a8cbc72 | |||
| a40f14af8e | |||
| 466b8c518d | |||
| 1c5396b37d | |||
| c23deebde5 |
@@ -1,5 +1,69 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.21] – 2026-06-14
|
||||
|
||||
### Behoben
|
||||
- **Kamera-Stream auf Android (Chrome / Firefox) nicht sichtbar.** Android-Browser
|
||||
unterstützen `multipart/x-mixed-replace` (MJPEG) nicht. Die UI erkennt Android
|
||||
jetzt automatisch und fällt auf Snapshot-Polling mit 5 fps zurück
|
||||
(`/api/camera/snapshot` alle 200 ms) — keine Server-Änderung nötig.
|
||||
|
||||
### Geändert
|
||||
- Docker-Image auf **Debian 12 (Bookworm)** gepinnt (`python:3.11-slim-bookworm`),
|
||||
um Kompatibilitätsprobleme mit glibc 2.41 zu vermeiden, die das aktuell von
|
||||
`python:3.11-slim` gezogene Debian 13 Basis-Image mitbringt.
|
||||
- MQTT- und HTTP-Verbindungen erzwingen jetzt **IPv4** (`AF_INET`), um
|
||||
Verbindungsfehler auf Hosts zu verhindern, bei denen der Drucker nur über IPv4
|
||||
erreichbar ist, das OS aber IPv6 bevorzugt.
|
||||
- Extruder-Stub in der Moonraker-`configfile`-Antwort enthält jetzt `sensor_type`
|
||||
und `filament_diameter` — behebt einen Mobileraker-Absturz
|
||||
(`Null is not a subtype of Object`, Issue #48).
|
||||
|
||||
## [0.9.20] – 2026-06-08
|
||||
|
||||
### Neu
|
||||
- **Französische Sprachunterstützung (PR #45 von @Nathacks)**
|
||||
- **Z-Höhe in der Print-UI (PR #49 von @Nathacks).** Zeigt die aktuelle
|
||||
Z-Position in mm unterhalb des Layer-Zählers.
|
||||
|
||||
### Behoben
|
||||
- **Kamera-Autostart ignorierte das "Kamera bei Druckstart einschalten"-
|
||||
Setting nach einem Bridge-Restart (Issue #50).** Das Setting wurde in
|
||||
der Prozessumgebung gecacht — nach dem Speichern in der UI überlebte
|
||||
der alte Wert den Restart und der neue Wert aus `config.ini` wurde
|
||||
nicht gelesen.
|
||||
- **Kamera startete nach manuellem Stopp während eines Drucks automatisch
|
||||
neu (Issue #50).** Ein neues `_camera_user_stopped`-Flag unterdrückt
|
||||
den Autostart für die aktuelle Drucksitzung. Es wird beim Druckende
|
||||
zurückgesetzt.
|
||||
- **Falscher "Stream nicht verfügbar"-Fehler-Toast beim manuellen
|
||||
Kamera-Stopp.** Der Bild-Fehler-Handler war noch registriert als
|
||||
`img.src` geleert wurde.
|
||||
- **JS-Fehler (`ReferenceError: br is not defined`) beim Licht-Toggle.**
|
||||
Variable wurde aus dem falschen Scope referenziert.
|
||||
- Webcam-URLs sind jetzt absolut, damit Mobileraker/Obico-Clients sie
|
||||
erreichen können.
|
||||
|
||||
## [0.9.19.1] – 2026-06-04
|
||||
|
||||
### Behoben
|
||||
- Standalone-Binaries (Linux/Windows) zeigten `vunknown` als Version.
|
||||
Die `VERSION`-Datei ist jetzt ins PyInstaller-Onefile eingebettet.
|
||||
- Bei fehlenden TLS-Zertifikaten (`anycubic_slicer.crt`/`.key`) gab
|
||||
es nur den rohen Fehler `[Errno 2] No such file or directory`. Die
|
||||
Bridge meldet jetzt klar, wo die Dateien hingelegt werden müssen
|
||||
und dass `anycubic-certs.zip` aus dem Gitea-Release stammt.
|
||||
|
||||
### Geändert
|
||||
- Filament-Profil-Liste neu kuratiert: 209 statt 399 Einträge.
|
||||
Profile die nur für drucker-spezifische Vendor-Bundles existieren
|
||||
(z.B. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) sind rausgeflogen
|
||||
— OrcaSlicer hätte sie im Standard-Kobra-X-Setup beim Sync
|
||||
ohnehin nicht gefunden, weil die jeweiligen Vendor-Bundles nur
|
||||
bei aktivem Drucker-Vendor geladen werden. Für solche Filamente
|
||||
bleibt der Custom-Profile-Import (Issue #41) der Weg.
|
||||
|
||||
## [0.9.19] – 2026-06-02
|
||||
|
||||
### Neu
|
||||
@@ -19,9 +83,9 @@
|
||||
wird beim Sync an Orca als User-Match weitergegeben. Funktioniert
|
||||
über HTTP, also auch wenn die Bridge im Docker auf Raspi/NAS läuft
|
||||
und OrcaSlicer auf dem Desktop. Auch reine Override-Profile mit nur
|
||||
`inherits: "Generic PLA @System"` + ein paar Tweaks (z.B.
|
||||
„Bert - PLA") werden korrekt erkannt — die Bridge resolved die
|
||||
vererbten Felder aus dem System-Parent.
|
||||
`inherits: "Generic PLA @System"` + ein paar Tweaks werden korrekt
|
||||
erkannt — die Bridge resolved die vererbten Felder aus dem
|
||||
System-Parent.
|
||||
|
||||
### Fixes
|
||||
- **AMS-Sync landete hartnäckig auf „Generic PLA":** das Orca-
|
||||
|
||||
72
CHANGELOG.md
72
CHANGELOG.md
@@ -1,5 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.21] – 2026-06-14
|
||||
|
||||
### Fixed
|
||||
- **Camera stream not visible on Android (Chrome / Firefox).** Android
|
||||
browsers do not support `multipart/x-mixed-replace` (MJPEG). The UI
|
||||
now detects Android and falls back to snapshot-polling at 5 fps
|
||||
(`/api/camera/snapshot` every 200 ms) — no server-side change needed.
|
||||
|
||||
### Changed
|
||||
- Docker image now pinned to **Debian 12 (Bookworm)** (`python:3.11-slim-bookworm`)
|
||||
to avoid glibc 2.41 compatibility issues introduced by the Debian 13
|
||||
base image that `python:3.11-slim` recently started pulling.
|
||||
- MQTT and HTTP connections now **force IPv4** (`AF_INET`) to prevent
|
||||
connection failures on hosts where the printer is only reachable via
|
||||
IPv4 but the OS prefers IPv6.
|
||||
- Extruder stub in the Moonraker `configfile` response now includes
|
||||
`sensor_type` and `filament_diameter` — fixes a Mobileraker crash
|
||||
(`Null is not a subtype of Object`, issue #48).
|
||||
|
||||
## [0.9.20] – 2026-06-08
|
||||
|
||||
### New
|
||||
- **French language support (PR #45 by @Nathacks)**
|
||||
- **Z height display in the print UI (PR #49 by @Nathacks).** Shows
|
||||
current Z position in mm below the layer counter.
|
||||
|
||||
### Fixed
|
||||
- **Camera auto-start ignored "Enable camera on print start" setting
|
||||
after a bridge restart (issue #50).** The setting was cached in the
|
||||
process environment — after saving it in the UI, the old value
|
||||
survived the restart and the new value from `config.ini` was never
|
||||
read.
|
||||
- **Camera restarted automatically after manual stop during a print
|
||||
(issue #50).** A new `_camera_user_stopped` flag suppresses
|
||||
auto-restart for the current print session. It resets when the
|
||||
print ends.
|
||||
- **Spurious "stream unavailable" error toast when stopping the camera
|
||||
manually.** The image error handler was still registered when
|
||||
`img.src` was cleared.
|
||||
- **JS error (`ReferenceError: br is not defined`) when toggling the
|
||||
light.** Variable was referenced from the wrong scope.
|
||||
- Webcam URLs are now absolute so that Mobileraker/Obico clients can
|
||||
reach them.
|
||||
|
||||
## [0.9.19.1] – 2026-06-04
|
||||
|
||||
### Fixed
|
||||
- Standalone binaries (Linux/Windows) reported `vunknown` as their
|
||||
version. The `VERSION` file is now embedded into the PyInstaller
|
||||
onefile bundle.
|
||||
- When the TLS certificates (`anycubic_slicer.crt`/`.key`) were
|
||||
missing, the bridge only logged the raw `[Errno 2] No such file
|
||||
or directory`. It now states clearly where the files need to be
|
||||
placed and that `anycubic-certs.zip` from the Gitea release is the
|
||||
source.
|
||||
|
||||
### Changed
|
||||
- Filament profile list re-curated: 209 entries instead of 399.
|
||||
Profiles that only exist inside printer-specific vendor bundles
|
||||
(e.g. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) were dropped —
|
||||
OrcaSlicer wouldn't have found them in a default Kobra X setup
|
||||
anyway, because the matching vendor bundle is only loaded when
|
||||
the corresponding printer vendor is active. For those filaments
|
||||
the custom profile import (issue #41) remains the way.
|
||||
|
||||
## [0.9.19] – 2026-06-02
|
||||
|
||||
### New
|
||||
@@ -19,9 +85,9 @@
|
||||
are passed through to Orca on sync as user matches. Works over HTTP
|
||||
so the bridge can run in Docker on a Raspi/NAS while OrcaSlicer
|
||||
lives on a desktop. Override-only profiles with just
|
||||
`inherits: "Generic PLA @System"` + a few tweaks (e.g. "Bert - PLA")
|
||||
are detected correctly — the bridge resolves the inherited fields
|
||||
from the system parent.
|
||||
`inherits: "Generic PLA @System"` + a few tweaks are detected
|
||||
correctly — the bridge resolves the inherited fields from the
|
||||
system parent.
|
||||
|
||||
### Fixes
|
||||
- **AMS sync stuck on "Generic PLA":** the Orca data model has 68
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
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
|
||||
|
||||
|
||||
34
README.es.md
34
README.es.md
@@ -66,17 +66,43 @@ docker compose up -d
|
||||
|
||||
**Binario Linux (sin Docker):**
|
||||
```bash
|
||||
chmod +x kx-bridge && ./kx-bridge
|
||||
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
|
||||
```
|
||||
|
||||
**EXE Windows (sin Docker):**
|
||||
```
|
||||
kx-bridge.exe
|
||||
```
|
||||
> `config\` y `data\` se crean junto al EXE — instalación portátil.
|
||||
|
||||
> Con los binarios de Linux y Windows, `config/` y `data/` (configuración, SQLite, almacén de GCode)
|
||||
> viven junto al programa. Copia toda la carpeta para mover la instalación.
|
||||
> ⚠️ **Certificados TLS necesarios para el binario standalone**
|
||||
>
|
||||
> El bridge habla con el MQTT de la impresora vía mTLS y necesita dos
|
||||
> ficheros de certificado **junto al binario**:
|
||||
>
|
||||
> - `anycubic_slicer.crt`
|
||||
> - `anycubic_slicer.key`
|
||||
>
|
||||
> Ambos vienen en **`anycubic-certs.zip`** en la misma página de release.
|
||||
> Descárgalo y extrae los dos ficheros en el mismo directorio que
|
||||
> `kx-bridge-linux-amd64` o `kx-bridge.exe`. Sin ellos verás
|
||||
> `Verbindung fehlgeschlagen: TLS-Zertifikate fehlen …` (0.9.19.1+) o
|
||||
> `[Errno 2] No such file or directory` (versiones anteriores).
|
||||
>
|
||||
> Estructura correcta:
|
||||
> ```
|
||||
> ~/kx-bridge/
|
||||
> ├── kx-bridge-linux-amd64 (o kx-bridge.exe)
|
||||
> ├── anycubic_slicer.crt ← de anycubic-certs.zip
|
||||
> ├── anycubic_slicer.key ← de anycubic-certs.zip
|
||||
> └── config/ (se crea en el primer arranque)
|
||||
> ```
|
||||
>
|
||||
> Los usuarios de Docker no necesitan hacer esto — los certificados
|
||||
> están incluidos en la imagen.
|
||||
|
||||
> Con los binarios de Linux y Windows, `config/` y `data/` (configuración,
|
||||
> SQLite, almacén de GCode) viven junto al programa. Copia toda la carpeta
|
||||
> para mover la instalación.
|
||||
|
||||
**Python directamente:**
|
||||
```bash
|
||||
|
||||
@@ -34,6 +34,9 @@ auto_leveling = 1
|
||||
# Kamera-Stream bei Druckstart automatisch einschalten (1 = an, 0 = aus)
|
||||
camera_on_print = 0
|
||||
|
||||
# Statt grünem Ready-Banner den Filament/Color-Selector automatisch öffnen (1 = an, 0 = aus)
|
||||
print_start_dialog = 1
|
||||
|
||||
# Warnung vor Druck von Web-Uploads (1 = an, 0 = aus)
|
||||
web_upload_warning = 1
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ def _load_config_file(path: pathlib.Path):
|
||||
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
|
||||
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||
}
|
||||
@@ -72,6 +73,17 @@ def _load_config_file(path: pathlib.Path):
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
pass
|
||||
|
||||
# Backward compatibility: legacy option and env var
|
||||
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."""
|
||||
@@ -98,6 +110,7 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
||||
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||
"print_start_dialog": env_vals.get("PRINT_START_DIALOG", env_vals.get("FILE_READY_DIALOG", "1")),
|
||||
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
|
||||
}
|
||||
cfg[CONFIG_SECTION_BRIDGE] = {
|
||||
@@ -258,4 +271,5 @@ DEVICE_ID = get("DEVICE_ID", "")
|
||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG","1")))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
29
docker-compose.portainer.yml
Normal file
29
docker-compose.portainer.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# KX-Bridge — Portainer Stack
|
||||
#
|
||||
# Paste this into Portainer → Stacks → Add stack → Web editor
|
||||
#
|
||||
# No configuration needed upfront — just deploy, open http://HOST-IP:7125
|
||||
# and add your printer via the UI (IP only, credentials are fetched automatically).
|
||||
#
|
||||
# All data (config, GCode store, database) is stored in named Docker volumes
|
||||
# managed by Portainer.
|
||||
|
||||
services:
|
||||
kx-bridge:
|
||||
image: gitea.it-drui.de/viewit/kx-bridge:latest
|
||||
volumes:
|
||||
- kx-bridge-config:/app/config
|
||||
- kx-bridge-data:/app/data
|
||||
ports:
|
||||
# Port 7125 = first printer. Add 7126, 7127, … for each additional printer.
|
||||
- "7125:7125"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
kx-bridge-config:
|
||||
kx-bridge-data:
|
||||
344
docs/filament-preset-bridge-guide.md
Normal file
344
docs/filament-preset-bridge-guide.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# Eigene Filament-Presets anlegen, prüfen und mit KX-Bridge verknüpfen
|
||||
|
||||
> **Gilt für:** OrcaSlicer-KX v2.4.0-alpha-kx2 oder neuer
|
||||
|
||||
---
|
||||
|
||||
## Was ist die `filament_id` und warum ist sie wichtig?
|
||||
|
||||
Jedes Filament-Preset in OrcaSlicer hat eine interne `filament_id`. Diese ID wird von der KX-Bridge genutzt, um beim AMS-Sync das richtige Preset zuzuordnen.
|
||||
|
||||
- System-Presets (z.B. "Polymaker PolyTerra PLA") haben eine feste ID wie `GFL99` oder `OGFL04`.
|
||||
- **Eigene (User-)Presets** bekommen in OrcaSlicer-KX automatisch eine eindeutige ID, die mit `P` beginnt (z.B. `P3a7f2c1`).
|
||||
|
||||
Ohne eindeutige ID zeigt OrcaSlicer beim Sync immer "Generic PLA" — auch wenn das Preset existiert.
|
||||
|
||||
---
|
||||
|
||||
## 1. Eigenes Filament-Preset anlegen
|
||||
|
||||
1. OrcaSlicer-KX starten
|
||||
2. Rechts oben im **Filament-Dropdown** ein passendes Basis-Preset wählen (z.B. "Generic PLA" oder ein Hersteller-Preset)
|
||||
3. Einstellungen nach Wunsch anpassen (Temperaturen, Kühlung, etc.)
|
||||
4. Auf das **Speichern-Symbol** (Diskette) klicken → **"Save as new preset"**
|
||||
5. Namen eingeben — z.B. `SUNLU PLA+ 2.0`
|
||||
> Der Name muss später exakt so in der Bridge eingetragen werden.
|
||||
6. Drucker auswählen: **Anycubic Kobra X 0.4 nozzle** — wichtig für die Kompatibilität!
|
||||
7. **Speichern** klicken
|
||||
8. OrcaSlicer **einmal neu starten** — erst dann wird die `filament_id` dauerhaft gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## 2. Eindeutige ID prüfen
|
||||
|
||||
Nach dem Neustart prüfen, ob die ID korrekt gesetzt wurde:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Die Datei öffnen und nach `filament_id` suchen:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Korrekt: ID beginnt mit `P` gefolgt von 7 Hex-Zeichen
|
||||
❌ Fehlt oder leer: OrcaSlicer-KX zu alt — Update auf v2.4.0-alpha-kx2 oder neuer
|
||||
|
||||
---
|
||||
|
||||
## 3. Preset auf einen anderen PC übertragen (Import)
|
||||
|
||||
### Exportieren (Quell-PC)
|
||||
|
||||
Die Preset-Datei einfach kopieren:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Importieren (Ziel-PC)
|
||||
|
||||
**Methode A — Datei direkt kopieren:**
|
||||
1. Die `.json`-Datei in das gleiche Verzeichnis auf dem Ziel-PC kopieren
|
||||
2. OrcaSlicer neu starten → Preset erscheint im Dropdown
|
||||
|
||||
**Methode B — OrcaSlicer Import-Funktion:**
|
||||
1. In OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Die `.json`-Datei auswählen
|
||||
3. OrcaSlicer neu starten
|
||||
|
||||
> **Wichtig:** Die `filament_id` in der Datei bleibt erhalten — das Preset wird auf dem Ziel-PC genauso erkannt wie auf dem Quell-PC.
|
||||
|
||||
---
|
||||
|
||||
## 4. Preset in KX-Bridge verknüpfen
|
||||
|
||||
1. KX-Bridge UI öffnen
|
||||
2. **Filament-Verwaltung** → AMS-Slot auswählen
|
||||
3. Im Feld **Filament-Name** exakt den OrcaSlicer-Preset-Namen eintragen:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Speichern
|
||||
|
||||
Die Bridge sendet beim Sync `filament_name: "SUNLU PLA+ 2.0"` → OrcaSlicer findet das Preset anhand von Name und `filament_id` → zeigt es korrekt an.
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
| Was | Warum |
|
||||
|-----|-------|
|
||||
| Name in OrcaSlicer und Bridge müssen **exakt** übereinstimmen | Groß-/Kleinschreibung und Sonderzeichen werden verglichen |
|
||||
| Preset muss für **Anycubic Kobra X 0.4 nozzle** kompatibel sein | Beim Speichern den richtigen Drucker auswählen |
|
||||
| Nach dem ersten Speichern OrcaSlicer **neu starten** | Erst dann wird die `filament_id` persistent geschrieben |
|
||||
| **OrcaSlicer-KX v2.4.0-alpha-kx2** oder neuer verwenden | Ältere Versionen generieren keine eindeutige `filament_id` für User-Presets |
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# How to Create, Verify and Import Custom Filament Presets for KX-Bridge
|
||||
|
||||
> **Requires:** OrcaSlicer-KX v2.4.0-alpha-kx2 or newer
|
||||
|
||||
---
|
||||
|
||||
## What is the `filament_id` and why does it matter?
|
||||
|
||||
Every filament preset in OrcaSlicer has an internal `filament_id`. The KX-Bridge uses this ID to match the correct preset during AMS sync.
|
||||
|
||||
- System presets (e.g. "Polymaker PolyTerra PLA") have a fixed ID like `GFL99` or `OGFL04`.
|
||||
- **Custom (user) presets** automatically receive a unique ID starting with `P` (e.g. `P3a7f2c1`) in OrcaSlicer-KX.
|
||||
|
||||
Without a unique ID, OrcaSlicer will always show "Generic PLA" during sync — even if the preset exists.
|
||||
|
||||
---
|
||||
|
||||
## 1. Create a Custom Filament Preset
|
||||
|
||||
1. Launch OrcaSlicer-KX
|
||||
2. Select a suitable base preset from the **filament dropdown** (e.g. "Generic PLA" or a vendor preset)
|
||||
3. Adjust settings as needed (temperatures, cooling, etc.)
|
||||
4. Click the **save icon** (floppy disk) → **"Save as new preset"**
|
||||
5. Enter a name — e.g. `SUNLU PLA+ 2.0`
|
||||
> This name must be entered in the bridge exactly as typed here.
|
||||
6. Select printer: **Anycubic Kobra X 0.4 nozzle** — required for compatibility!
|
||||
7. Click **Save**
|
||||
8. **Restart OrcaSlicer once** — the `filament_id` is only written permanently after a restart.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verify the Unique ID
|
||||
|
||||
After restarting, check that the ID was set correctly:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Open the file and look for `filament_id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Correct: ID starts with `P` followed by 7 hex characters
|
||||
❌ Missing or empty: Your OrcaSlicer-KX version is too old — update to v2.4.0-alpha-kx2 or newer
|
||||
|
||||
---
|
||||
|
||||
## 3. Transfer a Preset to Another PC (Import)
|
||||
|
||||
### Export (source PC)
|
||||
|
||||
Simply copy the preset file:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Import (target PC)
|
||||
|
||||
**Method A — Copy file directly:**
|
||||
1. Copy the `.json` file to the same directory on the target PC
|
||||
2. Restart OrcaSlicer → preset appears in the dropdown
|
||||
|
||||
**Method B — OrcaSlicer import function:**
|
||||
1. In OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Select the `.json` file
|
||||
3. Restart OrcaSlicer
|
||||
|
||||
> **Note:** The `filament_id` inside the file is preserved — the preset will be recognized on the target PC exactly as on the source PC.
|
||||
|
||||
---
|
||||
|
||||
## 4. Link the Preset in KX-Bridge
|
||||
|
||||
1. Open the KX-Bridge UI
|
||||
2. Go to **Filament Management** → select the AMS slot
|
||||
3. In the **Filament Name** field, enter the OrcaSlicer preset name exactly:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Save
|
||||
|
||||
The bridge sends `filament_name: "SUNLU PLA+ 2.0"` during sync → OrcaSlicer matches by name and `filament_id` → displays the preset correctly.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| What | Why |
|
||||
|------|-----|
|
||||
| Name in OrcaSlicer and Bridge must match **exactly** | Case and special characters are compared |
|
||||
| Preset must be compatible with **Anycubic Kobra X 0.4 nozzle** | Select the correct printer when saving |
|
||||
| **Restart OrcaSlicer** after saving for the first time | The `filament_id` is only written persistently after a restart |
|
||||
| Use **OrcaSlicer-KX v2.4.0-alpha-kx2** or newer | Older versions do not generate a unique `filament_id` for user presets |
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# Cómo crear, verificar e importar perfiles de filamento personalizados para KX-Bridge
|
||||
|
||||
> **Requiere:** OrcaSlicer-KX v2.4.0-alpha-kx2 o superior
|
||||
|
||||
---
|
||||
|
||||
## ¿Qué es el `filament_id` y por qué es importante?
|
||||
|
||||
Cada perfil de filamento en OrcaSlicer tiene un `filament_id` interno. KX-Bridge usa este ID para asignar el perfil correcto durante la sincronización AMS.
|
||||
|
||||
- Los perfiles del sistema (p. ej. "Polymaker PolyTerra PLA") tienen un ID fijo como `GFL99` o `OGFL04`.
|
||||
- Los **perfiles personalizados (usuario)** reciben automáticamente un ID único que empieza por `P` (p. ej. `P3a7f2c1`) en OrcaSlicer-KX.
|
||||
|
||||
Sin un ID único, OrcaSlicer mostrará siempre "Generic PLA" durante la sincronización, aunque el perfil exista.
|
||||
|
||||
---
|
||||
|
||||
## 1. Crear un perfil de filamento personalizado
|
||||
|
||||
1. Iniciar OrcaSlicer-KX
|
||||
2. Seleccionar un perfil base adecuado en el **menú desplegable de filamento** (p. ej. "Generic PLA" o un perfil de fabricante)
|
||||
3. Ajustar la configuración según sea necesario (temperaturas, refrigeración, etc.)
|
||||
4. Hacer clic en el **icono de guardar** (disquete) → **"Save as new preset"**
|
||||
5. Introducir un nombre — p. ej. `SUNLU PLA+ 2.0`
|
||||
> Este nombre debe introducirse en la bridge exactamente igual.
|
||||
6. Seleccionar impresora: **Anycubic Kobra X 0.4 nozzle** — ¡necesario para la compatibilidad!
|
||||
7. Hacer clic en **Guardar**
|
||||
8. **Reiniciar OrcaSlicer una vez** — el `filament_id` solo se escribe de forma permanente tras un reinicio.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verificar el ID único
|
||||
|
||||
Tras reiniciar, comprobar que el ID se ha establecido correctamente:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
Abrir el archivo y buscar `filament_id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"filament_id": "P3a7f2c1",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
✅ Correcto: el ID empieza por `P` seguido de 7 caracteres hexadecimales
|
||||
❌ Falta o está vacío: la versión de OrcaSlicer-KX es demasiado antigua — actualizar a v2.4.0-alpha-kx2 o superior
|
||||
|
||||
---
|
||||
|
||||
## 3. Transferir un perfil a otro PC (importar)
|
||||
|
||||
### Exportar (PC de origen)
|
||||
|
||||
Simplemente copiar el archivo del perfil:
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
%APPDATA%\OrcaSlicer\user\default\filament\SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```
|
||||
~/.config/OrcaSlicer/user/default/filament/SUNLU PLA+ 2.0.json
|
||||
```
|
||||
|
||||
### Importar (PC de destino)
|
||||
|
||||
**Método A — Copiar el archivo directamente:**
|
||||
1. Copiar el archivo `.json` al mismo directorio en el PC de destino
|
||||
2. Reiniciar OrcaSlicer → el perfil aparece en el menú desplegable
|
||||
|
||||
**Método B — Función de importación de OrcaSlicer:**
|
||||
1. En OrcaSlicer: **File → Import → Import Configs...**
|
||||
2. Seleccionar el archivo `.json`
|
||||
3. Reiniciar OrcaSlicer
|
||||
|
||||
> **Nota:** El `filament_id` dentro del archivo se conserva — el perfil se reconocerá en el PC de destino exactamente igual que en el de origen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Vincular el perfil en KX-Bridge
|
||||
|
||||
1. Abrir la interfaz de KX-Bridge
|
||||
2. Ir a **Gestión de filamentos** → seleccionar la ranura AMS
|
||||
3. En el campo **Nombre de filamento**, introducir el nombre exacto del perfil de OrcaSlicer:
|
||||
```
|
||||
SUNLU PLA+ 2.0
|
||||
```
|
||||
4. Guardar
|
||||
|
||||
La bridge envía `filament_name: "SUNLU PLA+ 2.0"` durante la sincronización → OrcaSlicer busca por nombre y `filament_id` → muestra el perfil correctamente.
|
||||
|
||||
---
|
||||
|
||||
## Referencia rápida
|
||||
|
||||
| Qué | Por qué |
|
||||
|-----|---------|
|
||||
| El nombre en OrcaSlicer y en Bridge debe coincidir **exactamente** | Se comparan mayúsculas, minúsculas y caracteres especiales |
|
||||
| El perfil debe ser compatible con **Anycubic Kobra X 0.4 nozzle** | Seleccionar la impresora correcta al guardar |
|
||||
| **Reiniciar OrcaSlicer** tras guardar por primera vez | El `filament_id` solo se escribe de forma permanente tras un reinicio |
|
||||
| Usar **OrcaSlicer-KX v2.4.0-alpha-kx2** o superior | Las versiones anteriores no generan un `filament_id` único para perfiles de usuario |
|
||||
@@ -48,3 +48,4 @@ MODE_ID = get("MODE_ID", "")
|
||||
DEVICE_ID = get("DEVICE_ID", "")
|
||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
|
||||
@@ -154,13 +154,21 @@ class KobraXClient:
|
||||
# -- Connection ----------------------------------------------------------
|
||||
|
||||
def _do_connect(self):
|
||||
if not os.path.exists(CERT_FILE) or not os.path.exists(KEY_FILE):
|
||||
raise FileNotFoundError(
|
||||
f"TLS-Zertifikate fehlen: anycubic_slicer.crt + anycubic_slicer.key "
|
||||
f"müssen neben der kx-bridge Binary liegen ({_SCRIPT_DIR}/). "
|
||||
f"Lade anycubic-certs.zip vom Gitea-Release herunter und entpacke "
|
||||
f"die Dateien dorthin."
|
||||
)
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
|
||||
ctx.load_cert_chain(CERT_FILE, KEY_FILE)
|
||||
|
||||
raw = socket.create_connection((self.host, self.port), timeout=5)
|
||||
_ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM)
|
||||
raw = socket.create_connection(_ai[0][4], timeout=5)
|
||||
self._sock = ctx.wrap_socket(raw)
|
||||
log.info("TLS connected cipher=%s", self._sock.cipher()[0])
|
||||
|
||||
@@ -589,7 +597,8 @@ class KobraXClient:
|
||||
# langsamerem WLAN am Drucker dauert das Schieben sonst >30 s und
|
||||
# würde den Connect-Timeout fälschlich auslösen. Read-Timeout danach
|
||||
# generös (Drucker verarbeitet die Datei bevor er antwortet).
|
||||
sock = socket.create_connection((self.host, 18910), timeout=10)
|
||||
_ai = socket.getaddrinfo(self.host, 18910, socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock = socket.create_connection(_ai[0][4], timeout=10)
|
||||
sock.settimeout(None) # blocking während Send
|
||||
sock.sendall(headers + body)
|
||||
sock.settimeout(180)
|
||||
|
||||
@@ -222,7 +222,7 @@ def _parse_gcode_estimated_time(data: bytes) -> int:
|
||||
elif unit == "m": secs += int(val) * 60
|
||||
elif unit == "s": secs += int(val)
|
||||
if secs:
|
||||
log.info(f"Slicer-Schätzzeit: {secs}s ({m.group(1).strip()})")
|
||||
log.info(f"Slicer estimate: {secs}s ({m.group(1).strip()})")
|
||||
return secs
|
||||
|
||||
|
||||
@@ -783,6 +783,7 @@ class KobraXBridge:
|
||||
"print_speed_mode": 2,
|
||||
"connection_error": "",
|
||||
"file_ready": "",
|
||||
"print_start_dialog": getattr(args, "print_start_dialog", 0),
|
||||
"filament_mode": "toolhead",
|
||||
"ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None},
|
||||
}
|
||||
@@ -798,6 +799,7 @@ class KobraXBridge:
|
||||
self._serve_dir_path: str = self._store._gcode_dir
|
||||
self._current_job_id: str = ""
|
||||
self._camera_autostarted: bool = False
|
||||
self._camera_user_stopped: bool = False # User hat Kamera während Druck manuell gestoppt
|
||||
self.camera_cache: CameraCache = CameraCache()
|
||||
|
||||
self._thumbnail_b64: str = ""
|
||||
@@ -805,11 +807,14 @@ 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 confirmed/applied via skip/start
|
||||
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("Ungültiger UI-Theme-Name %r – nutze default", raw_theme)
|
||||
log.warning("Invalid UI theme name %r – using default", raw_theme)
|
||||
raw_theme = "default"
|
||||
self._ui_theme = raw_theme
|
||||
self._index_tpl_cache: str | None = None
|
||||
@@ -914,7 +919,9 @@ class KobraXBridge:
|
||||
# Zentral hier, damit es alle Druck-Startwege abdeckt (OrcaSlicer + UI).
|
||||
# _camera_autostarted verhindert Mehrfach-Trigger pro Druck.
|
||||
if kobra_state == "printing":
|
||||
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
|
||||
if (getattr(self._args, "camera_on_print", 0)
|
||||
and not self._camera_autostarted
|
||||
and not self._camera_user_stopped):
|
||||
self._camera_autostarted = True
|
||||
try:
|
||||
self.client.start_camera()
|
||||
@@ -923,6 +930,7 @@ class KobraXBridge:
|
||||
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||
self._camera_autostarted = False
|
||||
self._camera_user_stopped = False # für nächsten Druck freigeben
|
||||
|
||||
# Job-History: Druckstart erkennen
|
||||
if kobra_state == "printing" and not self._current_job_id:
|
||||
@@ -934,7 +942,7 @@ class KobraXBridge:
|
||||
gcode_file_id=gf["id"],
|
||||
printer_id=self._printer_id,
|
||||
)
|
||||
log.info(f"Job gestartet: {self._current_job_id} für {filename}")
|
||||
log.info(f"Job started: {self._current_job_id} for {filename}")
|
||||
|
||||
# Job-History: Druckende erkennen
|
||||
if kobra_state in ("finished",) and self._current_job_id:
|
||||
@@ -1001,7 +1009,9 @@ class KobraXBridge:
|
||||
# Kamera-Autostart auch hier (OrcaSlicer meldet Start oft via info/report).
|
||||
# _camera_autostarted-Guard verhindert Doppel-Start mit _on_print.
|
||||
if kobra_state == "printing":
|
||||
if getattr(self._args, "camera_on_print", 0) and not getattr(self, "_camera_autostarted", False):
|
||||
if (getattr(self._args, "camera_on_print", 0)
|
||||
and not self._camera_autostarted
|
||||
and not self._camera_user_stopped):
|
||||
self._camera_autostarted = True
|
||||
try:
|
||||
self.client.start_camera()
|
||||
@@ -1010,6 +1020,7 @@ class KobraXBridge:
|
||||
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||
self._camera_autostarted = False
|
||||
self._camera_user_stopped = False # für nächsten Druck freigeben
|
||||
if project:
|
||||
if "filename" in project:
|
||||
self._state["filename"] = project["filename"]
|
||||
@@ -1052,30 +1063,45 @@ class KobraXBridge:
|
||||
"""
|
||||
d = payload.get("data") or {}
|
||||
skipped = d.get("objects_skip_parts") or d.get("skipped") or d.get("skipped_parts") or []
|
||||
# Liste immer (auch leer) übernehmen – sonst bleibt sie auf alten Stand
|
||||
# Während ein Pre-Print-Skip noch als pending markiert ist, ignorieren wir
|
||||
# leere Früh-Reports kurzzeitig, damit die UI nicht sofort zurückspringt.
|
||||
now = time.time()
|
||||
if (not skipped and self._pending_preprint_skip
|
||||
and now <= self._pending_preprint_skip_deadline):
|
||||
return
|
||||
|
||||
# Liste übernehmen (auch leer), sobald kein Pending-Lock aktiv ist.
|
||||
self._skip_state = {
|
||||
"skipped": list(skipped),
|
||||
"ts": int(time.time()),
|
||||
}
|
||||
if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip):
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
if payload.get("state") == "done" or payload.get("code") == 200:
|
||||
log.info(f"Skip-Antwort: state={payload.get('state')} code={payload.get('code')} skipped={skipped}")
|
||||
|
||||
def _on_file(self, payload: dict):
|
||||
d = payload.get("data") or {}
|
||||
details = d.get("file_details") 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 ""
|
||||
thumb = details.get("thumbnail") or details.get("png_image") or ""
|
||||
if thumb:
|
||||
# Uploads während eines laufenden Drucks dürfen die aktive
|
||||
# Fortschritts-Vorschau nicht überschreiben.
|
||||
if thumb and (not active_print or (file_name and file_name == current_print_file)):
|
||||
self._thumbnail_b64 = thumb
|
||||
log.info(f"Vorschaubild empfangen: {len(thumb)} Zeichen base64")
|
||||
# Part-Skip: Objekt-Liste + optionales SVG (v0.9.10)
|
||||
objs = details.get("objects_skip_parts") or []
|
||||
svg = details.get("svg_image") or ""
|
||||
if objs:
|
||||
filename = d.get("filename") or details.get("filename") or self._last_uploaded_file
|
||||
filename = file_name
|
||||
if filename:
|
||||
try:
|
||||
self._store.update_file_objects(filename, objs, svg)
|
||||
log.info(f"Skip-Objekte für {filename}: {len(objs)} ({'mit SVG' if svg else 'ohne SVG'})")
|
||||
log.info(f"Skip objects for {filename}: {len(objs)} ({'with SVG' if svg else 'no SVG'})")
|
||||
except Exception as e:
|
||||
log.warning(f"update_file_objects fehlgeschlagen: {e}")
|
||||
self._push_status_update()
|
||||
@@ -1884,6 +1910,43 @@ class KobraXBridge:
|
||||
"gcode_macro TIMELAPSE_TAKE_FRAME": {
|
||||
"is_paused": False,
|
||||
},
|
||||
# configfile stub — Mobileraker und andere Clients crashen ohne
|
||||
# dieses Objekt (Missing field: configFile). Werte aus der
|
||||
# entschlüsselten avata_main.conf (ACCFG1.0 — Kobra X Firmware).
|
||||
"configfile": {
|
||||
"config": {},
|
||||
"settings": {
|
||||
"printer": {
|
||||
"kinematics": "cartesian",
|
||||
"max_velocity": 450,
|
||||
"max_accel": 10000,
|
||||
"max_z_velocity": 12,
|
||||
"max_z_accel": 100,
|
||||
"square_corner_velocity": 20.0,
|
||||
},
|
||||
"extruder": {
|
||||
"nozzle_diameter": 0.4,
|
||||
"filament_diameter": 1.75,
|
||||
"sensor_type": "ATC Semitec 104GT-2",
|
||||
"min_temp": 0,
|
||||
"max_temp": 320,
|
||||
"min_extrude_temp": 10,
|
||||
},
|
||||
"heater_bed": {
|
||||
"min_temp": 0,
|
||||
"max_temp": 120,
|
||||
},
|
||||
"stepper_x": {"position_min": -18.5, "position_max": 280},
|
||||
"stepper_y": {"position_min": -6.5, "position_max": 272.5},
|
||||
"stepper_z": {"position_min": -4, "position_max": 262},
|
||||
"virtual_sdcard": {"path": "/data/gcodes"},
|
||||
"pause_resume": {},
|
||||
"display_status": {},
|
||||
},
|
||||
"warnings": [],
|
||||
"save_config_pending": False,
|
||||
"save_config_pending_items": {},
|
||||
},
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2389,6 +2452,15 @@ class KobraXBridge:
|
||||
excluded_objects = body.get("excluded_objects") or []
|
||||
if not isinstance(excluded_objects, list):
|
||||
excluded_objects = []
|
||||
# Pre-Print-Auswahl sofort im UI-Status spiegeln (wird später durch
|
||||
# skip/report vom Drucker überschrieben/aktualisiert).
|
||||
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
|
||||
|
||||
if assignments:
|
||||
ams_box_mapping, unused_count, invalid_count = self._build_assigned_ams_box_mapping(assignments)
|
||||
@@ -2403,7 +2475,7 @@ class KobraXBridge:
|
||||
ams_box_mapping = self._build_auto_ams_box_mapping()
|
||||
|
||||
use_ams = len(ams_box_mapping) > 0
|
||||
auto_leveling = getattr(self._args, "auto_leveling", 1)
|
||||
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
|
||||
filename = gcode_file["filename"]
|
||||
file_path = gcode_file["path"]
|
||||
|
||||
@@ -2431,6 +2503,9 @@ class KobraXBridge:
|
||||
"ai_settings": {"status": 0, "count": 0, "type": 1},
|
||||
"timelapse": {"status": 0, "count": 0, "type": 64},
|
||||
"drying_settings": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0},
|
||||
# Firmware-Variante A (funktioniert bei mehreren Builds)
|
||||
"objects_skip_parts": excluded_objects,
|
||||
# Firmware-Variante B (beibehalten für Kompatibilität)
|
||||
"model_objects_skip_parts": excluded_objects,
|
||||
},
|
||||
}
|
||||
@@ -2443,6 +2518,9 @@ class KobraXBridge:
|
||||
if result is None:
|
||||
return self._json_cors({"error": "Keine Antwort vom Drucker"}, status=504)
|
||||
|
||||
if excluded_objects:
|
||||
loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects))
|
||||
|
||||
# Job in History starten
|
||||
self._current_job_id = self._store.start_job(
|
||||
gcode_file_id=gcode_file["id"],
|
||||
@@ -2655,7 +2733,20 @@ class KobraXBridge:
|
||||
return web.json_response({"result": {"count": len(result_jobs), "jobs": result_jobs}})
|
||||
|
||||
async def handle_webcams_list(self, request):
|
||||
"""Moonraker /server/webcams/list — Obico holt die Webcam-URLs hier."""
|
||||
"""Moonraker /server/webcams/list — Obico holt die Webcam-URLs hier.
|
||||
|
||||
Wenn der Client von einem anderen Host kommt (z.B. moonraker-obico auf
|
||||
separatem Server), braucht er absolute URLs damit er den Stream erreicht.
|
||||
Host-Header mit localhost/127.0.0.1 wird durch die echte LAN-IP ersetzt."""
|
||||
host_hdr = request.headers.get("Host", "") if request else ""
|
||||
host_name = (host_hdr or "").split(":")[0]
|
||||
port_part = f":{host_hdr.split(':')[1]}" if ":" in (host_hdr or "") else f":{self._args.port}"
|
||||
local_ip = getattr(self, "_local_ip", None) or host_name
|
||||
if host_name in ("localhost", "127.0.0.1", ""):
|
||||
host_name = local_ip
|
||||
base = f"http://{host_name}{port_part}"
|
||||
stream_url = f"{base}/api/camera/stream"
|
||||
snapshot_url = f"{base}/api/camera/snapshot"
|
||||
return web.json_response({
|
||||
"result": {
|
||||
"webcams": [
|
||||
@@ -2667,8 +2758,8 @@ class KobraXBridge:
|
||||
"icon": "mdiWebcam",
|
||||
"target_fps": 5,
|
||||
"target_fps_idle": 2,
|
||||
"stream_url": "/api/camera/stream",
|
||||
"snapshot_url": "/api/camera/snapshot",
|
||||
"stream_url": stream_url,
|
||||
"snapshot_url": snapshot_url,
|
||||
"flip_horizontal": False,
|
||||
"flip_vertical": False,
|
||||
"rotation": 0,
|
||||
@@ -2684,6 +2775,7 @@ class KobraXBridge:
|
||||
ct = request.headers.get("Content-Type", "")
|
||||
if "multipart" not in ct:
|
||||
return web.json_response({"error": "expected multipart"}, status=400)
|
||||
active_print = self._state.get("print_state") in ("printing", "paused")
|
||||
auto_print = False
|
||||
web_upload = False
|
||||
reader = await request.multipart()
|
||||
@@ -2716,12 +2808,15 @@ class KobraXBridge:
|
||||
|
||||
# Slicer-Zeitschätzung + Thumbnail aus GCode auslesen
|
||||
est_time = _parse_gcode_estimated_time(file_data)
|
||||
self._state["slicer_time"] = est_time
|
||||
thumbnail_b64 = _extract_thumbnail(file_data)
|
||||
gcode_filaments = _extract_filament_info(file_data)
|
||||
layer_h, first_h = _parse_gcode_layer_heights(file_data)
|
||||
self._state["layer_height"] = layer_h
|
||||
self._state["first_layer_height"] = first_h
|
||||
# Aktiven Druck nicht mit Metadaten eines neu hochgeladenen Files
|
||||
# überschreiben (Fortschrittsansicht muss beim laufenden Job bleiben).
|
||||
if not active_print:
|
||||
self._state["slicer_time"] = est_time
|
||||
self._state["layer_height"] = layer_h
|
||||
self._state["first_layer_height"] = first_h
|
||||
|
||||
# Datei persistent im GCode-Store ablegen
|
||||
self._store.save_file(
|
||||
@@ -2762,9 +2857,12 @@ class KobraXBridge:
|
||||
if not auto_print:
|
||||
auto_print = request.rel_url.query.get("print", "false").lower() == "true"
|
||||
|
||||
# Thumbnail immer anfordern (Drucker antwortet async mit file/report)
|
||||
self._thumbnail_b64 = ""
|
||||
self.client.publish("file", "fileDetails", {"root": "local", "filename": remote_filename}, timeout=0)
|
||||
# Thumbnail nur dann im Runtime-State aktualisieren, wenn kein aktiver
|
||||
# Druck läuft. Sonst könnte das Upload-Thumbnail die laufende
|
||||
# Fortschrittsvorschau verdrängen.
|
||||
if not active_print:
|
||||
self._thumbnail_b64 = ""
|
||||
self.client.publish("file", "fileDetails", {"root": "local", "filename": remote_filename}, timeout=0)
|
||||
|
||||
self._state["last_upload_url"] = serve_url
|
||||
self._state["last_upload_md5"] = file_md5
|
||||
@@ -2802,6 +2900,10 @@ class KobraXBridge:
|
||||
def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0,
|
||||
gcode_filaments: list | None = None):
|
||||
self._state["file_ready"] = ""
|
||||
# Neuer Druck ohne Pre-Print-Excludes: Skip-State zurücksetzen.
|
||||
self._skip_state = {"skipped": [], "ts": int(time.time())}
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
loaded = self._select_loaded_slots_for_print(warn_on_empty_default=True)
|
||||
|
||||
# Nur die im GCode TATSÄCHLICH genutzten Paints auf Slots mappen. OrcaSlicer
|
||||
@@ -2825,7 +2927,7 @@ class KobraXBridge:
|
||||
use_ams = len(loaded) > 0
|
||||
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded)
|
||||
log.debug(f"AMS-Slots: {len(loaded)} gemappt (genutzte Paints: {used_paint_indices}) → {[i for i, _ in loaded]}")
|
||||
auto_leveling = getattr(self._args, "auto_leveling", 1)
|
||||
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
|
||||
payload = {
|
||||
"taskid": "-1",
|
||||
"url": url,
|
||||
@@ -2853,10 +2955,41 @@ class KobraXBridge:
|
||||
log.info(f"print/start → {filename} url={url} ams={len(ams_box_mapping)} slots mode={self._filament_mode}")
|
||||
result = self.client.publish("print", "start", payload, timeout=15.0)
|
||||
if result:
|
||||
log.info(f"Druckstart bestätigt: state={result.get('state')}")
|
||||
log.info(f"Print start confirmed: state={result.get('state')}")
|
||||
else:
|
||||
log.warning("Druckstart: keine Antwort vom Drucker")
|
||||
|
||||
def _apply_preprint_skip_after_start(self, names: list[str], retries: int = 20, delay_s: float = 0.75):
|
||||
wanted = [str(n) for n in (names or []) if isinstance(n, str) and n]
|
||||
if not wanted:
|
||||
return False
|
||||
|
||||
self._pending_preprint_skip = list(wanted)
|
||||
self._pending_preprint_skip_deadline = time.time() + max(3.0, float(retries) * float(delay_s) + 2.0)
|
||||
|
||||
# UI should already reflect intended skips immediately.
|
||||
self._skip_state = {"skipped": list(wanted), "ts": int(time.time())}
|
||||
|
||||
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
|
||||
|
||||
def _theme_index_path(self) -> str:
|
||||
return os.path.join(_WEB_BASE, "web", "themes", self._ui_theme, "index.html")
|
||||
|
||||
@@ -2900,6 +3033,15 @@ class KobraXBridge:
|
||||
excluded_objects = body.get("excluded_objects") or []
|
||||
if not isinstance(excluded_objects, list):
|
||||
excluded_objects = []
|
||||
# Pre-Print-Auswahl sofort im UI-Status spiegeln (wird später durch
|
||||
# skip/report vom Drucker überschrieben/aktualisiert).
|
||||
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
|
||||
if filament_assignments is not None:
|
||||
ams_box_mapping, unused_count, invalid_count = self._build_assigned_ams_box_mapping(filament_assignments)
|
||||
if unused_count:
|
||||
@@ -2913,7 +3055,7 @@ class KobraXBridge:
|
||||
ams_box_mapping = self._build_auto_ams_box_mapping()
|
||||
|
||||
use_ams = len(ams_box_mapping) > 0
|
||||
auto_leveling = getattr(self._args, "auto_leveling", 1)
|
||||
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
|
||||
url = self._state.get("last_upload_url", "")
|
||||
filesize = self._state.get("last_upload_size", 0)
|
||||
md5 = self._state.get("last_upload_md5", "")
|
||||
@@ -2939,6 +3081,9 @@ class KobraXBridge:
|
||||
"ai_settings": {"status": 0, "count": 0, "type": 0},
|
||||
"timelapse": {"status": 0, "count": 0, "type": 0},
|
||||
"drying_settings": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0},
|
||||
# Firmware-Variante A (funktioniert bei mehreren Builds)
|
||||
"objects_skip_parts": excluded_objects,
|
||||
# Firmware-Variante B (beibehalten für Kompatibilität)
|
||||
"model_objects_skip_parts": excluded_objects,
|
||||
},
|
||||
}
|
||||
@@ -2955,6 +3100,9 @@ class KobraXBridge:
|
||||
if result is None:
|
||||
return web.json_response({"error": "Keine Antwort vom Drucker"}, status=504)
|
||||
|
||||
if excluded_objects:
|
||||
loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects))
|
||||
|
||||
return web.json_response({"result": "ok"})
|
||||
|
||||
async def handle_print_pause(self, request):
|
||||
@@ -2977,6 +3125,15 @@ class KobraXBridge:
|
||||
|
||||
async def handle_api_file_ready_clear(self, request):
|
||||
self._state["file_ready"] = ""
|
||||
self._state["filename"] = ""
|
||||
self._state["progress"] = 0.0
|
||||
self._state["print_duration"] = 0
|
||||
self._state["remain_time"] = 0
|
||||
self._state["curr_layer"] = 0
|
||||
self._state["total_layers"] = 0
|
||||
self._state["slicer_time"] = 0
|
||||
self._state["layer_height"] = 0.0
|
||||
self._state["first_layer_height"] = 0.0
|
||||
self._thumbnail_b64 = ""
|
||||
self._push_status_update()
|
||||
return web.json_response({"result": "ok"})
|
||||
@@ -3108,7 +3265,7 @@ class KobraXBridge:
|
||||
return web.json_response({"result": "disconnected"})
|
||||
|
||||
async def handle_api_restart(self, request):
|
||||
log.info("Neustart über API angefordert")
|
||||
log.info("Restart requested via API")
|
||||
response = web.json_response({"status": "restarting"})
|
||||
asyncio.get_event_loop().call_later(0.3, self._restart_bridge)
|
||||
return response
|
||||
@@ -3388,6 +3545,9 @@ class KobraXBridge:
|
||||
await loop.run_in_executor(None, lambda: self.client.publish(
|
||||
"video", "stopCapture", None, timeout=0
|
||||
))
|
||||
# Verhindert dass der Autostart-Guard die Kamera während des
|
||||
# laufenden Drucks wieder einschaltet (State-Flicker-Problem).
|
||||
self._camera_user_stopped = True
|
||||
return web.json_response({"result": "ok"})
|
||||
|
||||
async def handle_api_camera_snapshot(self, request):
|
||||
@@ -3447,7 +3607,7 @@ class KobraXBridge:
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
log.warning("Kamera: ffmpeg nicht gefunden – Kamerastream nicht verfügbar")
|
||||
log.warning("Camera: ffmpeg not found – camera stream unavailable")
|
||||
return web.Response(status=503, text="ffmpeg not found")
|
||||
except Exception as e:
|
||||
log.warning(f"Kamera: ffmpeg konnte nicht gestartet werden: {e}")
|
||||
@@ -3542,7 +3702,7 @@ class KobraXBridge:
|
||||
if not os.path.isfile(serve_path):
|
||||
return web.Response(status=404, text="not found")
|
||||
size = os.path.getsize(serve_path)
|
||||
log.info(f"Drucker lädt Datei ab: {filename} ({size} bytes)")
|
||||
log.info(f"Printer downloading file: {filename} ({size} bytes)")
|
||||
return web.FileResponse(serve_path, headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
})
|
||||
@@ -3580,11 +3740,13 @@ 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"],
|
||||
"fan_speed": s["fan_speed"],
|
||||
"print_speed_mode": s["print_speed_mode"],
|
||||
"auto_leveling": getattr(self._args, "auto_leveling", 1),
|
||||
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
|
||||
"light_on": s["light_on"],
|
||||
"light_brightness": s["light_brightness"],
|
||||
@@ -3598,6 +3760,7 @@ class KobraXBridge:
|
||||
"thumbnail": thumbnail,
|
||||
"connection_error": s["connection_error"],
|
||||
"file_ready": s["file_ready"],
|
||||
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
|
||||
"version": self._read_version(),
|
||||
})
|
||||
|
||||
@@ -3735,6 +3898,7 @@ class KobraXBridge:
|
||||
"default_ams_slot": getattr(self._args, "default_ams_slot", "auto"),
|
||||
"auto_leveling": getattr(self._args, "auto_leveling", 1),
|
||||
"camera_on_print": getattr(self._args, "camera_on_print", 0),
|
||||
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
|
||||
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
|
||||
"ace_dry_presets": self._ace_dry_presets,
|
||||
})
|
||||
@@ -3765,6 +3929,8 @@ class KobraXBridge:
|
||||
cfg.set("print", "default_ams_slot", str(data.get("default_ams_slot", getattr(self._args, "default_ams_slot", "auto"))))
|
||||
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))))))
|
||||
print_start_dialog = int(bool(data.get("print_start_dialog", data.get("file_ready_dialog", getattr(self._args, "print_start_dialog", 0)))))
|
||||
cfg.set("print", "print_start_dialog", str(print_start_dialog))
|
||||
cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1))))))
|
||||
if not cfg.has_option("bridge", "poll_interval"):
|
||||
cfg.set("bridge", "poll_interval", "3")
|
||||
@@ -3774,6 +3940,9 @@ class KobraXBridge:
|
||||
elif cfg.has_option("bridge", "printer_name"):
|
||||
cfg.remove_option("bridge", "printer_name")
|
||||
|
||||
self._args.print_start_dialog = print_start_dialog
|
||||
self._args.auto_leveling = int(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
|
||||
|
||||
incoming_presets = data.get("ace_dry_presets") if isinstance(data, dict) else None
|
||||
presets = self._sanitize_ace_dry_presets(incoming_presets if isinstance(incoming_presets, dict) else self._ace_dry_presets)
|
||||
for key, val in presets.items():
|
||||
@@ -3857,7 +4026,7 @@ class KobraXBridge:
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write("# KX-Bridge Konfigurationsdatei\n\n")
|
||||
cfg.write(f)
|
||||
log.info(f"Drucker '{name or creds['model']}' als {sec} hinzugefügt (Port {new_port})")
|
||||
log.info(f"Printer '{name or creds['model']}' added as {sec} (port {new_port})")
|
||||
response = self._json_cors({"status": "restarting", "section": sec, "http_port": new_port})
|
||||
asyncio.get_event_loop().call_later(0.5, self._restart_bridge)
|
||||
return response
|
||||
@@ -3931,14 +4100,14 @@ class KobraXBridge:
|
||||
# Bei einem Restart muss environ bereinigt werden, sonst liest der neue Prozess
|
||||
# die alten Werte statt der geänderten config.ini.
|
||||
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
|
||||
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
|
||||
"BRIDGE_PRINTER_NAME"):
|
||||
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
|
||||
"CAMERA_ON_PRINT", "PRINT_START_DIALOG", "FILE_READY_DIALOG", "WEB_UPLOAD_WARNING", "BRIDGE_PRINTER_NAME"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
|
||||
if in_docker:
|
||||
# Docker/systemd: Prozess beenden reicht – der Supervisor startet neu (frische environ)
|
||||
log.info("Container-Umgebung erkannt – beende Prozess für Supervisor-Restart")
|
||||
log.info("Container environment detected – exiting for supervisor restart")
|
||||
os._exit(0)
|
||||
|
||||
frozen = getattr(sys, "frozen", False)
|
||||
@@ -3973,7 +4142,9 @@ class KobraXBridge:
|
||||
GITEA_RAW_BASE = "https://gitea.it-drui.de/viewit/KX-Bridge-Release/raw/tag"
|
||||
|
||||
def _read_version(self) -> str:
|
||||
for base in (pathlib.Path(_BASE), pathlib.Path(_BASE).parent):
|
||||
# PyInstaller-Onefile entpackt VERSION (per kx-bridge.spec datas) nach
|
||||
# sys._MEIPASS — daher _WEB_BASE statt _BASE benutzen.
|
||||
for base in (pathlib.Path(_WEB_BASE), pathlib.Path(_BASE), pathlib.Path(_BASE).parent):
|
||||
p = base / "VERSION"
|
||||
if p.is_file():
|
||||
return p.read_text(encoding="utf-8").strip()
|
||||
@@ -4115,7 +4286,7 @@ class KobraXBridge:
|
||||
if fname == "kobrax_moonraker_bridge.py":
|
||||
return web.json_response(
|
||||
{"error": f"Download {fname}: HTTP {resp.status}"}, status=502)
|
||||
log.warning(f"Update: {fname} nicht im Release ({resp.status}) – übersprungen")
|
||||
log.warning(f"Update: {fname} not found in release ({resp.status}) – skipped")
|
||||
continue
|
||||
downloaded.append((app_dir / fname, await resp.read()))
|
||||
# Phase 2: atomar ersetzen (erst nach komplettem, erfolgreichem Download)
|
||||
@@ -4392,11 +4563,14 @@ class KobraXBridge:
|
||||
# Obico registriert obico_remote_event-Callback. Wir akzeptieren leer.
|
||||
result = "ok"
|
||||
elif method == "server.webcams.list":
|
||||
# WS-Variante des HTTP-Endpoints
|
||||
# WS-Variante: absolute URL mit echter LAN-IP statt localhost
|
||||
_lip = getattr(self, "_local_ip", None) or "127.0.0.1"
|
||||
_base = f"http://{_lip}:{self._args.port}"
|
||||
result = {"webcams": [{
|
||||
"name": "KX-Bridge", "location": "printer", "service": "mjpegstreamer",
|
||||
"enabled": True, "stream_url": "/api/camera/stream",
|
||||
"snapshot_url": "/api/camera/snapshot",
|
||||
"enabled": True,
|
||||
"stream_url": f"{_base}/api/camera/stream",
|
||||
"snapshot_url": f"{_base}/api/camera/snapshot",
|
||||
"flip_horizontal": False, "flip_vertical": False, "rotation": 0,
|
||||
"target_fps": 5, "aspect_ratio": "16:9",
|
||||
}]}
|
||||
@@ -4446,7 +4620,7 @@ class KobraXBridge:
|
||||
log.debug(f"Unbekannte RPC-Methode: {method}")
|
||||
result = {}
|
||||
except Exception as e:
|
||||
log.error(f"RPC-Fehler für {method}: {e}")
|
||||
log.error(f"RPC error for {method}: {e}")
|
||||
error = {"code": -32603, "message": str(e)}
|
||||
|
||||
if rpc_id is not None:
|
||||
@@ -4760,7 +4934,7 @@ async def run_bridge(args):
|
||||
site = web.TCPSite(runner, args.host, per_args.port)
|
||||
await site.start()
|
||||
runners.append((runner, client, pid))
|
||||
log.info(f"[Drucker {pid}] Bridge läuft auf http://{args.host}:{per_args.port}")
|
||||
log.info(f"[Printer {pid}] Bridge running on http://{args.host}:{per_args.port}")
|
||||
|
||||
import socket as _socket
|
||||
try:
|
||||
@@ -4769,6 +4943,9 @@ async def run_bridge(args):
|
||||
_local_ip = _s.getsockname()[0]
|
||||
except Exception:
|
||||
_local_ip = args.host
|
||||
# An alle Bridge-Instanzen weitergeben — wird für absolute Webcam-URLs genutzt
|
||||
for _b in all_bridges.values():
|
||||
_b._local_ip = _local_ip
|
||||
log.info(f"OrcaSlicer → Klipper → Host: {_local_ip} Ports: " +
|
||||
", ".join(str(getattr(b._args, 'port', 0)) for b in all_bridges.values()))
|
||||
log.info("Ctrl-C zum Beenden")
|
||||
@@ -4816,6 +4993,8 @@ def main():
|
||||
parser.add_argument("--default-ams-slot",default=env_loader.DEFAULT_AMS_SLOT)
|
||||
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("--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("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
|
||||
|
||||
parser.add_argument("--host", default="0.0.0.0",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas = [("web", "web"), ("data", "static")] # bridge/data/ → static/ im _MEIPASS
|
||||
datas = [("web", "web"), ("data", "static"), ("VERSION", ".")] # bridge/data/ → static/ im _MEIPASS
|
||||
binaries = []
|
||||
hiddenimports = []
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
// ── State ──
|
||||
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
|
||||
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
|
||||
curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–',
|
||||
curr_layer:0,total_layers:0,z_mm:0,printer_name:'Kobra X',firmware_version:'–',
|
||||
camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,
|
||||
auto_leveling:1,
|
||||
ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]},web_upload_warning:1};
|
||||
var tempHistory={n:[],b:[]};
|
||||
var camOn=false;
|
||||
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
|
||||
var _camPollInterval=null; // snapshot-polling interval for Android (no MJPEG support)
|
||||
var currentStep=1;
|
||||
var currentPanel='dashboard';
|
||||
var _printOptionsAutoLeveling=1;
|
||||
var _printOptionsAutoLevelingSnapshot=1;
|
||||
var aceAutoRefillPrefs=(function(){
|
||||
try{return JSON.parse(localStorage.getItem('aceAutoRefillPrefs')||'{}')||{};}catch(_){return {};}
|
||||
})();
|
||||
@@ -101,6 +106,7 @@ function tr(key,fallback){
|
||||
function _langToggleLabel(lang){
|
||||
if(lang==='de')return 'Deutsch';
|
||||
if(lang==='en')return 'English';
|
||||
if(lang==='fr')return 'Français';
|
||||
if(lang==='zh-cn')return '简体中文';
|
||||
return 'Espanol';
|
||||
}
|
||||
@@ -108,10 +114,10 @@ function _langToggleLabel(lang){
|
||||
function _mapSupportedLang(lang){
|
||||
if(!lang)return '';
|
||||
var l=String(lang).toLowerCase().replace(/_/g,'-').trim();
|
||||
if(l==='de'||l==='en'||l==='es'||l==='zh-cn')return l;
|
||||
if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='zh-cn')return l;
|
||||
|
||||
var base=l.split('-')[0];
|
||||
if(base==='de'||base==='en'||base==='es')return base;
|
||||
if(base==='de'||base==='en'||base==='es'||base==='fr')return base;
|
||||
|
||||
if(base==='zh'){
|
||||
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
|
||||
@@ -257,6 +263,8 @@ function applyLang(){
|
||||
setText('apd-cancel',T.apd_cancel);
|
||||
setText('apd-confirm',T.apd_confirm);
|
||||
setText('fd-slots-hint',T.fd_slots_hint);
|
||||
setText('skip-cancel',T.skip_cancel);
|
||||
setText('skip-confirm',T.skip_confirm);
|
||||
setText('fd-cancel',T.fd_cancel);
|
||||
setText('fd-print',T.fd_print);
|
||||
setText('store-panel-title','🗂 '+T.panel_browser_title);
|
||||
@@ -282,6 +290,7 @@ function applyLang(){
|
||||
setText('d-lbl-remain',T.lbl_remaining);
|
||||
setText('d-slicer-label',T.lbl_slicer_time);
|
||||
setText('d-lbl-layers',T.lbl_layers);
|
||||
setText('d-lbl-zpos',T.lbl_zpos);
|
||||
setText('d-lbl-light',T.lbl_light);
|
||||
setText('d-lbl-nozzle',T.label_nozzle);
|
||||
setText('d-lbl-bed',T.label_bed);
|
||||
@@ -334,7 +343,12 @@ 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('fd-options-title',T.fd_options_title);
|
||||
setText('lbl-fd-auto-leveling',T.print_auto_leveling);
|
||||
setText('lbl-camera-on-print',T.settings_camera_on_print);
|
||||
setText('lbl-file-ready-mode',T.settings_file_ready_mode);
|
||||
setText('opt-file-ready-banner',T.settings_file_ready_banner);
|
||||
setText('opt-file-ready-dialog',T.settings_file_ready_dialog);
|
||||
setText('lbl-web-upload-warning',T.settings_web_upload_warning);
|
||||
|
||||
setText('lbl-update-check',T.update_check);
|
||||
@@ -381,7 +395,22 @@ function applyLang(){
|
||||
var mi=document.getElementById('slot-edit-mat');if(mi)mi.setAttribute('placeholder',T.slot_edit_custom);
|
||||
setText('logdir-all',T.log_dir_all);
|
||||
setText('loglvl-all',T.log_dir_all);
|
||||
setText('logdir-rx',T.log_dir_rx);
|
||||
setText('logdir-tx',T.log_dir_tx);
|
||||
setText('loglvl-err',T.log_lvl_err);
|
||||
setText('loglvl-warn',T.log_lvl_warn);
|
||||
setText('log-lbl-dir',T.log_dir_label);
|
||||
setText('log-lbl-level',T.log_lvl_label);
|
||||
setText('log-lbl-topic',T.log_topic_label);
|
||||
setText('log-topic-ams',T.log_topic_ams);
|
||||
setText('log-topic-print',T.log_topic_print);
|
||||
setText('log-topic-info',T.log_topic_info);
|
||||
setText('log-topic-status',T.log_topic_status);
|
||||
setText('btn-log-dl',T.log_download);
|
||||
setText('btn-autoscroll',T.log_auto);
|
||||
setText('btn-log-clear',T.log_clear);
|
||||
var lf=document.getElementById('log-filter');if(lf)lf.setAttribute('placeholder',T.log_filter_placeholder);
|
||||
var sb=document.getElementById('settings-btn');if(sb)sb.setAttribute('title',T.settings_title);
|
||||
setText('file-ready-btn',T.file_ready_btn);
|
||||
setText('file-slots-btn',T.file_slots_btn);
|
||||
setText('file-cancel-btn',T.file_cancel_btn);
|
||||
@@ -616,12 +645,25 @@ function applyState(){
|
||||
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 frb=document.getElementById('file-ready-banner');
|
||||
var filamentDialog=document.getElementById('filament-dialog');
|
||||
var filamentDialogOpen=!!(filamentDialog&&filamentDialog.classList.contains('open'));
|
||||
var readyFile=s.file_ready||'';
|
||||
var readyStandby=!!(readyFile&&s.print_state==='standby');
|
||||
var dialogMode=!!(s.print_start_dialog!==undefined?s.print_start_dialog:s.file_ready_dialog);
|
||||
if(frb){
|
||||
if(s.file_ready&&s.print_state==='standby'){
|
||||
document.getElementById('file-ready-name').textContent=s.file_ready;
|
||||
if(readyStandby&&!dialogMode){
|
||||
document.getElementById('file-ready-name').textContent=readyFile;
|
||||
frb.style.display='flex';
|
||||
}else{frb.style.display='none';}
|
||||
}else{
|
||||
frb.style.display='none';
|
||||
}
|
||||
}
|
||||
var shouldAutoOpen=(_readyStateInitialized&&readyStandby&&dialogMode&&!filamentDialogOpen&&readyFile!==_lastReadyFile);
|
||||
if(shouldAutoOpen){
|
||||
setTimeout(function(){ startReadyFileWithSlots(); },0);
|
||||
}
|
||||
_lastReadyFile=readyFile;
|
||||
_readyStateInitialized=true;
|
||||
// skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird
|
||||
var printing=(s.print_state==='printing'||s.print_state==='paused');
|
||||
var skipBtn=document.getElementById('d-btn-skip');
|
||||
@@ -630,6 +672,10 @@ function applyState(){
|
||||
// der Drucker idle ist). Pause-Button rendert sich passend zum State um.
|
||||
var ctrlBtns=document.getElementById('d-ctrl-btns');
|
||||
if(ctrlBtns) ctrlBtns.style.display=printing?'':'none';
|
||||
var hasLoadedFile=!!((s.file_ready||'').trim()||(s.filename||'').trim());
|
||||
var showReadyBtns=!printing&&hasLoadedFile;
|
||||
var readyBtns=document.getElementById('d-ready-btns');
|
||||
if(readyBtns) readyBtns.style.display=showReadyBtns?'':'none';
|
||||
updatePauseResumeBtn();
|
||||
|
||||
// header
|
||||
@@ -659,6 +705,7 @@ function applyState(){
|
||||
|
||||
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–';
|
||||
var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers;
|
||||
var dzpos=document.getElementById('d-zpos');if(dzpos)dzpos.textContent=s.z_mm>0?s.z_mm.toFixed(2)+' mm':'–';
|
||||
|
||||
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration);
|
||||
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–';
|
||||
@@ -669,7 +716,7 @@ function applyState(){
|
||||
else{dslrow.style.display='none';}
|
||||
}
|
||||
|
||||
var fn=s.filename||'–';
|
||||
var fn=printing?(s.filename||'–'):(s.filename||s.file_ready||'–');
|
||||
var dfname=document.getElementById('d-fname');if(dfname){dfname.textContent=fn;dfname.title=fn};
|
||||
var pfname=document.getElementById('p-fname');if(pfname){pfname.textContent=fn;pfname.title=fn};
|
||||
var cfo=document.getElementById('cam-fname');if(cfo)cfo.textContent=fn!=='–'?fn:'';
|
||||
@@ -822,10 +869,14 @@ function applyState(){
|
||||
var co=document.getElementById('cam-overlay');
|
||||
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
|
||||
|
||||
// auto-start camera during print
|
||||
if(s.print_state==='printing'&&!camOn&&s.camera_url){
|
||||
// auto-start camera during print (unless user explicitly stopped it)
|
||||
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){
|
||||
camStart();
|
||||
}
|
||||
// reset user-stopped flag when print ends so next print auto-starts again
|
||||
if(s.print_state!=='printing'){
|
||||
camUserStopped=false;
|
||||
}
|
||||
|
||||
updateConnBtn();
|
||||
}
|
||||
@@ -901,7 +952,9 @@ function openSettings(){
|
||||
document.getElementById('s-mode-id').value=d.mode_id||'';
|
||||
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);
|
||||
S.auto_leveling=(d.auto_leveling===undefined?1:(d.auto_leveling?1:0));
|
||||
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=Number((d.print_start_dialog!==undefined)?d.print_start_dialog:d.file_ready_dialog)?'dialog':'banner';
|
||||
var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning);
|
||||
});
|
||||
var v=localStorage.getItem('pollInterval')||'2000';
|
||||
@@ -1161,6 +1214,26 @@ function cancelReadyFile(){
|
||||
post('/api/file_ready/clear',{})
|
||||
.then(function(){document.getElementById('file-ready-banner').style.display='none';});
|
||||
}
|
||||
function startProgressReadyFile(){
|
||||
var target=((S.file_ready||'')||(S.filename||'')).trim();
|
||||
if(!target)return;
|
||||
S.file_ready=target;
|
||||
startReadyFileWithSlots();
|
||||
}
|
||||
function clearProgressReadyFile(){
|
||||
post('/api/file_ready/clear',{})
|
||||
.then(function(){
|
||||
S.file_ready='';
|
||||
S.filename='';
|
||||
S.thumbnail='';
|
||||
S.slicer_time=0;
|
||||
applyState();
|
||||
poll();
|
||||
})
|
||||
.catch(function(e){
|
||||
clog((T.log_error||'Error')+' '+e,'msg-err');
|
||||
});
|
||||
}
|
||||
function selectMatPreset(m){
|
||||
document.getElementById('slot-edit-mat').value=m;
|
||||
highlightMatBtn(m);
|
||||
@@ -1260,6 +1333,9 @@ function saveSettings(){
|
||||
btn.disabled=true;btn.textContent='…';
|
||||
var webUploadWarning=(document.getElementById('s-web-upload-warning')||{}).checked?1:0;
|
||||
S.web_upload_warning=webUploadWarning;
|
||||
var printStartDialog=(document.getElementById('s-file-ready-mode')||{}).value==='dialog'?1:0;
|
||||
S.print_start_dialog=printStartDialog;
|
||||
S.auto_leveling=(document.getElementById('s-auto-leveling')||{}).checked?1:0;
|
||||
post('/api/settings',{
|
||||
printer_name: document.getElementById('s-printer-name').value,
|
||||
printer_ip: document.getElementById('s-printer-ip').value,
|
||||
@@ -1271,6 +1347,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:printStartDialog,
|
||||
web_upload_warning:webUploadWarning,
|
||||
}).then(function(){
|
||||
btn.textContent=T.update_restarting;
|
||||
@@ -1437,7 +1514,8 @@ function setBed(){
|
||||
function setLight(){
|
||||
var on=document.getElementById('d-light-toggle').checked;
|
||||
post('/api/light',{on:on,brightness:80})
|
||||
.then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')})
|
||||
.then(function(){clog('Licht '+(on?'an, 80%':'aus'),'msg-ok')})
|
||||
|
||||
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
|
||||
}
|
||||
|
||||
@@ -1492,23 +1570,24 @@ function camStart(){
|
||||
img.style.display='none';
|
||||
sp.style.display='block';
|
||||
post('/api/camera/start',{}).then(function(){
|
||||
img.onerror=function(){
|
||||
sp.style.display='none';
|
||||
img.style.display='none';
|
||||
ph.style.display='flex';
|
||||
camOn=false;
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
|
||||
clog(tr('log_error')+' '+tr('cam_stream_unavailable'),'msg-err');
|
||||
};
|
||||
img.src='/api/camera/stream?t='+Date.now();
|
||||
camOn=true;
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_stop');
|
||||
clog(tr('log_cam_start'),'msg-ok');
|
||||
// MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden
|
||||
setTimeout(function(){
|
||||
sp.style.display='none';
|
||||
img.style.display='block';
|
||||
},1200);
|
||||
setTimeout(function(){ sp.style.display='none'; img.style.display='block'; },1200);
|
||||
if(/Android/i.test(navigator.userAgent)){
|
||||
// Android browsers don't support multipart/x-mixed-replace (MJPEG) — poll snapshots instead
|
||||
_camPollInterval=setInterval(function(){ img.src='/api/camera/snapshot?t='+Date.now(); },200);
|
||||
} else {
|
||||
img.onerror=function(){
|
||||
sp.style.display='none';
|
||||
img.style.display='none';
|
||||
ph.style.display='flex';
|
||||
camOn=false;
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
|
||||
clog(tr('log_error')+' '+tr('cam_stream_unavailable'),'msg-err');
|
||||
};
|
||||
img.src='/api/camera/stream?t='+Date.now();
|
||||
}
|
||||
}).catch(function(e){
|
||||
sp.style.display='none';
|
||||
ph.style.display='flex';
|
||||
@@ -1518,11 +1597,14 @@ 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');
|
||||
}
|
||||
@@ -1601,9 +1683,13 @@ function aceDryDialogIsCustomPreset(key){
|
||||
}
|
||||
|
||||
function aceDryDialogSyncCustomButtonNames(){
|
||||
['pla','pla_plus','petg','tpu','abs_asa','pa_pc'].forEach(function(k){
|
||||
var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]');
|
||||
if(b)b.textContent=tr('ace_dry_preset_'+k);
|
||||
});
|
||||
['custom_1','custom_2','custom_3'].forEach(function(k){
|
||||
var b=document.querySelector('.ace-dry-preset-btn[data-preset="'+k+'"]');
|
||||
if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||('Custom '+k.slice(-1));
|
||||
if(b)b.textContent=(ACE_DRY_PRESETS[k]&&ACE_DRY_PRESETS[k].name)||(tr('ace_dry_preset_custom')+' '+k.slice(-1));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1667,7 +1753,7 @@ function aceDryDialogInputsChanged(){
|
||||
var i=document.getElementById('ace-dry-dialog-custom-name');
|
||||
if(b&&i){
|
||||
var t=(i.value||'').trim();
|
||||
b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||('Custom '+_aceDryDialogPresetKey.slice(-1)));
|
||||
b.textContent=t||((ACE_DRY_PRESETS[_aceDryDialogPresetKey]&&ACE_DRY_PRESETS[_aceDryDialogPresetKey].name)||(tr('ace_dry_preset_custom')+' '+_aceDryDialogPresetKey.slice(-1)));
|
||||
}
|
||||
}
|
||||
aceDryDialogUpdateSaveButton();
|
||||
@@ -1913,6 +1999,8 @@ function formatDur(sec){
|
||||
var _storeFileId=null;
|
||||
var _storeFilename=null;
|
||||
var _filamentDialogMode='store'; // 'store' oder 'banner'
|
||||
var _readyStateInitialized=false;
|
||||
var _lastReadyFile='';
|
||||
var _pendingWebVerifyFileId=null;
|
||||
var _pendingWebVerifyFilename='';
|
||||
var _pendingWebVerifyAction=null;
|
||||
@@ -1946,9 +2034,13 @@ function storePrint(fileId, filename){
|
||||
}
|
||||
|
||||
function openStorePrintDialog(fileId, filename, fileObj){
|
||||
openStoreLikePrintDialog(fileId, filename, fileObj, 'store');
|
||||
}
|
||||
|
||||
function openStoreLikePrintDialog(fileId, filename, fileObj, dialogMode){
|
||||
_storeFileId=fileId;
|
||||
_storeFilename=filename;
|
||||
_filamentDialogMode='store';
|
||||
_filamentDialogMode=dialogMode||'store';
|
||||
maybeGateWebUpload(fileObj, function(){
|
||||
// GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog)
|
||||
_setGcodeFilamentsFromFileObj(fileObj);
|
||||
@@ -2055,37 +2147,36 @@ function startReadyFileWithSlots(){
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(); });
|
||||
return;
|
||||
}
|
||||
_filamentDialogMode='banner';
|
||||
_storeFilename=S.file_ready||'';
|
||||
// Banner must never reuse stale store-file context.
|
||||
_storeFileId=null;
|
||||
_gcodeFilaments=[];
|
||||
|
||||
function openWithSlots(){
|
||||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
|
||||
openFilamentDialog(d.result||[]);
|
||||
}).catch(function(){openFilamentDialog([]);});
|
||||
}
|
||||
|
||||
var fileObj=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
|
||||
var targetFilename=S.file_ready||'';
|
||||
var fileObj=(storeFiles||[]).find(function(f){return f.filename===targetFilename;});
|
||||
if(fileObj){
|
||||
_storeFileId=fileObj.id;
|
||||
_setGcodeFilamentsFromFileObj(fileObj);
|
||||
openWithSlots();
|
||||
openStoreLikePrintDialog(fileObj.id, targetFilename, fileObj, 'store');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: refresh file list, then resolve current file by filename.
|
||||
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
|
||||
storeFiles=d.result||[];
|
||||
var refreshed=(storeFiles||[]).find(function(f){return f.filename===_storeFilename;});
|
||||
var refreshed=(storeFiles||[]).find(function(f){return f.filename===targetFilename;});
|
||||
if(refreshed){
|
||||
_storeFileId=refreshed.id;
|
||||
_setGcodeFilamentsFromFileObj(refreshed);
|
||||
openStoreLikePrintDialog(refreshed.id, targetFilename, refreshed, 'store');
|
||||
return;
|
||||
}
|
||||
openWithSlots();
|
||||
_filamentDialogMode='banner';
|
||||
_storeFilename=targetFilename;
|
||||
_storeFileId=null;
|
||||
_gcodeFilaments=[];
|
||||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(data){
|
||||
openFilamentDialog(data.result||[]);
|
||||
}).catch(function(){openFilamentDialog([]);});
|
||||
}).catch(function(){
|
||||
openWithSlots();
|
||||
_filamentDialogMode='banner';
|
||||
_storeFilename=targetFilename;
|
||||
_storeFileId=null;
|
||||
_gcodeFilaments=[];
|
||||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(data){
|
||||
openFilamentDialog(data.result||[]);
|
||||
}).catch(function(){openFilamentDialog([]);});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2152,6 +2243,8 @@ function openFilamentDialog(slots){
|
||||
}
|
||||
}).catch(function(){});
|
||||
}
|
||||
_printOptionsAutoLeveling=(S.auto_leveling===undefined?1:(S.auto_leveling?1:0));
|
||||
syncPrintOptionsDialog();
|
||||
|
||||
// GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten
|
||||
var channels=_gcodeFilaments.length?_gcodeFilaments:_amsSlots.map(function(s,i){
|
||||
@@ -2243,10 +2336,36 @@ function closeFilamentDialog(){
|
||||
if(dlg)dlg.classList.remove('open');
|
||||
}
|
||||
|
||||
function syncPrintOptionsDialog(){
|
||||
var autoLeveling=document.getElementById('fd-auto-leveling');
|
||||
if(autoLeveling)autoLeveling.checked=!!_printOptionsAutoLeveling;
|
||||
}
|
||||
|
||||
function openPrintOptionsDialog(){
|
||||
_printOptionsAutoLevelingSnapshot=_printOptionsAutoLeveling;
|
||||
syncPrintOptionsDialog();
|
||||
var autoLeveling=document.getElementById('fd-auto-leveling');
|
||||
if(autoLeveling)autoLeveling.focus();
|
||||
}
|
||||
|
||||
function closePrintOptionsDialog(restore){
|
||||
if(restore){
|
||||
_printOptionsAutoLeveling=_printOptionsAutoLevelingSnapshot;
|
||||
syncPrintOptionsDialog();
|
||||
}
|
||||
}
|
||||
|
||||
function confirmPrintOptionsDialog(){
|
||||
_printOptionsAutoLeveling=(document.getElementById('fd-auto-leveling')||{}).checked?1:0;
|
||||
_printOptionsAutoLevelingSnapshot=_printOptionsAutoLeveling;
|
||||
closePrintOptionsDialog(false);
|
||||
}
|
||||
|
||||
function confirmFilamentPrint(){
|
||||
var selects=document.querySelectorAll('#fd-slots select');
|
||||
var assignments=[];
|
||||
var missingCompatible=0;
|
||||
var autoLeveling=_printOptionsAutoLeveling;
|
||||
selects.forEach(function(sel){
|
||||
var paintIdx=parseInt(sel.dataset.paint);
|
||||
var paintColor=sel.dataset.paintColor;
|
||||
@@ -2285,7 +2404,7 @@ function confirmFilamentPrint(){
|
||||
// Banner-Modus: normaler print/start mit Slot-Override
|
||||
var btn=document.getElementById('file-ready-btn');
|
||||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||||
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects})
|
||||
post('/printer/print/start',{filename:S.file_ready,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:autoLeveling})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(){
|
||||
document.getElementById('file-ready-banner').style.display='none';
|
||||
@@ -2300,7 +2419,7 @@ function confirmFilamentPrint(){
|
||||
fetch(_apiUrl('/kx/print'),{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects})
|
||||
body:JSON.stringify({file_id:_storeFileId,filament_assignments:assignments,excluded_objects:excludedObjects,auto_leveling:autoLeveling})
|
||||
}).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');}
|
||||
|
||||
@@ -38,10 +38,11 @@
|
||||
<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>
|
||||
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title="">⚙</button>
|
||||
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
||||
</header>
|
||||
|
||||
@@ -88,6 +89,13 @@
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-print">Druckeinstellungen</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-file-ready-mode"></label>
|
||||
<select id="s-file-ready-mode">
|
||||
<option value="dialog" id="opt-file-ready-dialog"></option>
|
||||
<option value="banner" id="opt-file-ready-banner"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
|
||||
<select id="s-default-slot">
|
||||
@@ -266,9 +274,15 @@
|
||||
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
|
||||
<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
|
||||
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
|
||||
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0">
|
||||
<div class="time-label" id="d-lbl-layers"></div>
|
||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
|
||||
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center">
|
||||
<div class="time-label" id="d-lbl-layers"></div>
|
||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||
</div>
|
||||
<div class="time-block" style="padding:4px 10px;min-width:72px;text-align:center">
|
||||
<div class="time-label" id="d-lbl-zpos">Z</div>
|
||||
<div class="time-val" style="font-size:13px" id="d-zpos">–</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-grid">
|
||||
@@ -286,6 +300,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="fname" id="d-fname" title="" style="margin-top:6px">–</div>
|
||||
<div class="ctrl-btns" id="d-ready-btns" style="margin-top:12px;display:none">
|
||||
<button class="btn btn-accent btn-sm" id="d-btn-ready-print" onclick="startProgressReadyFile()">▶ Print</button>
|
||||
<button class="btn btn-cancel btn-sm" id="d-btn-ready-clear" onclick="clearProgressReadyFile()">✕ Clear</button>
|
||||
</div>
|
||||
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
|
||||
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
|
||||
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none">✂ <span id="d-btn-skip-label">Objekte</span></button>
|
||||
@@ -483,32 +501,32 @@
|
||||
<div class="card">
|
||||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span><span>≡</span> <span id="ptitle-console">Ereignis-Log</span></span>
|
||||
<a id="btn-log-dl" href="/api/log/download" download
|
||||
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none">⬇ Download</a>
|
||||
<a id="btn-log-dl" href="/api/log/download" download
|
||||
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none"></a>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;align-items:center">
|
||||
<input id="log-filter" type="text" placeholder="Filter…"
|
||||
<input id="log-filter" type="text" placeholder=""
|
||||
oninput="renderLog()"
|
||||
style="flex:1;min-width:120px;padding:5px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;font-family:var(--mono)">
|
||||
<button id="btn-autoscroll" onclick="toggleAutoScroll()"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap">⬇ Auto</button>
|
||||
<button onclick="consoleLogs=[];renderLog()"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">✕ Clear</button>
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap"></button>
|
||||
<button id="btn-log-clear" onclick="consoleLogs=[];renderLog()"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
</div>
|
||||
<div style="display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap">
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px">Dir:</span>
|
||||
<span id="log-lbl-dir" style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px"></span>
|
||||
<button class="log-dir-btn active" id="logdir-all" onclick="setLogDir('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">RX</button>
|
||||
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">TX</button>
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level">Level:</span>
|
||||
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level"></span>
|
||||
<button class="log-lvl-btn active" id="loglvl-all" onclick="setLogLevel('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⛔ Errors</button>
|
||||
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⚠ Warn</button>
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px">Topic:</span>
|
||||
<button class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">AMS</button>
|
||||
<button class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">print</button>
|
||||
<button class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">info</button>
|
||||
<button class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">status</button>
|
||||
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<span id="log-lbl-topic" style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px"></span>
|
||||
<button id="log-topic-ams" class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button id="log-topic-print" class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button id="log-topic-info" class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button id="log-topic-status" class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
</div>
|
||||
<div class="console" id="console-log" style="height:calc(100vh - 260px);min-height:160px" onscroll="onLogScroll()"></div>
|
||||
</div>
|
||||
@@ -527,7 +545,7 @@
|
||||
<!-- Web-Upload-Verify-Dialog -->
|
||||
<div class="modal-overlay" id="store-web-verify-dialog" onclick="if(event.target===this)closeStoreWebVerifyDialog()">
|
||||
<div class="modal-box" style="max-width:420px;width:100%">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<div class="modal-header" style="margin-bottom:12px">
|
||||
<span class="modal-title" id="store-web-verify-title">Datei verifizieren</span>
|
||||
<button onclick="closeStoreWebVerifyDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
@@ -542,15 +560,22 @@
|
||||
|
||||
<!-- Filament-Slot-Dialog -->
|
||||
<div class="modal-overlay" id="filament-dialog" onclick="if(event.target===this)closeFilamentDialog()">
|
||||
<div class="modal-box" style="max-width:380px;width:100%">
|
||||
<div class="modal-box" style="max-width:380px;width:100%;gap:0">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<span class="modal-title" id="fd-title" style="font-size:14px;word-break:break-all"></span>
|
||||
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
|
||||
<div id="fd-options-section" style="margin-bottom:6px">
|
||||
<p id="fd-options-title" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0"></p>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px;padding:8px 10px;background:var(--raised);border:1px solid var(--border);border-radius:8px">
|
||||
<input type="checkbox" id="fd-auto-leveling" style="width:auto;margin:0" onchange="_printOptionsAutoLeveling=this.checked?1:0">
|
||||
<label id="lbl-fd-auto-leveling" style="margin:0;cursor:pointer;line-height:1.2" for="fd-auto-leveling"></label>
|
||||
</div>
|
||||
</div>
|
||||
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0">GCode-Kanal → AMS-Slot zuweisen:</p>
|
||||
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
|
||||
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
|
||||
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin-bottom:8px">Objekte überspringen (optional):</p>
|
||||
<div id="fd-objects-section" style="display:none;margin-bottom:14px">
|
||||
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0">Objekte überspringen (optional):</p>
|
||||
<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>
|
||||
@@ -590,7 +615,7 @@
|
||||
<div id="skip-list" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;margin-bottom:12px"></div>
|
||||
<div id="skip-status" style="font-size:12px;color:var(--txt2);min-height:16px;margin-bottom:8px"></div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="skip-cancel" onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer"></button>
|
||||
<button id="skip-confirm" onclick="confirmSkip()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Überspringen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -626,15 +651,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:8px">
|
||||
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA+</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PETG</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">TPU</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">ABS / ASA</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PA / PC</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 1</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 2</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 3</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
</div>
|
||||
<div id="ace-dry-dialog-custom-name-row" style="display:none;align-items:center;gap:12px;margin-bottom:14px">
|
||||
<label id="ace-dry-dialog-custom-name-label" style="min-width:190px;font-size:12px;color:var(--txt)">Custom Name</label>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"lbl_remaining": "Restzeit:",
|
||||
"lbl_slicer_time": "Slicer-Schätzung:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Leise",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -68,6 +69,13 @@
|
||||
"ace_dry_dialog_save_restart": "Speichern & Neustart",
|
||||
"ace_dry_dialog_custom_name": "Eigener Name",
|
||||
"ace_dry_dialog_reset_default": "Auf Standard zurücksetzen",
|
||||
"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": "📷 Kamera nicht gestartet",
|
||||
"cam_stream_unavailable": "Stream nicht verfügbar",
|
||||
"btn_cam_start": "▶ Kamera",
|
||||
@@ -140,7 +148,12 @@
|
||||
"settings_default_slot": "Standard-Slot (Einfarbdruck)",
|
||||
"settings_slot_auto": "Auto (alle belegten Slots)",
|
||||
"settings_auto_leveling": "Auto-Leveling vor Druck",
|
||||
"fd_options_title": "Optionen",
|
||||
"print_auto_leveling": "Auto-Leveling für diesen Druck",
|
||||
"settings_camera_on_print": "Kamera bei Druckstart einschalten",
|
||||
"settings_file_ready_mode": "Druckdialog starten",
|
||||
"settings_file_ready_banner": "Druckleiste",
|
||||
"settings_file_ready_dialog": "Druckdialog",
|
||||
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
|
||||
"update_check": "Auf Updates prüfen",
|
||||
"update_checking": "Prüfe...",
|
||||
@@ -178,7 +191,21 @@
|
||||
"orca_profile_done": "Importiert",
|
||||
"orca_profile_skipped": "übersprungen",
|
||||
"log_dir_all": "Alle",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Richtung:",
|
||||
"log_lvl_label": "Level:",
|
||||
"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…",
|
||||
"file_ready_btn": "▶ Druck starten",
|
||||
"file_slots_btn": "🎨 Slots wählen",
|
||||
"file_cancel_btn": "✕ Abbrechen",
|
||||
@@ -191,6 +218,8 @@
|
||||
"skip_select_at_least_one": "Bitte mindestens ein Objekt wählen.",
|
||||
"skip_sending": "Sende …",
|
||||
"skip_success": "Objekte werden übersprungen.",
|
||||
"skip_cancel": "Abbrechen",
|
||||
"skip_confirm": "Überspringen",
|
||||
"fd_objects_hint": "Objekte überspringen (optional):",
|
||||
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
|
||||
"fd_cancel": "Abbrechen",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"lbl_remaining": "Remaining:",
|
||||
"lbl_slicer_time": "Slicer estimate:",
|
||||
"lbl_layers": "Layer",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silent",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -68,6 +69,13 @@
|
||||
"ace_dry_dialog_save_restart": "Save & Restart",
|
||||
"ace_dry_dialog_custom_name": "Custom Name",
|
||||
"ace_dry_dialog_reset_default": "Reset to Default",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Custom",
|
||||
"cam_placeholder": "📷 Camera not started",
|
||||
"cam_stream_unavailable": "Stream unavailable",
|
||||
"btn_cam_start": "▶ Camera",
|
||||
@@ -139,8 +147,13 @@
|
||||
"hint_ip_no_port": "IP address only, no port (e.g. 192.168.1.102)",
|
||||
"settings_default_slot": "Default Slot (single color)",
|
||||
"settings_slot_auto": "Auto (all loaded slots)",
|
||||
"settings_auto_leveling": "Auto-Leveling before print",
|
||||
"settings_auto_leveling": "Auto-Leveling Default",
|
||||
"fd_options_title": "Print Options",
|
||||
"print_auto_leveling": "Auto-Leveling",
|
||||
"settings_camera_on_print": "Turn camera on at print start",
|
||||
"settings_file_ready_mode": "Start Print Behavior",
|
||||
"settings_file_ready_banner": "Print Bar",
|
||||
"settings_file_ready_dialog": "Print Dialog",
|
||||
"settings_web_upload_warning": "Show warning when printing web uploads",
|
||||
"update_check": "Check for Updates",
|
||||
"update_checking": "Checking...",
|
||||
@@ -178,7 +191,21 @@
|
||||
"orca_profile_done": "Imported",
|
||||
"orca_profile_skipped": "skipped",
|
||||
"log_dir_all": "All",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dir:",
|
||||
"log_lvl_label": "Level:",
|
||||
"log_lvl_err": "⛔ Errors",
|
||||
"log_lvl_warn": "⚠ Warn",
|
||||
"log_topic_label": "Topic:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Print",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Status",
|
||||
"log_download": "⬇ Download",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Clear",
|
||||
"log_filter_placeholder": "Filter…",
|
||||
"file_ready_btn": "▶ Start Print",
|
||||
"file_slots_btn": "🎨 Select Slots",
|
||||
"file_cancel_btn": "✕ Cancel",
|
||||
@@ -191,6 +218,8 @@
|
||||
"skip_select_at_least_one": "Please pick at least one object.",
|
||||
"skip_sending": "Sending …",
|
||||
"skip_success": "Objects will be skipped.",
|
||||
"skip_cancel": "Cancel",
|
||||
"skip_confirm": "Skip",
|
||||
"fd_objects_hint": "Skip objects (optional):",
|
||||
"fd_slots_hint": "Assign GCode channel to AMS slot:",
|
||||
"fd_cancel": "Cancel",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"lbl_remaining": "Restante:",
|
||||
"lbl_slicer_time": "Estimación del slicer:",
|
||||
"lbl_layers": "Capa",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silencioso",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
@@ -68,6 +69,13 @@
|
||||
"ace_dry_dialog_save_restart": "Guardar y reiniciar",
|
||||
"ace_dry_dialog_custom_name": "Nombre personalizado",
|
||||
"ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
|
||||
"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",
|
||||
"cam_placeholder": "📷 Cámara no iniciada",
|
||||
"cam_stream_unavailable": "Stream no disponible",
|
||||
"btn_cam_start": "▶ Cámara",
|
||||
@@ -140,7 +148,12 @@
|
||||
"settings_default_slot": "Ranura predeterminada (un color)",
|
||||
"settings_slot_auto": "Auto (todos los slots cargados)",
|
||||
"settings_auto_leveling": "Autonivelado antes de imprimir",
|
||||
"fd_options_title": "Opciones",
|
||||
"print_auto_leveling": "Autonivelado para esta impresión",
|
||||
"settings_camera_on_print": "Encender cámara al iniciar 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",
|
||||
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
|
||||
"update_check": "Buscar actualizaciones",
|
||||
"update_checking": "Comprobando...",
|
||||
@@ -178,7 +191,21 @@
|
||||
"orca_profile_done": "Importado",
|
||||
"orca_profile_skipped": "omitido",
|
||||
"log_dir_all": "Todos",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dirección:",
|
||||
"log_lvl_label": "Nivel:",
|
||||
"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…",
|
||||
"file_ready_btn": "▶ Iniciar impresión",
|
||||
"file_slots_btn": "🎨 Seleccionar ranuras",
|
||||
"file_cancel_btn": "✕ Cancelar",
|
||||
@@ -191,6 +218,8 @@
|
||||
"skip_select_at_least_one": "Selecciona al menos un objeto.",
|
||||
"skip_sending": "Enviando …",
|
||||
"skip_success": "Se omitirán los objetos.",
|
||||
"skip_cancel": "Cancelar",
|
||||
"skip_confirm": "Omitir",
|
||||
"fd_objects_hint": "Omitir objetos (opcional):",
|
||||
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
|
||||
"fd_cancel": "Cancelar",
|
||||
|
||||
277
web/translations/fr.json
Normal file
277
web/translations/fr.json
Normal file
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"header_status_standby": "Prêt",
|
||||
"header_status_printing": "Impression",
|
||||
"header_status_complete": "Terminé",
|
||||
"header_status_error": "Erreur",
|
||||
"kobra_free": "Disponible",
|
||||
"kobra_busy": "Occupé",
|
||||
"kobra_printing": "Impression",
|
||||
"kobra_preheating": "Préchauffage",
|
||||
"kobra_auto_leveling": "Mise à niveau auto",
|
||||
"kobra_checking": "Vérification",
|
||||
"kobra_updated": "Mise à jour",
|
||||
"kobra_init": "Initialisation",
|
||||
"kobra_pausing": "Pause en cours…",
|
||||
"kobra_paused": "En pause",
|
||||
"kobra_resuming": "Reprise en cours…",
|
||||
"kobra_resumed": "Repris",
|
||||
"kobra_stopping": "Arrêt en cours…",
|
||||
"kobra_stoped": "Arrêté",
|
||||
"kobra_finished": "Terminé",
|
||||
"kobra_failed": "Erreur",
|
||||
"kobra_canceled": "Annulé",
|
||||
"kobra_offline": "Hors ligne",
|
||||
"nav_dashboard": "Tableau de bord",
|
||||
"nav_print": "Impression",
|
||||
"nav_temps": "Températures",
|
||||
"nav_motion": "Mouvement",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Lumière / Ventilateur",
|
||||
"nav_console": "Console",
|
||||
"card_progress": "Progression",
|
||||
"card_temps": "Températures",
|
||||
"card_light_fan": "Ventilateur",
|
||||
"card_speed": "Vitesse d'impression",
|
||||
"card_cam": "Caméra",
|
||||
"lbl_elapsed": "Écoulé :",
|
||||
"lbl_remaining": "Restant :",
|
||||
"lbl_slicer_time": "Estimation slicer :",
|
||||
"lbl_layers": "Couche",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 Silencieux",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Lumière",
|
||||
"lbl_feed": "Charger",
|
||||
"lbl_unload": "Décharger",
|
||||
"card_ace_dry": "Séchage ACE",
|
||||
"ace_dry_dryer": "Séchoir",
|
||||
"ace_dry_status_off": "Statut : Arrêté",
|
||||
"ace_dry_status_on": "Statut : Actif",
|
||||
"ace_dry_status_remaining": "Restant",
|
||||
"ace_dry_humidity": "Humidité",
|
||||
"ace_dry_current_temp": "Température",
|
||||
"ace_dry_chart": "Historique (Temp/Humidité)",
|
||||
"ace_dry_temp": "Température (°C)",
|
||||
"ace_dry_duration": "Durée (min)",
|
||||
"ace_dry_start": "▶ Démarrer",
|
||||
"ace_dry_stop": "■ Arrêter",
|
||||
"ace_dry_auto_refill": "Remplissage auto",
|
||||
"ace_dry_enable": "Activer le séchage",
|
||||
"ace_dry_temp_line": "Température de séchage",
|
||||
"ace_dry_time_line": "Durée de séchage",
|
||||
"ace_dry_ui_pending": "(Interface seule, backend suivant)",
|
||||
"ace_dry_dialog_title": "Réglages Temp/Durée du séchoir",
|
||||
"ace_dry_dialog_temp": "Température (30-80°C)",
|
||||
"ace_dry_dialog_time": "Temps restant (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Confirmer",
|
||||
"ace_dry_dialog_cancel": "Annuler",
|
||||
"ace_dry_dialog_save_restart": "Enregistrer et redémarrer",
|
||||
"ace_dry_dialog_custom_name": "Nom personnalisé",
|
||||
"ace_dry_dialog_reset_default": "Réinitialiser",
|
||||
"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é",
|
||||
"cam_placeholder": "📷 Caméra non démarrée",
|
||||
"cam_stream_unavailable": "Flux indisponible",
|
||||
"btn_cam_start": "▶ Caméra",
|
||||
"btn_cam_stop": "◼ Caméra",
|
||||
"btn_pause": "⏸ Pause",
|
||||
"btn_resume": "▶ Reprendre",
|
||||
"btn_cancel": "✕ Arrêter",
|
||||
"label_nozzle": "Buse",
|
||||
"label_bed": "Plateau",
|
||||
"label_fan": "🌀 Ventilateur",
|
||||
"label_light": "💡 Lumière",
|
||||
"label_on_off": "On / Off",
|
||||
"label_speed": "Vitesse",
|
||||
"panel_print_title": "Contrôle impression",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Reprendre",
|
||||
"panel_print_btn_cancel": "✕ Annuler",
|
||||
"panel_print_temps_live": "Températures (en direct)",
|
||||
"label_set": "Définir",
|
||||
"label_off": "Éteint",
|
||||
"panel_temps_nozzle": "Buse",
|
||||
"panel_temps_bed": "Plateau chauffant",
|
||||
"panel_temps_chart": "Historique (60 dernières valeurs)",
|
||||
"label_target_c": "Cible :",
|
||||
"panel_motion_xy": "Axes XY",
|
||||
"panel_motion_z": "Axe Z",
|
||||
"label_step": "Pas :",
|
||||
"btn_home_z": "Origine Z",
|
||||
"btn_home_xy": "Origine XY",
|
||||
"btn_home_all": "Origine Tout",
|
||||
"btn_disable_motors": "Moteurs Off",
|
||||
"panel_ams_title": "Filament",
|
||||
"card_ams": "Filament",
|
||||
"ams_no_data": "Aucune donnée AMS reçue",
|
||||
"label_slot": "Slot",
|
||||
"ams_empty": "Vide",
|
||||
"panel_extras_light": "Lumière",
|
||||
"panel_extras_fan": "Ventilateur",
|
||||
"panel_extras_camera": "Caméra",
|
||||
"btn_cam_start2": "▶ Démarrer",
|
||||
"btn_cam_stop2": "◼ Arrêter",
|
||||
"panel_console_title": "Journal d'événements",
|
||||
"log_light_on": "Lumière allumée",
|
||||
"log_light_off": "Lumière éteinte",
|
||||
"log_fan": "Ventilateur →",
|
||||
"log_nozzle": "Buse →",
|
||||
"log_bed": "Plateau →",
|
||||
"log_axis": "Axe",
|
||||
"log_home": "Origine",
|
||||
"log_home_all": "Origine Tout",
|
||||
"log_cam_start": "Caméra démarrée :",
|
||||
"log_cam_stop": "Caméra arrêtée",
|
||||
"log_poll_error": "Erreur de sondage :",
|
||||
"log_error": "Erreur :",
|
||||
"confirm_cancel": "Vraiment annuler l'impression ?",
|
||||
"settings_title": "Paramètres",
|
||||
"settings_connection": "Connexion",
|
||||
"settings_print": "Paramètres d'impression",
|
||||
"settings_poll": "Intervalle de sondage",
|
||||
"settings_version": "Version",
|
||||
"settings_save": "Enregistrer et redémarrer",
|
||||
"settings_printer_name": "Nom de l'imprimante",
|
||||
"settings_printer_ip": "IP de l'imprimante",
|
||||
"settings_mqtt_port": "Port MQTT",
|
||||
"settings_username": "Nom d'utilisateur MQTT",
|
||||
"settings_password": "Mot de passe MQTT",
|
||||
"settings_device_id": "ID de l'appareil",
|
||||
"settings_mode_id": "ID du mode",
|
||||
"hint_ip_no_port": "Adresse IP uniquement, sans port (ex. 192.168.1.102)",
|
||||
"settings_default_slot": "Slot par défaut (couleur unique)",
|
||||
"settings_slot_auto": "Auto (tous les slots chargés)",
|
||||
"settings_auto_leveling": "Mise à niveau auto avant impression",
|
||||
"fd_options_title": "Options",
|
||||
"print_auto_leveling": "Mise à niveau auto pour cette impression",
|
||||
"settings_camera_on_print": "Activer la caméra au démarrage de l'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",
|
||||
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
|
||||
"update_check": "Vérifier les mises à jour",
|
||||
"update_checking": "Vérification…",
|
||||
"update_available": "disponible",
|
||||
"update_none": "Déjà à jour",
|
||||
"update_apply": "Installer maintenant",
|
||||
"update_applying": "Téléchargement…",
|
||||
"update_restarting": "Redémarrage…",
|
||||
"update_error": "Erreur",
|
||||
"btn_connect": "⚡ Connecter",
|
||||
"btn_disconnect": "✕ Déconnecter",
|
||||
"lbl_conn_error": "Erreur de connexion :",
|
||||
"slot_edit_title": "Modifier le slot",
|
||||
"slot_edit_color": "Couleur",
|
||||
"slot_edit_material": "Matériau",
|
||||
"slot_edit_load": "⬇ Charger",
|
||||
"slot_edit_unload": "⬆ Décharger",
|
||||
"slot_edit_save": "💾 Enregistrer",
|
||||
"slot_edit_custom": "ex. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "Slot AMS",
|
||||
"slot_edit_profile": "Profil OrcaSlicer",
|
||||
"slot_edit_profile_hint": "Envoyé lors de la synchronisation OrcaSlicer comme marque spécifique au lieu de \"Générique\"",
|
||||
"slot_edit_profile_default": "— Générique (défaut) —",
|
||||
"orca_profile_section": "Profils OrcaSlicer",
|
||||
"orca_profile_hint": "Importez vos propres profils de filament OrcaSlicer (ouvrez le dossier utilisateur via Aide → Afficher le dossier de configuration)",
|
||||
"orca_profile_import_btn": "Importer des profils",
|
||||
"orca_profile_import_link": "★ Importer mes profils…",
|
||||
"orca_profile_import_title": "Importer vos profils OrcaSlicer",
|
||||
"orca_profile_help_html": "Déposez un <b>ZIP</b> de votre dossier filament OrcaSlicer ou des fichiers <b>.json</b> individuels.<br>Dans OrcaSlicer : <i>Aide → Afficher le dossier de configuration → user/<id>/filament/</i>",
|
||||
"orca_profile_dropmsg": "Déposez ici ou cliquez",
|
||||
"orca_profile_list_label": "Profils importés",
|
||||
"orca_profile_user_label": "Mes profils",
|
||||
"orca_profile_user_empty": "– aucun –",
|
||||
"orca_profile_uploading": "Envoi en cours…",
|
||||
"orca_profile_done": "Importé",
|
||||
"orca_profile_skipped": "ignoré",
|
||||
"log_dir_all": "Tout",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Sens :",
|
||||
"log_lvl_label": "Niveau :",
|
||||
"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…",
|
||||
"file_ready_btn": "▶ Lancer l'impression",
|
||||
"file_slots_btn": "🎨 Choisir les slots",
|
||||
"file_cancel_btn": "✕ Annuler",
|
||||
"nav_printers": "Imprimantes",
|
||||
"skip_title": "✂ Ignorer des objets",
|
||||
"skip_hint": "Décochez les objets que vous ne souhaitez plus imprimer :",
|
||||
"skip_btn_label": "Objets",
|
||||
"skip_no_objects": "Aucun objet dans cette impression.",
|
||||
"skip_already": "ignoré",
|
||||
"skip_select_at_least_one": "Veuillez sélectionner au moins un objet.",
|
||||
"skip_sending": "Envoi …",
|
||||
"skip_success": "Les objets seront ignorés.",
|
||||
"skip_cancel": "Annuler",
|
||||
"skip_confirm": "Ignorer",
|
||||
"fd_objects_hint": "Ignorer des objets (optionnel) :",
|
||||
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
|
||||
"fd_cancel": "Annuler",
|
||||
"fd_print": "▶ Imprimer",
|
||||
"fd_no_slots_msg": "Aucun slot AMS chargé.{br}Lancer l'impression quand même ?",
|
||||
"fd_slot": "Slot",
|
||||
"fd_no_matching_material": "Aucun matériau correspondant",
|
||||
"fd_used": "UTILISÉ",
|
||||
"add_printer": "Ajouter une imprimante",
|
||||
"apd_lbl_ip": "IP de l'imprimante",
|
||||
"apd_lbl_name": "Nom (optionnel)",
|
||||
"apd_placeholder_name": "ex. Kobra X Salon",
|
||||
"apd_cancel": "Annuler",
|
||||
"apd_confirm": "Ajouter",
|
||||
"apd_fetching": "Récupération des données de l'imprimante…",
|
||||
"apd_success": "Imprimante ajoutée, redémarrage du bridge…",
|
||||
"apd_err_ip": "Veuillez saisir une adresse IP",
|
||||
"printers_remove": "Supprimer l'imprimante",
|
||||
"printers_remove_confirm": "Supprimer l'imprimante \"{name}\" ? Le bridge va redémarrer.",
|
||||
"printers_active": "● actif",
|
||||
"printers_switch": "Changer →",
|
||||
"printers_current": "Imprimante actuelle",
|
||||
"printers_loading": "Chargement…",
|
||||
"printers_none": "Aucune imprimante configurée.",
|
||||
"printers_empty_hint": "Aucune imprimante configurée.",
|
||||
"nav_browser": "Navigateur",
|
||||
"panel_browser_title": "Explorateur de fichiers",
|
||||
"store_search_placeholder": "🔍 Rechercher…",
|
||||
"store_empty": "Aucun fichier uploadé.",
|
||||
"store_refresh": "↻ Actualiser",
|
||||
"store_print": "▶ Imprimer",
|
||||
"store_download": "⬇ Télécharger",
|
||||
"store_delete_confirm": "Supprimer le fichier ?",
|
||||
"store_print_confirm": "Imprimer le fichier ?",
|
||||
"store_web_verify_title": "Vérifier le fichier",
|
||||
"store_web_verify_msg": "Veuillez vérifier que ce fichier a été créé pour l'Anycubic Kobra X.",
|
||||
"store_web_verify_confirm": "Confirmer",
|
||||
"store_web_verify_abort": "Annuler",
|
||||
"store_no_results": "Aucun fichier trouvé.",
|
||||
"store_never": "jamais imprimé",
|
||||
"store_estimate": "Estimation",
|
||||
"store_upload_label_prefix": "Déposez un GCode ici ou ",
|
||||
"store_upload_label_browse": "parcourir",
|
||||
"store_upload_busy": "⏳ Envoi en cours…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"sf_all": "Tout",
|
||||
"sf_ok": "✓ Terminés",
|
||||
"sf_err": "✗ Échoués",
|
||||
"sf_new": "Nouveau",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_name": "A–Z Nom",
|
||||
"ss_dur": "⏱ Durée d'impression"
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"lbl_remaining": "剩余时间:",
|
||||
"lbl_slicer_time": "切片预估:",
|
||||
"lbl_layers": "层",
|
||||
"lbl_zpos": "Z (mm)",
|
||||
"speed_silent": "🐢 静音",
|
||||
"speed_normal": "⚡ 标准",
|
||||
"speed_sport": "🚀 运动",
|
||||
@@ -68,6 +69,13 @@
|
||||
"ace_dry_dialog_save_restart": "保存并重启",
|
||||
"ace_dry_dialog_custom_name": "自定义名称",
|
||||
"ace_dry_dialog_reset_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": "自定义",
|
||||
"cam_placeholder": "📷 相机未启动",
|
||||
"cam_stream_unavailable": "视频流不可用",
|
||||
"btn_cam_start": "▶ 相机",
|
||||
@@ -140,7 +148,12 @@
|
||||
"settings_default_slot": "默认槽位 (单色)",
|
||||
"settings_slot_auto": "自动 (所有已装载槽位)",
|
||||
"settings_auto_leveling": "打印前自动调平",
|
||||
"fd_options_title": "选项",
|
||||
"print_auto_leveling": "本次打印自动调平",
|
||||
"settings_camera_on_print": "打印开始时开启相机",
|
||||
"settings_file_ready_mode": "开始打印对话框",
|
||||
"settings_file_ready_banner": "打印栏",
|
||||
"settings_file_ready_dialog": "打印对话框",
|
||||
"settings_web_upload_warning": "打印网页上传文件时显示警告",
|
||||
"update_check": "检查更新",
|
||||
"update_checking": "检查中...",
|
||||
@@ -178,7 +191,21 @@
|
||||
"orca_profile_done": "已导入",
|
||||
"orca_profile_skipped": "跳过",
|
||||
"log_dir_all": "全部",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "方向:",
|
||||
"log_lvl_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": "筛选…",
|
||||
"file_ready_btn": "▶ 开始打印",
|
||||
"file_slots_btn": "🎨 选择槽位",
|
||||
"file_cancel_btn": "✕ 取消",
|
||||
@@ -191,6 +218,8 @@
|
||||
"skip_select_at_least_one": "请至少选择一个对象。",
|
||||
"skip_sending": "发送中 …",
|
||||
"skip_success": "对象将被跳过。",
|
||||
"skip_cancel": "取消",
|
||||
"skip_confirm": "跳过",
|
||||
"fd_objects_hint": "跳过对象 (可选):",
|
||||
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
|
||||
"fd_cancel": "取消",
|
||||
|
||||
Reference in New Issue
Block a user