Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f6ea269e6 | |||
| 3fff6e25f0 | |||
| 0f5a8cbc72 | |||
| a40f14af8e | |||
| 466b8c518d | |||
| 1c5396b37d | |||
| c23deebde5 | |||
| 76738e5961 | |||
| 9c82073540 | |||
| 031e34d8ea | |||
|
|
fc89dfffa5 | ||
| ac695ecf36 | |||
| 23b8a69065 | |||
| 22dc58258c | |||
| e4b4d091f3 | |||
| ba209827ce | |||
| d26b37b332 | |||
| 6f269833d2 | |||
| d808cd3ea8 | |||
|
|
ecd444525a | ||
|
|
d4bb79a68f | ||
|
|
cdaf74985c | ||
|
|
8383c59b39 | ||
| 1645de4cad | |||
| 42898c385c | |||
| 6c5dd14dbd | |||
| c2d16270bc | |||
| fd4b9b1254 | |||
| 21cd356757 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -9,3 +9,11 @@ releases/*/extract_credentials
|
|||||||
releases/*/extract_credentials.exe
|
releases/*/extract_credentials.exe
|
||||||
|
|
||||||
!kx-bridge.spec
|
!kx-bridge.spec
|
||||||
|
|
||||||
|
# Laufzeit-Daten und Drucker-Credentials — nie committen
|
||||||
|
config/config.ini
|
||||||
|
config/*.ini
|
||||||
|
!config/config.ini.example
|
||||||
|
data/
|
||||||
|
|
||||||
|
!data/orca_filaments.json
|
||||||
|
|||||||
232
CHANGELOG.de.md
232
CHANGELOG.de.md
@@ -1,5 +1,237 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.9.20] – 2026-06-08
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- **Französische Sprachunterstützung (PR #45 von @Nathacks)**
|
||||||
|
- **Z-Höhe in der Print-UI (PR #49 von @Nathacks).** Zeigt die aktuelle
|
||||||
|
Z-Position in mm unterhalb des Layer-Zählers.
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- **Kamera-Autostart ignorierte das "Kamera bei Druckstart einschalten"-
|
||||||
|
Setting nach einem Bridge-Restart (Issue #50).** Das Setting wurde in
|
||||||
|
der Prozessumgebung gecacht — nach dem Speichern in der UI überlebte
|
||||||
|
der alte Wert den Restart und der neue Wert aus `config.ini` wurde
|
||||||
|
nicht gelesen.
|
||||||
|
- **Kamera startete nach manuellem Stopp während eines Drucks automatisch
|
||||||
|
neu (Issue #50).** Ein neues `_camera_user_stopped`-Flag unterdrückt
|
||||||
|
den Autostart für die aktuelle Drucksitzung. Es wird beim Druckende
|
||||||
|
zurückgesetzt.
|
||||||
|
- **Falscher "Stream nicht verfügbar"-Fehler-Toast beim manuellen
|
||||||
|
Kamera-Stopp.** Der Bild-Fehler-Handler war noch registriert als
|
||||||
|
`img.src` geleert wurde.
|
||||||
|
- **JS-Fehler (`ReferenceError: br is not defined`) beim Licht-Toggle.**
|
||||||
|
Variable wurde aus dem falschen Scope referenziert.
|
||||||
|
- Webcam-URLs sind jetzt absolut, damit Mobileraker/Obico-Clients sie
|
||||||
|
erreichen können.
|
||||||
|
|
||||||
|
## [0.9.19.1] – 2026-06-04
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Standalone-Binaries (Linux/Windows) zeigten `vunknown` als Version.
|
||||||
|
Die `VERSION`-Datei ist jetzt ins PyInstaller-Onefile eingebettet.
|
||||||
|
- Bei fehlenden TLS-Zertifikaten (`anycubic_slicer.crt`/`.key`) gab
|
||||||
|
es nur den rohen Fehler `[Errno 2] No such file or directory`. Die
|
||||||
|
Bridge meldet jetzt klar, wo die Dateien hingelegt werden müssen
|
||||||
|
und dass `anycubic-certs.zip` aus dem Gitea-Release stammt.
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Filament-Profil-Liste neu kuratiert: 209 statt 399 Einträge.
|
||||||
|
Profile die nur für drucker-spezifische Vendor-Bundles existieren
|
||||||
|
(z.B. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||||
|
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) sind rausgeflogen
|
||||||
|
— OrcaSlicer hätte sie im Standard-Kobra-X-Setup beim Sync
|
||||||
|
ohnehin nicht gefunden, weil die jeweiligen Vendor-Bundles nur
|
||||||
|
bei aktivem Drucker-Vendor geladen werden. Für solche Filamente
|
||||||
|
bleibt der Custom-Profile-Import (Issue #41) der Weg.
|
||||||
|
|
||||||
|
## [0.9.19] – 2026-06-02
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- **🎯 Filament-Sync mit OrcaSlicer matched jetzt das richtige Preset**
|
||||||
|
statt immer auf „Generic PLA" zu landen. Voraussetzung: ein
|
||||||
|
OrcaSlicer-Build mit dem
|
||||||
|
[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)
|
||||||
|
Empfangs-Patch (im OrcaSlicer-KX-Build dabei). Die Bridge sendet pro
|
||||||
|
AMS-Slot jetzt `name` + `vendor_name` im Lane-Pfad UND
|
||||||
|
`gate_filament_name` im Happy-Hare-MMU-Pfad (OrcaSlicer wechselt bei
|
||||||
|
AMS-Setups automatisch auf den HH-Pfad).
|
||||||
|
- **Eigene OrcaSlicer-Profile in die Bridge importieren (Issue #41).**
|
||||||
|
Settings-Tab → „OrcaSlicer-Profile" oder direkt im Slot-Edit-Dialog
|
||||||
|
(„★ Eigene Profile importieren…") lädst du deine `.json`-Files aus
|
||||||
|
`~/.config/OrcaSlicer/user/<id>/filament/` hoch — einzeln oder als
|
||||||
|
ZIP. Erscheint dann im Slot-Dropdown unter „★ Eigene Profile" und
|
||||||
|
wird beim Sync an Orca als User-Match weitergegeben. Funktioniert
|
||||||
|
über HTTP, also auch wenn die Bridge im Docker auf Raspi/NAS läuft
|
||||||
|
und OrcaSlicer auf dem Desktop. Auch reine Override-Profile mit nur
|
||||||
|
`inherits: "Generic PLA @System"` + ein paar Tweaks werden korrekt
|
||||||
|
erkannt — die Bridge resolved die vererbten Felder aus dem
|
||||||
|
System-Parent.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **AMS-Sync landete hartnäckig auf „Generic PLA":** das Orca-
|
||||||
|
Datenmodell hat 68 duplikate `filament_id`-Werte (`OGFL99` allein
|
||||||
|
136 mal), und die Bridge wählte oft eine ID die für den Kobra X
|
||||||
|
nicht `is_compatible` war (z.B. `GFL92` aus dem Kobra-2-Profil →
|
||||||
|
Orca verwarf es). Generator priorisiert jetzt Kobra-X-Varianten und
|
||||||
|
filtert Phantom-Profile (Cross-Vendor-Overrides) raus —
|
||||||
|
`orca_filaments.json` von 1035 → 400 saubere Profile.
|
||||||
|
- **Slot ohne expliziten Override sendet jetzt `Generic <Typ>`** statt
|
||||||
|
einer impliziten Vendor-Annahme. Library-Generic-Profile haben
|
||||||
|
`compatible_printers: []` (= alle Drucker), sind also immer sichtbar
|
||||||
|
und matchen verlässlich.
|
||||||
|
- **Slot-Karte zeigt den Hersteller direkt nach dem Speichern** —
|
||||||
|
ohne Browser-Reload. `poll()` war async, das Re-Render kam erst
|
||||||
|
beim nächsten Tick.
|
||||||
|
- **ACE-Trockner-Toggle warf 502-Fehler obwohl der Trockner ein-/aus-
|
||||||
|
ging (PR #42 von @gangoke):** `setDry` jetzt fire-and-forget wie
|
||||||
|
`setAutoFeed`. Der Drucker antwortet auf diesem Push-Topic mit
|
||||||
|
`code: 0` statt `code: 200`, das hat die Bridge fälschlich als
|
||||||
|
Fehler interpretiert.
|
||||||
|
|
||||||
|
### Datenmodell / API
|
||||||
|
- `orca_filaments.json` regeneriert: nur echte Vendor-Profile und
|
||||||
|
OrcaFilamentLibrary-Profile bleiben drin. Bambu/Polymaker/SUNLU-
|
||||||
|
Library-Profile dabei, Qidi-Cross-Bundles raus.
|
||||||
|
- Neue Endpoints: `POST /kx/filament/profiles/user` (ZIP/JSON-Upload),
|
||||||
|
`GET /kx/filament/profiles/user` (Liste der User-Imports),
|
||||||
|
`DELETE /kx/filament/profiles/user[?vendor=…&name=…]`. Persistenz
|
||||||
|
in `<KX_DATA_DIR>/orca_filaments.user.json` (Volume — überlebt
|
||||||
|
Image-Updates).
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- Neues Modul `bridge/orca_filaments.py` (gemeinsame Parser-Helfer
|
||||||
|
für Generator und Import-Endpoint).
|
||||||
|
- Dockerfile + release.sh um `orca_filaments.py` erweitert.
|
||||||
|
|
||||||
|
## [0.9.18] – 2026-05-31
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- **🎉 Filament-Material und -Farbe pro AMS-Slot aus der Bridge an den
|
||||||
|
Drucker senden:** Im Slot-Edit-Dialog gewählte Werte gehen jetzt
|
||||||
|
tatsächlich an den Drucker und werden persistent übernommen — am
|
||||||
|
Drucker-Display siehst du sofort dieselbe Belegung wie in der Bridge-UI.
|
||||||
|
In 0.9.17 wurde der Befehl über das falsche MQTT-Topic
|
||||||
|
(`slicer/printer/…`) gesendet, der Drucker hat ihn stillschweigend
|
||||||
|
ignoriert. Jetzt geht er über `web/printer/…` wie der Anycubic
|
||||||
|
Slicer Next es macht (verifiziert via Live-MQTT-Sniff +
|
||||||
|
Workbench-Vue-Source). **Achtung:** der Drucker muss im Idle-Zustand
|
||||||
|
sein, und leere Slots lassen sich nicht beschriften.
|
||||||
|
- **Mehrsprachiges UI – spanische Übersetzung von Muttersprachler
|
||||||
|
überarbeitet (PR #40 von @pezfisk):** fehlende Akzente
|
||||||
|
(impresión, cámara, después, animación, …), Begriffe vereinheitlicht
|
||||||
|
(Pause → Pausa, Start → Iniciar, Layer → Capa). Plus neues
|
||||||
|
`README.es.md` und Cross-Links in den drei READMEs.
|
||||||
|
- **Z-Höhen-Anzeige in Obico** funktioniert jetzt. Der Drucker liefert
|
||||||
|
keine echte Z-Position via MQTT (per Live-Sniff bestätigt), die Bridge
|
||||||
|
schätzt sie aus `curr_layer × layer_height + first_layer_height`.
|
||||||
|
Layer-Heights kommen aus dem GCode-Header beim Upload, persistiert
|
||||||
|
im GCode-Store; Fallback aus dem OrcaSlicer-Default-Filename
|
||||||
|
(`…_0.2_…gcode`). `/server/files/metadata` liefert zusätzlich
|
||||||
|
`object_height` (Gesamt-Z), damit Obicos `mmProgress`-Widget
|
||||||
|
`aktuelles Z / Gesamt-Z` anzeigt.
|
||||||
|
- **Slot-Karte zeigt den OrcaSlicer-Profil-Vendor** unter dem Material
|
||||||
|
(z.B. „PLA / Polymaker"), mit Profil-Namen + interner ID als
|
||||||
|
Tooltip. So ist auf einen Blick erkennbar welcher Slot-Override
|
||||||
|
aktiv ist.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Slot-Profil-Auswahl im AMS-Dialog (Issue #39 von @harrygeier):**
|
||||||
|
drei separate Bugs in 0.9.17 sorgten dafür dass die gewählte Marke
|
||||||
|
nach dem Speichern verschwand und beim erneuten Öffnen ein falsches
|
||||||
|
Material angezeigt wurde.
|
||||||
|
- `multiColorBox/setInfo` über das falsche MQTT-Topic — siehe oben.
|
||||||
|
- Speichern lief in zwei parallelen Requests (Profil-Override +
|
||||||
|
Material/Farbe) → Race-Bedingung. Läuft jetzt sequenziell und
|
||||||
|
reloaded den lokalen State bevor der Dialog geschlossen wird.
|
||||||
|
- OrcaSlicer-Filament-IDs sind nicht eindeutig — `orca_filaments.json`
|
||||||
|
hat 68 duplikate IDs, `OGFL99` allein ist 136 Vendor-Profilen
|
||||||
|
zugeordnet (Erkenntnis von @gangoke). Der primäre Selector ist
|
||||||
|
jetzt `(vendor, name)` — über alle 1002 Profile eindeutig.
|
||||||
|
- **MQTT-Reconnect (Issue #33 von @icebear):** wurde der Drucker über
|
||||||
|
Nacht ausgeschaltet, schlug die Bridge nach 5 Reconnect-Versuchen
|
||||||
|
(~60 s gesamt) endgültig fehl — Filament-Sync ging morgens noch
|
||||||
|
(weil das HTTP ist), aber Print-Start scheiterte mit
|
||||||
|
„connection refused", User musste die Bridge selbst neu starten.
|
||||||
|
Reader-Thread reconnectet jetzt **endlos** (Backoff cappt bei 60 s)
|
||||||
|
bis der Drucker wieder antwortet, mit DEBUG-Logging nach den ersten
|
||||||
|
5 Versuchen damit das Log nicht über Nacht zugemüllt wird.
|
||||||
|
- **„Unknown child pid"-Warnungen im Log:** beim Killen der Kamera-
|
||||||
|
`ffmpeg`-Prozesse fehlte das `wait()` — Children blieben als
|
||||||
|
Zombies und asyncio meldete sie alle ~20 s. Gefixt im CameraCache
|
||||||
|
+ `/api/camera/stream`.
|
||||||
|
|
||||||
|
### UI-Aufräumen
|
||||||
|
- **Pause-Button als Toggle:** druckt der Drucker → `⏸ Pause`, ist
|
||||||
|
pausiert → `▶ Weiter`. Der separate „Weiter"-Button entfällt.
|
||||||
|
- **Pause + Stopp komplett ausgeblendet wenn Drucker idle** — bei
|
||||||
|
Standby waren beide Buttons vorher dauerhaft sichtbar, was beim
|
||||||
|
Idle-Drucker verwirrend wirkte.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- **GCode-Store-Migration:** neue Spalten `layer_height` +
|
||||||
|
`first_layer_height` in `gcode_files` (automatisch beim ersten
|
||||||
|
Start von 0.9.18 angelegt).
|
||||||
|
|
||||||
|
## [0.9.17] – 2026-05-30
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- **🧪 Obico-Anbindung (experimentell):** Die Bridge spielt jetzt einen
|
||||||
|
Moonraker, der vom [moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||||
|
Plugin akzeptiert wird. Damit funktionieren Time-Lapse, Layer-aligned
|
||||||
|
First-Layer-Scan und WebRTC-Live-Stream gegen einen (selbst gehosteten oder
|
||||||
|
Cloud-) Obico-Server. **Hinweis:** Das KI-Modell zur Spaghetti-Erkennung
|
||||||
|
ist auf seitliche Kamera-Winkel (Ender/Voron) trainiert — wie zuverlässig
|
||||||
|
es beim Kobra X mit Top-Down-Kamera funktioniert, muss empirisch getestet
|
||||||
|
werden (bei uns ging es schon ganz gut). Stream, Time-Lapse und Telemetrie
|
||||||
|
laufen, die Failure-Erkennung ist deshalb noch als experimentell markiert.
|
||||||
|
- **Mehrsprachiges UI (PR #37 von @gangoke):** Inline-Translations sind raus,
|
||||||
|
stattdessen wechselbares Sprach-Dropdown mit Globe-Icon. Auto-Auswahl nach
|
||||||
|
Browser-Locale, manuelle Wahl wird im LocalStorage gemerkt. Sprachen: 🇩🇪 🇬🇧
|
||||||
|
🇪🇸 🇨🇳 (ES + ZH-CN sind KI-übersetzt und noch nicht von Muttersprachlern
|
||||||
|
geprüft).
|
||||||
|
- **OrcaSlicer-Filament-Profil pro AMS-Slot:** Im Slot-Bearbeiten-Dialog kannst
|
||||||
|
du jetzt ein konkretes OrcaSlicer-Profil (z.B. „PolyTerra PLA — Polymaker")
|
||||||
|
pro Slot wählen — die Bridge sendet diese Information beim AMS-Sync mit,
|
||||||
|
statt nur „Generic PLA". Die Profil-Liste wird aus dem OrcaSlicer-Source
|
||||||
|
generiert (~1000 Profile, 43 Hersteller). Damit OrcaSlicer den Hint
|
||||||
|
vollständig respektiert, wird ein passender Patch im OrcaSlicer-KX-Build
|
||||||
|
folgen.
|
||||||
|
- **H.264-Direkt-Stream:** Neuer Endpunkt `/api/camera/h264` liefert den
|
||||||
|
Drucker-Kamera-Stream ohne Re-Encoding als MPEG-TS — Latenz drastisch
|
||||||
|
reduziert, Bridge-CPU bei Obico-Stream von ~13 % auf ~3 %.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Temperatur-Setzen über Bridge-UI / Obico löste Drucker-Systemfehler aus:**
|
||||||
|
Per Live-MQTT-Sniff vom Anycubic Slicer Next korrigiert — der Befehl
|
||||||
|
`tempature/set` braucht ein `type`-Feld (0=Nozzle, 1=Bett, 2=beide) und
|
||||||
|
muss über das `web/printer/…`-Topic, nicht `slicer/printer/…`. Nozzle/Bett
|
||||||
|
über die Bridge heizen jetzt sauber.
|
||||||
|
- **Große GCode-Uploads (>50 MB) brachen mit Timeout ab:** Der
|
||||||
|
Connect-Timeout vom Socket lief auch während des `sendall()` — bei ~200 MB
|
||||||
|
über LAN brauchte das Schieben mehr als die 30 s und wurde fälschlich als
|
||||||
|
Connect-Timeout abgebrochen. Jetzt sind Connect-, Send- und Read-Phase
|
||||||
|
separat getimeoutet.
|
||||||
|
- **Kamera-Snapshot war langsam und konnte sich mit dem Live-Stream blockieren:**
|
||||||
|
Die Bridge hält nun einen zentralen Kamera-Cache (ein einziger ffmpeg-Prozess
|
||||||
|
zieht vom Drucker, alle Konsumenten teilen sich den Stream). Snapshots
|
||||||
|
kommen in ~1.3 ms aus dem RAM statt nach 1-2 s per neuer ffmpeg-Instanz.
|
||||||
|
Behebt außerdem das Single-Client-Limit am Drucker (HTTP 429 bei parallelen
|
||||||
|
Zugriffen).
|
||||||
|
- **Sprachwechsel aktualisierte den GCode-Browser nicht:** Die in die
|
||||||
|
File-Karten eingebackenen Texte („Drucken", „Schätzung", „Download") blieben
|
||||||
|
in der alten Sprache. Beim Sprachwechsel werden die Karten jetzt neu
|
||||||
|
gerendert.
|
||||||
|
- **GCode Web-Upload + Download + Verify-Dialog (PR #32 von @gangoke):**
|
||||||
|
Dateien können direkt im Browser hoch/runtergeladen werden, mit
|
||||||
|
Warn-Dialog wenn ein nicht durch OrcaSlicer hochgeladener GCode gestartet
|
||||||
|
wird.
|
||||||
|
|
||||||
|
### CI/Build
|
||||||
|
- Multi-Arch Docker-Image (amd64 + arm64) per Gitea-Actions automatisiert.
|
||||||
|
- Release-Build über lokalen CodeBuilder für alle drei Targets
|
||||||
|
(linux-amd64, linux-arm64, windows.exe).
|
||||||
|
|
||||||
## [0.9.16] – 2026-05-22
|
## [0.9.16] – 2026-05-22
|
||||||
|
|
||||||
### Neu
|
### Neu
|
||||||
|
|||||||
232
CHANGELOG.md
232
CHANGELOG.md
@@ -1,5 +1,237 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.9.20] – 2026-06-08
|
||||||
|
|
||||||
|
### New
|
||||||
|
- **French language support (PR #45 by @Nathacks)**
|
||||||
|
- **Z height display in the print UI (PR #49 by @Nathacks).** Shows
|
||||||
|
current Z position in mm below the layer counter.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Camera auto-start ignored "Enable camera on print start" setting
|
||||||
|
after a bridge restart (issue #50).** The setting was cached in the
|
||||||
|
process environment — after saving it in the UI, the old value
|
||||||
|
survived the restart and the new value from `config.ini` was never
|
||||||
|
read.
|
||||||
|
- **Camera restarted automatically after manual stop during a print
|
||||||
|
(issue #50).** A new `_camera_user_stopped` flag suppresses
|
||||||
|
auto-restart for the current print session. It resets when the
|
||||||
|
print ends.
|
||||||
|
- **Spurious "stream unavailable" error toast when stopping the camera
|
||||||
|
manually.** The image error handler was still registered when
|
||||||
|
`img.src` was cleared.
|
||||||
|
- **JS error (`ReferenceError: br is not defined`) when toggling the
|
||||||
|
light.** Variable was referenced from the wrong scope.
|
||||||
|
- Webcam URLs are now absolute so that Mobileraker/Obico clients can
|
||||||
|
reach them.
|
||||||
|
|
||||||
|
## [0.9.19.1] – 2026-06-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Standalone binaries (Linux/Windows) reported `vunknown` as their
|
||||||
|
version. The `VERSION` file is now embedded into the PyInstaller
|
||||||
|
onefile bundle.
|
||||||
|
- When the TLS certificates (`anycubic_slicer.crt`/`.key`) were
|
||||||
|
missing, the bridge only logged the raw `[Errno 2] No such file
|
||||||
|
or directory`. It now states clearly where the files need to be
|
||||||
|
placed and that `anycubic-certs.zip` from the Gitea release is the
|
||||||
|
source.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Filament profile list re-curated: 209 entries instead of 399.
|
||||||
|
Profiles that only exist inside printer-specific vendor bundles
|
||||||
|
(e.g. Eryone Thinker X400, Artillery M1 Pro, WonderMaker ZR,
|
||||||
|
Tiertime, Cubicon, CoLiDo, Afinia, Snapmaker) were dropped —
|
||||||
|
OrcaSlicer wouldn't have found them in a default Kobra X setup
|
||||||
|
anyway, because the matching vendor bundle is only loaded when
|
||||||
|
the corresponding printer vendor is active. For those filaments
|
||||||
|
the custom profile import (issue #41) remains the way.
|
||||||
|
|
||||||
|
## [0.9.19] – 2026-06-02
|
||||||
|
|
||||||
|
### New
|
||||||
|
- **🎯 Filament sync with OrcaSlicer now picks the right preset**
|
||||||
|
instead of always falling back to "Generic PLA". Requires an
|
||||||
|
OrcaSlicer build with the
|
||||||
|
[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)
|
||||||
|
receive-side patch (included in the OrcaSlicer-KX build). The bridge
|
||||||
|
sends `name` + `vendor_name` per AMS slot on the lane path AND
|
||||||
|
`gate_filament_name` per gate on the Happy-Hare MMU path (OrcaSlicer
|
||||||
|
switches to the HH path automatically for AMS setups).
|
||||||
|
- **Import your own OrcaSlicer profiles into the bridge (issue #41).**
|
||||||
|
Settings → "OrcaSlicer Profiles" or directly in the slot-edit dialog
|
||||||
|
("★ Import own profiles…") lets you upload `.json` files from
|
||||||
|
`~/.config/OrcaSlicer/user/<id>/filament/` — single files or as a
|
||||||
|
ZIP. They show up in the slot dropdown under "★ Own profiles" and
|
||||||
|
are passed through to Orca on sync as user matches. Works over HTTP
|
||||||
|
so the bridge can run in Docker on a Raspi/NAS while OrcaSlicer
|
||||||
|
lives on a desktop. Override-only profiles with just
|
||||||
|
`inherits: "Generic PLA @System"` + a few tweaks are detected
|
||||||
|
correctly — the bridge resolves the inherited fields from the
|
||||||
|
system parent.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **AMS sync stuck on "Generic PLA":** the Orca data model has 68
|
||||||
|
duplicate `filament_id` values (`OGFL99` alone shared by 136
|
||||||
|
profiles), and the bridge often picked an ID that was not
|
||||||
|
`is_compatible` with the Kobra X (e.g. `GFL92` from the Kobra-2
|
||||||
|
profile → Orca rejected it). The generator now prioritises Kobra-X
|
||||||
|
variants and filters out phantom profiles (cross-vendor overrides) —
|
||||||
|
`orca_filaments.json` dropped from 1035 to 400 clean profiles.
|
||||||
|
- **Slot without an explicit override now sends `Generic <type>`**
|
||||||
|
instead of an implicit vendor guess. Library generic profiles have
|
||||||
|
`compatible_printers: []` (= all printers), so they are always
|
||||||
|
visible and match reliably.
|
||||||
|
- **Slot card shows the vendor right after save** without a browser
|
||||||
|
reload. `poll()` was async, the re-render only happened on the next
|
||||||
|
tick.
|
||||||
|
- **ACE dryer toggle threw a 502 even though the dryer worked
|
||||||
|
(PR #42 by @gangoke):** `setDry` is now fire-and-forget like
|
||||||
|
`setAutoFeed`. The printer answers on that push topic with
|
||||||
|
`code: 0` instead of `code: 200`, which the bridge wrongly treated
|
||||||
|
as an error.
|
||||||
|
|
||||||
|
### Data model / API
|
||||||
|
- `orca_filaments.json` regenerated: only real vendor profiles and
|
||||||
|
OrcaFilamentLibrary profiles. Bambu/Polymaker/SUNLU library profiles
|
||||||
|
in, Qidi cross-bundles out.
|
||||||
|
- New endpoints: `POST /kx/filament/profiles/user` (ZIP/JSON upload),
|
||||||
|
`GET /kx/filament/profiles/user` (list user imports),
|
||||||
|
`DELETE /kx/filament/profiles/user[?vendor=…&name=…]`. Persisted in
|
||||||
|
`<KX_DATA_DIR>/orca_filaments.user.json` (volume — survives image
|
||||||
|
updates).
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- New module `bridge/orca_filaments.py` (shared parser helpers used by
|
||||||
|
the generator and the import endpoint).
|
||||||
|
- Dockerfile + release.sh updated to include `orca_filaments.py`.
|
||||||
|
|
||||||
|
## [0.9.18] – 2026-05-31
|
||||||
|
|
||||||
|
### New
|
||||||
|
- **🎉 Push filament material and colour from the bridge to the
|
||||||
|
printer:** The values you pick in the slot-edit dialog now actually
|
||||||
|
reach the printer and stick — the printer display shows the same
|
||||||
|
slot setup as the bridge UI right away. In 0.9.17 the command was
|
||||||
|
sent over the wrong MQTT topic (`slicer/printer/…`) and the printer
|
||||||
|
silently dropped it. It now goes via `web/printer/…` like the
|
||||||
|
Anycubic Slicer Next does (verified by live MQTT sniff +
|
||||||
|
Workbench-Vue source). **Note:** the printer must be idle, and
|
||||||
|
empty slots can not be labelled.
|
||||||
|
- **Spanish translation reviewed by a native speaker (PR #40 by
|
||||||
|
@pezfisk):** missing accents (impresión, cámara, después,
|
||||||
|
animación, …) and term consistency (Pause → Pausa, Start →
|
||||||
|
Iniciar, Layer → Capa). New `README.es.md` and cross-links between
|
||||||
|
the three READMEs.
|
||||||
|
- **Z-height now shows up in Obico.** The printer does not report a
|
||||||
|
real Z position over MQTT (live-sniff confirmed), so the bridge
|
||||||
|
estimates it from `current_layer × layer_height + first_layer_height`.
|
||||||
|
Layer heights are parsed from the gcode header at upload time and
|
||||||
|
persisted in the gcode store; fallback for prints started directly
|
||||||
|
from the slicer is the OrcaSlicer default filename pattern
|
||||||
|
(`…_0.2_…gcode`). `/server/files/metadata` also serves
|
||||||
|
`object_height` (total Z) so Obicos `mmProgress` widget can render
|
||||||
|
`current Z / total Z`.
|
||||||
|
- **Slot card shows the OrcaSlicer profile vendor** under the
|
||||||
|
material (e.g. `PLA / Polymaker`), with the profile name + internal
|
||||||
|
ID as tooltip. Lets you see at a glance which slot override is
|
||||||
|
active.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Slot profile picker in the AMS dialog (issue #39 by
|
||||||
|
@harrygeier):** three separate bugs in 0.9.17 caused the chosen
|
||||||
|
brand to disappear after save and a different material to show up
|
||||||
|
on re-open.
|
||||||
|
- `multiColorBox/setInfo` was sent on the wrong MQTT topic — see
|
||||||
|
above.
|
||||||
|
- Save fired two parallel requests (profile override + material/
|
||||||
|
colour) → race. Now sequential, and the local state is reloaded
|
||||||
|
before the dialog closes.
|
||||||
|
- OrcaSlicer filament IDs are not unique — `orca_filaments.json`
|
||||||
|
has 68 duplicate IDs, `OGFL99` alone is shared by 136 vendor
|
||||||
|
profiles (caught by @gangoke). The primary selector is now
|
||||||
|
`(vendor, name)` — unique across all 1002 profiles.
|
||||||
|
- **MQTT reconnect (issue #33 by @icebear):** if the printer was
|
||||||
|
powered off overnight the bridge gave up after 5 reconnect attempts
|
||||||
|
(~60 s total) — filament sync still worked in the morning (its
|
||||||
|
HTTP), but starting a print failed with `connection refused` and
|
||||||
|
the user had to restart the bridge itself. The reader thread now
|
||||||
|
reconnects **forever** (backoff caps at 60 s) until the printer
|
||||||
|
responds again, with logs dropping to DEBUG after the first 5
|
||||||
|
attempts so an overnight outage does not spam the log.
|
||||||
|
- **`Unknown child pid` warnings in the log:** the camera ffmpeg
|
||||||
|
helpers were killed without awaiting their `wait()` — children
|
||||||
|
lingered as zombies and asyncio reported them every ~20 s. Fixed
|
||||||
|
in CameraCache + `/api/camera/stream`.
|
||||||
|
|
||||||
|
### UI polish
|
||||||
|
- **Pause button is now a toggle:** while printing → `⏸ Pause`,
|
||||||
|
while paused → `▶ Resume`. The separate resume button is gone.
|
||||||
|
- **Pause + stop hidden when the printer is idle** — both used to be
|
||||||
|
visible at all times, which was confusing on a standby printer.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- **gcode store migration:** new columns `layer_height` +
|
||||||
|
`first_layer_height` on `gcode_files` (added automatically on first
|
||||||
|
start of 0.9.18).
|
||||||
|
|
||||||
|
## [0.9.17] – 2026-05-30
|
||||||
|
|
||||||
|
### New
|
||||||
|
- **🧪 Obico integration (experimental):** The bridge now exposes a
|
||||||
|
Moonraker-compatible surface that the
|
||||||
|
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||||
|
plugin accepts. Time-lapses, layer-aligned first-layer scan and WebRTC
|
||||||
|
live streaming work against a (self-hosted or cloud) Obico server.
|
||||||
|
**Note:** the spaghetti-detection ML model is trained on side-view
|
||||||
|
cameras (Ender/Voron); how well it works with the Kobra X's top-down
|
||||||
|
camera is still to be evaluated empirically (it already looked
|
||||||
|
promising in our tests). Stream, time-lapse and telemetry work — the
|
||||||
|
failure-detection side stays flagged as experimental for now.
|
||||||
|
- **Multi-language UI (PR #37 by @gangoke):** Inline translations have
|
||||||
|
moved into JSON files; a globe-icon dropdown lets you switch language.
|
||||||
|
Browser locale is auto-detected; manual choice persists in
|
||||||
|
LocalStorage. Languages: 🇩🇪 🇬🇧 🇪🇸 🇨🇳 (ES + ZH-CN are AI-translated
|
||||||
|
and not verified by native speakers yet).
|
||||||
|
- **OrcaSlicer filament profile per AMS slot:** The slot-edit dialog now
|
||||||
|
lets you pick a concrete OrcaSlicer profile (e.g. "PolyTerra PLA —
|
||||||
|
Polymaker") per slot; the bridge sends it along on AMS sync instead
|
||||||
|
of just "Generic PLA". Profile list is generated from the OrcaSlicer
|
||||||
|
source (~1000 profiles, 43 vendors). A matching patch in
|
||||||
|
OrcaSlicer-KX is on the way so OrcaSlicer fully honours the hint.
|
||||||
|
- **H.264 direct stream:** New `/api/camera/h264` endpoint serves the
|
||||||
|
printer camera stream as MPEG-TS without re-encoding — dramatically
|
||||||
|
reduces latency, bridge CPU during Obico streaming drops from ~13 %
|
||||||
|
to ~3 %.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Setting temperature via bridge UI / Obico caused a printer system
|
||||||
|
error:** Fixed via live MQTT capture from Anycubic Slicer Next — the
|
||||||
|
`tempature/set` command needs a `type` field (0=nozzle, 1=bed,
|
||||||
|
2=both) and must go over the `web/printer/…` topic, not
|
||||||
|
`slicer/printer/…`. Nozzle/bed heating from the bridge now works.
|
||||||
|
- **Large GCode uploads (>50 MB) timed out:** The socket connect timeout
|
||||||
|
was active during `sendall()` too — pushing ~200 MB over LAN took
|
||||||
|
more than 30 s and was falsely aborted. Connect / send / read phases
|
||||||
|
are now timed out separately.
|
||||||
|
- **Camera snapshots were slow and could collide with the live stream:**
|
||||||
|
The bridge now keeps a central camera cache (one ffmpeg pulls from
|
||||||
|
the printer, all consumers share it). Snapshots return in ~1.3 ms
|
||||||
|
from RAM instead of 1–2 s per spawned ffmpeg. Also resolves the
|
||||||
|
single-client limit on the printer (HTTP 429 on parallel access).
|
||||||
|
- **Language switch did not refresh the GCode browser:** Strings baked
|
||||||
|
into the file cards ("Print", "Estimate", "Download") stayed in the
|
||||||
|
previous language. Cards are now re-rendered on language switch.
|
||||||
|
- **GCode web upload + download + verify dialog (PR #32 by @gangoke):**
|
||||||
|
Files can be uploaded / downloaded directly in the browser, with a
|
||||||
|
warning dialog when starting a GCode that was not uploaded via
|
||||||
|
OrcaSlicer.
|
||||||
|
|
||||||
|
### CI/Build
|
||||||
|
- Multi-arch Docker image (amd64 + arm64) automated via Gitea Actions.
|
||||||
|
- Release builds for all three targets (linux-amd64, linux-arm64,
|
||||||
|
windows.exe) via the local CodeBuilder.
|
||||||
|
|
||||||
## [0.9.16] – 2026-05-22
|
## [0.9.16] – 2026-05-22
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ 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
|
||||||
|
|
||||||
COPY kobrax_moonraker_bridge.py .
|
COPY kobrax_moonraker_bridge.py .
|
||||||
COPY web/ ./web/
|
COPY web/ ./web/
|
||||||
|
# Statische Daten (orca_filaments.json etc.) liegen in /app/static/, NICHT in
|
||||||
|
# /app/data/ — letzteres wird vom User als Volume gemountet (Runtime-State).
|
||||||
|
COPY data/ ./static/
|
||||||
COPY config_loader.py .
|
COPY config_loader.py .
|
||||||
COPY env_loader.py .
|
COPY env_loader.py .
|
||||||
COPY kobrax_client.py .
|
COPY kobrax_client.py .
|
||||||
|
COPY orca_filaments.py .
|
||||||
COPY VERSION .
|
COPY VERSION .
|
||||||
COPY anycubic_slicer.crt .
|
COPY anycubic_slicer.crt .
|
||||||
COPY anycubic_slicer.key .
|
COPY anycubic_slicer.key .
|
||||||
|
|||||||
57
README.de.md
57
README.de.md
@@ -8,7 +8,12 @@
|
|||||||
|
|
||||||
Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
||||||
|
|
||||||
<sub>🇬🇧 <a href="README.md">English version</a></sub>
|
<sub>🧪 Ein Community-Bericht auf Reddit deutet darauf hin, dass die Bridge auch
|
||||||
|
mit dem **Kobra S1** und **Kobra S1 Max** funktioniert — die Protokolle wirken
|
||||||
|
kompatibel, beides ist aber weder offiziell getestet noch unterstützt.
|
||||||
|
Feedback willkommen.</sub>
|
||||||
|
|
||||||
|
<sub>🇬🇧 <a href="README.md">English version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@@ -32,12 +37,17 @@ Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 🖨️ | **Druckersteuerung** — Start, Pause, Resume, Abbruch, Temperaturen, Druckgeschwindigkeit |
|
| 🖨️ | **Druckersteuerung** — Start, Pause, Resume, Abbruch, Temperaturen, Druckgeschwindigkeit |
|
||||||
| 📊 | **Live-Status** — Temperatur, Fortschritt, Layer, Restzeit, Kamera-Stream |
|
| 📊 | **Live-Status** — Temperatur, Fortschritt, Layer, Restzeit, Kamera-Stream |
|
||||||
| 🎨 | **AMS / Multicolor** — Filament-Slots, Per-Kanal-Remapping, MMU-Emulation für OrcaSlicer Filament-Sync |
|
| 🎨 | **AMS / Multicolor** — Slots mit **Profil-Picker pro Slot** (eigene Marke aus OrcaSlicer-Profilen pro Slot zuweisen); Bridge schreibt Material und Farbe ans Drucker-Display zurück |
|
||||||
|
| 📦 | **Eigene OrcaSlicer-Profile importieren** — ZIP aus `~/.config/OrcaSlicer/user/<id>/filament/` in die Bridge ziehen; tauchen im Slot-Dropdown unter ★ Eigene Profile auf |
|
||||||
|
| 📷 | **Obico-Integration (experimentell)** — Time-Lapse und WebRTC-Livestream gegen einen selbst gehosteten [Obico-Server](https://github.com/TheSpaghettiDetective/obico-server) via moonraker-obico |
|
||||||
|
| 📐 | **H.264-Direkt-Stream + Z-Höhe** — sparsamer Kamera-Pfad für Obico, aktuelle Z aus der Layer-Höhe abgeleitet (Mm-Progress-Widget) |
|
||||||
| 🗂️ | **GCode-Browser** — hochgeladene Dateien mit Thumbnail, Druckhistorie, Suche & Filter |
|
| 🗂️ | **GCode-Browser** — hochgeladene Dateien mit Thumbnail, Druckhistorie, Suche & Filter |
|
||||||
| 🧩 | **Multi-Printer** — mehrere Drucker in **einer** Bridge-Instanz, Umschalten per Dropdown |
|
| 🧩 | **Multi-Printer** — mehrere Drucker in **einer** Bridge-Instanz, Umschalten per Dropdown |
|
||||||
| ➕ | **Drucker hinzufügen per Klick** — nur die IP eingeben, Zugangsdaten werden automatisch importiert |
|
| ➕ | **Drucker hinzufügen per Klick** — nur die IP eingeben, Zugangsdaten werden automatisch importiert |
|
||||||
|
| 🔁 | **Robuster MQTT-Reconnect** — Bridge überlebt nächtlichen Drucker-Reboot ohne manuellen Neustart |
|
||||||
|
| 🌐 | **Mehrsprachiges UI** — DE / EN / ES / 中文, Browser-Sprache automatisch erkannt |
|
||||||
| 🔄 | **Self-Update** — neue Versionen direkt im Browser installieren |
|
| 🔄 | **Self-Update** — neue Versionen direkt im Browser installieren |
|
||||||
| 🌐 | **OrcaSlicer** — volles Moonraker-Protokoll (HTTP + WebSocket), DE/EN UI |
|
| 🧠 | **OrcaSlicer** — volles Moonraker-Protokoll (HTTP + WebSocket); für korrekten Vendor-Match pro Slot den [OrcaSlicer-KX-Build](#-empfohlener-slicer) nutzen |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -101,11 +111,45 @@ Drucker → Verbindungstyp **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🎨 Empfohlener Slicer
|
||||||
|
|
||||||
|
Für sauberen AMS-Filament-Sync gibt es einen **gepatchten OrcaSlicer-Build**:
|
||||||
|
|
||||||
|
→ **[OrcaSlicer-KX Releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||||
|
|
||||||
|
**Upstream-PRs im KX-Build:**
|
||||||
|
|
||||||
|
- **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Moonraker / Happy-Hare AMS-Sync-Fix (Slot-Positionen bleiben auch bei leerem Slot in der Mitte korrekt)
|
||||||
|
- **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Vendor- + Name-Matching für Moonraker (liest `name` + `vendor_name` pro Slot und matched gegen deine Filament-Presets), von [@LordGuenni](https://github.com/LordGuenni)
|
||||||
|
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — Eindeutige `filament_id` für User-Presets (neu erstellte eigene Profile bekommen eine frische ID statt das `OGFL99` vom Generic-Parent zu erben), von [@mrnoisytiger](https://github.com/mrnoisytiger)
|
||||||
|
|
||||||
|
**Plus vier KX-eigene Verbesserungen on top:**
|
||||||
|
|
||||||
|
- Bridge-Filament-Hint (`tray_info_idx` + Vendor) respektieren
|
||||||
|
- Vendor-Match auch wenn das gewählte Base-Preset **nicht is_compatible** mit dem aktiven Drucker ist (so matchen Profile aus anderen Drucker-Setups trotzdem über die Marke)
|
||||||
|
- Vendor-Match wenn `tray_info_idx` gesetzt ist, das Preset aber inkompatibel
|
||||||
|
- Zwei-Pass-Suche: erst kompatible Presets, dann alle sichtbaren
|
||||||
|
|
||||||
|
**Warum das zusammen wichtig ist:** ohne #13719 landen die AMS-Slots in OrcaSlicer alle auf `Generic PLA` / `Generic PETG`, obwohl die Bridge die konkrete Marke schon mitsendet (`name + vendor_name + gate_filament_name`). Mit dem KX-Build matched OrcaSlicer deine echten User-Presets — auch die, die du via [Eigene OrcaSlicer-Profile importieren](#-features) in die Bridge gezogen hast.
|
||||||
|
|
||||||
|
Stock-Upstream-OrcaSlicer funktioniert für Slicing und Drucken weiterhin — nur das Per-Slot-Vendor-Matching beim AMS-Sync fällt dann weg. Material und Farbe pro Slot kannst du auch ohne den KX-Build über die Bridge ans Drucker-Display schreiben (das läuft über MQTT, nicht über den Slicer).
|
||||||
|
|
||||||
|
OrcaSlicer-KX ist ein Build von [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); der Quellcode der Upstream-PRs ist auf GitHub, die KX-spezifischen Patches im OrcaSlicer-KX-Repo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🏠 Community & Integrationen
|
## 🏠 Community & Integrationen
|
||||||
|
|
||||||
- **[Home-Assistant-Integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
- **[Home-Assistant-Integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||||
von [@gangoke](https://github.com/gangoke) — bindet Sensoren, Drucksteuerung,
|
von [@gangoke](https://github.com/gangoke) — bindet Sensoren, Drucksteuerung,
|
||||||
Licht, Kamera und das GCode-Vorschaubild als native Home-Assistant-Entitäten ein.
|
Licht, Kamera und das GCode-Vorschaubild als native Home-Assistant-Entitäten ein.
|
||||||
|
- **[Obico (selbst gehostet)](https://github.com/TheSpaghettiDetective/obico-server)** —
|
||||||
|
die Bridge bietet eine Moonraker-kompatible API, die
|
||||||
|
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||||
|
akzeptiert; damit hast du Time-Lapse und WebRTC-Live-Stream gegen deinen
|
||||||
|
eigenen Obico-Server. Die KI-Spaghetti-Erkennung ist beim Kobra X
|
||||||
|
experimentell — der Top-Down-Kamerawinkel weicht von dem ab, auf den
|
||||||
|
das Modell trainiert wurde.
|
||||||
|
|
||||||
> Dies sind **Community-Projekte**, die nicht von KX-Bridge betreut oder
|
> Dies sind **Community-Projekte**, die nicht von KX-Bridge betreut oder
|
||||||
> supportet werden. Bei Fragen oder Problemen bitte das verlinkte Repository nutzen.
|
> supportet werden. Bei Fragen oder Problemen bitte das verlinkte Repository nutzen.
|
||||||
@@ -132,9 +176,10 @@ dann `extract_credentials` ausführen → gibt Username, Passwort, Device-ID und
|
|||||||
## ⚙️ Nützliche Befehle
|
## ⚙️ Nützliche Befehle
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f # Logs anzeigen
|
docker compose logs -f # Logs anzeigen
|
||||||
docker compose down # Bridge stoppen
|
docker compose down # Bridge stoppen
|
||||||
docker compose up -d --build # Bridge neu bauen & starten (nach Update)
|
docker compose pull && docker compose up -d # auf neueste veröffentlichte Version updaten
|
||||||
|
docker compose up -d --build # lokal selber bauen (statt zu pullen)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
277
README.es.md
Normal file
277
README.es.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="knlogo.png" alt="KX-Bridge" width="160"/>
|
||||||
|
|
||||||
|
# KX-Bridge
|
||||||
|
|
||||||
|
**Controla tu Anycubic Kobra X con OrcaSlicer — sin Klipper, sin Raspberry Pi.**
|
||||||
|
|
||||||
|
Un puente compatible con Moonraker que se comunica directamente con la impresora.
|
||||||
|
|
||||||
|
<sub>🧪 Un usuario en Reddit ha reportado que el puente también funciona con la
|
||||||
|
**Kobra S1** y la **Kobra S1 Max** — los protocolos parecen compatibles, pero
|
||||||
|
ninguna está oficialmente probada ni soportada. Se agradece el feedback.</sub>
|
||||||
|
|
||||||
|
<sub>🇬🇧 <a href="README.md">English version</a> · 🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
[](https://ko-fi.com/viewitde)
|
||||||
|
|
||||||
|
[](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
|
||||||
|
|
||||||
|
[](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||||
|
|
||||||
|
<sub>¿Te gusta KX-Bridge? Un café en <a href="https://ko-fi.com/viewitde">Ko-fi</a> mantiene el proyecto vivo. ☕</sub>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Características
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| 🖨️ | **Control de impresora** — iniciar, pausar, reanudar, cancelar, temperaturas, velocidad de impresión |
|
||||||
|
| 📊 | **Estado en tiempo real** — temperatura, progreso, capas, tiempo restante, transmisión de cámara |
|
||||||
|
| 🎨 | **AMS / multicolor** — ranuras con **selector de perfil por ranura** (asigna tu propia marca de los perfiles de OrcaSlicer a cada ranura); el puente escribe material y color al display de la impresora |
|
||||||
|
| 📦 | **Importa tus propios perfiles de OrcaSlicer** — arrastra un ZIP de `~/.config/OrcaSlicer/user/<id>/filament/` al puente; aparecen en el desplegable de la ranura bajo ★ Perfiles propios |
|
||||||
|
| 📷 | **Integración con Obico (experimental)** — Time-Lapse y stream en vivo WebRTC contra un [servidor Obico](https://github.com/TheSpaghettiDetective/obico-server) autoalojado vía moonraker-obico |
|
||||||
|
| 📐 | **Stream H.264 directo + altura Z** — ruta de cámara de bajo consumo de CPU para Obico, Z actual derivada de la altura de capa (widget de progreso) |
|
||||||
|
| 🗂️ | **Explorador de GCode** — archivos subidos con vistas previas, historial de impresión, búsqueda y filtros |
|
||||||
|
| 🧩 | **Multi-impresora** — múltiples impresoras en **una** instancia del puente, cambia mediante un menú desplegable |
|
||||||
|
| ➕ | **Añade una impresora con un clic** — solo introduce la IP, las credenciales se importan automáticamente |
|
||||||
|
| 🔁 | **Reconexión MQTT robusta** — el puente sobrevive a reinicios nocturnos de la impresora sin reinicio manual |
|
||||||
|
| 🌐 | **Interfaz multilingüe** — DE / EN / ES / 中文, detecta automáticamente el idioma del navegador |
|
||||||
|
| 🔄 | **Actualización automática** — instala nuevas versiones directamente desde el navegador |
|
||||||
|
| 🧠 | **OrcaSlicer** — protocolo Moonraker completo (HTTP + WebSocket); usa el [build OrcaSlicer-KX](#-slicer-recomendado) para emparejamiento correcto de vendor por ranura |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Inicio rápido
|
||||||
|
|
||||||
|
### 1. Prepara la impresora
|
||||||
|
|
||||||
|
Activa el modo LAN en la Kobra X:
|
||||||
|
**Pantalla de la impresora → Ajustes → Activar modo LAN**
|
||||||
|
|
||||||
|
### 2. Inicia el puente
|
||||||
|
|
||||||
|
**Docker (recomendado):**
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Binario Linux (sin Docker):**
|
||||||
|
```bash
|
||||||
|
chmod +x kx-bridge-linux-amd64 && ./kx-bridge-linux-amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
**EXE Windows (sin Docker):**
|
||||||
|
```
|
||||||
|
kx-bridge.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **Certificados TLS necesarios para el binario standalone**
|
||||||
|
>
|
||||||
|
> El bridge habla con el MQTT de la impresora vía mTLS y necesita dos
|
||||||
|
> ficheros de certificado **junto al binario**:
|
||||||
|
>
|
||||||
|
> - `anycubic_slicer.crt`
|
||||||
|
> - `anycubic_slicer.key`
|
||||||
|
>
|
||||||
|
> Ambos vienen en **`anycubic-certs.zip`** en la misma página de release.
|
||||||
|
> Descárgalo y extrae los dos ficheros en el mismo directorio que
|
||||||
|
> `kx-bridge-linux-amd64` o `kx-bridge.exe`. Sin ellos verás
|
||||||
|
> `Verbindung fehlgeschlagen: TLS-Zertifikate fehlen …` (0.9.19.1+) o
|
||||||
|
> `[Errno 2] No such file or directory` (versiones anteriores).
|
||||||
|
>
|
||||||
|
> Estructura correcta:
|
||||||
|
> ```
|
||||||
|
> ~/kx-bridge/
|
||||||
|
> ├── kx-bridge-linux-amd64 (o kx-bridge.exe)
|
||||||
|
> ├── anycubic_slicer.crt ← de anycubic-certs.zip
|
||||||
|
> ├── anycubic_slicer.key ← de anycubic-certs.zip
|
||||||
|
> └── config/ (se crea en el primer arranque)
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Los usuarios de Docker no necesitan hacer esto — los certificados
|
||||||
|
> están incluidos en la imagen.
|
||||||
|
|
||||||
|
> Con los binarios de Linux y Windows, `config/` y `data/` (configuración,
|
||||||
|
> SQLite, almacén de GCode) viven junto al programa. Copia toda la carpeta
|
||||||
|
> para mover la instalación.
|
||||||
|
|
||||||
|
**Python directamente:**
|
||||||
|
```bash
|
||||||
|
pip install -r bridge/requirements.txt
|
||||||
|
python bridge/kobrax_moonraker_bridge.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configura la impresora
|
||||||
|
|
||||||
|
Abre la interfaz web: **`http://IP-DEL-PUENTE:7125`**
|
||||||
|
|
||||||
|
En el primer inicio, la pestaña **Impresoras** muestra *"+ Añadir impresora"* — solo introduce la dirección IP
|
||||||
|
de la impresora, el resto (usuario, contraseña, ID de dispositivo) se obtiene de la impresora y se desencripta
|
||||||
|
automáticamente. Listo.
|
||||||
|
|
||||||
|
> ¿Más de una impresora? Simplemente haz clic en *"+ Añadir impresora"* de nuevo — cada una recibe su propio puerto
|
||||||
|
> (7125, 7126, …) y se puede seleccionar desde el menú desplegable del encabezado.
|
||||||
|
|
||||||
|
### 4. Conecta OrcaSlicer
|
||||||
|
|
||||||
|
Impresora → Tipo de conexión **Moonraker** → Host: `http://IP-DEL-PUENTE:7125`
|
||||||
|
|
||||||
|
> ⚠️ El tipo de conexión debe ser **Moonraker** (no "Bambu" ni "Klipper").
|
||||||
|
> Introduce la URL completa incluyendo `http://` y el puerto `:7125` en el campo de host.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📺 Vídeo tutorial
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Slicer recomendado
|
||||||
|
|
||||||
|
Para una sincronización de filamento AMS correcta ofrecemos una **versión modificada de OrcaSlicer**:
|
||||||
|
|
||||||
|
→ **[Lanzamientos de OrcaSlicer-KX](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||||
|
|
||||||
|
**PRs upstream incluidos en el build KX:**
|
||||||
|
|
||||||
|
- **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Corrección de sincronización Moonraker / Happy-Hare AMS (las posiciones de las ranuras se mantienen correctas incluso con ranuras vacías)
|
||||||
|
- **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Coincidencia de Vendor + Nombre para Moonraker (lee `name` + `vendor_name` por ranura y los empareja con los presets de filamento del usuario), por [@LordGuenni](https://github.com/LordGuenni)
|
||||||
|
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — `filament_id` único para presets de usuario (los perfiles nuevos reciben un ID nuevo en vez de heredar `OGFL99` del padre genérico), por [@mrnoisytiger](https://github.com/mrnoisytiger)
|
||||||
|
|
||||||
|
**Más cuatro mejoras específicas de KX encima:**
|
||||||
|
|
||||||
|
- Respetar el hint de filamento del puente (`tray_info_idx` + vendor)
|
||||||
|
- Coincidencia por vendor incluso cuando el preset base no es **is_compatible** con la impresora activa (así un perfil copiado de otra máquina sigue coincidiendo por vendor)
|
||||||
|
- Coincidencia por vendor cuando `tray_info_idx` está definido pero su preset es incompatible
|
||||||
|
- Búsqueda de dos pasadas: primero presets compatibles, luego todos los visibles
|
||||||
|
|
||||||
|
**Por qué importa:** sin #13719 todas las ranuras AMS caen en `Generic PLA` / `Generic PETG` aunque el puente ya envíe la marca concreta (`name + vendor_name + gate_filament_name`). Con el build KX, OrcaSlicer coincide con tus presets de usuario reales — incluyendo los perfiles que importaste al puente vía [Importa tus propios perfiles de OrcaSlicer](#-características).
|
||||||
|
|
||||||
|
OrcaSlicer upstream también funciona para rebanar e imprimir — solo pierdes la coincidencia de vendor por ranura en la sincronización AMS. El material y color por ranura se pueden empujar puente → impresora con cualquier slicer (eso va por MQTT, no por el slicer).
|
||||||
|
|
||||||
|
OrcaSlicer-KX es un build de [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); el código fuente de cada PR upstream está en GitHub, los parches específicos de KX en el repo OrcaSlicer-KX.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏠 Comunidad e integraciones
|
||||||
|
|
||||||
|
- **[Integración con Home Assistant](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||||
|
por [@gangoke](https://github.com/gangoke) — expone sensores, controles de impresión,
|
||||||
|
luz, cámara y la vista previa del GCode como entidades nativas de Home Assistant.
|
||||||
|
- **[Obico (autoalojado)](https://github.com/TheSpaghettiDetective/obico-server)** —
|
||||||
|
el puente expone una API compatible con Moonraker que
|
||||||
|
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||||
|
acepta, así obtienes Time-Lapse y streaming en vivo WebRTC contra tu propio
|
||||||
|
servidor Obico. La detección de fallos por IA es experimental en la Kobra X
|
||||||
|
(el ángulo cenital de la cámara difiere del que el modelo fue entrenado).
|
||||||
|
|
||||||
|
> Estos son **proyectos de la comunidad**, no mantenidos ni soportados por KX-Bridge.
|
||||||
|
> Para preguntas o problemas, utiliza el repositorio enlazado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Obtener credenciales manualmente
|
||||||
|
|
||||||
|
Normalmente no es necesario — *"+ Añadir impresora"* lo hace automáticamente. Si lo necesitas:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fetch_credentials --ip 192.168.x.x --write-config
|
||||||
|
```
|
||||||
|
Obtiene las credenciales directamente de la impresora vía HTTP y las escribe en `config/config.ini`.
|
||||||
|
Solo se requiere la IP de la impresora, no hace falta un slicer.
|
||||||
|
|
||||||
|
Alternativamente (si se desconoce la IP): abre AnycubicSlicerNext, conecta la impresora, luego ejecuta
|
||||||
|
`extract_credentials` → muestra usuario, contraseña, ID de dispositivo y la IP de la impresora.
|
||||||
|
|
||||||
|
> **Descargas:** [Lanzamientos](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases) → `fetch_credentials` / `extract_credentials` (Linux y Windows)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Comandos útiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f # mostrar registros
|
||||||
|
docker compose down # detener el puente
|
||||||
|
docker compose pull && docker compose up -d # actualizar a la imagen publicada más reciente
|
||||||
|
docker compose up -d --build # recompilar localmente (en lugar de descargar)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🩹 Solución de problemas
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>"Credenciales MQTT incorrectas" al iniciar</b></summary>
|
||||||
|
|
||||||
|
- Vuelve a añadir la impresora mediante *"+ Añadir impresora"*, o ejecuta
|
||||||
|
`fetch_credentials --ip <ip> --write-config` y reinicia el puente
|
||||||
|
- Introduce solo la dirección IP, sin puerto (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`)
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Impresora no encontrada / modo LAN no activado</b></summary>
|
||||||
|
|
||||||
|
- En la pantalla de la impresora: Ajustes → Activar modo LAN
|
||||||
|
- La impresora y el puente deben estar en la misma red
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Docker: Permiso denegado</b></summary>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker $USER # luego cierra la sesión y vuelve a iniciarla
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Actualizar desde 0.9.1 o anterior</b></summary>
|
||||||
|
|
||||||
|
A partir de 0.9.2, KX-Bridge almacena la configuración en `config/config.ini` en lugar de `.env`.
|
||||||
|
La migración se ejecuta automáticamente en el primer inicio después de la actualización — no requiere acción.
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Seguridad
|
||||||
|
|
||||||
|
- El puente es accesible en la red local en `http://<IP-del-host>:7125` — **no** lo expongas a internet
|
||||||
|
- `config/config.ini` contiene las credenciales de la impresora — no las compartas públicamente
|
||||||
|
- Las credenciales **no** otorgan acceso a los servicios en la nube de Anycubic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Licencia
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
KX-Bridge se publica bajo la **GNU General Public License v3.0**. Consulta
|
||||||
|
[LICENSE](LICENSE) para el texto completo. Las bifurcaciones y modificaciones deben
|
||||||
|
permanecer bajo GPLv3 si se redistribuyen.
|
||||||
|
|
||||||
|
La implementación del protocolo MQTT es el resultado de una ingeniería inversa
|
||||||
|
independiente con fines de interoperabilidad (§69e UrhG / Directiva de Software de la UE
|
||||||
|
Art. 6). El material de terceros en el repositorio (certificados TLS de Anycubic)
|
||||||
|
**no** está cubierto por GPLv3 y se incluye únicamente para permitir la
|
||||||
|
autenticación contra impresoras que el usuario final ya posee. Consulta
|
||||||
|
[NOTICE.md](NOTICE.md) para más detalles y el aviso legal.
|
||||||
|
|
||||||
|
Este proyecto es independiente y no está afiliado con Anycubic.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<br>
|
||||||
|
|
||||||
|
**Si KX-Bridge te ayuda, el proyecto agradece tu apoyo:**
|
||||||
|
|
||||||
|
[](https://ko-fi.com/viewitde)
|
||||||
|
|
||||||
|
</div>
|
||||||
55
README.md
55
README.md
@@ -8,7 +8,11 @@
|
|||||||
|
|
||||||
A Moonraker-compatible bridge that talks directly to the printer.
|
A Moonraker-compatible bridge that talks directly to the printer.
|
||||||
|
|
||||||
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
|
<sub>🧪 A community report on Reddit suggests the bridge also works with the
|
||||||
|
**Kobra S1** and **Kobra S1 Max** — protocols look compatible, but neither is
|
||||||
|
officially tested or supported. Feedback welcome.</sub>
|
||||||
|
|
||||||
|
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@@ -32,12 +36,17 @@ A Moonraker-compatible bridge that talks directly to the printer.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 🖨️ | **Printer control** — start, pause, resume, cancel, temperatures, print speed |
|
| 🖨️ | **Printer control** — start, pause, resume, cancel, temperatures, print speed |
|
||||||
| 📊 | **Live status** — temperature, progress, layers, remaining time, camera stream |
|
| 📊 | **Live status** — temperature, progress, layers, remaining time, camera stream |
|
||||||
| 🎨 | **AMS / multicolor** — filament slots, per-channel remapping, MMU emulation for OrcaSlicer filament sync |
|
| 🎨 | **AMS / multicolor** — slots with per-slot **profile picker** (assign your own brand from OrcaSlicer profiles per slot); bridge writes material & colour back to the printer display |
|
||||||
|
| 📦 | **Import your own OrcaSlicer profiles** — drag a ZIP from `~/.config/OrcaSlicer/user/<id>/filament/` into the bridge; they show up in the slot dropdown under ★ Own profiles |
|
||||||
|
| 📷 | **Obico integration (experimental)** — Time-Lapse and WebRTC live stream against a self-hosted [Obico server](https://github.com/TheSpaghettiDetective/obico-server) via moonraker-obico |
|
||||||
|
| 📐 | **Direct H.264 stream + Z-height** — low-CPU camera path for Obico, current Z derived from layer-height for the print-progress widget |
|
||||||
| 🗂️ | **GCode browser** — uploaded files with thumbnails, print history, search & filter |
|
| 🗂️ | **GCode browser** — uploaded files with thumbnails, print history, search & filter |
|
||||||
| 🧩 | **Multi-printer** — multiple printers in **one** bridge instance, switch via dropdown |
|
| 🧩 | **Multi-printer** — multiple printers in **one** bridge instance, switch via dropdown |
|
||||||
| ➕ | **Add a printer with one click** — just enter the IP, credentials are imported automatically |
|
| ➕ | **Add a printer with one click** — just enter the IP, credentials are imported automatically |
|
||||||
|
| 🔁 | **Robust MQTT reconnect** — bridge survives overnight printer reboots without manual restart |
|
||||||
|
| 🌐 | **Multi-language UI** — DE / EN / ES / 中文, auto-detect browser locale |
|
||||||
| 🔄 | **Self-update** — install new versions directly in the browser |
|
| 🔄 | **Self-update** — install new versions directly in the browser |
|
||||||
| 🌐 | **OrcaSlicer** — full Moonraker protocol (HTTP + WebSocket), EN/DE UI |
|
| 🧠 | **OrcaSlicer** — full Moonraker protocol (HTTP + WebSocket); pair with the [OrcaSlicer-KX build](#-recommended-slicer) for proper per-slot vendor matching |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -101,11 +110,44 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🎨 Recommended Slicer
|
||||||
|
|
||||||
|
For proper AMS filament-sync we ship a **patched OrcaSlicer build**:
|
||||||
|
|
||||||
|
→ **[OrcaSlicer-KX releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||||
|
|
||||||
|
**Upstream PRs bundled in the KX build:**
|
||||||
|
|
||||||
|
- **[PR #13372](https://github.com/SoftFever/OrcaSlicer/pull/13372)** — Moonraker / Happy-Hare AMS sync fix (slot positions stay correct even with empty slots)
|
||||||
|
- **[PR #13719](https://github.com/SoftFever/OrcaSlicer/pull/13719)** — Vendor + Name matching for Moonraker (reads `name` + `vendor_name` per slot and matches against the user's filament presets), by [@LordGuenni](https://github.com/LordGuenni)
|
||||||
|
- **[PR #13315](https://github.com/SoftFever/OrcaSlicer/pull/13315)** — Unique `filament_id` for user presets (so newly created custom profiles get a fresh ID instead of inheriting `OGFL99` from the generic parent), by [@mrnoisytiger](https://github.com/mrnoisytiger)
|
||||||
|
|
||||||
|
**Plus four KX-specific matching improvements on top:**
|
||||||
|
|
||||||
|
- Respect the bridge filament hint (`tray_info_idx` + vendor)
|
||||||
|
- Vendor match also when the chosen base preset is **not is_compatible** with the active printer (so a profile copied from a different machine still matches by vendor)
|
||||||
|
- Vendor match when `tray_info_idx` is set but its preset is incompatible
|
||||||
|
- Two-pass lookup: first compatible presets, then all visible ones
|
||||||
|
|
||||||
|
**Why this matters:** without #13719 the AMS slots in OrcaSlicer all fall back to `Generic PLA` / `Generic PETG` even though the bridge already sends the concrete brand (`name + vendor_name + gate_filament_name`). With the KX build OrcaSlicer matches your actual user presets — including profiles you imported into the bridge via the [Import your own OrcaSlicer profiles](#-features) flow.
|
||||||
|
|
||||||
|
Stock upstream OrcaSlicer still works for slicing and printing — you just lose the per-slot brand matching on AMS sync. Slot material + colour can still be pushed bridge → printer either way (that goes over MQTT, not via the slicer).
|
||||||
|
|
||||||
|
OrcaSlicer-KX is a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); source for each upstream PR is on GitHub, KX-specific patches live in the OrcaSlicer-KX repo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🏠 Community & Integrations
|
## 🏠 Community & Integrations
|
||||||
|
|
||||||
- **[Home Assistant integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
- **[Home Assistant integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||||
by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls,
|
by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls,
|
||||||
light, camera and the GCode thumbnail as native Home Assistant entities.
|
light, camera and the GCode thumbnail as native Home Assistant entities.
|
||||||
|
- **[Obico (self-hosted)](https://github.com/TheSpaghettiDetective/obico-server)** —
|
||||||
|
the bridge exposes a Moonraker-compatible surface that
|
||||||
|
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||||
|
accepts, so you get Time-Lapse and WebRTC live streaming against your own
|
||||||
|
Obico server. The AI failure-detection side is experimental on the Kobra X
|
||||||
|
(top-down camera angle differs from what the model was trained on).
|
||||||
|
|
||||||
> These are **community projects**, not maintained or supported by KX-Bridge.
|
> These are **community projects**, not maintained or supported by KX-Bridge.
|
||||||
> For questions or issues, please use the linked repository.
|
> For questions or issues, please use the linked repository.
|
||||||
@@ -132,9 +174,10 @@ Alternatively (if the IP is unknown): open AnycubicSlicerNext, connect the print
|
|||||||
## ⚙️ Useful commands
|
## ⚙️ Useful commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f # show logs
|
docker compose logs -f # show logs
|
||||||
docker compose down # stop the bridge
|
docker compose down # stop the bridge
|
||||||
docker compose up -d --build # rebuild & start (after an update)
|
docker compose pull && docker compose up -d # update to the latest published image
|
||||||
|
docker compose up -d --build # rebuild locally (instead of pulling)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
|
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
|
||||||
# cp config.ini.example config.ini
|
# cp config.ini.example config.ini
|
||||||
#
|
#
|
||||||
# Credentials mit extract_credentials.exe (Windows) oder
|
# Credentials automatisch eintragen:
|
||||||
# extract_credentials (Linux) aus dem laufenden AnycubicSlicerNext auslesen.
|
# python3 tools/fetch_credentials.py --ip 192.168.x.x --write-config
|
||||||
|
# Alternativ (Windows, ohne Drucker-IP bekannt):
|
||||||
|
# extract_credentials.exe --write-env (liest aus laufendem AnycubicSlicerNext)
|
||||||
|
|
||||||
[connection]
|
[connection]
|
||||||
# IP-Adresse des Druckers im lokalen Netzwerk
|
# IP-Adresse des Druckers im lokalen Netzwerk
|
||||||
@@ -29,6 +31,65 @@ default_ams_slot = auto
|
|||||||
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
|
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
|
||||||
auto_leveling = 1
|
auto_leveling = 1
|
||||||
|
|
||||||
|
# Kamera-Stream bei Druckstart automatisch einschalten (1 = an, 0 = aus)
|
||||||
|
camera_on_print = 0
|
||||||
|
|
||||||
|
# Warnung vor Druck von Web-Uploads (1 = an, 0 = aus)
|
||||||
|
web_upload_warning = 1
|
||||||
|
|
||||||
[bridge]
|
[bridge]
|
||||||
# Poll-Intervall in Sekunden
|
# Poll-Intervall in Sekunden
|
||||||
poll_interval = 3
|
poll_interval = 3
|
||||||
|
|
||||||
|
# ─── Multi-Printer (optional) ──────────────────────────────────────────────────
|
||||||
|
# Mehrere Drucker können als [printer_1], [printer_2], … definiert werden.
|
||||||
|
# Jede Bridge-Instanz verbindet sich mit einem Drucker (je eigener Port).
|
||||||
|
# bridge_url zeigt auf die jeweilige Bridge-Instanz (für den /kx/printers-Endpunkt).
|
||||||
|
# Die [connection]-Sektion wird weiterhin als Fallback für diese Instanz verwendet.
|
||||||
|
#
|
||||||
|
# Beispiel:
|
||||||
|
# [printer_1]
|
||||||
|
# name = Kobra X Links
|
||||||
|
# bridge_url = http://192.168.178.95:7125
|
||||||
|
# printer_ip = 192.168.178.95
|
||||||
|
# mqtt_port = 9883
|
||||||
|
# username = userXXXXXXXXXX
|
||||||
|
# password = XXXXXXXXXXXXXXX
|
||||||
|
# device_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
# mode_id = 20030
|
||||||
|
#
|
||||||
|
# [printer_2]
|
||||||
|
# name = Kobra X Rechts
|
||||||
|
# bridge_url = http://192.168.178.96:7125
|
||||||
|
# printer_ip = 192.168.178.96
|
||||||
|
# mqtt_port = 9883
|
||||||
|
# username = userYYYYYYYYYY
|
||||||
|
# password = YYYYYYYYYYYYYYY
|
||||||
|
# device_id = yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||||
|
# mode_id = 20030
|
||||||
|
|
||||||
|
[ace_dry_presets]
|
||||||
|
# Vordefinierte Dry-Set Presets (Temp in °C, Dauer in Sekunden)
|
||||||
|
pla_temp = 45
|
||||||
|
pla_duration_sec = 14400
|
||||||
|
pla_plus_temp = 45
|
||||||
|
pla_plus_duration_sec = 14400
|
||||||
|
petg_temp = 50
|
||||||
|
petg_duration_sec = 14400
|
||||||
|
tpu_temp = 55
|
||||||
|
tpu_duration_sec = 14400
|
||||||
|
abs_asa_temp = 45
|
||||||
|
abs_asa_duration_sec = 28800
|
||||||
|
pa_pc_temp = 55
|
||||||
|
pa_pc_duration_sec = 43200
|
||||||
|
|
||||||
|
# Custom Presets (Name + Temp + Dauer)
|
||||||
|
custom_1_name = Custom 1
|
||||||
|
custom_1_temp = 45
|
||||||
|
custom_1_duration_sec = 14400
|
||||||
|
custom_2_name = Custom 2
|
||||||
|
custom_2_temp = 45
|
||||||
|
custom_2_duration_sec = 14400
|
||||||
|
custom_3_name = Custom 3
|
||||||
|
custom_3_temp = 45
|
||||||
|
custom_3_duration_sec = 14400
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ def _load_config_file(path: pathlib.Path):
|
|||||||
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
||||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||||
|
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||||
}
|
}
|
||||||
for env_key, (section, option) in mapping.items():
|
for env_key, (section, option) in mapping.items():
|
||||||
@@ -97,6 +98,7 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
|||||||
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
||||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||||
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||||
|
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
|
||||||
}
|
}
|
||||||
cfg[CONFIG_SECTION_BRIDGE] = {
|
cfg[CONFIG_SECTION_BRIDGE] = {
|
||||||
"poll_interval": "3",
|
"poll_interval": "3",
|
||||||
@@ -164,6 +166,84 @@ def list_printers() -> list[dict]:
|
|||||||
return printers
|
return printers
|
||||||
|
|
||||||
|
|
||||||
|
def list_filament_profiles() -> dict[int, dict]:
|
||||||
|
"""Liest die [filament_profiles]-Sektion aus config.ini.
|
||||||
|
|
||||||
|
Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
|
||||||
|
aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
|
||||||
|
(als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
|
||||||
|
derselben filament_id wie 'OGFL99', d.h. die ID ist nicht eindeutig):
|
||||||
|
|
||||||
|
[filament_profiles]
|
||||||
|
slot_0_vendor = Polymaker
|
||||||
|
slot_0_name = PolyTerra PLA
|
||||||
|
slot_0_id = OGFL01
|
||||||
|
|
||||||
|
Gibt einen Dict {slot_index: {"id": ..., "vendor": ..., "name": ...}}
|
||||||
|
zurück. Leere/fehlende Slots werden NICHT aufgenommen — das Default-Mapping
|
||||||
|
(per filament_type) in der Bridge bleibt dann aktiv.
|
||||||
|
|
||||||
|
Backwards-Kompat: alte Configs mit nur (vendor, id) bleiben lesbar; `name`
|
||||||
|
fehlt dann und der Aufrufer kann optional aus der orca_filaments.json
|
||||||
|
rekonstruieren.
|
||||||
|
"""
|
||||||
|
path = _find_config_file()
|
||||||
|
if not path:
|
||||||
|
return {}
|
||||||
|
cfg = configparser.ConfigParser()
|
||||||
|
cfg.read(path, encoding="utf-8")
|
||||||
|
if not cfg.has_section("filament_profiles"):
|
||||||
|
return {}
|
||||||
|
result: dict[int, dict] = {}
|
||||||
|
for key, value in cfg.items("filament_profiles"):
|
||||||
|
# Erwartet: slot_<idx>_id oder slot_<idx>_vendor oder slot_<idx>_name
|
||||||
|
if not key.startswith("slot_"):
|
||||||
|
continue
|
||||||
|
parts = key.split("_", 2)
|
||||||
|
if len(parts) < 3:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
slot_idx = int(parts[1])
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
field = parts[2]
|
||||||
|
if field not in ("id", "vendor", "name"):
|
||||||
|
continue
|
||||||
|
if not value.strip():
|
||||||
|
continue
|
||||||
|
result.setdefault(slot_idx, {})[field] = value.strip()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def save_filament_profiles(profiles: dict[int, dict]) -> bool:
|
||||||
|
"""Schreibt die übergebenen Slot-Profile in die [filament_profiles]-
|
||||||
|
Sektion der config.ini. Existierende Einträge werden komplett ersetzt.
|
||||||
|
|
||||||
|
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
|
||||||
|
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
|
||||||
|
"""
|
||||||
|
path = _find_config_file()
|
||||||
|
if not path:
|
||||||
|
return False
|
||||||
|
cfg = configparser.ConfigParser()
|
||||||
|
cfg.read(path, encoding="utf-8")
|
||||||
|
if cfg.has_section("filament_profiles"):
|
||||||
|
cfg.remove_section("filament_profiles")
|
||||||
|
if profiles:
|
||||||
|
cfg["filament_profiles"] = {}
|
||||||
|
for slot_idx in sorted(profiles.keys()):
|
||||||
|
entry = profiles[slot_idx] or {}
|
||||||
|
if entry.get("vendor"):
|
||||||
|
cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"]
|
||||||
|
if entry.get("name"):
|
||||||
|
cfg["filament_profiles"][f"slot_{slot_idx}_name"] = entry["name"]
|
||||||
|
if entry.get("id"):
|
||||||
|
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
cfg.write(f)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get(key: str, default: str = "") -> str:
|
def get(key: str, default: str = "") -> str:
|
||||||
return os.environ.get(key, default)
|
return os.environ.get(key, default)
|
||||||
|
|
||||||
@@ -178,3 +258,4 @@ DEVICE_ID = get("DEVICE_ID", "")
|
|||||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||||
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||||
|
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||||
|
|||||||
1465
data/orca_filaments.json
Normal file
1465
data/orca_filaments.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
kx-bridge:
|
kx-bridge:
|
||||||
image: kx-bridge:latest
|
image: gitea.it-drui.de/viewit/kx-bridge:latest
|
||||||
build: .
|
# Selbst bauen statt das Registry-Image zu pullen?
|
||||||
|
# Dann image-Zeile auskommentieren und folgende aktivieren:
|
||||||
|
# build: .
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|||||||
@@ -154,6 +154,13 @@ class KobraXClient:
|
|||||||
# -- Connection ----------------------------------------------------------
|
# -- Connection ----------------------------------------------------------
|
||||||
|
|
||||||
def _do_connect(self):
|
def _do_connect(self):
|
||||||
|
if not os.path.exists(CERT_FILE) or not os.path.exists(KEY_FILE):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"TLS-Zertifikate fehlen: anycubic_slicer.crt + anycubic_slicer.key "
|
||||||
|
f"müssen neben der kx-bridge Binary liegen ({_SCRIPT_DIR}/). "
|
||||||
|
f"Lade anycubic-certs.zip vom Gitea-Release herunter und entpacke "
|
||||||
|
f"die Dateien dorthin."
|
||||||
|
)
|
||||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
ctx.check_hostname = False
|
ctx.check_hostname = False
|
||||||
ctx.verify_mode = ssl.CERT_NONE
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
@@ -179,10 +186,24 @@ class KobraXClient:
|
|||||||
def connect(self):
|
def connect(self):
|
||||||
self._do_connect()
|
self._do_connect()
|
||||||
self._running = True
|
self._running = True
|
||||||
t = threading.Thread(target=self._read_loop, daemon=True)
|
self._ensure_reader()
|
||||||
t.start()
|
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
def _ensure_reader(self):
|
||||||
|
"""Stellt sicher dass der Reader-Thread lebt. Wenn der Reader nach einer
|
||||||
|
früheren disconnect/reconnect-Sequenz oder einem unbehandelten Fehler
|
||||||
|
gestorben ist, würden empfangene Replies sonst nie ankommen — publish()
|
||||||
|
würde dann zwar senden, aber auf Antworten ewig warten."""
|
||||||
|
if not self._running:
|
||||||
|
return # gewollter disconnect
|
||||||
|
t = getattr(self, "_reader_thread", None)
|
||||||
|
if t is not None and t.is_alive():
|
||||||
|
return
|
||||||
|
self._reader_thread = threading.Thread(
|
||||||
|
target=self._read_loop, daemon=True, name="kobrax-mqtt-reader",
|
||||||
|
)
|
||||||
|
self._reader_thread.start()
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
try:
|
try:
|
||||||
@@ -191,20 +212,34 @@ class KobraXClient:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _reconnect(self):
|
def _reconnect(self):
|
||||||
|
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
|
||||||
|
antwortet oder disconnect() gerufen wurde. Backoff cappt bei 60 s. Die
|
||||||
|
ersten 5 Versuche loggen als WARNING (akute Verbindungsstörung), danach
|
||||||
|
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
|
||||||
|
ausgeschaltet) zu vermeiden."""
|
||||||
log.warning("Verbindung verloren – reconnect…")
|
log.warning("Verbindung verloren – reconnect…")
|
||||||
try:
|
try:
|
||||||
self._sock.close()
|
self._sock.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
for delay in [2, 4, 8, 15, 30]:
|
delays = [2, 4, 8, 15, 30, 60]
|
||||||
|
attempt = 0
|
||||||
|
while self._running:
|
||||||
|
delay = delays[min(attempt, len(delays) - 1)]
|
||||||
try:
|
try:
|
||||||
self._do_connect()
|
self._do_connect()
|
||||||
log.info("Reconnect erfolgreich")
|
log.info("Reconnect erfolgreich (nach %d Versuchen)", attempt + 1)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("Reconnect fehlgeschlagen (%s), warte %ss…", e, delay)
|
attempt += 1
|
||||||
time.sleep(delay)
|
lvl = log.warning if attempt <= 5 else log.debug
|
||||||
return False
|
lvl("Reconnect fehlgeschlagen (%s, Versuch %d), warte %ss…", e, attempt, delay)
|
||||||
|
# Geteiltes Sleep damit disconnect() den Loop schneller bricht.
|
||||||
|
slept = 0.0
|
||||||
|
while slept < delay and self._running:
|
||||||
|
time.sleep(min(0.5, delay - slept))
|
||||||
|
slept += 0.5
|
||||||
|
return False # nur wenn disconnect() gerufen wurde
|
||||||
|
|
||||||
def _subscribe(self, topic: str):
|
def _subscribe(self, topic: str):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -348,6 +383,9 @@ class KobraXClient:
|
|||||||
# -- Publish + request/response ------------------------------------------
|
# -- Publish + request/response ------------------------------------------
|
||||||
|
|
||||||
def publish(self, msg_type: str, action: str, data=None, timeout: float = 5.0) -> dict | None:
|
def publish(self, msg_type: str, action: str, data=None, timeout: float = 5.0) -> dict | None:
|
||||||
|
# Falls Reader-Thread aus historischen Gründen tot ist, wiederbeleben —
|
||||||
|
# sonst würden Replies nie ankommen und event.wait() läuft ins Timeout.
|
||||||
|
self._ensure_reader()
|
||||||
msgid = str(uuid.uuid4())
|
msgid = str(uuid.uuid4())
|
||||||
payload = json.dumps({
|
payload = json.dumps({
|
||||||
"type": msg_type,
|
"type": msg_type,
|
||||||
@@ -413,6 +451,7 @@ class KobraXClient:
|
|||||||
|
|
||||||
def publish_web(self, msg_type: str, action: str, data=None) -> None:
|
def publish_web(self, msg_type: str, action: str, data=None) -> None:
|
||||||
"""Fire-and-forget publish on the web/printer topic (used for runtime updates during print)."""
|
"""Fire-and-forget publish on the web/printer topic (used for runtime updates during print)."""
|
||||||
|
self._ensure_reader()
|
||||||
msgid = str(uuid.uuid4())
|
msgid = str(uuid.uuid4())
|
||||||
payload = json.dumps({
|
payload = json.dumps({
|
||||||
"type": msg_type,
|
"type": msg_type,
|
||||||
@@ -429,7 +468,14 @@ class KobraXClient:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
self._sock.sendall(_build_publish(topic, payload))
|
self._sock.sendall(_build_publish(topic, payload))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("web send error: %s", e)
|
log.error("web send error: %s, reconnecting…", e)
|
||||||
|
# Reconnect triggern (analog zu publish()); ohne Retry weil
|
||||||
|
# fire-and-forget — der nächste Aufruf wird auf den frischen Socket
|
||||||
|
# treffen.
|
||||||
|
try:
|
||||||
|
self._reconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# -- High-level commands -------------------------------------------------
|
# -- High-level commands -------------------------------------------------
|
||||||
|
|
||||||
@@ -545,9 +591,15 @@ class KobraXClient:
|
|||||||
f"Connection: close\r\n\r\n"
|
f"Connection: close\r\n\r\n"
|
||||||
).encode()
|
).encode()
|
||||||
|
|
||||||
sock = socket.create_connection((self.host, 18910), timeout=30)
|
# Connect-Timeout kurz (LAN). Während sendall() darf der Socket so
|
||||||
|
# lange brauchen wie nötig — bei großen Dateien (>100 MB) und
|
||||||
|
# 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)
|
||||||
|
sock.settimeout(None) # blocking während Send
|
||||||
sock.sendall(headers + body)
|
sock.sendall(headers + body)
|
||||||
sock.settimeout(120) # große GCode-Dateien brauchen Zeit bis der Drucker antwortet
|
sock.settimeout(180)
|
||||||
response = b""
|
response = b""
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
|
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
|
||||||
from PyInstaller.utils.hooks import collect_all
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
datas = [("web", "web")]
|
datas = [("web", "web"), ("data", "static"), ("VERSION", ".")] # bridge/data/ → static/ im _MEIPASS
|
||||||
binaries = []
|
binaries = []
|
||||||
hiddenimports = []
|
hiddenimports = []
|
||||||
|
|
||||||
|
|||||||
147
orca_filaments.py
Normal file
147
orca_filaments.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""OrcaSlicer Filament-Profil Parser.
|
||||||
|
|
||||||
|
Geteilt zwischen dem Generator (tools/gen_orca_filament_list.py) und dem
|
||||||
|
Custom-Profile-Import-Endpoint (bridge/kobrax_moonraker_bridge.py).
|
||||||
|
|
||||||
|
Liest Orca-Filament-JSON-Dateien (System- oder User-Profile) und gibt
|
||||||
|
sie als normalisierte Liste mit (id, name, vendor, type, color) zurück.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def first_str(value, default: str = "") -> str:
|
||||||
|
"""Orca-Profile speichern manche Felder als ['wert']. Liefert erstes
|
||||||
|
Element als String."""
|
||||||
|
if isinstance(value, list):
|
||||||
|
return str(value[0]) if value else default
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def clean_name(raw: str) -> str:
|
||||||
|
"""Strippt printer-/varianten-spezifische Suffixe:
|
||||||
|
'PolyTerra PLA @base' → 'PolyTerra PLA'
|
||||||
|
'Anycubic PLA @Anycubic Kobra X 0.4 nozzle' → 'Anycubic PLA'
|
||||||
|
'Anker Generic PLA 0.4 nozzle' → 'Anker Generic PLA'
|
||||||
|
"""
|
||||||
|
name = re.sub(r"\s*@.*$", "", raw).strip()
|
||||||
|
name = re.sub(r"\s+\d+(\.\d+)?\s*nozzle\s*$", "", name, flags=re.IGNORECASE).strip()
|
||||||
|
return name or raw
|
||||||
|
|
||||||
|
|
||||||
|
def parse_profile(data: dict, by_name: dict | None = None,
|
||||||
|
path_vendor: str | None = None,
|
||||||
|
source_path: str = "",
|
||||||
|
system_index: list | None = None) -> dict | None:
|
||||||
|
"""Parsed ein einzelnes Orca-Filament-Profil zum Bridge-Schema.
|
||||||
|
|
||||||
|
`by_name` ist optional ein {name: [profile, …]}-Index für Inherits-Resolve
|
||||||
|
aus dem rohen Source-Tree (Generator). Bei Single-File-Import (User-Datei
|
||||||
|
aus OrcaSlicer-User-Dir) reichen wir stattdessen `system_index` rein —
|
||||||
|
die fertige System-Profile-Liste aus orca_filaments.json. Damit können
|
||||||
|
wir filament_id/vendor/type/color über die `inherits`-Kette aus dem
|
||||||
|
System-Parent ableiten, auch wenn das User-Profil diese Felder nicht
|
||||||
|
selbst setzt (typisch: User-Override-Profile haben nur Tweaks).
|
||||||
|
|
||||||
|
Liefert {id, name, vendor, type, color} oder None wenn das Profil
|
||||||
|
keine filament_id hat (z.B. abstrakte @base-Templates).
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
# User-Profile aus dem OrcaSlicer-User-Dir setzen oft KEIN "type"-Feld —
|
||||||
|
# das kommt vom System-Parent. Wir akzeptieren das wenn entweder "type"
|
||||||
|
# explizit "filament" ist ODER ein "inherits" auf ein anderes Profil zeigt.
|
||||||
|
if data.get("type") not in (None, "filament") and not data.get("inherits"):
|
||||||
|
return None
|
||||||
|
if data.get("type") == "filament" and data.get("inherits") is None and not data.get("filament_id"):
|
||||||
|
# type=filament aber kein parent + keine ID → wertloses Stub
|
||||||
|
return None
|
||||||
|
inst = data.get("instantiation", "true")
|
||||||
|
if isinstance(inst, str) and inst.lower() == "false":
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Build system-name-Index für den fallback-Lookup wenn system_index gesetzt.
|
||||||
|
sys_by_name: dict[str, dict] = {}
|
||||||
|
if system_index:
|
||||||
|
for p in system_index:
|
||||||
|
if isinstance(p, dict) and p.get("name"):
|
||||||
|
sys_by_name[p["name"]] = p
|
||||||
|
|
||||||
|
def _resolve(key: str, depth: int = 5):
|
||||||
|
cur_list = [data]
|
||||||
|
for _ in range(depth):
|
||||||
|
for cur in cur_list:
|
||||||
|
v = cur.get(key)
|
||||||
|
if v not in ("", [], None, [""]) and v is not None:
|
||||||
|
return v
|
||||||
|
# Erst raw-Inherits via by_name (Generator-Pfad)
|
||||||
|
if by_name:
|
||||||
|
next_list: list[dict] = []
|
||||||
|
for cur in cur_list:
|
||||||
|
parent_name = cur.get("inherits")
|
||||||
|
if parent_name and parent_name in by_name:
|
||||||
|
next_list.extend(by_name[parent_name])
|
||||||
|
if next_list:
|
||||||
|
cur_list = next_list
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _resolve_via_system_index(key: str):
|
||||||
|
"""Inherits-Kette über system_index (clean_name-Match)."""
|
||||||
|
parent_raw = data.get("inherits")
|
||||||
|
if not parent_raw or not sys_by_name:
|
||||||
|
return None
|
||||||
|
parent_clean = clean_name(parent_raw)
|
||||||
|
sys_p = sys_by_name.get(parent_clean)
|
||||||
|
if not sys_p:
|
||||||
|
return None
|
||||||
|
# System-JSON benutzt schon das normalisierte Schema
|
||||||
|
mapping = {
|
||||||
|
"filament_id": "id",
|
||||||
|
"filament_vendor": "vendor",
|
||||||
|
"filament_type": "type",
|
||||||
|
"default_filament_colour": "color",
|
||||||
|
}
|
||||||
|
return sys_p.get(mapping.get(key, key))
|
||||||
|
|
||||||
|
def _resolve_full(key: str):
|
||||||
|
v = _resolve(key)
|
||||||
|
if v not in ("", [], None, [""]) and v is not None:
|
||||||
|
return v
|
||||||
|
return _resolve_via_system_index(key)
|
||||||
|
|
||||||
|
fid = _resolve_full("filament_id")
|
||||||
|
if not fid or not isinstance(fid, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
name_raw = data.get("name", fid)
|
||||||
|
name = clean_name(name_raw)
|
||||||
|
vendor = first_str(_resolve_full("filament_vendor")) or (path_vendor or "Generic")
|
||||||
|
ftype = first_str(_resolve_full("filament_type"), "")
|
||||||
|
color = first_str(_resolve_full("default_filament_colour"), "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": fid,
|
||||||
|
"name": name,
|
||||||
|
"vendor": vendor,
|
||||||
|
"type": ftype,
|
||||||
|
"color": color,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_profile_bytes(blob: bytes, source_name: str = "",
|
||||||
|
system_index: list | None = None) -> dict | None:
|
||||||
|
"""Liest ein einzelnes Profil aus JSON-Bytes. Für File-Upload-Pfad.
|
||||||
|
`system_index` ist optional die fertige Liste aus orca_filaments.json —
|
||||||
|
wird für die Inherits-Resolve von User-Profilen genutzt die das volle
|
||||||
|
Schema vom System-Parent erben."""
|
||||||
|
try:
|
||||||
|
data = json.loads(blob.decode("utf-8", errors="replace"))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return parse_profile(data, source_path=source_name, system_index=system_index)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,16 @@
|
|||||||
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
|
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
|
||||||
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
|
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
|
||||||
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
|
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
|
||||||
<button class="theme-btn" onclick="toggleLang()" id="lang-btn">EN</button>
|
<div style="display:flex;align-items:center;gap:6px">
|
||||||
|
<span aria-hidden="true" style="font-size:15px;line-height:1;opacity:.85">🌐</span>
|
||||||
|
<select class="theme-btn" id="lang-select" onchange="setLanguageFromSelect()" style="padding:6px 10px">
|
||||||
|
<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="Einstellungen">⚙</button>
|
||||||
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -98,6 +107,10 @@
|
|||||||
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||||
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||||
|
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
|
||||||
|
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -109,6 +122,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- OrcaSlicer-Profile (Custom-Profile-Import, Issue #41) -->
|
||||||
|
<div>
|
||||||
|
<div class="modal-section" id="modal-sec-orca-profiles">OrcaSlicer-Profile</div>
|
||||||
|
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
|
||||||
|
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
|
||||||
|
</div>
|
||||||
|
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
|
||||||
|
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
|
||||||
|
style="background:var(--raised);color:var(--txt)">
|
||||||
|
⬆ <span id="lbl-orca-profiles-import">Profile importieren</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="modal-section" id="modal-sec-version">Version</div>
|
<div class="modal-section" id="modal-sec-version">Version</div>
|
||||||
<div class="update-row">
|
<div class="update-row">
|
||||||
@@ -150,11 +176,50 @@
|
|||||||
oninput="highlightMatBtn(this.value)"
|
oninput="highlightMatBtn(this.value)"
|
||||||
style="margin-top:8px;width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
|
style="margin-top:8px;width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Orca-Filament-Profil-Override (für AMS-Sync) -->
|
||||||
|
<div style="margin-bottom:20px">
|
||||||
|
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-profile"></div>
|
||||||
|
<select id="slot-edit-profile"
|
||||||
|
style="width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
|
||||||
|
<option value="" id="slot-profile-default-opt"></option>
|
||||||
|
</select>
|
||||||
|
<div style="font-size:11px;color:var(--txt2);margin-top:4px" id="slot-profile-hint"></div>
|
||||||
|
<a href="#" onclick="event.preventDefault();openProfileImport()"
|
||||||
|
style="display:inline-block;margin-top:6px;font-size:11px;color:var(--accent);text-decoration:none"
|
||||||
|
id="lbl-slot-profile-import">★ Eigene Profile importieren…</a>
|
||||||
|
</div>
|
||||||
<button class="btn" id="btn-slot-edit-feed" style="width:100%;margin-bottom:8px" onclick="slotEditFeed()"></button>
|
<button class="btn" id="btn-slot-edit-feed" style="width:100%;margin-bottom:8px" onclick="slotEditFeed()"></button>
|
||||||
<button class="modal-save" id="btn-slot-edit-save" onclick="saveSlotEdit()"></button>
|
<button class="modal-save" id="btn-slot-edit-save" onclick="saveSlotEdit()"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ ORCA-PROFILE-IMPORT-DIALOG (Issue #41) ═══ -->
|
||||||
|
<div class="modal-overlay" id="profile-import-modal" onclick="if(event.target===this)closeProfileImport()">
|
||||||
|
<div class="modal-box" style="max-width:480px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||||
|
<div style="font-size:16px;font-weight:600" id="profile-import-title">Eigene OrcaSlicer-Profile importieren</div>
|
||||||
|
<button onclick="closeProfileImport()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px;color:var(--txt2);margin-bottom:12px;line-height:1.5" id="profile-import-help">
|
||||||
|
Lade ein <b>ZIP</b> deines OrcaSlicer-Filament-Ordners oder einzelne <b>.json</b>-Files hoch.<br>
|
||||||
|
In OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>
|
||||||
|
</div>
|
||||||
|
<div id="profile-import-drop" style="border:2px dashed var(--border);border-radius:8px;padding:24px;text-align:center;cursor:pointer;margin-bottom:12px"
|
||||||
|
ondragover="event.preventDefault();this.style.borderColor='var(--accent)'"
|
||||||
|
ondragleave="this.style.borderColor='var(--border)'"
|
||||||
|
ondrop="event.preventDefault();this.style.borderColor='var(--border)';doProfileImportUpload(event.dataTransfer.files)"
|
||||||
|
onclick="document.getElementById('profile-import-file').click()">
|
||||||
|
<div style="font-size:32px;margin-bottom:8px">⬆</div>
|
||||||
|
<div style="font-size:13px;color:var(--txt2)" id="profile-import-dropmsg">Hierher ziehen oder klicken</div>
|
||||||
|
<input type="file" id="profile-import-file" accept=".zip,.json" multiple
|
||||||
|
style="display:none" onchange="doProfileImportUpload(this.files);this.value=''">
|
||||||
|
</div>
|
||||||
|
<div id="profile-import-status" style="font-size:12px;margin-bottom:12px;min-height:18px"></div>
|
||||||
|
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="profile-import-list-label">Aktuell importiert</div>
|
||||||
|
<div id="profile-import-list" style="max-height:240px;overflow-y:auto;font-size:12px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<button class="nav-btn active" onclick="showPanel('dashboard')" id="nb-dashboard">
|
<button class="nav-btn active" onclick="showPanel('dashboard')" id="nb-dashboard">
|
||||||
@@ -202,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">
|
||||||
@@ -222,9 +293,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fname" id="d-fname" title="" style="margin-top:6px">–</div>
|
<div class="fname" id="d-fname" title="" style="margin-top:6px">–</div>
|
||||||
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px">
|
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
|
||||||
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="printAction('pause')">⏸ Pause</button>
|
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
|
||||||
<button class="btn btn-resume btn-sm" id="d-btn-resume" onclick="printAction('resume')">▶ Weiter</button>
|
|
||||||
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none">✂ <span id="d-btn-skip-label">Objekte</span></button>
|
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none">✂ <span id="d-btn-skip-label">Objekte</span></button>
|
||||||
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
|
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,7 +305,7 @@
|
|||||||
<div class="card-title"><span>⊙</span> <span id="d-card-temps">Temperaturen</span></div>
|
<div class="card-title"><span>⊙</span> <span id="d-card-temps">Temperaturen</span></div>
|
||||||
<div class="temp-card-inner">
|
<div class="temp-card-inner">
|
||||||
<div class="temp-block">
|
<div class="temp-block">
|
||||||
<div class="temp-label">Nozzle</div>
|
<div class="temp-label" id="d-lbl-nozzle">Nozzle</div>
|
||||||
<div class="temp-row">
|
<div class="temp-row">
|
||||||
<div class="temp-val" id="d-nt">–</div>
|
<div class="temp-val" id="d-nt">–</div>
|
||||||
<div class="temp-unit">°C</div>
|
<div class="temp-unit">°C</div>
|
||||||
@@ -400,6 +470,16 @@
|
|||||||
<option value="duration_asc" id="ss-dur">⏱ Druckzeit</option>
|
<option value="duration_asc" id="ss-dur">⏱ Druckzeit</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="store-upload-zone" onclick="document.getElementById('store-upload-input').click()"
|
||||||
|
ondragover="event.preventDefault();this.classList.add('drag-over')"
|
||||||
|
ondragleave="this.classList.remove('drag-over')"
|
||||||
|
ondrop="event.preventDefault();this.classList.remove('drag-over');uploadGcode(event.dataTransfer.files[0])">
|
||||||
|
<input type="file" id="store-upload-input" accept=".gcode,.bgcode"
|
||||||
|
style="display:none" onchange="uploadGcode(this.files[0]);this.value=''">
|
||||||
|
<span id="store-upload-icon">⬆</span>
|
||||||
|
<span id="store-upload-label"><span id="store-upload-label-prefix">GCode hierher ziehen oder </span><u id="store-upload-label-browse">durchsuchen</u></span>
|
||||||
|
<span id="store-upload-status" style="display:none"></span>
|
||||||
|
</div>
|
||||||
<div id="store-empty" style="display:none;color:var(--txt2);text-align:center;padding:40px 0;font-size:14px">
|
<div id="store-empty" style="display:none;color:var(--txt2);text-align:center;padding:40px 0;font-size:14px">
|
||||||
</div>
|
</div>
|
||||||
<div id="store-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px"></div>
|
<div id="store-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px"></div>
|
||||||
@@ -451,6 +531,22 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
<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>
|
||||||
|
<p id="store-web-verify-msg" style="font-size:13px;color:var(--txt);margin-bottom:12px">Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde.</p>
|
||||||
|
<div id="store-web-verify-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 id="store-web-verify-abort" onclick="closeStoreWebVerifyDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||||
|
<button id="store-web-verify-confirm" onclick="confirmStoreWebVerify()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Bestätigen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Filament-Slot-Dialog -->
|
<!-- Filament-Slot-Dialog -->
|
||||||
<div class="modal-overlay" id="filament-dialog" onclick="if(event.target===this)closeFilamentDialog()">
|
<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%">
|
||||||
@@ -484,7 +580,7 @@
|
|||||||
<input type="text" id="apd-name" placeholder="z.B. Kobra X Wohnzimmer" style="width:100%;box-sizing:border-box;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);margin-bottom:6px">
|
<input type="text" id="apd-name" placeholder="z.B. Kobra X Wohnzimmer" style="width:100%;box-sizing:border-box;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);margin-bottom:6px">
|
||||||
<div id="apd-status" style="font-size:12px;margin:8px 0;min-height:16px;color:var(--txt2)"></div>
|
<div id="apd-status" style="font-size:12px;margin:8px 0;min-height:16px;color:var(--txt2)"></div>
|
||||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||||
<button onclick="closeAddPrinterDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
<button id="apd-cancel" onclick="closeAddPrinterDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||||
<button id="apd-confirm" onclick="confirmAddPrinter()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Hinzufügen</button>
|
<button id="apd-confirm" onclick="confirmAddPrinter()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -212,6 +212,22 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
|
|||||||
.panel{display:none}
|
.panel{display:none}
|
||||||
.panel.active{display:block}
|
.panel.active{display:block}
|
||||||
|
|
||||||
|
/* ── FILE BROWSER UPLOAD ZONE ── */
|
||||||
|
#store-upload-zone{
|
||||||
|
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||||
|
gap:6px;padding:18px 12px;margin-bottom:14px;
|
||||||
|
border:2px dashed var(--border);border-radius:10px;
|
||||||
|
background:var(--raised);color:var(--txt2);
|
||||||
|
cursor:pointer;transition:border-color .15s,background .15s;
|
||||||
|
font-size:13px;text-align:center;user-select:none;
|
||||||
|
}
|
||||||
|
#store-upload-zone:hover{border-color:var(--accent);background:rgba(0,200,255,.06);color:var(--txt)}
|
||||||
|
#store-upload-zone.drag-over{border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)}
|
||||||
|
#store-upload-icon{font-size:22px;line-height:1}
|
||||||
|
.upload-status-busy{color:var(--txt2)}
|
||||||
|
.upload-status-ok{color:var(--ok)}
|
||||||
|
.upload-status-err{color:var(--err)}
|
||||||
|
|
||||||
/* ── MODAL ── */
|
/* ── MODAL ── */
|
||||||
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);
|
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);
|
||||||
z-index:200;align-items:center;justify-content:center;padding:16px}
|
z-index:200;align-items:center;justify-content:center;padding:16px}
|
||||||
|
|||||||
248
web/translations/de.json
Normal file
248
web/translations/de.json
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
{
|
||||||
|
"header_status_standby": "Bereit",
|
||||||
|
"header_status_printing": "Druckt",
|
||||||
|
"header_status_complete": "Fertig",
|
||||||
|
"header_status_error": "Fehler",
|
||||||
|
"kobra_free": "Bereit",
|
||||||
|
"kobra_busy": "Beschäftigt",
|
||||||
|
"kobra_printing": "Druckt",
|
||||||
|
"kobra_preheating": "Aufheizen",
|
||||||
|
"kobra_auto_leveling": "Nivellierung",
|
||||||
|
"kobra_checking": "Prüfung",
|
||||||
|
"kobra_updated": "Aktualisierung",
|
||||||
|
"kobra_init": "Initialisierung",
|
||||||
|
"kobra_pausing": "Pausiert...",
|
||||||
|
"kobra_paused": "Pausiert",
|
||||||
|
"kobra_resuming": "Fortsetzen...",
|
||||||
|
"kobra_resumed": "Fortgesetzt",
|
||||||
|
"kobra_stopping": "Stoppt...",
|
||||||
|
"kobra_stoped": "Gestoppt",
|
||||||
|
"kobra_finished": "Abgeschlossen",
|
||||||
|
"kobra_failed": "Fehler",
|
||||||
|
"kobra_canceled": "Abgebrochen",
|
||||||
|
"kobra_offline": "Offline",
|
||||||
|
"nav_dashboard": "Dashboard",
|
||||||
|
"nav_print": "Druck",
|
||||||
|
"nav_temps": "Temperaturen",
|
||||||
|
"nav_motion": "Achsen",
|
||||||
|
"nav_ams": "AMS",
|
||||||
|
"nav_extras": "Licht / Lüfter",
|
||||||
|
"nav_console": "Konsole",
|
||||||
|
"card_progress": "Fortschritt",
|
||||||
|
"card_temps": "Temperaturen",
|
||||||
|
"card_light_fan": "Lüfter",
|
||||||
|
"card_speed": "Druckgeschwindigkeit",
|
||||||
|
"card_cam": "Kamera",
|
||||||
|
"lbl_elapsed": "Verstrichen:",
|
||||||
|
"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",
|
||||||
|
"lbl_light": "💡 Licht",
|
||||||
|
"lbl_feed": "Einziehen",
|
||||||
|
"lbl_unload": "Ausziehen",
|
||||||
|
"card_ace_dry": "ACE Trocknung",
|
||||||
|
"ace_dry_dryer": "Trockner",
|
||||||
|
"ace_dry_status_off": "Status: Aus",
|
||||||
|
"ace_dry_status_on": "Status: Aktiv",
|
||||||
|
"ace_dry_status_remaining": "Rest",
|
||||||
|
"ace_dry_humidity": "Luftfeuchte",
|
||||||
|
"ace_dry_current_temp": "Temperatur",
|
||||||
|
"ace_dry_chart": "Verlauf (Temp/Feuchte)",
|
||||||
|
"ace_dry_temp": "Temperatur (°C)",
|
||||||
|
"ace_dry_duration": "Dauer (Min)",
|
||||||
|
"ace_dry_start": "▶ Start",
|
||||||
|
"ace_dry_stop": "■ Stop",
|
||||||
|
"ace_dry_auto_refill": "Auto-Nachschub",
|
||||||
|
"ace_dry_enable": "Trocknung aktivieren",
|
||||||
|
"ace_dry_temp_line": "Trocknungstemperatur",
|
||||||
|
"ace_dry_time_line": "Trocknungszeit",
|
||||||
|
"ace_dry_ui_pending": "(nur UI, Backend folgt)",
|
||||||
|
"ace_dry_dialog_title": "Trockner Temp/Zeit-Einstellungen",
|
||||||
|
"ace_dry_dialog_temp": "Temperatur (30-80°C)",
|
||||||
|
"ace_dry_dialog_time": "Restzeit (h:m:s)",
|
||||||
|
"ace_dry_dialog_confirm": "Bestätigen",
|
||||||
|
"ace_dry_dialog_cancel": "Abbrechen",
|
||||||
|
"ace_dry_dialog_save_restart": "Speichern & Neustart",
|
||||||
|
"ace_dry_dialog_custom_name": "Eigener Name",
|
||||||
|
"ace_dry_dialog_reset_default": "Auf Standard zurücksetzen",
|
||||||
|
"cam_placeholder": "📷 Kamera nicht gestartet",
|
||||||
|
"cam_stream_unavailable": "Stream nicht verfügbar",
|
||||||
|
"btn_cam_start": "▶ Kamera",
|
||||||
|
"btn_cam_stop": "◼ Kamera",
|
||||||
|
"btn_pause": "⏸ Pause",
|
||||||
|
"btn_resume": "▶ Weiter",
|
||||||
|
"btn_cancel": "✕ Stopp",
|
||||||
|
"label_nozzle": "Düse",
|
||||||
|
"label_bed": "Bett",
|
||||||
|
"label_fan": "🌀 Lüfter",
|
||||||
|
"label_light": "💡 Licht",
|
||||||
|
"label_on_off": "Ein / Aus",
|
||||||
|
"label_speed": "Geschwindigkeit",
|
||||||
|
"panel_print_title": "Drucksteuerung",
|
||||||
|
"panel_print_btn_pause": "⏸ Pause",
|
||||||
|
"panel_print_btn_resume": "▶ Fortsetzen",
|
||||||
|
"panel_print_btn_cancel": "✕ Abbrechen",
|
||||||
|
"panel_print_temps_live": "Temperaturen (Live)",
|
||||||
|
"label_set": "Setzen",
|
||||||
|
"label_off": "Aus",
|
||||||
|
"panel_temps_nozzle": "Düse",
|
||||||
|
"panel_temps_bed": "Heizbett",
|
||||||
|
"panel_temps_chart": "Verlauf (letzte 60 Messungen)",
|
||||||
|
"label_target_c": "Ziel:",
|
||||||
|
"panel_motion_xy": "XY-Achsen",
|
||||||
|
"panel_motion_z": "Z-Achse",
|
||||||
|
"label_step": "Schrittweite:",
|
||||||
|
"btn_home_z": "Home Z",
|
||||||
|
"btn_home_xy": "Home XY",
|
||||||
|
"btn_home_all": "Home All",
|
||||||
|
"btn_disable_motors": "Motoren aus",
|
||||||
|
"panel_ams_title": "Filament",
|
||||||
|
"card_ams": "Filament",
|
||||||
|
"ams_no_data": "Keine AMS-Daten empfangen",
|
||||||
|
"label_slot": "Slot",
|
||||||
|
"ams_empty": "Leer",
|
||||||
|
"panel_extras_light": "Licht",
|
||||||
|
"panel_extras_fan": "Lüfter",
|
||||||
|
"panel_extras_camera": "Kamera",
|
||||||
|
"btn_cam_start2": "▶ Start",
|
||||||
|
"btn_cam_stop2": "◼ Stop",
|
||||||
|
"panel_console_title": "Ereignis-Log",
|
||||||
|
"log_light_on": "Licht an",
|
||||||
|
"log_light_off": "Licht aus",
|
||||||
|
"log_fan": "Lüfter →",
|
||||||
|
"log_nozzle": "Düse →",
|
||||||
|
"log_bed": "Bett →",
|
||||||
|
"log_axis": "Achse",
|
||||||
|
"log_home": "Home",
|
||||||
|
"log_home_all": "Home All",
|
||||||
|
"log_cam_start": "Kamera gestartet:",
|
||||||
|
"log_cam_stop": "Kamera gestoppt",
|
||||||
|
"log_poll_error": "Poll-Fehler:",
|
||||||
|
"log_error": "Fehler:",
|
||||||
|
"confirm_cancel": "Druck wirklich abbrechen?",
|
||||||
|
"settings_title": "Einstellungen",
|
||||||
|
"settings_connection": "Verbindung",
|
||||||
|
"settings_print": "Druckeinstellungen",
|
||||||
|
"settings_poll": "Poll-Intervall",
|
||||||
|
"settings_version": "Version",
|
||||||
|
"settings_save": "Speichern & Neustart",
|
||||||
|
"settings_printer_name": "Drucker-Name",
|
||||||
|
"settings_printer_ip": "Drucker-IP",
|
||||||
|
"settings_mqtt_port": "MQTT-Port",
|
||||||
|
"settings_username": "MQTT-Benutzername",
|
||||||
|
"settings_password": "MQTT-Passwort",
|
||||||
|
"settings_device_id": "Device-ID",
|
||||||
|
"settings_mode_id": "Mode-ID",
|
||||||
|
"hint_ip_no_port": "Nur IP-Adresse, kein Port (z.B. 192.168.1.102)",
|
||||||
|
"settings_default_slot": "Standard-Slot (Einfarbdruck)",
|
||||||
|
"settings_slot_auto": "Auto (alle belegten Slots)",
|
||||||
|
"settings_auto_leveling": "Auto-Leveling vor Druck",
|
||||||
|
"settings_camera_on_print": "Kamera bei Druckstart einschalten",
|
||||||
|
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
|
||||||
|
"update_check": "Auf Updates prüfen",
|
||||||
|
"update_checking": "Prüfe...",
|
||||||
|
"update_available": "verfügbar",
|
||||||
|
"update_none": "Bereits aktuell",
|
||||||
|
"update_apply": "Jetzt installieren",
|
||||||
|
"update_applying": "Lade herunter...",
|
||||||
|
"update_restarting": "Starte neu...",
|
||||||
|
"update_error": "Fehler",
|
||||||
|
"btn_connect": "⚡ Verbinden",
|
||||||
|
"btn_disconnect": "✕ Trennen",
|
||||||
|
"lbl_conn_error": "Verbindungsfehler:",
|
||||||
|
"slot_edit_title": "Slot bearbeiten",
|
||||||
|
"slot_edit_color": "Farbe",
|
||||||
|
"slot_edit_material": "Material",
|
||||||
|
"slot_edit_load": "⬇ Einziehen",
|
||||||
|
"slot_edit_unload": "⬆ Ausziehen",
|
||||||
|
"slot_edit_save": "💾 Speichern",
|
||||||
|
"slot_edit_custom": "z.B. PLA, PETG, ABS…",
|
||||||
|
"slot_edit_ok": "AMS Slot",
|
||||||
|
"slot_edit_profile": "OrcaSlicer-Profil",
|
||||||
|
"slot_edit_profile_hint": "Sendet beim OrcaSlicer-Sync die konkrete Marke statt nur „Generic\"",
|
||||||
|
"slot_edit_profile_default": "— Generic (Default) —",
|
||||||
|
"orca_profile_section": "OrcaSlicer-Profile",
|
||||||
|
"orca_profile_hint": "Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)",
|
||||||
|
"orca_profile_import_btn": "Profile importieren",
|
||||||
|
"orca_profile_import_link": "★ Eigene Profile importieren…",
|
||||||
|
"orca_profile_import_title": "Eigene OrcaSlicer-Profile importieren",
|
||||||
|
"orca_profile_help_html": "Lade ein <b>ZIP</b> deines OrcaSlicer-Filament-Ordners oder einzelne <b>.json</b>-Files hoch.<br>In OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||||
|
"orca_profile_dropmsg": "Hierher ziehen oder klicken",
|
||||||
|
"orca_profile_list_label": "Aktuell importiert",
|
||||||
|
"orca_profile_user_label": "Eigene Profile",
|
||||||
|
"orca_profile_user_empty": "– keine –",
|
||||||
|
"orca_profile_uploading": "Lade hoch…",
|
||||||
|
"orca_profile_done": "Importiert",
|
||||||
|
"orca_profile_skipped": "übersprungen",
|
||||||
|
"log_dir_all": "Alle",
|
||||||
|
"log_lvl_label": "Level:",
|
||||||
|
"file_ready_btn": "▶ Druck starten",
|
||||||
|
"file_slots_btn": "🎨 Slots wählen",
|
||||||
|
"file_cancel_btn": "✕ Abbrechen",
|
||||||
|
"nav_printers": "Drucker",
|
||||||
|
"skip_title": "✂ Objekte überspringen",
|
||||||
|
"skip_hint": "Objekte abwählen, die nicht weiter gedruckt werden sollen:",
|
||||||
|
"skip_btn_label": "Objekte",
|
||||||
|
"skip_no_objects": "Keine Objekte in diesem Druck.",
|
||||||
|
"skip_already": "übersprungen",
|
||||||
|
"skip_select_at_least_one": "Bitte mindestens ein Objekt wählen.",
|
||||||
|
"skip_sending": "Sende …",
|
||||||
|
"skip_success": "Objekte werden übersprungen.",
|
||||||
|
"fd_objects_hint": "Objekte überspringen (optional):",
|
||||||
|
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
|
||||||
|
"fd_cancel": "Abbrechen",
|
||||||
|
"fd_print": "▶ Drucken",
|
||||||
|
"fd_no_slots_msg": "Keine belegten AMS-Slots.{br}Druck trotzdem starten?",
|
||||||
|
"fd_slot": "Slot",
|
||||||
|
"fd_no_matching_material": "Kein passendes Material",
|
||||||
|
"fd_used": "BELEGT",
|
||||||
|
"add_printer": "Drucker hinzufügen",
|
||||||
|
"apd_lbl_ip": "Drucker-IP",
|
||||||
|
"apd_lbl_name": "Name (optional)",
|
||||||
|
"apd_placeholder_name": "z.B. Kobra X Wohnzimmer",
|
||||||
|
"apd_cancel": "Abbrechen",
|
||||||
|
"apd_confirm": "Hinzufügen",
|
||||||
|
"apd_fetching": "Hole Daten vom Drucker…",
|
||||||
|
"apd_success": "Drucker hinzugefügt, Bridge startet neu…",
|
||||||
|
"apd_err_ip": "Bitte IP-Adresse eingeben",
|
||||||
|
"printers_remove": "Drucker entfernen",
|
||||||
|
"printers_remove_confirm": "Drucker \"{name}\" entfernen? Die Bridge startet neu.",
|
||||||
|
"printers_active": "● aktiv",
|
||||||
|
"printers_switch": "Wechseln →",
|
||||||
|
"printers_current": "Aktueller Drucker",
|
||||||
|
"printers_loading": "Lade…",
|
||||||
|
"printers_none": "Keine Drucker konfiguriert.",
|
||||||
|
"printers_empty_hint": "Noch kein Drucker eingerichtet.",
|
||||||
|
"nav_browser": "Browser",
|
||||||
|
"panel_browser_title": "Datei-Browser",
|
||||||
|
"store_search_placeholder": "🔍 Suche…",
|
||||||
|
"store_empty": "Noch keine Dateien hochgeladen.",
|
||||||
|
"store_refresh": "↻ Aktualisieren",
|
||||||
|
"store_print": "▶ Drucken",
|
||||||
|
"store_download": "⬇ Download",
|
||||||
|
"store_delete_confirm": "Datei löschen?",
|
||||||
|
"store_print_confirm": "Datei drucken?",
|
||||||
|
"store_web_verify_title": "Datei verifizieren",
|
||||||
|
"store_web_verify_msg": "Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde.",
|
||||||
|
"store_web_verify_confirm": "Bestätigen",
|
||||||
|
"store_web_verify_abort": "Abbrechen",
|
||||||
|
"store_no_results": "Keine Dateien gefunden.",
|
||||||
|
"store_never": "noch nicht gedruckt",
|
||||||
|
"store_estimate": "Schätzung",
|
||||||
|
"store_upload_label_prefix": "GCode hierher ziehen oder ",
|
||||||
|
"store_upload_label_browse": "durchsuchen",
|
||||||
|
"store_upload_busy": "⏳ Hochladen…",
|
||||||
|
"store_upload_success": "✓ {file}",
|
||||||
|
"store_upload_error": "✗ {error}",
|
||||||
|
"sf_all": "Alle",
|
||||||
|
"sf_ok": "✓ Erfolgreich",
|
||||||
|
"sf_err": "✗ Fehler",
|
||||||
|
"sf_new": "Neu",
|
||||||
|
"ss_date": "↓ Datum",
|
||||||
|
"ss_name": "A–Z Name",
|
||||||
|
"ss_dur": "⏱ Druckzeit"
|
||||||
|
}
|
||||||
248
web/translations/en.json
Normal file
248
web/translations/en.json
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
{
|
||||||
|
"header_status_standby": "Ready",
|
||||||
|
"header_status_printing": "Printing",
|
||||||
|
"header_status_complete": "Complete",
|
||||||
|
"header_status_error": "Error",
|
||||||
|
"kobra_free": "Ready",
|
||||||
|
"kobra_busy": "Busy",
|
||||||
|
"kobra_printing": "Printing",
|
||||||
|
"kobra_preheating": "Preheating",
|
||||||
|
"kobra_auto_leveling": "Auto Leveling",
|
||||||
|
"kobra_checking": "Checking",
|
||||||
|
"kobra_updated": "Updating",
|
||||||
|
"kobra_init": "Initializing",
|
||||||
|
"kobra_pausing": "Pausing...",
|
||||||
|
"kobra_paused": "Paused",
|
||||||
|
"kobra_resuming": "Resuming...",
|
||||||
|
"kobra_resumed": "Resumed",
|
||||||
|
"kobra_stopping": "Stopping...",
|
||||||
|
"kobra_stoped": "Stopped",
|
||||||
|
"kobra_finished": "Finished",
|
||||||
|
"kobra_failed": "Error",
|
||||||
|
"kobra_canceled": "Cancelled",
|
||||||
|
"kobra_offline": "Offline",
|
||||||
|
"nav_dashboard": "Dashboard",
|
||||||
|
"nav_print": "Print",
|
||||||
|
"nav_temps": "Temperatures",
|
||||||
|
"nav_motion": "Motion",
|
||||||
|
"nav_ams": "AMS",
|
||||||
|
"nav_extras": "Light / Fan",
|
||||||
|
"nav_console": "Console",
|
||||||
|
"card_progress": "Progress",
|
||||||
|
"card_temps": "Temperatures",
|
||||||
|
"card_light_fan": "Fan",
|
||||||
|
"card_speed": "Print Speed",
|
||||||
|
"card_cam": "Camera",
|
||||||
|
"lbl_elapsed": "Elapsed:",
|
||||||
|
"lbl_remaining": "Remaining:",
|
||||||
|
"lbl_slicer_time": "Slicer estimate:",
|
||||||
|
"lbl_layers": "Layer",
|
||||||
|
"lbl_zpos": "Z (mm)",
|
||||||
|
"speed_silent": "🐢 Silent",
|
||||||
|
"speed_normal": "⚡ Normal",
|
||||||
|
"speed_sport": "🚀 Sport",
|
||||||
|
"lbl_light": "💡 Light",
|
||||||
|
"lbl_feed": "Load",
|
||||||
|
"lbl_unload": "Unload",
|
||||||
|
"card_ace_dry": "ACE Drying",
|
||||||
|
"ace_dry_dryer": "Dryer",
|
||||||
|
"ace_dry_status_off": "Status: Off",
|
||||||
|
"ace_dry_status_on": "Status: Active",
|
||||||
|
"ace_dry_status_remaining": "Remaining",
|
||||||
|
"ace_dry_humidity": "Humidity",
|
||||||
|
"ace_dry_current_temp": "Temperature",
|
||||||
|
"ace_dry_chart": "History (Temp/Humidity)",
|
||||||
|
"ace_dry_temp": "Temperature (°C)",
|
||||||
|
"ace_dry_duration": "Duration (min)",
|
||||||
|
"ace_dry_start": "▶ Start",
|
||||||
|
"ace_dry_stop": "■ Stop",
|
||||||
|
"ace_dry_auto_refill": "Auto Refill",
|
||||||
|
"ace_dry_enable": "Enable Drying",
|
||||||
|
"ace_dry_temp_line": "Drying Temperature",
|
||||||
|
"ace_dry_time_line": "Drying Time",
|
||||||
|
"ace_dry_ui_pending": "(UI only, backend next)",
|
||||||
|
"ace_dry_dialog_title": "Dryer Temp/Time Settings",
|
||||||
|
"ace_dry_dialog_temp": "Temperature (30-80°C)",
|
||||||
|
"ace_dry_dialog_time": "Rem. Time (h:m:s)",
|
||||||
|
"ace_dry_dialog_confirm": "Confirm",
|
||||||
|
"ace_dry_dialog_cancel": "Cancel",
|
||||||
|
"ace_dry_dialog_save_restart": "Save & Restart",
|
||||||
|
"ace_dry_dialog_custom_name": "Custom Name",
|
||||||
|
"ace_dry_dialog_reset_default": "Reset to Default",
|
||||||
|
"cam_placeholder": "📷 Camera not started",
|
||||||
|
"cam_stream_unavailable": "Stream unavailable",
|
||||||
|
"btn_cam_start": "▶ Camera",
|
||||||
|
"btn_cam_stop": "◼ Camera",
|
||||||
|
"btn_pause": "⏸ Pause",
|
||||||
|
"btn_resume": "▶ Resume",
|
||||||
|
"btn_cancel": "✕ Stop",
|
||||||
|
"label_nozzle": "Nozzle",
|
||||||
|
"label_bed": "Bed",
|
||||||
|
"label_fan": "🌀 Fan",
|
||||||
|
"label_light": "💡 Light",
|
||||||
|
"label_on_off": "On / Off",
|
||||||
|
"label_speed": "Speed",
|
||||||
|
"panel_print_title": "Print Control",
|
||||||
|
"panel_print_btn_pause": "⏸ Pause",
|
||||||
|
"panel_print_btn_resume": "▶ Resume",
|
||||||
|
"panel_print_btn_cancel": "✕ Cancel",
|
||||||
|
"panel_print_temps_live": "Temperatures (Live)",
|
||||||
|
"label_set": "Set",
|
||||||
|
"label_off": "Off",
|
||||||
|
"panel_temps_nozzle": "Nozzle",
|
||||||
|
"panel_temps_bed": "Heated Bed",
|
||||||
|
"panel_temps_chart": "History (last 60 readings)",
|
||||||
|
"label_target_c": "Target:",
|
||||||
|
"panel_motion_xy": "XY Axes",
|
||||||
|
"panel_motion_z": "Z Axis",
|
||||||
|
"label_step": "Step size:",
|
||||||
|
"btn_home_z": "Home Z",
|
||||||
|
"btn_home_xy": "Home XY",
|
||||||
|
"btn_home_all": "Home All",
|
||||||
|
"btn_disable_motors": "Motors Off",
|
||||||
|
"panel_ams_title": "Filament",
|
||||||
|
"card_ams": "Filament",
|
||||||
|
"ams_no_data": "No AMS data received",
|
||||||
|
"label_slot": "Slot",
|
||||||
|
"ams_empty": "Empty",
|
||||||
|
"panel_extras_light": "Light",
|
||||||
|
"panel_extras_fan": "Fan",
|
||||||
|
"panel_extras_camera": "Camera",
|
||||||
|
"btn_cam_start2": "▶ Start",
|
||||||
|
"btn_cam_stop2": "◼ Stop",
|
||||||
|
"panel_console_title": "Event Log",
|
||||||
|
"log_light_on": "Light on",
|
||||||
|
"log_light_off": "Light off",
|
||||||
|
"log_fan": "Fan →",
|
||||||
|
"log_nozzle": "Nozzle →",
|
||||||
|
"log_bed": "Bed →",
|
||||||
|
"log_axis": "Axis",
|
||||||
|
"log_home": "Home",
|
||||||
|
"log_home_all": "Home All",
|
||||||
|
"log_cam_start": "Camera started:",
|
||||||
|
"log_cam_stop": "Camera stopped",
|
||||||
|
"log_poll_error": "Poll error:",
|
||||||
|
"log_error": "Error:",
|
||||||
|
"confirm_cancel": "Really cancel the print?",
|
||||||
|
"settings_title": "Settings",
|
||||||
|
"settings_connection": "Connection",
|
||||||
|
"settings_print": "Print Settings",
|
||||||
|
"settings_poll": "Poll Interval",
|
||||||
|
"settings_version": "Version",
|
||||||
|
"settings_save": "Save & Restart",
|
||||||
|
"settings_printer_name": "Printer Name",
|
||||||
|
"settings_printer_ip": "Printer IP",
|
||||||
|
"settings_mqtt_port": "MQTT Port",
|
||||||
|
"settings_username": "MQTT Username",
|
||||||
|
"settings_password": "MQTT Password",
|
||||||
|
"settings_device_id": "Device ID",
|
||||||
|
"settings_mode_id": "Mode ID",
|
||||||
|
"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_camera_on_print": "Turn camera on at print start",
|
||||||
|
"settings_web_upload_warning": "Show warning when printing web uploads",
|
||||||
|
"update_check": "Check for Updates",
|
||||||
|
"update_checking": "Checking...",
|
||||||
|
"update_available": "available",
|
||||||
|
"update_none": "Already up to date",
|
||||||
|
"update_apply": "Install Now",
|
||||||
|
"update_applying": "Downloading...",
|
||||||
|
"update_restarting": "Restarting...",
|
||||||
|
"update_error": "Error",
|
||||||
|
"btn_connect": "⚡ Connect",
|
||||||
|
"btn_disconnect": "✕ Disconnect",
|
||||||
|
"lbl_conn_error": "Connection error:",
|
||||||
|
"slot_edit_title": "Edit Slot",
|
||||||
|
"slot_edit_color": "Color",
|
||||||
|
"slot_edit_material": "Material",
|
||||||
|
"slot_edit_load": "⬇ Load",
|
||||||
|
"slot_edit_unload": "⬆ Unload",
|
||||||
|
"slot_edit_save": "💾 Save",
|
||||||
|
"slot_edit_custom": "e.g. PLA, PETG, ABS…",
|
||||||
|
"slot_edit_ok": "AMS Slot",
|
||||||
|
"slot_edit_profile": "OrcaSlicer profile",
|
||||||
|
"slot_edit_profile_hint": "Sent on OrcaSlicer sync as the specific brand instead of just \"Generic\"",
|
||||||
|
"slot_edit_profile_default": "— Generic (default) —",
|
||||||
|
"orca_profile_section": "OrcaSlicer Profiles",
|
||||||
|
"orca_profile_hint": "Import your own OrcaSlicer filament profiles (open the user dir via Help → Show Configuration Folder)",
|
||||||
|
"orca_profile_import_btn": "Import profiles",
|
||||||
|
"orca_profile_import_link": "★ Import own profiles…",
|
||||||
|
"orca_profile_import_title": "Import your OrcaSlicer profiles",
|
||||||
|
"orca_profile_help_html": "Upload a <b>ZIP</b> of your OrcaSlicer filament folder or single <b>.json</b> files.<br>In OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||||
|
"orca_profile_dropmsg": "Drop here or click",
|
||||||
|
"orca_profile_list_label": "Currently imported",
|
||||||
|
"orca_profile_user_label": "Own profiles",
|
||||||
|
"orca_profile_user_empty": "– none –",
|
||||||
|
"orca_profile_uploading": "Uploading…",
|
||||||
|
"orca_profile_done": "Imported",
|
||||||
|
"orca_profile_skipped": "skipped",
|
||||||
|
"log_dir_all": "All",
|
||||||
|
"log_lvl_label": "Level:",
|
||||||
|
"file_ready_btn": "▶ Start Print",
|
||||||
|
"file_slots_btn": "🎨 Select Slots",
|
||||||
|
"file_cancel_btn": "✕ Cancel",
|
||||||
|
"nav_printers": "Printers",
|
||||||
|
"skip_title": "✂ Skip objects",
|
||||||
|
"skip_hint": "Uncheck objects you no longer want to print:",
|
||||||
|
"skip_btn_label": "Objects",
|
||||||
|
"skip_no_objects": "No objects in this print.",
|
||||||
|
"skip_already": "skipped",
|
||||||
|
"skip_select_at_least_one": "Please pick at least one object.",
|
||||||
|
"skip_sending": "Sending …",
|
||||||
|
"skip_success": "Objects will be skipped.",
|
||||||
|
"fd_objects_hint": "Skip objects (optional):",
|
||||||
|
"fd_slots_hint": "Assign GCode channel to AMS slot:",
|
||||||
|
"fd_cancel": "Cancel",
|
||||||
|
"fd_print": "▶ Print",
|
||||||
|
"fd_no_slots_msg": "No loaded AMS slots.{br}Start print anyway?",
|
||||||
|
"fd_slot": "Slot",
|
||||||
|
"fd_no_matching_material": "No matching material",
|
||||||
|
"fd_used": "USED",
|
||||||
|
"add_printer": "Add printer",
|
||||||
|
"apd_lbl_ip": "Printer IP",
|
||||||
|
"apd_lbl_name": "Name (optional)",
|
||||||
|
"apd_placeholder_name": "e.g. Kobra X Living Room",
|
||||||
|
"apd_cancel": "Cancel",
|
||||||
|
"apd_confirm": "Add",
|
||||||
|
"apd_fetching": "Fetching data from printer…",
|
||||||
|
"apd_success": "Printer added, bridge restarting…",
|
||||||
|
"apd_err_ip": "Please enter an IP address",
|
||||||
|
"printers_remove": "Remove printer",
|
||||||
|
"printers_remove_confirm": "Remove printer \"{name}\"? The bridge will restart.",
|
||||||
|
"printers_active": "● active",
|
||||||
|
"printers_switch": "Switch →",
|
||||||
|
"printers_current": "Current printer",
|
||||||
|
"printers_loading": "Loading…",
|
||||||
|
"printers_none": "No printers configured.",
|
||||||
|
"printers_empty_hint": "No printer set up yet.",
|
||||||
|
"nav_browser": "Browser",
|
||||||
|
"panel_browser_title": "File Browser",
|
||||||
|
"store_search_placeholder": "🔍 Search…",
|
||||||
|
"store_empty": "No files uploaded yet.",
|
||||||
|
"store_refresh": "↻ Refresh",
|
||||||
|
"store_print": "▶ Print",
|
||||||
|
"store_download": "⬇ Download",
|
||||||
|
"store_delete_confirm": "Delete file?",
|
||||||
|
"store_print_confirm": "Print file?",
|
||||||
|
"store_web_verify_title": "Verify file",
|
||||||
|
"store_web_verify_msg": "Please verify this file was made for Anycubic Kobra X.",
|
||||||
|
"store_web_verify_confirm": "Confirm",
|
||||||
|
"store_web_verify_abort": "Abort",
|
||||||
|
"store_no_results": "No files found.",
|
||||||
|
"store_never": "never printed",
|
||||||
|
"store_estimate": "Estimate",
|
||||||
|
"store_upload_label_prefix": "Drag GCode here or ",
|
||||||
|
"store_upload_label_browse": "browse",
|
||||||
|
"store_upload_busy": "⏳ Uploading…",
|
||||||
|
"store_upload_success": "✓ {file}",
|
||||||
|
"store_upload_error": "✗ {error}",
|
||||||
|
"sf_all": "All",
|
||||||
|
"sf_ok": "✓ Completed",
|
||||||
|
"sf_err": "✗ Failed",
|
||||||
|
"sf_new": "New",
|
||||||
|
"ss_date": "↓ Date",
|
||||||
|
"ss_name": "A–Z Name",
|
||||||
|
"ss_dur": "⏱ Print time"
|
||||||
|
}
|
||||||
248
web/translations/es.json
Normal file
248
web/translations/es.json
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
{
|
||||||
|
"header_status_standby": "Listo",
|
||||||
|
"header_status_printing": "Imprimiendo",
|
||||||
|
"header_status_complete": "Completado",
|
||||||
|
"header_status_error": "Error",
|
||||||
|
"kobra_free": "Listo",
|
||||||
|
"kobra_busy": "Ocupado",
|
||||||
|
"kobra_printing": "Imprimiendo",
|
||||||
|
"kobra_preheating": "Precalentando",
|
||||||
|
"kobra_auto_leveling": "Autonivelado",
|
||||||
|
"kobra_checking": "Comprobando",
|
||||||
|
"kobra_updated": "Actualizando",
|
||||||
|
"kobra_init": "Inicializando",
|
||||||
|
"kobra_pausing": "Pausando...",
|
||||||
|
"kobra_paused": "Pausado",
|
||||||
|
"kobra_resuming": "Reanudando...",
|
||||||
|
"kobra_resumed": "Reanudado",
|
||||||
|
"kobra_stopping": "Deteniendo...",
|
||||||
|
"kobra_stoped": "Detenido",
|
||||||
|
"kobra_finished": "Finalizado",
|
||||||
|
"kobra_failed": "Error",
|
||||||
|
"kobra_canceled": "Cancelado",
|
||||||
|
"kobra_offline": "Desconectada",
|
||||||
|
"nav_dashboard": "Panel",
|
||||||
|
"nav_print": "Impresión",
|
||||||
|
"nav_temps": "Temperaturas",
|
||||||
|
"nav_motion": "Movimiento",
|
||||||
|
"nav_ams": "AMS",
|
||||||
|
"nav_extras": "Luz / Ventilador",
|
||||||
|
"nav_console": "Consola",
|
||||||
|
"card_progress": "Progreso",
|
||||||
|
"card_temps": "Temperaturas",
|
||||||
|
"card_light_fan": "Ventilador",
|
||||||
|
"card_speed": "Velocidad de impresión",
|
||||||
|
"card_cam": "Cámara",
|
||||||
|
"lbl_elapsed": "Transcurrido:",
|
||||||
|
"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",
|
||||||
|
"lbl_light": "💡 Luz",
|
||||||
|
"lbl_feed": "Cargar",
|
||||||
|
"lbl_unload": "Descargar",
|
||||||
|
"card_ace_dry": "Secado ACE",
|
||||||
|
"ace_dry_dryer": "Secador",
|
||||||
|
"ace_dry_status_off": "Estado: Apagado",
|
||||||
|
"ace_dry_status_on": "Estado: Activo",
|
||||||
|
"ace_dry_status_remaining": "Restante",
|
||||||
|
"ace_dry_humidity": "Humedad",
|
||||||
|
"ace_dry_current_temp": "Temperatura",
|
||||||
|
"ace_dry_chart": "Historial (Temp/Humedad)",
|
||||||
|
"ace_dry_temp": "Temperatura (°C)",
|
||||||
|
"ace_dry_duration": "Duración (min)",
|
||||||
|
"ace_dry_start": "▶ Iniciar",
|
||||||
|
"ace_dry_stop": "■ Parar",
|
||||||
|
"ace_dry_auto_refill": "Relleno automático",
|
||||||
|
"ace_dry_enable": "Activar secado",
|
||||||
|
"ace_dry_temp_line": "Temperatura de secado",
|
||||||
|
"ace_dry_time_line": "Tiempo de secado",
|
||||||
|
"ace_dry_ui_pending": "(solo UI, backend después)",
|
||||||
|
"ace_dry_dialog_title": "Ajustes de temp/tiempo del secador",
|
||||||
|
"ace_dry_dialog_temp": "Temperatura (30-80°C)",
|
||||||
|
"ace_dry_dialog_time": "Tiempo restante (h:m:s)",
|
||||||
|
"ace_dry_dialog_confirm": "Confirmar",
|
||||||
|
"ace_dry_dialog_cancel": "Cancelar",
|
||||||
|
"ace_dry_dialog_save_restart": "Guardar y reiniciar",
|
||||||
|
"ace_dry_dialog_custom_name": "Nombre personalizado",
|
||||||
|
"ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
|
||||||
|
"cam_placeholder": "📷 Cámara no iniciada",
|
||||||
|
"cam_stream_unavailable": "Stream no disponible",
|
||||||
|
"btn_cam_start": "▶ Cámara",
|
||||||
|
"btn_cam_stop": "◼ Cámara",
|
||||||
|
"btn_pause": "⏸ Pausa",
|
||||||
|
"btn_resume": "▶ Reanudar",
|
||||||
|
"btn_cancel": "✕ Detener",
|
||||||
|
"label_nozzle": "Boquilla",
|
||||||
|
"label_bed": "Cama",
|
||||||
|
"label_fan": "🌀 Ventilador",
|
||||||
|
"label_light": "💡 Luz",
|
||||||
|
"label_on_off": "Encendido / Apagado",
|
||||||
|
"label_speed": "Velocidad",
|
||||||
|
"panel_print_title": "Control de impresión",
|
||||||
|
"panel_print_btn_pause": "⏸ Pausa",
|
||||||
|
"panel_print_btn_resume": "▶ Reanudar",
|
||||||
|
"panel_print_btn_cancel": "✕ Cancelar",
|
||||||
|
"panel_print_temps_live": "Temperaturas (en vivo)",
|
||||||
|
"label_set": "Set",
|
||||||
|
"label_off": "Apagado",
|
||||||
|
"panel_temps_nozzle": "Boquilla",
|
||||||
|
"panel_temps_bed": "Cama caliente",
|
||||||
|
"panel_temps_chart": "Historial (últimas 60 lecturas)",
|
||||||
|
"label_target_c": "Objetivo:",
|
||||||
|
"panel_motion_xy": "Ejes XY",
|
||||||
|
"panel_motion_z": "Eje Z",
|
||||||
|
"label_step": "Tamaño del paso:",
|
||||||
|
"btn_home_z": "Home Z",
|
||||||
|
"btn_home_xy": "Home XY",
|
||||||
|
"btn_home_all": "Home All",
|
||||||
|
"btn_disable_motors": "Motores apagados",
|
||||||
|
"panel_ams_title": "Filamento",
|
||||||
|
"card_ams": "Filamento",
|
||||||
|
"ams_no_data": "No se recibieron datos de AMS",
|
||||||
|
"label_slot": "Ranura",
|
||||||
|
"ams_empty": "Vacío",
|
||||||
|
"panel_extras_light": "Luz",
|
||||||
|
"panel_extras_fan": "Ventilador",
|
||||||
|
"panel_extras_camera": "Cámara",
|
||||||
|
"btn_cam_start2": "▶ Iniciar",
|
||||||
|
"btn_cam_stop2": "◼ Detener",
|
||||||
|
"panel_console_title": "Registro de eventos",
|
||||||
|
"log_light_on": "Luz encendida",
|
||||||
|
"log_light_off": "Luz apagada",
|
||||||
|
"log_fan": "Ventilador →",
|
||||||
|
"log_nozzle": "Boquilla →",
|
||||||
|
"log_bed": "Cama →",
|
||||||
|
"log_axis": "Eje",
|
||||||
|
"log_home": "Home",
|
||||||
|
"log_home_all": "Home All",
|
||||||
|
"log_cam_start": "Cámara iniciada:",
|
||||||
|
"log_cam_stop": "Cámara detenida",
|
||||||
|
"log_poll_error": "Error de sondeo:",
|
||||||
|
"log_error": "Error:",
|
||||||
|
"confirm_cancel": "¿Realmente cancelar la impresión?",
|
||||||
|
"settings_title": "Configuración",
|
||||||
|
"settings_connection": "Conexión",
|
||||||
|
"settings_print": "Ajustes de impresión",
|
||||||
|
"settings_poll": "Intervalo de sondeo",
|
||||||
|
"settings_version": "Versión",
|
||||||
|
"settings_save": "Guardar y reiniciar",
|
||||||
|
"settings_printer_name": "Nombre de impresora",
|
||||||
|
"settings_printer_ip": "IP de impresora",
|
||||||
|
"settings_mqtt_port": "MQTT Port",
|
||||||
|
"settings_username": "Usuario MQTT",
|
||||||
|
"settings_password": "Contraseña MQTT",
|
||||||
|
"settings_device_id": "ID del dispositivo",
|
||||||
|
"settings_mode_id": "ID de modo",
|
||||||
|
"hint_ip_no_port": "Solo dirección IP, sin puerto (p. ej. 192.168.1.102)",
|
||||||
|
"settings_default_slot": "Ranura predeterminada (un color)",
|
||||||
|
"settings_slot_auto": "Auto (todos los slots cargados)",
|
||||||
|
"settings_auto_leveling": "Autonivelado antes de imprimir",
|
||||||
|
"settings_camera_on_print": "Encender cámara al iniciar impresión",
|
||||||
|
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
|
||||||
|
"update_check": "Buscar actualizaciones",
|
||||||
|
"update_checking": "Comprobando...",
|
||||||
|
"update_available": "disponible",
|
||||||
|
"update_none": "Ya actualizado",
|
||||||
|
"update_apply": "Instalar ahora",
|
||||||
|
"update_applying": "Descargando...",
|
||||||
|
"update_restarting": "Reiniciando...",
|
||||||
|
"update_error": "Error",
|
||||||
|
"btn_connect": "⚡ Conectar",
|
||||||
|
"btn_disconnect": "✕ Desconectar",
|
||||||
|
"lbl_conn_error": "Error de conexión:",
|
||||||
|
"slot_edit_title": "Editar slot",
|
||||||
|
"slot_edit_color": "Color",
|
||||||
|
"slot_edit_material": "Material",
|
||||||
|
"slot_edit_load": "⬇ Cargar",
|
||||||
|
"slot_edit_unload": "⬆ Descargar",
|
||||||
|
"slot_edit_save": "💾 Guardar",
|
||||||
|
"slot_edit_custom": "p. ej. PLA, PETG, ABS…",
|
||||||
|
"slot_edit_ok": "Ranura AMS",
|
||||||
|
"slot_edit_profile": "Perfil de OrcaSlicer",
|
||||||
|
"slot_edit_profile_hint": "Envía al sincronizar con OrcaSlicer la marca concreta en lugar de solo \"Generic\"",
|
||||||
|
"slot_edit_profile_default": "— Genérico (Predeterminado) —",
|
||||||
|
"orca_profile_section": "Perfiles de OrcaSlicer",
|
||||||
|
"orca_profile_hint": "Importa tus propios perfiles de filamento de OrcaSlicer (abre el directorio del usuario vía Help → Show Configuration Folder)",
|
||||||
|
"orca_profile_import_btn": "Importar perfiles",
|
||||||
|
"orca_profile_import_link": "★ Importar perfiles propios…",
|
||||||
|
"orca_profile_import_title": "Importar tus perfiles de OrcaSlicer",
|
||||||
|
"orca_profile_help_html": "Sube un <b>ZIP</b> de tu carpeta de filamentos de OrcaSlicer o archivos <b>.json</b> sueltos.<br>En OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||||
|
"orca_profile_dropmsg": "Suelta aquí o haz clic",
|
||||||
|
"orca_profile_list_label": "Actualmente importados",
|
||||||
|
"orca_profile_user_label": "Perfiles propios",
|
||||||
|
"orca_profile_user_empty": "– ninguno –",
|
||||||
|
"orca_profile_uploading": "Subiendo…",
|
||||||
|
"orca_profile_done": "Importado",
|
||||||
|
"orca_profile_skipped": "omitido",
|
||||||
|
"log_dir_all": "Todos",
|
||||||
|
"log_lvl_label": "Nivel:",
|
||||||
|
"file_ready_btn": "▶ Iniciar impresión",
|
||||||
|
"file_slots_btn": "🎨 Seleccionar ranuras",
|
||||||
|
"file_cancel_btn": "✕ Cancelar",
|
||||||
|
"nav_printers": "Impresoras",
|
||||||
|
"skip_title": "✂ Omitir objetos",
|
||||||
|
"skip_hint": "Deselecciona los objetos que ya no quieras imprimir:",
|
||||||
|
"skip_btn_label": "Objetos",
|
||||||
|
"skip_no_objects": "No hay objetos en esta impresión.",
|
||||||
|
"skip_already": "omitido",
|
||||||
|
"skip_select_at_least_one": "Selecciona al menos un objeto.",
|
||||||
|
"skip_sending": "Enviando …",
|
||||||
|
"skip_success": "Se omitirán los objetos.",
|
||||||
|
"fd_objects_hint": "Omitir objetos (opcional):",
|
||||||
|
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
|
||||||
|
"fd_cancel": "Cancelar",
|
||||||
|
"fd_print": "▶ Imprimir",
|
||||||
|
"fd_no_slots_msg": "No hay slots AMS cargados.{br}¿Iniciar impresión de todos modos?",
|
||||||
|
"fd_slot": "Ranura",
|
||||||
|
"fd_no_matching_material": "No hay material compatible",
|
||||||
|
"fd_used": "USADO",
|
||||||
|
"add_printer": "Añadir impresora",
|
||||||
|
"apd_lbl_ip": "IP de impresora",
|
||||||
|
"apd_lbl_name": "Nombre (opcional)",
|
||||||
|
"apd_placeholder_name": "p. ej. Kobra X Sala",
|
||||||
|
"apd_cancel": "Cancelar",
|
||||||
|
"apd_confirm": "Añadir",
|
||||||
|
"apd_fetching": "Obteniendo datos de la impresora…",
|
||||||
|
"apd_success": "Impresora añadida, reiniciando bridge…",
|
||||||
|
"apd_err_ip": "Introduce una dirección IP",
|
||||||
|
"printers_remove": "Eliminar impresora",
|
||||||
|
"printers_remove_confirm": "¿Eliminar impresora \"{name}\"? El bridge se reiniciará.",
|
||||||
|
"printers_active": "● activa",
|
||||||
|
"printers_switch": "Cambiar →",
|
||||||
|
"printers_current": "Impresora actual",
|
||||||
|
"printers_loading": "Cargando…",
|
||||||
|
"printers_none": "No hay impresoras configuradas.",
|
||||||
|
"printers_empty_hint": "Aún no hay impresora configurada.",
|
||||||
|
"nav_browser": "Explorador",
|
||||||
|
"panel_browser_title": "Explorador de archivos",
|
||||||
|
"store_search_placeholder": "🔍 Buscar…",
|
||||||
|
"store_empty": "Aún no hay archivos subidos.",
|
||||||
|
"store_refresh": "↻ Actualizar",
|
||||||
|
"store_print": "▶ Imprimir",
|
||||||
|
"store_download": "⬇ Descargar",
|
||||||
|
"store_delete_confirm": "¿Eliminar archivo?",
|
||||||
|
"store_print_confirm": "¿Imprimir archivo?",
|
||||||
|
"store_web_verify_title": "Verificar archivo",
|
||||||
|
"store_web_verify_msg": "Verifica que este archivo fue creado para Anycubic Kobra X.",
|
||||||
|
"store_web_verify_confirm": "Confirmar",
|
||||||
|
"store_web_verify_abort": "Abortar",
|
||||||
|
"store_no_results": "No se encontraron archivos.",
|
||||||
|
"store_never": "nunca impreso",
|
||||||
|
"store_estimate": "Estimación",
|
||||||
|
"store_upload_label_prefix": "Arrastra el GCode aquí o ",
|
||||||
|
"store_upload_label_browse": "buscar",
|
||||||
|
"store_upload_busy": "⏳ Subiendo…",
|
||||||
|
"store_upload_success": "✓ {file}",
|
||||||
|
"store_upload_error": "✗ {error}",
|
||||||
|
"sf_all": "Todos",
|
||||||
|
"sf_ok": "✓ Completado",
|
||||||
|
"sf_err": "✗ Fallido",
|
||||||
|
"sf_new": "Nuevo",
|
||||||
|
"ss_date": "↓ Fecha",
|
||||||
|
"ss_name": "A–Z Nombre",
|
||||||
|
"ss_dur": "⏱ Tiempo de impresión"
|
||||||
|
}
|
||||||
249
web/translations/fr.json
Normal file
249
web/translations/fr.json
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
{
|
||||||
|
"header_status_standby": "Prêt",
|
||||||
|
"header_status_printing": "Impression",
|
||||||
|
"header_status_complete": "Terminé",
|
||||||
|
"header_status_error": "Erreur",
|
||||||
|
"kobra_free": "Disponible",
|
||||||
|
"kobra_busy": "Occupé",
|
||||||
|
"kobra_printing": "Impression",
|
||||||
|
"kobra_preheating": "Préchauffage",
|
||||||
|
"kobra_auto_leveling": "Mise à niveau auto",
|
||||||
|
"kobra_checking": "Vérification",
|
||||||
|
"kobra_updated": "Mise à jour",
|
||||||
|
"kobra_init": "Initialisation",
|
||||||
|
"kobra_pausing": "Pause en cours…",
|
||||||
|
"kobra_paused": "En pause",
|
||||||
|
"kobra_resuming": "Reprise en cours…",
|
||||||
|
"kobra_resumed": "Repris",
|
||||||
|
"kobra_stopping": "Arrêt en cours…",
|
||||||
|
"kobra_stoped": "Arrêté",
|
||||||
|
"kobra_finished": "Terminé",
|
||||||
|
"kobra_failed": "Erreur",
|
||||||
|
"kobra_canceled": "Annulé",
|
||||||
|
"kobra_offline": "Hors ligne",
|
||||||
|
"nav_dashboard": "Tableau de bord",
|
||||||
|
"nav_print": "Impression",
|
||||||
|
"nav_temps": "Températures",
|
||||||
|
"nav_motion": "Mouvement",
|
||||||
|
"nav_ams": "AMS",
|
||||||
|
"nav_extras": "Lumière / Ventilateur",
|
||||||
|
"nav_console": "Console",
|
||||||
|
"card_progress": "Progression",
|
||||||
|
"card_temps": "Températures",
|
||||||
|
"card_light_fan": "Ventilateur",
|
||||||
|
"card_speed": "Vitesse d'impression",
|
||||||
|
"card_cam": "Caméra",
|
||||||
|
"lbl_elapsed": "Écoulé :",
|
||||||
|
"lbl_remaining": "Restant :",
|
||||||
|
"lbl_slicer_time": "Estimation slicer :",
|
||||||
|
"lbl_layers": "Couche",
|
||||||
|
"lbl_zpos": "Z (mm)",
|
||||||
|
"speed_silent": "🐢 Silencieux",
|
||||||
|
"speed_normal": "⚡ Normal",
|
||||||
|
"speed_sport": "🚀 Sport",
|
||||||
|
"lbl_light": "💡 Lumière",
|
||||||
|
"lbl_feed": "Charger",
|
||||||
|
"lbl_unload": "Décharger",
|
||||||
|
"card_ace_dry": "Séchage ACE",
|
||||||
|
"ace_dry_dryer": "Séchoir",
|
||||||
|
"ace_dry_status_off": "Statut : Arrêté",
|
||||||
|
"ace_dry_status_on": "Statut : Actif",
|
||||||
|
"ace_dry_status_remaining": "Restant",
|
||||||
|
"ace_dry_humidity": "Humidité",
|
||||||
|
"ace_dry_current_temp": "Température",
|
||||||
|
"ace_dry_chart": "Historique (Temp/Humidité)",
|
||||||
|
"ace_dry_temp": "Température (°C)",
|
||||||
|
"ace_dry_duration": "Durée (min)",
|
||||||
|
"ace_dry_start": "▶ Démarrer",
|
||||||
|
"ace_dry_stop": "■ Arrêter",
|
||||||
|
"ace_dry_auto_refill": "Remplissage auto",
|
||||||
|
"ace_dry_enable": "Activer le séchage",
|
||||||
|
"ace_dry_temp_line": "Température de séchage",
|
||||||
|
"ace_dry_time_line": "Durée de séchage",
|
||||||
|
"ace_dry_ui_pending": "(Interface seule, backend suivant)",
|
||||||
|
"ace_dry_dialog_title": "Réglages Temp/Durée du séchoir",
|
||||||
|
"ace_dry_dialog_temp": "Température (30-80°C)",
|
||||||
|
"ace_dry_dialog_time": "Temps restant (h:m:s)",
|
||||||
|
"ace_dry_dialog_confirm": "Confirmer",
|
||||||
|
"ace_dry_dialog_cancel": "Annuler",
|
||||||
|
"ace_dry_dialog_save_restart": "Enregistrer et redémarrer",
|
||||||
|
"ace_dry_dialog_custom_name": "Nom personnalisé",
|
||||||
|
"ace_dry_dialog_reset_default": "Réinitialiser",
|
||||||
|
"cam_placeholder": "📷 Caméra non démarrée",
|
||||||
|
"cam_stream_unavailable": "Flux indisponible",
|
||||||
|
"btn_cam_start": "▶ Caméra",
|
||||||
|
"btn_cam_stop": "◼ Caméra",
|
||||||
|
"btn_pause": "⏸ Pause",
|
||||||
|
"btn_resume": "▶ Reprendre",
|
||||||
|
"btn_cancel": "✕ Arrêter",
|
||||||
|
"label_nozzle": "Buse",
|
||||||
|
"label_bed": "Plateau",
|
||||||
|
"label_fan": "🌀 Ventilateur",
|
||||||
|
"label_light": "💡 Lumière",
|
||||||
|
"label_on_off": "On / Off",
|
||||||
|
"label_speed": "Vitesse",
|
||||||
|
"panel_print_title": "Contrôle impression",
|
||||||
|
"panel_print_btn_pause": "⏸ Pause",
|
||||||
|
"panel_print_btn_resume": "▶ Reprendre",
|
||||||
|
"panel_print_btn_cancel": "✕ Annuler",
|
||||||
|
"panel_print_temps_live": "Températures (en direct)",
|
||||||
|
"label_set": "Définir",
|
||||||
|
"label_off": "Éteint",
|
||||||
|
"panel_temps_nozzle": "Buse",
|
||||||
|
"panel_temps_bed": "Plateau chauffant",
|
||||||
|
"panel_temps_chart": "Historique (60 dernières valeurs)",
|
||||||
|
"label_target_c": "Cible :",
|
||||||
|
"panel_motion_xy": "Axes XY",
|
||||||
|
"panel_motion_z": "Axe Z",
|
||||||
|
"label_step": "Pas :",
|
||||||
|
"btn_home_z": "Origine Z",
|
||||||
|
"btn_home_xy": "Origine XY",
|
||||||
|
"btn_home_all": "Origine Tout",
|
||||||
|
"btn_disable_motors": "Moteurs Off",
|
||||||
|
"panel_ams_title": "Filament",
|
||||||
|
"card_ams": "Filament",
|
||||||
|
"ams_no_data": "Aucune donnée AMS reçue",
|
||||||
|
"label_slot": "Slot",
|
||||||
|
"ams_empty": "Vide",
|
||||||
|
"panel_extras_light": "Lumière",
|
||||||
|
"panel_extras_fan": "Ventilateur",
|
||||||
|
"panel_extras_camera": "Caméra",
|
||||||
|
"btn_cam_start2": "▶ Démarrer",
|
||||||
|
"btn_cam_stop2": "◼ Arrêter",
|
||||||
|
"panel_console_title": "Journal d'événements",
|
||||||
|
"log_light_on": "Lumière allumée",
|
||||||
|
"log_light_off": "Lumière éteinte",
|
||||||
|
"log_fan": "Ventilateur →",
|
||||||
|
"log_nozzle": "Buse →",
|
||||||
|
"log_bed": "Plateau →",
|
||||||
|
"log_axis": "Axe",
|
||||||
|
"log_home": "Origine",
|
||||||
|
"log_home_all": "Origine Tout",
|
||||||
|
"log_cam_start": "Caméra démarrée :",
|
||||||
|
"log_cam_stop": "Caméra arrêtée",
|
||||||
|
"log_poll_error": "Erreur de sondage :",
|
||||||
|
"log_error": "Erreur :",
|
||||||
|
"confirm_cancel": "Vraiment annuler l'impression ?",
|
||||||
|
"settings_title": "Paramètres",
|
||||||
|
"settings_connection": "Connexion",
|
||||||
|
"settings_print": "Paramètres d'impression",
|
||||||
|
"settings_poll": "Intervalle de sondage",
|
||||||
|
"settings_version": "Version",
|
||||||
|
"settings_save": "Enregistrer et redémarrer",
|
||||||
|
"settings_printer_name": "Nom de l'imprimante",
|
||||||
|
"settings_printer_ip": "IP de l'imprimante",
|
||||||
|
"settings_mqtt_port": "Port MQTT",
|
||||||
|
"settings_username": "Nom d'utilisateur MQTT",
|
||||||
|
"settings_password": "Mot de passe MQTT",
|
||||||
|
"settings_device_id": "ID de l'appareil",
|
||||||
|
"settings_mode_id": "ID du mode",
|
||||||
|
"hint_ip_no_port": "Adresse IP uniquement, sans port (ex. 192.168.1.102)",
|
||||||
|
"settings_default_slot": "Slot par défaut (couleur unique)",
|
||||||
|
"settings_slot_auto": "Auto (tous les slots chargés)",
|
||||||
|
"settings_auto_leveling": "Mise à niveau auto avant impression",
|
||||||
|
"settings_camera_on_print": "Activer la caméra au démarrage de l'impression",
|
||||||
|
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
|
||||||
|
"update_check": "Vérifier les mises à jour",
|
||||||
|
"update_checking": "Vérification…",
|
||||||
|
"update_available": "disponible",
|
||||||
|
"update_none": "Déjà à jour",
|
||||||
|
"update_apply": "Installer maintenant",
|
||||||
|
"update_applying": "Téléchargement…",
|
||||||
|
"update_restarting": "Redémarrage…",
|
||||||
|
"update_error": "Erreur",
|
||||||
|
"btn_connect": "⚡ Connecter",
|
||||||
|
"btn_disconnect": "✕ Déconnecter",
|
||||||
|
"lbl_conn_error": "Erreur de connexion :",
|
||||||
|
"slot_edit_title": "Modifier le slot",
|
||||||
|
"slot_edit_color": "Couleur",
|
||||||
|
"slot_edit_material": "Matériau",
|
||||||
|
"slot_edit_load": "⬇ Charger",
|
||||||
|
"slot_edit_unload": "⬆ Décharger",
|
||||||
|
"slot_edit_save": "💾 Enregistrer",
|
||||||
|
"slot_edit_custom": "ex. PLA, PETG, ABS…",
|
||||||
|
"slot_edit_ok": "Slot AMS",
|
||||||
|
"slot_edit_profile": "Profil OrcaSlicer",
|
||||||
|
"slot_edit_profile_hint": "Envoyé lors de la synchronisation OrcaSlicer comme marque spécifique au lieu de \"Générique\"",
|
||||||
|
"slot_edit_profile_default": "— Générique (défaut) —",
|
||||||
|
"orca_profile_section": "Profils OrcaSlicer",
|
||||||
|
"orca_profile_hint": "Importez vos propres profils de filament OrcaSlicer (ouvrez le dossier utilisateur via Aide → Afficher le dossier de configuration)",
|
||||||
|
"orca_profile_import_btn": "Importer des profils",
|
||||||
|
"orca_profile_import_link": "★ Importer mes profils…",
|
||||||
|
"orca_profile_import_title": "Importer vos profils OrcaSlicer",
|
||||||
|
"orca_profile_help_html": "Déposez un <b>ZIP</b> de votre dossier filament OrcaSlicer ou des fichiers <b>.json</b> individuels.<br>Dans OrcaSlicer : <i>Aide → Afficher le dossier de configuration → user/<id>/filament/</i>",
|
||||||
|
"orca_profile_dropmsg": "Déposez ici ou cliquez",
|
||||||
|
"orca_profile_list_label": "Profils importés",
|
||||||
|
"orca_profile_user_label": "Mes profils",
|
||||||
|
"orca_profile_user_empty": "– aucun –",
|
||||||
|
"orca_profile_uploading": "Envoi en cours…",
|
||||||
|
"orca_profile_done": "Importé",
|
||||||
|
"orca_profile_skipped": "ignoré",
|
||||||
|
"log_dir_all": "Tout",
|
||||||
|
"log_lvl_label": "Niveau :",
|
||||||
|
"file_ready_btn": "▶ Lancer l'impression",
|
||||||
|
"file_slots_btn": "🎨 Choisir les slots",
|
||||||
|
"file_cancel_btn": "✕ Annuler",
|
||||||
|
"nav_printers": "Imprimantes",
|
||||||
|
"skip_title": "✂ Ignorer des objets",
|
||||||
|
"skip_hint": "Décochez les objets que vous ne souhaitez plus imprimer :",
|
||||||
|
"skip_btn_label": "Objets",
|
||||||
|
"skip_no_objects": "Aucun objet dans cette impression.",
|
||||||
|
"skip_already": "ignoré",
|
||||||
|
"skip_select_at_least_one": "Veuillez sélectionner au moins un objet.",
|
||||||
|
"skip_sending": "Envoi …",
|
||||||
|
"skip_success": "Les objets seront ignorés.",
|
||||||
|
"fd_objects_hint": "Ignorer des objets (optionnel) :",
|
||||||
|
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
|
||||||
|
"fd_cancel": "Annuler",
|
||||||
|
"fd_print": "▶ Imprimer",
|
||||||
|
"fd_no_slots_msg": "Aucun slot AMS chargé.{br}Lancer l'impression quand même ?",
|
||||||
|
"fd_slot": "Slot",
|
||||||
|
"fd_no_matching_material": "Aucun matériau correspondant",
|
||||||
|
"fd_used": "UTILISÉ",
|
||||||
|
"add_printer": "Ajouter une imprimante",
|
||||||
|
"apd_lbl_ip": "IP de l'imprimante",
|
||||||
|
"apd_lbl_name": "Nom (optionnel)",
|
||||||
|
"apd_placeholder_name": "ex. Kobra X Salon",
|
||||||
|
"apd_cancel": "Annuler",
|
||||||
|
"apd_confirm": "Ajouter",
|
||||||
|
"apd_fetching": "Récupération des données de l'imprimante…",
|
||||||
|
"apd_success": "Imprimante ajoutée, redémarrage du bridge…",
|
||||||
|
"apd_err_ip": "Veuillez saisir une adresse IP",
|
||||||
|
"printers_remove": "Supprimer l'imprimante",
|
||||||
|
"printers_remove_confirm": "Supprimer l'imprimante \"{name}\" ? Le bridge va redémarrer.",
|
||||||
|
"printers_active": "● actif",
|
||||||
|
"printers_switch": "Changer →",
|
||||||
|
"printers_current": "Imprimante actuelle",
|
||||||
|
"printers_loading": "Chargement…",
|
||||||
|
"printers_none": "Aucune imprimante configurée.",
|
||||||
|
"printers_empty_hint": "Aucune imprimante configurée.",
|
||||||
|
"nav_browser": "Navigateur",
|
||||||
|
"panel_browser_title": "Explorateur de fichiers",
|
||||||
|
"store_search_placeholder": "🔍 Rechercher…",
|
||||||
|
"store_empty": "Aucun fichier uploadé.",
|
||||||
|
"store_refresh": "↻ Actualiser",
|
||||||
|
"store_print": "▶ Imprimer",
|
||||||
|
"store_download": "⬇ Télécharger",
|
||||||
|
"store_delete_confirm": "Supprimer le fichier ?",
|
||||||
|
"store_print_confirm": "Imprimer le fichier ?",
|
||||||
|
"store_web_verify_title": "Vérifier le fichier",
|
||||||
|
"store_web_verify_msg": "Veuillez vérifier que ce fichier a été créé pour l'Anycubic Kobra X.",
|
||||||
|
"store_web_verify_confirm": "Confirmer",
|
||||||
|
"store_web_verify_abort": "Annuler",
|
||||||
|
"store_no_results": "Aucun fichier trouvé.",
|
||||||
|
"store_never": "jamais imprimé",
|
||||||
|
"store_estimate": "Estimation",
|
||||||
|
"store_upload_label_prefix": "Déposez un GCode ici ou ",
|
||||||
|
"store_upload_label_browse": "parcourir",
|
||||||
|
"store_upload_busy": "⏳ Envoi en cours…",
|
||||||
|
"store_upload_success": "✓ {file}",
|
||||||
|
"store_upload_error": "✗ {error}",
|
||||||
|
"sf_all": "Tout",
|
||||||
|
"sf_ok": "✓ Terminés",
|
||||||
|
"sf_err": "✗ Échoués",
|
||||||
|
"sf_new": "Nouveau",
|
||||||
|
"ss_date": "↓ Date",
|
||||||
|
"ss_name": "A–Z Nom",
|
||||||
|
"ss_dur": "⏱ Durée d'impression"
|
||||||
|
}
|
||||||
|
|
||||||
248
web/translations/zh-cn.json
Normal file
248
web/translations/zh-cn.json
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
{
|
||||||
|
"header_status_standby": "就绪",
|
||||||
|
"header_status_printing": "打印中",
|
||||||
|
"header_status_complete": "完成",
|
||||||
|
"header_status_error": "错误",
|
||||||
|
"kobra_free": "就绪",
|
||||||
|
"kobra_busy": "忙碌",
|
||||||
|
"kobra_printing": "打印中",
|
||||||
|
"kobra_preheating": "预热中",
|
||||||
|
"kobra_auto_leveling": "自动调平",
|
||||||
|
"kobra_checking": "检查中",
|
||||||
|
"kobra_updated": "更新中",
|
||||||
|
"kobra_init": "初始化中",
|
||||||
|
"kobra_pausing": "暂停中...",
|
||||||
|
"kobra_paused": "已暂停",
|
||||||
|
"kobra_resuming": "恢复中...",
|
||||||
|
"kobra_resumed": "已恢复",
|
||||||
|
"kobra_stopping": "停止中...",
|
||||||
|
"kobra_stoped": "已停止",
|
||||||
|
"kobra_finished": "已完成",
|
||||||
|
"kobra_failed": "错误",
|
||||||
|
"kobra_canceled": "已取消",
|
||||||
|
"kobra_offline": "离线",
|
||||||
|
"nav_dashboard": "仪表盘",
|
||||||
|
"nav_print": "打印",
|
||||||
|
"nav_temps": "温度",
|
||||||
|
"nav_motion": "运动",
|
||||||
|
"nav_ams": "AMS",
|
||||||
|
"nav_extras": "灯光 / 风扇",
|
||||||
|
"nav_console": "控制台",
|
||||||
|
"card_progress": "进度",
|
||||||
|
"card_temps": "温度",
|
||||||
|
"card_light_fan": "风扇",
|
||||||
|
"card_speed": "打印速度",
|
||||||
|
"card_cam": "相机",
|
||||||
|
"lbl_elapsed": "已用时间:",
|
||||||
|
"lbl_remaining": "剩余时间:",
|
||||||
|
"lbl_slicer_time": "切片预估:",
|
||||||
|
"lbl_layers": "层",
|
||||||
|
"lbl_zpos": "Z (mm)",
|
||||||
|
"speed_silent": "🐢 静音",
|
||||||
|
"speed_normal": "⚡ 标准",
|
||||||
|
"speed_sport": "🚀 运动",
|
||||||
|
"lbl_light": "💡 灯光",
|
||||||
|
"lbl_feed": "进料",
|
||||||
|
"lbl_unload": "退料",
|
||||||
|
"card_ace_dry": "ACE 烘干",
|
||||||
|
"ace_dry_dryer": "烘干机",
|
||||||
|
"ace_dry_status_off": "状态: 关闭",
|
||||||
|
"ace_dry_status_on": "状态: 运行中",
|
||||||
|
"ace_dry_status_remaining": "剩余",
|
||||||
|
"ace_dry_humidity": "湿度",
|
||||||
|
"ace_dry_current_temp": "温度",
|
||||||
|
"ace_dry_chart": "历史 (温度/湿度)",
|
||||||
|
"ace_dry_temp": "温度 (°C)",
|
||||||
|
"ace_dry_duration": "时长 (分钟)",
|
||||||
|
"ace_dry_start": "▶ 启动",
|
||||||
|
"ace_dry_stop": "■ 停止",
|
||||||
|
"ace_dry_auto_refill": "自动补料",
|
||||||
|
"ace_dry_enable": "启用烘干",
|
||||||
|
"ace_dry_temp_line": "烘干温度",
|
||||||
|
"ace_dry_time_line": "烘干时间",
|
||||||
|
"ace_dry_ui_pending": "(仅 UI,后端稍后支持)",
|
||||||
|
"ace_dry_dialog_title": "烘干温度/时间设置",
|
||||||
|
"ace_dry_dialog_temp": "温度 (30-80°C)",
|
||||||
|
"ace_dry_dialog_time": "剩余时间 (h:m:s)",
|
||||||
|
"ace_dry_dialog_confirm": "确认",
|
||||||
|
"ace_dry_dialog_cancel": "取消",
|
||||||
|
"ace_dry_dialog_save_restart": "保存并重启",
|
||||||
|
"ace_dry_dialog_custom_name": "自定义名称",
|
||||||
|
"ace_dry_dialog_reset_default": "恢复默认",
|
||||||
|
"cam_placeholder": "📷 相机未启动",
|
||||||
|
"cam_stream_unavailable": "视频流不可用",
|
||||||
|
"btn_cam_start": "▶ 相机",
|
||||||
|
"btn_cam_stop": "◼ 相机",
|
||||||
|
"btn_pause": "⏸ 暂停",
|
||||||
|
"btn_resume": "▶ 继续",
|
||||||
|
"btn_cancel": "✕ 停止",
|
||||||
|
"label_nozzle": "喷嘴",
|
||||||
|
"label_bed": "热床",
|
||||||
|
"label_fan": "🌀 风扇",
|
||||||
|
"label_light": "💡 灯光",
|
||||||
|
"label_on_off": "开 / 关",
|
||||||
|
"label_speed": "速度",
|
||||||
|
"panel_print_title": "打印控制",
|
||||||
|
"panel_print_btn_pause": "⏸ 暂停",
|
||||||
|
"panel_print_btn_resume": "▶ 继续",
|
||||||
|
"panel_print_btn_cancel": "✕ 取消",
|
||||||
|
"panel_print_temps_live": "温度 (实时)",
|
||||||
|
"label_set": "设置",
|
||||||
|
"label_off": "关闭",
|
||||||
|
"panel_temps_nozzle": "喷嘴",
|
||||||
|
"panel_temps_bed": "热床",
|
||||||
|
"panel_temps_chart": "历史 (最近 60 次读数)",
|
||||||
|
"label_target_c": "目标:",
|
||||||
|
"panel_motion_xy": "XY 轴",
|
||||||
|
"panel_motion_z": "Z 轴",
|
||||||
|
"label_step": "步进:",
|
||||||
|
"btn_home_z": "回零 Z",
|
||||||
|
"btn_home_xy": "回零 XY",
|
||||||
|
"btn_home_all": "全部回零",
|
||||||
|
"btn_disable_motors": "关闭电机",
|
||||||
|
"panel_ams_title": "耗材",
|
||||||
|
"card_ams": "耗材",
|
||||||
|
"ams_no_data": "未收到 AMS 数据",
|
||||||
|
"label_slot": "槽位",
|
||||||
|
"ams_empty": "空",
|
||||||
|
"panel_extras_light": "灯光",
|
||||||
|
"panel_extras_fan": "风扇",
|
||||||
|
"panel_extras_camera": "相机",
|
||||||
|
"btn_cam_start2": "▶ 启动",
|
||||||
|
"btn_cam_stop2": "◼ 停止",
|
||||||
|
"panel_console_title": "事件日志",
|
||||||
|
"log_light_on": "灯光已开",
|
||||||
|
"log_light_off": "灯光已关",
|
||||||
|
"log_fan": "风扇 →",
|
||||||
|
"log_nozzle": "喷嘴 →",
|
||||||
|
"log_bed": "热床 →",
|
||||||
|
"log_axis": "轴",
|
||||||
|
"log_home": "回零",
|
||||||
|
"log_home_all": "全部回零",
|
||||||
|
"log_cam_start": "相机已启动:",
|
||||||
|
"log_cam_stop": "相机已停止",
|
||||||
|
"log_poll_error": "轮询错误:",
|
||||||
|
"log_error": "错误:",
|
||||||
|
"confirm_cancel": "确定要取消打印吗?",
|
||||||
|
"settings_title": "设置",
|
||||||
|
"settings_connection": "连接",
|
||||||
|
"settings_print": "打印设置",
|
||||||
|
"settings_poll": "轮询间隔",
|
||||||
|
"settings_version": "版本",
|
||||||
|
"settings_save": "保存并重启",
|
||||||
|
"settings_printer_name": "打印机名称",
|
||||||
|
"settings_printer_ip": "打印机 IP",
|
||||||
|
"settings_mqtt_port": "MQTT 端口",
|
||||||
|
"settings_username": "MQTT 用户名",
|
||||||
|
"settings_password": "MQTT 密码",
|
||||||
|
"settings_device_id": "设备 ID",
|
||||||
|
"settings_mode_id": "模式 ID",
|
||||||
|
"hint_ip_no_port": "仅填写 IP,不要端口 (例如 192.168.1.102)",
|
||||||
|
"settings_default_slot": "默认槽位 (单色)",
|
||||||
|
"settings_slot_auto": "自动 (所有已装载槽位)",
|
||||||
|
"settings_auto_leveling": "打印前自动调平",
|
||||||
|
"settings_camera_on_print": "打印开始时开启相机",
|
||||||
|
"settings_web_upload_warning": "打印网页上传文件时显示警告",
|
||||||
|
"update_check": "检查更新",
|
||||||
|
"update_checking": "检查中...",
|
||||||
|
"update_available": "可用",
|
||||||
|
"update_none": "已是最新版本",
|
||||||
|
"update_apply": "立即安装",
|
||||||
|
"update_applying": "下载中...",
|
||||||
|
"update_restarting": "重启中...",
|
||||||
|
"update_error": "错误",
|
||||||
|
"btn_connect": "⚡ 连接",
|
||||||
|
"btn_disconnect": "✕ 断开",
|
||||||
|
"lbl_conn_error": "连接错误:",
|
||||||
|
"slot_edit_title": "编辑槽位",
|
||||||
|
"slot_edit_color": "颜色",
|
||||||
|
"slot_edit_material": "材料",
|
||||||
|
"slot_edit_load": "⬇ 进料",
|
||||||
|
"slot_edit_unload": "⬆ 退料",
|
||||||
|
"slot_edit_save": "💾 保存",
|
||||||
|
"slot_edit_custom": "例如 PLA, PETG, ABS…",
|
||||||
|
"slot_edit_ok": "AMS 槽位",
|
||||||
|
"slot_edit_profile": "OrcaSlicer 配置",
|
||||||
|
"slot_edit_profile_hint": "在 OrcaSlicer 同步时发送具体品牌,而不仅仅是“Generic”",
|
||||||
|
"slot_edit_profile_default": "— 通用 (默认) —",
|
||||||
|
"orca_profile_section": "OrcaSlicer 配置",
|
||||||
|
"orca_profile_hint": "导入你自己的 OrcaSlicer 耗材配置(在 Help → Show Configuration Folder 打开用户目录)",
|
||||||
|
"orca_profile_import_btn": "导入配置",
|
||||||
|
"orca_profile_import_link": "★ 导入自己的配置…",
|
||||||
|
"orca_profile_import_title": "导入你的 OrcaSlicer 配置",
|
||||||
|
"orca_profile_help_html": "上传 OrcaSlicer 耗材文件夹的 <b>ZIP</b> 或单个 <b>.json</b> 文件。<br>在 OrcaSlicer 中: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||||
|
"orca_profile_dropmsg": "拖到此处或点击",
|
||||||
|
"orca_profile_list_label": "已导入",
|
||||||
|
"orca_profile_user_label": "自己的配置",
|
||||||
|
"orca_profile_user_empty": "– 无 –",
|
||||||
|
"orca_profile_uploading": "上传中…",
|
||||||
|
"orca_profile_done": "已导入",
|
||||||
|
"orca_profile_skipped": "跳过",
|
||||||
|
"log_dir_all": "全部",
|
||||||
|
"log_lvl_label": "级别:",
|
||||||
|
"file_ready_btn": "▶ 开始打印",
|
||||||
|
"file_slots_btn": "🎨 选择槽位",
|
||||||
|
"file_cancel_btn": "✕ 取消",
|
||||||
|
"nav_printers": "打印机",
|
||||||
|
"skip_title": "✂ 跳过对象",
|
||||||
|
"skip_hint": "取消勾选不想继续打印的对象:",
|
||||||
|
"skip_btn_label": "对象",
|
||||||
|
"skip_no_objects": "此打印任务没有对象。",
|
||||||
|
"skip_already": "已跳过",
|
||||||
|
"skip_select_at_least_one": "请至少选择一个对象。",
|
||||||
|
"skip_sending": "发送中 …",
|
||||||
|
"skip_success": "对象将被跳过。",
|
||||||
|
"fd_objects_hint": "跳过对象 (可选):",
|
||||||
|
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
|
||||||
|
"fd_cancel": "取消",
|
||||||
|
"fd_print": "▶ 打印",
|
||||||
|
"fd_no_slots_msg": "没有已装载的 AMS 槽位。{br}仍要开始打印吗?",
|
||||||
|
"fd_slot": "槽位",
|
||||||
|
"fd_no_matching_material": "无匹配材料",
|
||||||
|
"fd_used": "已用",
|
||||||
|
"add_printer": "添加打印机",
|
||||||
|
"apd_lbl_ip": "打印机 IP",
|
||||||
|
"apd_lbl_name": "名称 (可选)",
|
||||||
|
"apd_placeholder_name": "例如 Kobra X 客厅",
|
||||||
|
"apd_cancel": "取消",
|
||||||
|
"apd_confirm": "添加",
|
||||||
|
"apd_fetching": "正在从打印机获取数据…",
|
||||||
|
"apd_success": "打印机已添加,Bridge 正在重启…",
|
||||||
|
"apd_err_ip": "请输入 IP 地址",
|
||||||
|
"printers_remove": "移除打印机",
|
||||||
|
"printers_remove_confirm": "移除打印机 \"{name}\"? Bridge 将重启。",
|
||||||
|
"printers_active": "● 活动",
|
||||||
|
"printers_switch": "切换 →",
|
||||||
|
"printers_current": "当前打印机",
|
||||||
|
"printers_loading": "加载中…",
|
||||||
|
"printers_none": "未配置打印机。",
|
||||||
|
"printers_empty_hint": "尚未设置打印机。",
|
||||||
|
"nav_browser": "浏览器",
|
||||||
|
"panel_browser_title": "文件浏览器",
|
||||||
|
"store_search_placeholder": "🔍 搜索…",
|
||||||
|
"store_empty": "尚未上传文件。",
|
||||||
|
"store_refresh": "↻ 刷新",
|
||||||
|
"store_print": "▶ 打印",
|
||||||
|
"store_download": "⬇ 下载",
|
||||||
|
"store_delete_confirm": "删除文件?",
|
||||||
|
"store_print_confirm": "打印文件?",
|
||||||
|
"store_web_verify_title": "验证文件",
|
||||||
|
"store_web_verify_msg": "请确认此文件是为 Anycubic Kobra X 创建的。",
|
||||||
|
"store_web_verify_confirm": "确认",
|
||||||
|
"store_web_verify_abort": "取消",
|
||||||
|
"store_no_results": "未找到文件。",
|
||||||
|
"store_never": "从未打印",
|
||||||
|
"store_estimate": "估算",
|
||||||
|
"store_upload_label_prefix": "将 GCode 拖到这里或 ",
|
||||||
|
"store_upload_label_browse": "浏览",
|
||||||
|
"store_upload_busy": "⏳ 上传中…",
|
||||||
|
"store_upload_success": "✓ {file}",
|
||||||
|
"store_upload_error": "✗ {error}",
|
||||||
|
"sf_all": "全部",
|
||||||
|
"sf_ok": "✓ 已完成",
|
||||||
|
"sf_err": "✗ 失败",
|
||||||
|
"sf_new": "新",
|
||||||
|
"ss_date": "↓ 日期",
|
||||||
|
"ss_name": "A–Z 名称",
|
||||||
|
"ss_dur": "⏱ 打印时间"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user