Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 636889bdbc | |||
| 3f6ea269e6 | |||
| 3fff6e25f0 |
@@ -1,5 +1,30 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.9.20] – 2026-06-08
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- **Französische Sprachunterstützung (PR #45 von @Nathacks)**
|
||||||
|
- **Z-Höhe in der Print-UI (PR #49 von @Nathacks).** Zeigt die aktuelle
|
||||||
|
Z-Position in mm unterhalb des Layer-Zählers.
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- **Kamera-Autostart ignorierte das "Kamera bei Druckstart einschalten"-
|
||||||
|
Setting nach einem Bridge-Restart (Issue #50).** Das Setting wurde in
|
||||||
|
der Prozessumgebung gecacht — nach dem Speichern in der UI überlebte
|
||||||
|
der alte Wert den Restart und der neue Wert aus `config.ini` wurde
|
||||||
|
nicht gelesen.
|
||||||
|
- **Kamera startete nach manuellem Stopp während eines Drucks automatisch
|
||||||
|
neu (Issue #50).** Ein neues `_camera_user_stopped`-Flag unterdrückt
|
||||||
|
den Autostart für die aktuelle Drucksitzung. Es wird beim Druckende
|
||||||
|
zurückgesetzt.
|
||||||
|
- **Falscher "Stream nicht verfügbar"-Fehler-Toast beim manuellen
|
||||||
|
Kamera-Stopp.** Der Bild-Fehler-Handler war noch registriert als
|
||||||
|
`img.src` geleert wurde.
|
||||||
|
- **JS-Fehler (`ReferenceError: br is not defined`) beim Licht-Toggle.**
|
||||||
|
Variable wurde aus dem falschen Scope referenziert.
|
||||||
|
- Webcam-URLs sind jetzt absolut, damit Mobileraker/Obico-Clients sie
|
||||||
|
erreichen können.
|
||||||
|
|
||||||
## [0.9.19.1] – 2026-06-04
|
## [0.9.19.1] – 2026-06-04
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|||||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -1,24 +1,51 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.9.20] – 2026-06-08
|
||||||
|
|
||||||
|
### New
|
||||||
|
- **French language support (PR #45 by @Nathacks)**
|
||||||
|
- **Z height display in the print UI (PR #49 by @Nathacks).** Shows
|
||||||
|
current Z position in mm below the layer counter.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Camera auto-start ignored "Enable camera on print start" setting
|
||||||
|
after a bridge restart (issue #50).** The setting was cached in the
|
||||||
|
process environment — after saving it in the UI, the old value
|
||||||
|
survived the restart and the new value from `config.ini` was never
|
||||||
|
read.
|
||||||
|
- **Camera restarted automatically after manual stop during a print
|
||||||
|
(issue #50).** A new `_camera_user_stopped` flag suppresses
|
||||||
|
auto-restart for the current print session. It resets when the
|
||||||
|
print ends.
|
||||||
|
- **Spurious "stream unavailable" error toast when stopping the camera
|
||||||
|
manually.** The image error handler was still registered when
|
||||||
|
`img.src` was cleared.
|
||||||
|
- **JS error (`ReferenceError: br is not defined`) when toggling the
|
||||||
|
light.** Variable was referenced from the wrong scope.
|
||||||
|
- Webcam URLs are now absolute so that Mobileraker/Obico clients can
|
||||||
|
reach them.
|
||||||
|
|
||||||
## [0.9.19.1] – 2026-06-04
|
## [0.9.19.1] – 2026-06-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Standalone-Binaries (Linux/Windows) zeigten `vunknown` als Version.
|
- Standalone binaries (Linux/Windows) reported `vunknown` as their
|
||||||
Die `VERSION`-Datei ist jetzt ins PyInstaller-Onefile eingebettet.
|
version. The `VERSION` file is now embedded into the PyInstaller
|
||||||
- Bei fehlenden TLS-Zertifikaten (`anycubic_slicer.crt`/`.key`) gab
|
onefile bundle.
|
||||||
es nur den rohen Fehler `[Errno 2] No such file or directory`. Die
|
- When the TLS certificates (`anycubic_slicer.crt`/`.key`) were
|
||||||
Bridge meldet jetzt klar, wo die Dateien hingelegt werden müssen
|
missing, the bridge only logged the raw `[Errno 2] No such file
|
||||||
und dass `anycubic-certs.zip` aus dem Gitea-Release stammt.
|
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
|
### Changed
|
||||||
- Filament-Profil-Liste neu kuratiert: 209 statt 399 Einträge.
|
- Filament profile list re-curated: 209 entries instead of 399.
|
||||||
Profile die nur für drucker-spezifische Vendor-Bundles existieren
|
Profiles that only exist inside printer-specific vendor bundles
|
||||||
(z.B. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
(e.g. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||||
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) sind rausgeflogen
|
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) were dropped —
|
||||||
— OrcaSlicer hätte sie im Standard-Kobra-X-Setup beim Sync
|
OrcaSlicer wouldn't have found them in a default Kobra X setup
|
||||||
ohnehin nicht gefunden, weil die jeweiligen Vendor-Bundles nur
|
anyway, because the matching vendor bundle is only loaded when
|
||||||
bei aktivem Drucker-Vendor geladen werden. Für solche Filamente
|
the corresponding printer vendor is active. For those filaments
|
||||||
bleibt der Custom-Profile-Import (Issue #41) der Weg.
|
the custom profile import (issue #41) remains the way.
|
||||||
|
|
||||||
## [0.9.19] – 2026-06-02
|
## [0.9.19] – 2026-06-02
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ FROM python:3.11-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ Für sauberen AMS-Filament-Sync gibt es einen **gepatchten OrcaSlicer-Build**:
|
|||||||
- Vendor-Match wenn `tray_info_idx` gesetzt ist, das Preset aber inkompatibel
|
- Vendor-Match wenn `tray_info_idx` gesetzt ist, das Preset aber inkompatibel
|
||||||
- Zwei-Pass-Suche: erst kompatible Presets, dann alle sichtbaren
|
- Zwei-Pass-Suche: erst kompatible Presets, dann alle sichtbaren
|
||||||
|
|
||||||
**Warum das zusammen wichtig ist:** ohne #13719 landen die AMS-Slots in OrcaSlicer alle auf `Generic PLA` / `Generic PETG`, obwohl die Bridge die konkrete Marke schon mitsendet (`name + vendor_name + gate_filament_name`). Mit dem KX-Build matched OrcaSlicer deine echten User-Presets — auch die, die du via [Eigene OrcaSlicer-Profile importieren](#-features) in die Bridge gezogen hast.
|
**Warum das zusammen wichtig ist:** ohne #13719 landen die AMS-Slots in OrcaSlicer alle auf `Generic PLA` / `Generic PETG`, obwohl die Bridge die konkrete Marke schon mitsendet (`name + vendor_name + gate_filament_name`). Mit dem KX-Build matched OrcaSlicer deine echten User-Presets — auch die, die du via [Eigene OrcaSlicer-Profile importieren](docs/filament-preset-bridge-guide.md) in die Bridge gezogen hast.
|
||||||
|
|
||||||
Stock-Upstream-OrcaSlicer funktioniert für Slicing und Drucken weiterhin — nur das Per-Slot-Vendor-Matching beim AMS-Sync fällt dann weg. Material und Farbe pro Slot kannst du auch ohne den KX-Build über die Bridge ans Drucker-Display schreiben (das läuft über MQTT, nicht über den Slicer).
|
Stock-Upstream-OrcaSlicer funktioniert für Slicing und Drucken weiterhin — nur das Per-Slot-Vendor-Matching beim AMS-Sync fällt dann weg. Material und Farbe pro Slot kannst du auch ohne den KX-Build über die Bridge ans Drucker-Display schreiben (das läuft über MQTT, nicht über den Slicer).
|
||||||
|
|
||||||
|
|||||||
34
README.es.md
34
README.es.md
@@ -66,17 +66,43 @@ docker compose up -d
|
|||||||
|
|
||||||
**Binario Linux (sin Docker):**
|
**Binario Linux (sin Docker):**
|
||||||
```bash
|
```bash
|
||||||
chmod +x kx-bridge && ./kx-bridge
|
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
|
||||||
```
|
```
|
||||||
|
|
||||||
**EXE Windows (sin Docker):**
|
**EXE Windows (sin Docker):**
|
||||||
```
|
```
|
||||||
kx-bridge.exe
|
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)
|
> ⚠️ **Certificados TLS necesarios para el binario standalone**
|
||||||
> viven junto al programa. Copia toda la carpeta para mover la instalación.
|
>
|
||||||
|
> 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:**
|
**Python directamente:**
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
|
|||||||
- Vendor match when `tray_info_idx` is set but its preset is incompatible
|
- Vendor match when `tray_info_idx` is set but its preset is incompatible
|
||||||
- Two-pass lookup: first compatible presets, then all visible ones
|
- Two-pass lookup: first compatible presets, then all visible ones
|
||||||
|
|
||||||
**Why this matters:** without #13719 the AMS slots in OrcaSlicer all fall back to `Generic PLA` / `Generic PETG` even though the bridge already sends the concrete brand (`name + vendor_name + gate_filament_name`). With the KX build OrcaSlicer matches your actual user presets — including profiles you imported into the bridge via the [Import your own OrcaSlicer profiles](#-features) flow.
|
**Why this matters:** without #13719 the AMS slots in OrcaSlicer all fall back to `Generic PLA` / `Generic PETG` even though the bridge already sends the concrete brand (`name + vendor_name + gate_filament_name`). With the KX build OrcaSlicer matches your actual user presets — including profiles you imported into the bridge via the [Import your own OrcaSlicer profiles](docs/filament-preset-bridge-guide.md) flow.
|
||||||
|
|
||||||
Stock upstream OrcaSlicer still works for slicing and printing — you just lose the per-slot brand matching on AMS sync. Slot material + colour can still be pushed bridge → printer either way (that goes over MQTT, not via the slicer).
|
Stock upstream OrcaSlicer still works for slicing and printing — you just lose the per-slot brand matching on AMS sync. Slot material + colour can still be pushed bridge → printer either way (that goes over MQTT, not via the slicer).
|
||||||
|
|
||||||
|
|||||||
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 |
|
||||||
@@ -222,7 +222,7 @@ def _parse_gcode_estimated_time(data: bytes) -> int:
|
|||||||
elif unit == "m": secs += int(val) * 60
|
elif unit == "m": secs += int(val) * 60
|
||||||
elif unit == "s": secs += int(val)
|
elif unit == "s": secs += int(val)
|
||||||
if secs:
|
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
|
return secs
|
||||||
|
|
||||||
|
|
||||||
@@ -798,6 +798,7 @@ class KobraXBridge:
|
|||||||
self._serve_dir_path: str = self._store._gcode_dir
|
self._serve_dir_path: str = self._store._gcode_dir
|
||||||
self._current_job_id: str = ""
|
self._current_job_id: str = ""
|
||||||
self._camera_autostarted: bool = False
|
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.camera_cache: CameraCache = CameraCache()
|
||||||
|
|
||||||
self._thumbnail_b64: str = ""
|
self._thumbnail_b64: str = ""
|
||||||
@@ -809,7 +810,7 @@ class KobraXBridge:
|
|||||||
# Theme-Name prüfen (keine Sonderzeichen oder Umlaute)
|
# Theme-Name prüfen (keine Sonderzeichen oder Umlaute)
|
||||||
raw_theme = (getattr(args, "ui_theme", None) or "default").strip()
|
raw_theme = (getattr(args, "ui_theme", None) or "default").strip()
|
||||||
if not _UI_THEME_NAME_RE.match(raw_theme):
|
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"
|
raw_theme = "default"
|
||||||
self._ui_theme = raw_theme
|
self._ui_theme = raw_theme
|
||||||
self._index_tpl_cache: str | None = None
|
self._index_tpl_cache: str | None = None
|
||||||
@@ -914,7 +915,9 @@ class KobraXBridge:
|
|||||||
# Zentral hier, damit es alle Druck-Startwege abdeckt (OrcaSlicer + UI).
|
# Zentral hier, damit es alle Druck-Startwege abdeckt (OrcaSlicer + UI).
|
||||||
# _camera_autostarted verhindert Mehrfach-Trigger pro Druck.
|
# _camera_autostarted verhindert Mehrfach-Trigger pro Druck.
|
||||||
if kobra_state == "printing":
|
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
|
self._camera_autostarted = True
|
||||||
try:
|
try:
|
||||||
self.client.start_camera()
|
self.client.start_camera()
|
||||||
@@ -923,6 +926,7 @@ class KobraXBridge:
|
|||||||
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||||
self._camera_autostarted = False
|
self._camera_autostarted = False
|
||||||
|
self._camera_user_stopped = False # für nächsten Druck freigeben
|
||||||
|
|
||||||
# Job-History: Druckstart erkennen
|
# Job-History: Druckstart erkennen
|
||||||
if kobra_state == "printing" and not self._current_job_id:
|
if kobra_state == "printing" and not self._current_job_id:
|
||||||
@@ -934,7 +938,7 @@ class KobraXBridge:
|
|||||||
gcode_file_id=gf["id"],
|
gcode_file_id=gf["id"],
|
||||||
printer_id=self._printer_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
|
# Job-History: Druckende erkennen
|
||||||
if kobra_state in ("finished",) and self._current_job_id:
|
if kobra_state in ("finished",) and self._current_job_id:
|
||||||
@@ -1001,7 +1005,9 @@ class KobraXBridge:
|
|||||||
# Kamera-Autostart auch hier (OrcaSlicer meldet Start oft via info/report).
|
# Kamera-Autostart auch hier (OrcaSlicer meldet Start oft via info/report).
|
||||||
# _camera_autostarted-Guard verhindert Doppel-Start mit _on_print.
|
# _camera_autostarted-Guard verhindert Doppel-Start mit _on_print.
|
||||||
if kobra_state == "printing":
|
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
|
self._camera_autostarted = True
|
||||||
try:
|
try:
|
||||||
self.client.start_camera()
|
self.client.start_camera()
|
||||||
@@ -1010,6 +1016,7 @@ class KobraXBridge:
|
|||||||
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
log.warning(f"Kamera-Autostart fehlgeschlagen: {e}")
|
||||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||||
self._camera_autostarted = False
|
self._camera_autostarted = False
|
||||||
|
self._camera_user_stopped = False # für nächsten Druck freigeben
|
||||||
if project:
|
if project:
|
||||||
if "filename" in project:
|
if "filename" in project:
|
||||||
self._state["filename"] = project["filename"]
|
self._state["filename"] = project["filename"]
|
||||||
@@ -1075,7 +1082,7 @@ class KobraXBridge:
|
|||||||
if filename:
|
if filename:
|
||||||
try:
|
try:
|
||||||
self._store.update_file_objects(filename, objs, svg)
|
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:
|
except Exception as e:
|
||||||
log.warning(f"update_file_objects fehlgeschlagen: {e}")
|
log.warning(f"update_file_objects fehlgeschlagen: {e}")
|
||||||
self._push_status_update()
|
self._push_status_update()
|
||||||
@@ -1884,6 +1891,41 @@ class KobraXBridge:
|
|||||||
"gcode_macro TIMELAPSE_TAKE_FRAME": {
|
"gcode_macro TIMELAPSE_TAKE_FRAME": {
|
||||||
"is_paused": False,
|
"is_paused": False,
|
||||||
},
|
},
|
||||||
|
# configfile stub — Mobileraker und andere Clients crashen ohne
|
||||||
|
# dieses Objekt (Missing field: configFile). Werte aus der
|
||||||
|
# entschlüsselten avata_main.conf (ACCFG1.0 — Kobra X Firmware).
|
||||||
|
"configfile": {
|
||||||
|
"config": {},
|
||||||
|
"settings": {
|
||||||
|
"printer": {
|
||||||
|
"kinematics": "cartesian",
|
||||||
|
"max_velocity": 450,
|
||||||
|
"max_accel": 10000,
|
||||||
|
"max_z_velocity": 12,
|
||||||
|
"max_z_accel": 100,
|
||||||
|
"square_corner_velocity": 20.0,
|
||||||
|
},
|
||||||
|
"extruder": {
|
||||||
|
"nozzle_diameter": 0.4,
|
||||||
|
"min_temp": 0,
|
||||||
|
"max_temp": 320,
|
||||||
|
"min_extrude_temp": 10,
|
||||||
|
},
|
||||||
|
"heater_bed": {
|
||||||
|
"min_temp": 0,
|
||||||
|
"max_temp": 120,
|
||||||
|
},
|
||||||
|
"stepper_x": {"position_min": -18.5, "position_max": 280},
|
||||||
|
"stepper_y": {"position_min": -6.5, "position_max": 272.5},
|
||||||
|
"stepper_z": {"position_min": -4, "position_max": 262},
|
||||||
|
"virtual_sdcard": {"path": "/data/gcodes"},
|
||||||
|
"pause_resume": {},
|
||||||
|
"display_status": {},
|
||||||
|
},
|
||||||
|
"warnings": [],
|
||||||
|
"save_config_pending": False,
|
||||||
|
"save_config_pending_items": {},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -2655,7 +2697,20 @@ class KobraXBridge:
|
|||||||
return web.json_response({"result": {"count": len(result_jobs), "jobs": result_jobs}})
|
return web.json_response({"result": {"count": len(result_jobs), "jobs": result_jobs}})
|
||||||
|
|
||||||
async def handle_webcams_list(self, request):
|
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({
|
return web.json_response({
|
||||||
"result": {
|
"result": {
|
||||||
"webcams": [
|
"webcams": [
|
||||||
@@ -2667,8 +2722,8 @@ class KobraXBridge:
|
|||||||
"icon": "mdiWebcam",
|
"icon": "mdiWebcam",
|
||||||
"target_fps": 5,
|
"target_fps": 5,
|
||||||
"target_fps_idle": 2,
|
"target_fps_idle": 2,
|
||||||
"stream_url": "/api/camera/stream",
|
"stream_url": stream_url,
|
||||||
"snapshot_url": "/api/camera/snapshot",
|
"snapshot_url": snapshot_url,
|
||||||
"flip_horizontal": False,
|
"flip_horizontal": False,
|
||||||
"flip_vertical": False,
|
"flip_vertical": False,
|
||||||
"rotation": 0,
|
"rotation": 0,
|
||||||
@@ -2853,7 +2908,7 @@ class KobraXBridge:
|
|||||||
log.info(f"print/start → {filename} url={url} ams={len(ams_box_mapping)} slots mode={self._filament_mode}")
|
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)
|
result = self.client.publish("print", "start", payload, timeout=15.0)
|
||||||
if result:
|
if result:
|
||||||
log.info(f"Druckstart bestätigt: state={result.get('state')}")
|
log.info(f"Print start confirmed: state={result.get('state')}")
|
||||||
else:
|
else:
|
||||||
log.warning("Druckstart: keine Antwort vom Drucker")
|
log.warning("Druckstart: keine Antwort vom Drucker")
|
||||||
|
|
||||||
@@ -3108,7 +3163,7 @@ class KobraXBridge:
|
|||||||
return web.json_response({"result": "disconnected"})
|
return web.json_response({"result": "disconnected"})
|
||||||
|
|
||||||
async def handle_api_restart(self, request):
|
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"})
|
response = web.json_response({"status": "restarting"})
|
||||||
asyncio.get_event_loop().call_later(0.3, self._restart_bridge)
|
asyncio.get_event_loop().call_later(0.3, self._restart_bridge)
|
||||||
return response
|
return response
|
||||||
@@ -3388,6 +3443,9 @@ class KobraXBridge:
|
|||||||
await loop.run_in_executor(None, lambda: self.client.publish(
|
await loop.run_in_executor(None, lambda: self.client.publish(
|
||||||
"video", "stopCapture", None, timeout=0
|
"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"})
|
return web.json_response({"result": "ok"})
|
||||||
|
|
||||||
async def handle_api_camera_snapshot(self, request):
|
async def handle_api_camera_snapshot(self, request):
|
||||||
@@ -3447,7 +3505,7 @@ class KobraXBridge:
|
|||||||
stderr=asyncio.subprocess.DEVNULL,
|
stderr=asyncio.subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
except (FileNotFoundError, OSError) as e:
|
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")
|
return web.Response(status=503, text="ffmpeg not found")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"Kamera: ffmpeg konnte nicht gestartet werden: {e}")
|
log.warning(f"Kamera: ffmpeg konnte nicht gestartet werden: {e}")
|
||||||
@@ -3542,7 +3600,7 @@ class KobraXBridge:
|
|||||||
if not os.path.isfile(serve_path):
|
if not os.path.isfile(serve_path):
|
||||||
return web.Response(status=404, text="not found")
|
return web.Response(status=404, text="not found")
|
||||||
size = os.path.getsize(serve_path)
|
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={
|
return web.FileResponse(serve_path, headers={
|
||||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||||
})
|
})
|
||||||
@@ -3580,6 +3638,7 @@ class KobraXBridge:
|
|||||||
"remain_time": s["remain_time"],
|
"remain_time": s["remain_time"],
|
||||||
"curr_layer": s["curr_layer"],
|
"curr_layer": s["curr_layer"],
|
||||||
"total_layers": s["total_layers"],
|
"total_layers": s["total_layers"],
|
||||||
|
"z_mm": self._estimate_current_z(),
|
||||||
"filename": s["filename"],
|
"filename": s["filename"],
|
||||||
"slicer_time": slicer_time,
|
"slicer_time": slicer_time,
|
||||||
"camera_url": s["camera_url"],
|
"camera_url": s["camera_url"],
|
||||||
@@ -3857,7 +3916,7 @@ class KobraXBridge:
|
|||||||
with open(config_path, "w", encoding="utf-8") as f:
|
with open(config_path, "w", encoding="utf-8") as f:
|
||||||
f.write("# KX-Bridge Konfigurationsdatei\n\n")
|
f.write("# KX-Bridge Konfigurationsdatei\n\n")
|
||||||
cfg.write(f)
|
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})
|
response = self._json_cors({"status": "restarting", "section": sec, "http_port": new_port})
|
||||||
asyncio.get_event_loop().call_later(0.5, self._restart_bridge)
|
asyncio.get_event_loop().call_later(0.5, self._restart_bridge)
|
||||||
return response
|
return response
|
||||||
@@ -3932,13 +3991,13 @@ class KobraXBridge:
|
|||||||
# die alten Werte statt der geänderten config.ini.
|
# die alten Werte statt der geänderten config.ini.
|
||||||
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
|
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
|
||||||
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
|
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
|
||||||
"BRIDGE_PRINTER_NAME"):
|
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "BRIDGE_PRINTER_NAME"):
|
||||||
os.environ.pop(_k, None)
|
os.environ.pop(_k, None)
|
||||||
|
|
||||||
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
|
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
|
||||||
if in_docker:
|
if in_docker:
|
||||||
# Docker/systemd: Prozess beenden reicht – der Supervisor startet neu (frische environ)
|
# 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)
|
os._exit(0)
|
||||||
|
|
||||||
frozen = getattr(sys, "frozen", False)
|
frozen = getattr(sys, "frozen", False)
|
||||||
@@ -4117,7 +4176,7 @@ class KobraXBridge:
|
|||||||
if fname == "kobrax_moonraker_bridge.py":
|
if fname == "kobrax_moonraker_bridge.py":
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"error": f"Download {fname}: HTTP {resp.status}"}, status=502)
|
{"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
|
continue
|
||||||
downloaded.append((app_dir / fname, await resp.read()))
|
downloaded.append((app_dir / fname, await resp.read()))
|
||||||
# Phase 2: atomar ersetzen (erst nach komplettem, erfolgreichem Download)
|
# Phase 2: atomar ersetzen (erst nach komplettem, erfolgreichem Download)
|
||||||
@@ -4394,11 +4453,14 @@ class KobraXBridge:
|
|||||||
# Obico registriert obico_remote_event-Callback. Wir akzeptieren leer.
|
# Obico registriert obico_remote_event-Callback. Wir akzeptieren leer.
|
||||||
result = "ok"
|
result = "ok"
|
||||||
elif method == "server.webcams.list":
|
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": [{
|
result = {"webcams": [{
|
||||||
"name": "KX-Bridge", "location": "printer", "service": "mjpegstreamer",
|
"name": "KX-Bridge", "location": "printer", "service": "mjpegstreamer",
|
||||||
"enabled": True, "stream_url": "/api/camera/stream",
|
"enabled": True,
|
||||||
"snapshot_url": "/api/camera/snapshot",
|
"stream_url": f"{_base}/api/camera/stream",
|
||||||
|
"snapshot_url": f"{_base}/api/camera/snapshot",
|
||||||
"flip_horizontal": False, "flip_vertical": False, "rotation": 0,
|
"flip_horizontal": False, "flip_vertical": False, "rotation": 0,
|
||||||
"target_fps": 5, "aspect_ratio": "16:9",
|
"target_fps": 5, "aspect_ratio": "16:9",
|
||||||
}]}
|
}]}
|
||||||
@@ -4448,7 +4510,7 @@ class KobraXBridge:
|
|||||||
log.debug(f"Unbekannte RPC-Methode: {method}")
|
log.debug(f"Unbekannte RPC-Methode: {method}")
|
||||||
result = {}
|
result = {}
|
||||||
except Exception as e:
|
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)}
|
error = {"code": -32603, "message": str(e)}
|
||||||
|
|
||||||
if rpc_id is not None:
|
if rpc_id is not None:
|
||||||
@@ -4762,7 +4824,7 @@ async def run_bridge(args):
|
|||||||
site = web.TCPSite(runner, args.host, per_args.port)
|
site = web.TCPSite(runner, args.host, per_args.port)
|
||||||
await site.start()
|
await site.start()
|
||||||
runners.append((runner, client, pid))
|
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
|
import socket as _socket
|
||||||
try:
|
try:
|
||||||
@@ -4771,6 +4833,9 @@ async def run_bridge(args):
|
|||||||
_local_ip = _s.getsockname()[0]
|
_local_ip = _s.getsockname()[0]
|
||||||
except Exception:
|
except Exception:
|
||||||
_local_ip = args.host
|
_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: " +
|
log.info(f"OrcaSlicer → Klipper → Host: {_local_ip} Ports: " +
|
||||||
", ".join(str(getattr(b._args, 'port', 0)) for b in all_bridges.values()))
|
", ".join(str(getattr(b._args, 'port', 0)) for b in all_bridges.values()))
|
||||||
log.info("Ctrl-C zum Beenden")
|
log.info("Ctrl-C zum Beenden")
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// ── State ──
|
// ── State ──
|
||||||
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
|
var S={nozzle_temp:0,nozzle_target:0,bed_temp:0,bed_target:0,
|
||||||
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
|
print_state:'standby',filename:'',progress:0,print_duration:0,remain_time:0,
|
||||||
curr_layer:0,total_layers:0,printer_name:'Kobra X',firmware_version:'–',
|
curr_layer:0,total_layers:0,z_mm:0,printer_name:'Kobra X',firmware_version:'–',
|
||||||
camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,
|
camera_url:'',fan_speed:0,print_speed_mode:2,light_on:false,light_brightness:80,
|
||||||
ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]},web_upload_warning:1};
|
ams_slots:[],filament_mode:'toolhead',ace_units:[],ace_dry_presets:null,ace_drying:{status:0,target_temp:0,duration:0,remain_time:0,humidity:null,current_temp:null,units:[]},web_upload_warning:1};
|
||||||
var tempHistory={n:[],b:[]};
|
var tempHistory={n:[],b:[]};
|
||||||
var camOn=false;
|
var camOn=false;
|
||||||
|
var camUserStopped=false; // user stopped camera manually — suppress auto-restart for this print
|
||||||
var currentStep=1;
|
var currentStep=1;
|
||||||
var currentPanel='dashboard';
|
var currentPanel='dashboard';
|
||||||
var aceAutoRefillPrefs=(function(){
|
var aceAutoRefillPrefs=(function(){
|
||||||
@@ -101,6 +102,7 @@ function tr(key,fallback){
|
|||||||
function _langToggleLabel(lang){
|
function _langToggleLabel(lang){
|
||||||
if(lang==='de')return 'Deutsch';
|
if(lang==='de')return 'Deutsch';
|
||||||
if(lang==='en')return 'English';
|
if(lang==='en')return 'English';
|
||||||
|
if(lang==='fr')return 'Français';
|
||||||
if(lang==='zh-cn')return '简体中文';
|
if(lang==='zh-cn')return '简体中文';
|
||||||
return 'Espanol';
|
return 'Espanol';
|
||||||
}
|
}
|
||||||
@@ -108,10 +110,10 @@ function _langToggleLabel(lang){
|
|||||||
function _mapSupportedLang(lang){
|
function _mapSupportedLang(lang){
|
||||||
if(!lang)return '';
|
if(!lang)return '';
|
||||||
var l=String(lang).toLowerCase().replace(/_/g,'-').trim();
|
var l=String(lang).toLowerCase().replace(/_/g,'-').trim();
|
||||||
if(l==='de'||l==='en'||l==='es'||l==='zh-cn')return l;
|
if(l==='de'||l==='en'||l==='es'||l==='fr'||l==='zh-cn')return l;
|
||||||
|
|
||||||
var base=l.split('-')[0];
|
var base=l.split('-')[0];
|
||||||
if(base==='de'||base==='en'||base==='es')return base;
|
if(base==='de'||base==='en'||base==='es'||base==='fr')return base;
|
||||||
|
|
||||||
if(base==='zh'){
|
if(base==='zh'){
|
||||||
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
|
if(l.indexOf('cn')>=0||l.indexOf('hans')>=0||l==='zh')return 'zh-cn';
|
||||||
@@ -282,6 +284,7 @@ function applyLang(){
|
|||||||
setText('d-lbl-remain',T.lbl_remaining);
|
setText('d-lbl-remain',T.lbl_remaining);
|
||||||
setText('d-slicer-label',T.lbl_slicer_time);
|
setText('d-slicer-label',T.lbl_slicer_time);
|
||||||
setText('d-lbl-layers',T.lbl_layers);
|
setText('d-lbl-layers',T.lbl_layers);
|
||||||
|
setText('d-lbl-zpos',T.lbl_zpos);
|
||||||
setText('d-lbl-light',T.lbl_light);
|
setText('d-lbl-light',T.lbl_light);
|
||||||
setText('d-lbl-nozzle',T.label_nozzle);
|
setText('d-lbl-nozzle',T.label_nozzle);
|
||||||
setText('d-lbl-bed',T.label_bed);
|
setText('d-lbl-bed',T.label_bed);
|
||||||
@@ -659,6 +662,7 @@ function applyState(){
|
|||||||
|
|
||||||
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–';
|
var layers=s.curr_layer&&s.total_layers?'L '+s.curr_layer+' / '+s.total_layers:'–';
|
||||||
var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers;
|
var dlayers=document.getElementById('d-layers');if(dlayers)dlayers.textContent=layers;
|
||||||
|
var dzpos=document.getElementById('d-zpos');if(dzpos)dzpos.textContent=s.z_mm>0?s.z_mm.toFixed(2)+' mm':'–';
|
||||||
|
|
||||||
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration);
|
var delapsed=document.getElementById('d-elapsed');if(delapsed)delapsed.textContent=fmtTime(s.print_duration);
|
||||||
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–';
|
var dremain=document.getElementById('d-remain');if(dremain)dremain.textContent=s.remain_time>0?fmtTime(s.remain_time):'–';
|
||||||
@@ -822,10 +826,14 @@ function applyState(){
|
|||||||
var co=document.getElementById('cam-overlay');
|
var co=document.getElementById('cam-overlay');
|
||||||
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
|
if(co)co.style.display=(s.print_state==='printing'&&camOn)?'block':'none';
|
||||||
|
|
||||||
// auto-start camera during print
|
// auto-start camera during print (unless user explicitly stopped it)
|
||||||
if(s.print_state==='printing'&&!camOn&&s.camera_url){
|
if(s.print_state==='printing'&&!camOn&&s.camera_url&&!camUserStopped){
|
||||||
camStart();
|
camStart();
|
||||||
}
|
}
|
||||||
|
// reset user-stopped flag when print ends so next print auto-starts again
|
||||||
|
if(s.print_state!=='printing'){
|
||||||
|
camUserStopped=false;
|
||||||
|
}
|
||||||
|
|
||||||
updateConnBtn();
|
updateConnBtn();
|
||||||
}
|
}
|
||||||
@@ -1437,7 +1445,8 @@ function setBed(){
|
|||||||
function setLight(){
|
function setLight(){
|
||||||
var on=document.getElementById('d-light-toggle').checked;
|
var on=document.getElementById('d-light-toggle').checked;
|
||||||
post('/api/light',{on:on,brightness:80})
|
post('/api/light',{on:on,brightness:80})
|
||||||
.then(function(){clog('Licht '+(on?'an, '+br+'%':'aus'),'msg-ok')})
|
.then(function(){clog('Licht '+(on?'an, 80%':'aus'),'msg-ok')})
|
||||||
|
|
||||||
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
|
.catch(function(e){clog('Licht-Fehler: '+e,'msg-err')});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1518,11 +1527,13 @@ function camStart(){
|
|||||||
|
|
||||||
function camStop(){
|
function camStop(){
|
||||||
var img=document.getElementById('cam-img');
|
var img=document.getElementById('cam-img');
|
||||||
|
img.onerror=null; // deregister error handler before clearing src to avoid spurious error toast
|
||||||
post('/api/camera/stop',{}).catch(function(){});
|
post('/api/camera/stop',{}).catch(function(){});
|
||||||
img.src='';
|
img.src='';
|
||||||
img.style.display='none';
|
img.style.display='none';
|
||||||
document.getElementById('cam-placeholder').style.display='flex';
|
document.getElementById('cam-placeholder').style.display='flex';
|
||||||
camOn=false;
|
camOn=false;
|
||||||
|
camUserStopped=true; // suppress auto-restart for remainder of this print
|
||||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
|
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
|
||||||
clog(tr('log_cam_stop'),'msg-ok');
|
clog(tr('log_cam_stop'),'msg-ok');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<option value="de">Deutsch</option>
|
<option value="de">Deutsch</option>
|
||||||
<option value="en">English</option>
|
<option value="en">English</option>
|
||||||
<option value="es">Espanol</option>
|
<option value="es">Espanol</option>
|
||||||
|
<option value="fr">Français</option>
|
||||||
<option value="zh-cn">中文(简体)</option>
|
<option value="zh-cn">中文(简体)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,9 +267,15 @@
|
|||||||
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
|
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
|
||||||
<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
|
<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
|
||||||
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
|
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
|
||||||
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0">
|
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
|
||||||
<div class="time-label" id="d-lbl-layers"></div>
|
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center">
|
||||||
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
<div class="time-label" id="d-lbl-layers"></div>
|
||||||
|
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||||
|
</div>
|
||||||
|
<div class="time-block" style="padding:4px 10px;min-width:72px;text-align:center">
|
||||||
|
<div class="time-label" id="d-lbl-zpos">Z</div>
|
||||||
|
<div class="time-val" style="font-size:13px" id="d-zpos">–</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="time-grid">
|
<div class="time-grid">
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"lbl_remaining": "Restzeit:",
|
"lbl_remaining": "Restzeit:",
|
||||||
"lbl_slicer_time": "Slicer-Schätzung:",
|
"lbl_slicer_time": "Slicer-Schätzung:",
|
||||||
"lbl_layers": "Layer",
|
"lbl_layers": "Layer",
|
||||||
|
"lbl_zpos": "Z (mm)",
|
||||||
"speed_silent": "🐢 Leise",
|
"speed_silent": "🐢 Leise",
|
||||||
"speed_normal": "⚡ Normal",
|
"speed_normal": "⚡ Normal",
|
||||||
"speed_sport": "🚀 Sport",
|
"speed_sport": "🚀 Sport",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"lbl_remaining": "Remaining:",
|
"lbl_remaining": "Remaining:",
|
||||||
"lbl_slicer_time": "Slicer estimate:",
|
"lbl_slicer_time": "Slicer estimate:",
|
||||||
"lbl_layers": "Layer",
|
"lbl_layers": "Layer",
|
||||||
|
"lbl_zpos": "Z (mm)",
|
||||||
"speed_silent": "🐢 Silent",
|
"speed_silent": "🐢 Silent",
|
||||||
"speed_normal": "⚡ Normal",
|
"speed_normal": "⚡ Normal",
|
||||||
"speed_sport": "🚀 Sport",
|
"speed_sport": "🚀 Sport",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"lbl_remaining": "Restante:",
|
"lbl_remaining": "Restante:",
|
||||||
"lbl_slicer_time": "Estimación del slicer:",
|
"lbl_slicer_time": "Estimación del slicer:",
|
||||||
"lbl_layers": "Capa",
|
"lbl_layers": "Capa",
|
||||||
|
"lbl_zpos": "Z (mm)",
|
||||||
"speed_silent": "🐢 Silencioso",
|
"speed_silent": "🐢 Silencioso",
|
||||||
"speed_normal": "⚡ Normal",
|
"speed_normal": "⚡ Normal",
|
||||||
"speed_sport": "🚀 Sport",
|
"speed_sport": "🚀 Sport",
|
||||||
|
|||||||
249
web/translations/fr.json
Normal file
249
web/translations/fr.json
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
{
|
||||||
|
"header_status_standby": "Prêt",
|
||||||
|
"header_status_printing": "Impression",
|
||||||
|
"header_status_complete": "Terminé",
|
||||||
|
"header_status_error": "Erreur",
|
||||||
|
"kobra_free": "Disponible",
|
||||||
|
"kobra_busy": "Occupé",
|
||||||
|
"kobra_printing": "Impression",
|
||||||
|
"kobra_preheating": "Préchauffage",
|
||||||
|
"kobra_auto_leveling": "Mise à niveau auto",
|
||||||
|
"kobra_checking": "Vérification",
|
||||||
|
"kobra_updated": "Mise à jour",
|
||||||
|
"kobra_init": "Initialisation",
|
||||||
|
"kobra_pausing": "Pause en cours…",
|
||||||
|
"kobra_paused": "En pause",
|
||||||
|
"kobra_resuming": "Reprise en cours…",
|
||||||
|
"kobra_resumed": "Repris",
|
||||||
|
"kobra_stopping": "Arrêt en cours…",
|
||||||
|
"kobra_stoped": "Arrêté",
|
||||||
|
"kobra_finished": "Terminé",
|
||||||
|
"kobra_failed": "Erreur",
|
||||||
|
"kobra_canceled": "Annulé",
|
||||||
|
"kobra_offline": "Hors ligne",
|
||||||
|
"nav_dashboard": "Tableau de bord",
|
||||||
|
"nav_print": "Impression",
|
||||||
|
"nav_temps": "Températures",
|
||||||
|
"nav_motion": "Mouvement",
|
||||||
|
"nav_ams": "AMS",
|
||||||
|
"nav_extras": "Lumière / Ventilateur",
|
||||||
|
"nav_console": "Console",
|
||||||
|
"card_progress": "Progression",
|
||||||
|
"card_temps": "Températures",
|
||||||
|
"card_light_fan": "Ventilateur",
|
||||||
|
"card_speed": "Vitesse d'impression",
|
||||||
|
"card_cam": "Caméra",
|
||||||
|
"lbl_elapsed": "Écoulé :",
|
||||||
|
"lbl_remaining": "Restant :",
|
||||||
|
"lbl_slicer_time": "Estimation slicer :",
|
||||||
|
"lbl_layers": "Couche",
|
||||||
|
"lbl_zpos": "Z (mm)",
|
||||||
|
"speed_silent": "🐢 Silencieux",
|
||||||
|
"speed_normal": "⚡ Normal",
|
||||||
|
"speed_sport": "🚀 Sport",
|
||||||
|
"lbl_light": "💡 Lumière",
|
||||||
|
"lbl_feed": "Charger",
|
||||||
|
"lbl_unload": "Décharger",
|
||||||
|
"card_ace_dry": "Séchage ACE",
|
||||||
|
"ace_dry_dryer": "Séchoir",
|
||||||
|
"ace_dry_status_off": "Statut : Arrêté",
|
||||||
|
"ace_dry_status_on": "Statut : Actif",
|
||||||
|
"ace_dry_status_remaining": "Restant",
|
||||||
|
"ace_dry_humidity": "Humidité",
|
||||||
|
"ace_dry_current_temp": "Température",
|
||||||
|
"ace_dry_chart": "Historique (Temp/Humidité)",
|
||||||
|
"ace_dry_temp": "Température (°C)",
|
||||||
|
"ace_dry_duration": "Durée (min)",
|
||||||
|
"ace_dry_start": "▶ Démarrer",
|
||||||
|
"ace_dry_stop": "■ Arrêter",
|
||||||
|
"ace_dry_auto_refill": "Remplissage auto",
|
||||||
|
"ace_dry_enable": "Activer le séchage",
|
||||||
|
"ace_dry_temp_line": "Température de séchage",
|
||||||
|
"ace_dry_time_line": "Durée de séchage",
|
||||||
|
"ace_dry_ui_pending": "(Interface seule, backend suivant)",
|
||||||
|
"ace_dry_dialog_title": "Réglages Temp/Durée du séchoir",
|
||||||
|
"ace_dry_dialog_temp": "Température (30-80°C)",
|
||||||
|
"ace_dry_dialog_time": "Temps restant (h:m:s)",
|
||||||
|
"ace_dry_dialog_confirm": "Confirmer",
|
||||||
|
"ace_dry_dialog_cancel": "Annuler",
|
||||||
|
"ace_dry_dialog_save_restart": "Enregistrer et redémarrer",
|
||||||
|
"ace_dry_dialog_custom_name": "Nom personnalisé",
|
||||||
|
"ace_dry_dialog_reset_default": "Réinitialiser",
|
||||||
|
"cam_placeholder": "📷 Caméra non démarrée",
|
||||||
|
"cam_stream_unavailable": "Flux indisponible",
|
||||||
|
"btn_cam_start": "▶ Caméra",
|
||||||
|
"btn_cam_stop": "◼ Caméra",
|
||||||
|
"btn_pause": "⏸ Pause",
|
||||||
|
"btn_resume": "▶ Reprendre",
|
||||||
|
"btn_cancel": "✕ Arrêter",
|
||||||
|
"label_nozzle": "Buse",
|
||||||
|
"label_bed": "Plateau",
|
||||||
|
"label_fan": "🌀 Ventilateur",
|
||||||
|
"label_light": "💡 Lumière",
|
||||||
|
"label_on_off": "On / Off",
|
||||||
|
"label_speed": "Vitesse",
|
||||||
|
"panel_print_title": "Contrôle impression",
|
||||||
|
"panel_print_btn_pause": "⏸ Pause",
|
||||||
|
"panel_print_btn_resume": "▶ Reprendre",
|
||||||
|
"panel_print_btn_cancel": "✕ Annuler",
|
||||||
|
"panel_print_temps_live": "Températures (en direct)",
|
||||||
|
"label_set": "Définir",
|
||||||
|
"label_off": "Éteint",
|
||||||
|
"panel_temps_nozzle": "Buse",
|
||||||
|
"panel_temps_bed": "Plateau chauffant",
|
||||||
|
"panel_temps_chart": "Historique (60 dernières valeurs)",
|
||||||
|
"label_target_c": "Cible :",
|
||||||
|
"panel_motion_xy": "Axes XY",
|
||||||
|
"panel_motion_z": "Axe Z",
|
||||||
|
"label_step": "Pas :",
|
||||||
|
"btn_home_z": "Origine Z",
|
||||||
|
"btn_home_xy": "Origine XY",
|
||||||
|
"btn_home_all": "Origine Tout",
|
||||||
|
"btn_disable_motors": "Moteurs Off",
|
||||||
|
"panel_ams_title": "Filament",
|
||||||
|
"card_ams": "Filament",
|
||||||
|
"ams_no_data": "Aucune donnée AMS reçue",
|
||||||
|
"label_slot": "Slot",
|
||||||
|
"ams_empty": "Vide",
|
||||||
|
"panel_extras_light": "Lumière",
|
||||||
|
"panel_extras_fan": "Ventilateur",
|
||||||
|
"panel_extras_camera": "Caméra",
|
||||||
|
"btn_cam_start2": "▶ Démarrer",
|
||||||
|
"btn_cam_stop2": "◼ Arrêter",
|
||||||
|
"panel_console_title": "Journal d'événements",
|
||||||
|
"log_light_on": "Lumière allumée",
|
||||||
|
"log_light_off": "Lumière éteinte",
|
||||||
|
"log_fan": "Ventilateur →",
|
||||||
|
"log_nozzle": "Buse →",
|
||||||
|
"log_bed": "Plateau →",
|
||||||
|
"log_axis": "Axe",
|
||||||
|
"log_home": "Origine",
|
||||||
|
"log_home_all": "Origine Tout",
|
||||||
|
"log_cam_start": "Caméra démarrée :",
|
||||||
|
"log_cam_stop": "Caméra arrêtée",
|
||||||
|
"log_poll_error": "Erreur de sondage :",
|
||||||
|
"log_error": "Erreur :",
|
||||||
|
"confirm_cancel": "Vraiment annuler l'impression ?",
|
||||||
|
"settings_title": "Paramètres",
|
||||||
|
"settings_connection": "Connexion",
|
||||||
|
"settings_print": "Paramètres d'impression",
|
||||||
|
"settings_poll": "Intervalle de sondage",
|
||||||
|
"settings_version": "Version",
|
||||||
|
"settings_save": "Enregistrer et redémarrer",
|
||||||
|
"settings_printer_name": "Nom de l'imprimante",
|
||||||
|
"settings_printer_ip": "IP de l'imprimante",
|
||||||
|
"settings_mqtt_port": "Port MQTT",
|
||||||
|
"settings_username": "Nom d'utilisateur MQTT",
|
||||||
|
"settings_password": "Mot de passe MQTT",
|
||||||
|
"settings_device_id": "ID de l'appareil",
|
||||||
|
"settings_mode_id": "ID du mode",
|
||||||
|
"hint_ip_no_port": "Adresse IP uniquement, sans port (ex. 192.168.1.102)",
|
||||||
|
"settings_default_slot": "Slot par défaut (couleur unique)",
|
||||||
|
"settings_slot_auto": "Auto (tous les slots chargés)",
|
||||||
|
"settings_auto_leveling": "Mise à niveau auto avant impression",
|
||||||
|
"settings_camera_on_print": "Activer la caméra au démarrage de l'impression",
|
||||||
|
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
|
||||||
|
"update_check": "Vérifier les mises à jour",
|
||||||
|
"update_checking": "Vérification…",
|
||||||
|
"update_available": "disponible",
|
||||||
|
"update_none": "Déjà à jour",
|
||||||
|
"update_apply": "Installer maintenant",
|
||||||
|
"update_applying": "Téléchargement…",
|
||||||
|
"update_restarting": "Redémarrage…",
|
||||||
|
"update_error": "Erreur",
|
||||||
|
"btn_connect": "⚡ Connecter",
|
||||||
|
"btn_disconnect": "✕ Déconnecter",
|
||||||
|
"lbl_conn_error": "Erreur de connexion :",
|
||||||
|
"slot_edit_title": "Modifier le slot",
|
||||||
|
"slot_edit_color": "Couleur",
|
||||||
|
"slot_edit_material": "Matériau",
|
||||||
|
"slot_edit_load": "⬇ Charger",
|
||||||
|
"slot_edit_unload": "⬆ Décharger",
|
||||||
|
"slot_edit_save": "💾 Enregistrer",
|
||||||
|
"slot_edit_custom": "ex. PLA, PETG, ABS…",
|
||||||
|
"slot_edit_ok": "Slot AMS",
|
||||||
|
"slot_edit_profile": "Profil OrcaSlicer",
|
||||||
|
"slot_edit_profile_hint": "Envoyé lors de la synchronisation OrcaSlicer comme marque spécifique au lieu de \"Générique\"",
|
||||||
|
"slot_edit_profile_default": "— Générique (défaut) —",
|
||||||
|
"orca_profile_section": "Profils OrcaSlicer",
|
||||||
|
"orca_profile_hint": "Importez vos propres profils de filament OrcaSlicer (ouvrez le dossier utilisateur via Aide → Afficher le dossier de configuration)",
|
||||||
|
"orca_profile_import_btn": "Importer des profils",
|
||||||
|
"orca_profile_import_link": "★ Importer mes profils…",
|
||||||
|
"orca_profile_import_title": "Importer vos profils OrcaSlicer",
|
||||||
|
"orca_profile_help_html": "Déposez un <b>ZIP</b> de votre dossier filament OrcaSlicer ou des fichiers <b>.json</b> individuels.<br>Dans OrcaSlicer : <i>Aide → Afficher le dossier de configuration → user/<id>/filament/</i>",
|
||||||
|
"orca_profile_dropmsg": "Déposez ici ou cliquez",
|
||||||
|
"orca_profile_list_label": "Profils importés",
|
||||||
|
"orca_profile_user_label": "Mes profils",
|
||||||
|
"orca_profile_user_empty": "– aucun –",
|
||||||
|
"orca_profile_uploading": "Envoi en cours…",
|
||||||
|
"orca_profile_done": "Importé",
|
||||||
|
"orca_profile_skipped": "ignoré",
|
||||||
|
"log_dir_all": "Tout",
|
||||||
|
"log_lvl_label": "Niveau :",
|
||||||
|
"file_ready_btn": "▶ Lancer l'impression",
|
||||||
|
"file_slots_btn": "🎨 Choisir les slots",
|
||||||
|
"file_cancel_btn": "✕ Annuler",
|
||||||
|
"nav_printers": "Imprimantes",
|
||||||
|
"skip_title": "✂ Ignorer des objets",
|
||||||
|
"skip_hint": "Décochez les objets que vous ne souhaitez plus imprimer :",
|
||||||
|
"skip_btn_label": "Objets",
|
||||||
|
"skip_no_objects": "Aucun objet dans cette impression.",
|
||||||
|
"skip_already": "ignoré",
|
||||||
|
"skip_select_at_least_one": "Veuillez sélectionner au moins un objet.",
|
||||||
|
"skip_sending": "Envoi …",
|
||||||
|
"skip_success": "Les objets seront ignorés.",
|
||||||
|
"fd_objects_hint": "Ignorer des objets (optionnel) :",
|
||||||
|
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
|
||||||
|
"fd_cancel": "Annuler",
|
||||||
|
"fd_print": "▶ Imprimer",
|
||||||
|
"fd_no_slots_msg": "Aucun slot AMS chargé.{br}Lancer l'impression quand même ?",
|
||||||
|
"fd_slot": "Slot",
|
||||||
|
"fd_no_matching_material": "Aucun matériau correspondant",
|
||||||
|
"fd_used": "UTILISÉ",
|
||||||
|
"add_printer": "Ajouter une imprimante",
|
||||||
|
"apd_lbl_ip": "IP de l'imprimante",
|
||||||
|
"apd_lbl_name": "Nom (optionnel)",
|
||||||
|
"apd_placeholder_name": "ex. Kobra X Salon",
|
||||||
|
"apd_cancel": "Annuler",
|
||||||
|
"apd_confirm": "Ajouter",
|
||||||
|
"apd_fetching": "Récupération des données de l'imprimante…",
|
||||||
|
"apd_success": "Imprimante ajoutée, redémarrage du bridge…",
|
||||||
|
"apd_err_ip": "Veuillez saisir une adresse IP",
|
||||||
|
"printers_remove": "Supprimer l'imprimante",
|
||||||
|
"printers_remove_confirm": "Supprimer l'imprimante \"{name}\" ? Le bridge va redémarrer.",
|
||||||
|
"printers_active": "● actif",
|
||||||
|
"printers_switch": "Changer →",
|
||||||
|
"printers_current": "Imprimante actuelle",
|
||||||
|
"printers_loading": "Chargement…",
|
||||||
|
"printers_none": "Aucune imprimante configurée.",
|
||||||
|
"printers_empty_hint": "Aucune imprimante configurée.",
|
||||||
|
"nav_browser": "Navigateur",
|
||||||
|
"panel_browser_title": "Explorateur de fichiers",
|
||||||
|
"store_search_placeholder": "🔍 Rechercher…",
|
||||||
|
"store_empty": "Aucun fichier uploadé.",
|
||||||
|
"store_refresh": "↻ Actualiser",
|
||||||
|
"store_print": "▶ Imprimer",
|
||||||
|
"store_download": "⬇ Télécharger",
|
||||||
|
"store_delete_confirm": "Supprimer le fichier ?",
|
||||||
|
"store_print_confirm": "Imprimer le fichier ?",
|
||||||
|
"store_web_verify_title": "Vérifier le fichier",
|
||||||
|
"store_web_verify_msg": "Veuillez vérifier que ce fichier a été créé pour l'Anycubic Kobra X.",
|
||||||
|
"store_web_verify_confirm": "Confirmer",
|
||||||
|
"store_web_verify_abort": "Annuler",
|
||||||
|
"store_no_results": "Aucun fichier trouvé.",
|
||||||
|
"store_never": "jamais imprimé",
|
||||||
|
"store_estimate": "Estimation",
|
||||||
|
"store_upload_label_prefix": "Déposez un GCode ici ou ",
|
||||||
|
"store_upload_label_browse": "parcourir",
|
||||||
|
"store_upload_busy": "⏳ Envoi en cours…",
|
||||||
|
"store_upload_success": "✓ {file}",
|
||||||
|
"store_upload_error": "✗ {error}",
|
||||||
|
"sf_all": "Tout",
|
||||||
|
"sf_ok": "✓ Terminés",
|
||||||
|
"sf_err": "✗ Échoués",
|
||||||
|
"sf_new": "Nouveau",
|
||||||
|
"ss_date": "↓ Date",
|
||||||
|
"ss_name": "A–Z Nom",
|
||||||
|
"ss_dur": "⏱ Durée d'impression"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
"lbl_remaining": "剩余时间:",
|
"lbl_remaining": "剩余时间:",
|
||||||
"lbl_slicer_time": "切片预估:",
|
"lbl_slicer_time": "切片预估:",
|
||||||
"lbl_layers": "层",
|
"lbl_layers": "层",
|
||||||
|
"lbl_zpos": "Z (mm)",
|
||||||
"speed_silent": "🐢 静音",
|
"speed_silent": "🐢 静音",
|
||||||
"speed_normal": "⚡ 标准",
|
"speed_normal": "⚡ 标准",
|
||||||
"speed_sport": "🚀 运动",
|
"speed_sport": "🚀 运动",
|
||||||
|
|||||||
Reference in New Issue
Block a user