Compare commits

..

12 Commits

23 changed files with 3600 additions and 1409 deletions

30
.editorconfig Normal file
View File

@@ -0,0 +1,30 @@
# EditorConfig helps maintain consistent coding styles across all files
# https://editorconfig.org
root = true
# Unix-style newlines, UTF-8 encoding for all files
[*]
end_of_line = lf
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
# Python: 4 spaces (PEP 8)
[*.py]
indent_style = space
indent_size = 4
# JavaScript/JSON/YAML: 2 spaces (common web standard)
[*.{js,json,yml,yaml}]
indent_style = space
indent_size = 2
# HTML/CSS: 2 spaces
[*.{html,css}]
indent_style = space
indent_size = 2
# Markdown: preserve formatting
[*.md]
trim_trailing_whitespace = false

2
.gitignore vendored
View File

@@ -8,6 +8,8 @@ releases/*/kx-bridge
releases/*/extract_credentials
releases/*/extract_credentials.exe
node_modules/*
!kx-bridge.spec
# Laufzeit-Daten und Drucker-Credentials — nie committen

View File

@@ -1,5 +1,94 @@
# Changelog
## [0.9.26] 2026-06-21
### Neu
- **Italienische Sprachunterstützung** (PR #66, @Alex_M). Die Bridge-UI ist jetzt vollständig auf Italienisch verfügbar.
### Behoben
- **Kamera startete immer beim Druckbeginn** (Issue #50). `camera_on_print` fehlte in der `/api/state`-Antwort — JavaScript las `undefined` und startete die Kamera unabhängig vom Setting. Jetzt korrekt im State enthalten.
- **Auto-Leveling-Setting wurde im Moonraker-Druckpfad ignoriert** (Issue #57). `handle_print_start` las den Wert nur aus den Bridge-Args, nicht aus dem Request-Body — Dialog-Checkbox und Per-Print-Override hatten keine Wirkung. Verhält sich jetzt identisch zum direkten Druckpfad.
- **Filament-Mapping: Freitext-Felder durch Dropdowns ersetzt** (Issue #57). Falsch getippte Vendor/Name-Kombination brach das Profil-Matching ohne Fehlermeldung; Felder sind jetzt Dropdowns (Vendor → Profil, vendor-gefiltert), sodass nur gültige Kombinationen gespeichert werden können.
- **Dashboard zeigte generischen Materialtyp statt Profilname** (Issue #57). AMS-Slot-Karten zeigen jetzt den gemappten Profilnamen (z.B. „eSUN PLA-Basic") statt nur „PLA". Fallback auf generischen Typ wenn kein Profil gemappt ist.
- **Ghost-Profil auf leerem Slot** (Issue #57). Verwaiste Mappings für leere Slots wurden weiterhin angezeigt; leere Slots zeigen jetzt korrekt „–".
- **Skip-Objects-Panel fehlte im Orca-Upload-Flow** (Issue #57). Panel erscheint jetzt in allen Druckflows; bei frischem Upload fragt die Bridge `fileDetails` beim Drucker nach und pollt die Objektliste bis zu 6 Sekunden nach.
- **Banner und Dialog erschienen gleichzeitig** (Issue #57). Settings-Save setzt jetzt den Dialog-Cancel-State zurück, sodass der Slot-Mapper nach Wechsel des Start-Print-Verhaltens zuverlässig öffnet.
- **„Leeren" lud idle-Datei beim nächsten Poll nach** (Issue #57). Leeren setzt jetzt den lokalen State sofort zurück (`file_ready`, `filename`, `thumbnail`) und löscht alle Dialog-Sperren — Vorschaubild und Aktions-Buttons verschwinden sofort und kommen nicht zurück.
- **Material-Matching für „PLA Silk", „Matte PLA" etc.** (PR #64, @p2l). Modifier+Basis-Muster in beliebiger Wortreihenfolge werden jetzt auf den Basis-Typ normalisiert; Dash-Varianten (PLA-CF) bleiben weiterhin korrekt inkompatibel mit ihrem Basis-Typ.
## [0.9.25] 2026-06-17
### Behoben
- **Zufällige Abstürze / Container-Restarts — Segfault in `libcrypto.so.3`
(Issue #53).** Der MQTT-über-TLS-Client teilte einen einzelnen SSL-Socket
zwischen dem Reader-Thread (`recv`) und den Sender-Threads (`sendall`), ohne sie
zu serialisieren. CPythons `ssl`-Modul erlaubt kein gleichzeitiges Lesen und
Schreiben auf demselben Socket — die Überlappung korrumpierte den internen
OpenSSL-Zustand und löste eine Heap-Corruption + Segfault aus, die auf manchen
Hosts timing-bedingt zuverlässig auftrat. Sämtliche Socket-Zugriffe (recv /
sendall / close / reconnect) werden nun unter einem einzigen Lock serialisiert;
der Reader prüft die Bereitschaft mit `select()` außerhalb des Locks, damit die
Sender nie ausgehungert werden. Reconnect und Disconnect tauschen den Socket
jetzt atomar. Dank an @BasK für den detaillierten Fault-Handler-Trace.
- **File-Browser akzeptierte Nicht-GCode-Uploads (Issue #59).** Drag & Drop umging
den `accept`-Filter des Dateidialogs, sodass z.B. ein JPG hochgeladen werden
konnte. Uploads werden jetzt client- und serverseitig validiert; nur `.gcode`,
`.gcode.3mf`, `.3mf` und `.bgcode` werden akzeptiert. Dank an @gangoke.
## [0.9.24] 2026-06-16
### Neu
- **Objekte überspringen in jedem Druck-Flow (Issue #57).** Der „Objekte
überspringen"-Bereich im Slot-Mapper erschien bisher nur beim Druck aus dem
Browser-Tab. Er ist jetzt in allen Flows verfügbar (inkl. Upload / Print-Leiste),
standardmäßig eingeklappt hinter einem `✂ Objekte überspringen (N)`-Header, damit
der Dialog kompakt bleibt — Klick klappt Vorschau + Checkliste auf.
- **Slot-Mapper zeigt konkreten Profilnamen (Issue #57).** Jeder Slot zeigt nun das
zugeordnete Filament-Profil (z.B. „PolyTerra PLA — Polymaker") in den Dropdown-
Optionen und als Hover-Tooltip am Slot-Marker, statt nur des generischen Typs.
Fällt auf den generischen Typ zurück, wenn kein Profil gemappt ist.
## [0.9.23] 2026-06-16
### Neu
- **Druckdialog nach Upload automatisch öffnen.** Eine neue Einstellung
`print_start_dialog` (Einstellungen → Drucker → „Druckstart-Verhalten") steuert,
was nach einem Upload bei leerlaufendem Drucker passiert: „Print-Dialog" öffnet
den Slot-Zuordnungs-Dialog automatisch, „Print-Leiste" behält das bisherige
Banner. Basiert auf PR #56 von @gangoke.
- **Auto-Leveling-Schalter pro Druck.** Der Druckdialog hat jetzt eine eigene
Auto-Leveling-Checkbox, die den globalen Standard für einen einzelnen Druck
überschreibt.
### Behoben
- **Objekt-Skip wurde beim Druckstart still ignoriert (PR #56, @gangoke).** Der
Skip-Befehl wurde gesendet, *bevor* der Drucker im `printing`-Status war, und
daher verworfen. Der Skip wird nun in einer Retry-Schleife erneut angewendet,
sobald der Druck bestätigt läuft — mit einer Pending-Sperre, damit die UI den
Skip-Status nicht vorzeitig zurücksetzt.
- **Upload während eines laufenden Drucks überschrieb die Vorschau des laufenden
Auftrags.** Ein neuer Upload während des Drucks ersetzt nicht mehr Thumbnail /
file_ready des Auftrags auf dem Druckbett.
## [0.9.22] 2026-06-16
### Neu
- **Neu strukturiertes Einstellungs-Panel.** Das Einstellungs-Modal wurde durch
ein dauerhaftes Master-Detail-Panel mit fünf Kategorien ersetzt: Verbindung,
Drucker, Darstellung, Filament und System. Das Poll-Intervall ist nun live
einstellbar.
- **Vendor-Sichtbarkeitsfilter (Issue #41).** Eine neue Checkliste in den
Filament-Einstellungen beschränkt das Slot-Profil-Dropdown auf bestimmte
Hersteller. „Generic" und eigene importierte Profile sind immer sichtbar.
- **Idle-Datei-Aktionen in der Fortschritts-Karte (Issue #55).** Nach einem
Upload bei leerlaufendem Drucker erscheinen drei Schnellaktionen direkt in der
Fortschritts-Karte: ▶ Drucken, ⚙ Slots zuordnen und ✕ Leeren.
### Behoben
- **Mobileraker-Kompatibilität (Issue #48).** Absturz in `ConfigExtruder.fromJson`
(leeres `configfile.config`), Hänger beim Refresh (Metadata-Endlosschleife) und
fehlende ETA/Restzeit behoben.
## [0.9.21] 2026-06-14
### Behoben

View File

@@ -1,5 +1,130 @@
# Changelog
## [0.9.26] 2026-06-21
### New
- **Italian language support** (PR #66, @Alex_M). The bridge UI is now fully
available in Italian.
### Fixed
- **Camera always started at print begin** (issue #50). `camera_on_print` was
missing from the `/api/state` response — JavaScript read `undefined` and started
the camera regardless of the setting. Now correctly exposed in state.
- **Auto-leveling setting ignored in Moonraker print path** (issue #57).
`handle_print_start` read the value only from bridge args, not from the request
body, so the dialog checkbox and the per-print override had no effect. Now
behaves identically to the direct print path.
- **Filament mapping free-text fields replaced by dropdowns** (issue #57). A
mistyped vendor/name broke profile matching silently; fields are now dropdowns
(vendor → profile, vendor-filtered) so only valid combinations can be saved.
- **Dashboard showed generic material type instead of profile name** (issue #57).
AMS slot cards now display the mapped profile name (e.g. "eSUN PLA-Basic")
instead of just "PLA". Falls back to the generic type when no profile is mapped.
- **Ghost profile shown on empty slot** (issue #57). Stale mappings for empty
slots were still rendered; empty slots now correctly show "".
- **Skip-Objects panel missing in Orca upload flow** (issue #57). Panel now
appears in all print flows; on fresh upload the bridge requests `fileDetails`
from the printer and retries the object list for up to 6 s.
- **Banner and dialog appeared simultaneously** (issue #57). Settings save now
resets the dialog cancel state so the slot mapper reliably opens after toggling
Start Print Behavior.
- **"Clear" reloaded idle file on next poll** (issue #57). Clear now immediately
resets local state (`file_ready`, `filename`, `thumbnail`) and clears all dialog
locks — the preview and action buttons disappear instantly and do not return.
- **Material matching for "PLA Silk", "Matte PLA" etc.** (PR #64, @p2l).
Modifier+base patterns in any word order are now normalised to the base type;
dash-suffix variants (PLA-CF) remain correctly incompatible with their base.
## [0.9.25] 2026-06-17
### Fixed
- **Random crashes / container restarts — segfault in `libcrypto.so.3` (issue #53).**
The MQTT-over-TLS client shared a single SSL socket between the reader thread
(`recv`) and the sender threads (`sendall`) without serializing them. CPython's
`ssl` module does not allow concurrent read and write on the same socket — the
overlap corrupted OpenSSL's internal state, causing a heap corruption and a
segfault that manifested reliably on some hosts (timing-dependent). All socket
access (recv / sendall / close / reconnect) is now serialized under a single
lock; the reader probes readiness with `select()` outside the lock so senders
are never starved. Reconnect and disconnect now swap the socket atomically.
Thanks to @BasK for the detailed fault-handler trace that pinpointed this.
- **File browser accepted non-GCode uploads (issue #59).** Drag & drop bypassed
the file picker's `accept` filter, so e.g. a JPG could be uploaded. Uploads are
now validated both client- and server-side; only `.gcode`, `.gcode.3mf`, `.3mf`
and `.bgcode` are accepted. Thanks @gangoke.
## [0.9.24] 2026-06-16
### New
- **Skip Objects available in every print flow (issue #57).** The "Skip objects"
panel in the Slot Mapper used to appear only when printing from the Browser tab.
It now shows in all flows (upload / print bar included), collapsed by default
behind a `✂ Skip objects (N)` header to keep the dialog compact, expanding on
click with the object preview and checklist.
- **Slot Mapper shows the specific profile name (issue #57).** Each slot now
displays its mapped filament profile (e.g. "PolyTerra PLA — Polymaker") in the
dropdown options and as a hover tooltip on the slot marker, instead of just the
generic type. Falls back to the generic type when no profile is mapped.
## [0.9.23] 2026-06-16
### New
- **Auto-open print dialog after upload.** A new `print_start_dialog` setting
(Settings → Printer → "Start Print Behavior") controls what happens after a
file is uploaded while the printer is idle: `Print Dialog` opens the
slot-assignment dialog automatically, `Print Bar` keeps the previous banner
behaviour. Based on PR #56 by @gangoke.
- **Per-print auto-leveling toggle.** The print dialog now has its own
auto-leveling checkbox that overrides the global default for a single print.
### Fixed
- **Object skip was silently ignored at print start (PR #56, @gangoke).** The
skip command was sent *before* the printer entered the `printing` state, so it
was dropped. The skip is now re-applied in a retry loop once the print is
confirmed running, with a pending-lock so the UI doesn't reset the skip state
prematurely.
- **Upload during an active print overwrote the running job's preview.**
Uploading a new file while printing no longer replaces the thumbnail /
file_ready of the job currently on the bed.
## [0.9.22] 2026-06-16
### New
- **Restructured Settings panel.** The settings modal has been replaced by a
persistent Master-Detail panel with five categories: Connection, Printer,
Appearance, Filament, and System. Poll interval is now adjustable live.
- **Vendor visibility filter (issue #41).** A new checklist in the Filament
settings lets you restrict the slot profile dropdown to specific manufacturers.
"Generic" and your own imported profiles are always visible. The list updates
automatically after a profile import.
- **Idle file actions in the progress card (issue #55).** After uploading a file
while the printer is idle, three quick-action buttons appear directly in the
progress card: ▶ Print, ⚙ Map Slots, and ✕ Clear — matching the file browser
workflow without navigating away.
### Fixed
- **Mobileraker: app crashed with `Null is not a subtype of Object` in
`ConfigExtruder.fromJson` (issue #48).** `configfile.config` was returned as
an empty object `{}`. Mobileraker parses both `configfile.settings` and
`configfile.config` through the same strict Dart parser — both are now
populated with the same extruder/bed/stepper stub.
- **Mobileraker: app hung indefinitely on refresh (issue #48).** The WebSocket
`server.files.metadata` handler called a non-existent store method
(`get_file_by_filename`), always returning empty metadata. Mobileraker retried
this thousands of times per second. Both the HTTP and WS paths now share a
single `_build_file_metadata()` method.
- **Mobileraker: ETA / remaining time not shown (issue #48).** A side effect of
the metadata loop fix — once `currentFile` resolves, Mobileraker can calculate
ETA from `estimated_time`.
- **Mobileraker: `notify_status_update` triggered repeated `ConfigFile.parse`
(issue #48).** Static objects (`configfile`, `webhooks`, `heaters`, `history`)
were included in every live status push. They are now filtered out; only live
telemetry is broadcast.
- `motion_report` (`live_position`, `live_velocity`) added to printer objects
for Mobileraker motion display.
- Saving filament slot profiles no longer silently drops the `visible_vendors`
setting from `config.ini`.
## [0.9.21] 2026-06-14
### Fixed

View File

@@ -1 +1 @@
0.9.21
0.9.27-nightly1

View File

@@ -13,6 +13,7 @@ _BASE = pathlib.Path(sys.executable).parent if getattr(sys, "frozen", False) els
CONFIG_SECTION_CONNECTION = "connection"
CONFIG_SECTION_PRINT = "print"
CONFIG_SECTION_BRIDGE = "bridge"
CONFIG_SECTION_SPOOLMAN = "spoolman"
def _find_config_file() -> pathlib.Path | None:
@@ -61,7 +62,10 @@ def _load_config_file(path: pathlib.Path):
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
"SPOOLMAN_SERVER": (CONFIG_SECTION_SPOOLMAN, "server"),
"SPOOLMAN_SYNC_RATE": (CONFIG_SECTION_SPOOLMAN, "sync_rate"),
}
for env_key, (section, option) in mapping.items():
if env_key not in os.environ:
@@ -73,6 +77,18 @@ def _load_config_file(path: pathlib.Path):
pass
# Backward compatibility: old key FILE_READY_DIALOG → PRINT_START_DIALOG
if "PRINT_START_DIALOG" not in os.environ:
try:
legacy = cfg.get(CONFIG_SECTION_PRINT, "file_ready_dialog")
if legacy:
os.environ["PRINT_START_DIALOG"] = legacy
except (configparser.NoSectionError, configparser.NoOptionError):
pass
if "PRINT_START_DIALOG" not in os.environ and "FILE_READY_DIALOG" in os.environ:
os.environ["PRINT_START_DIALOG"] = os.environ["FILE_READY_DIALOG"]
def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
"""Einmalige Migration: .env → config.ini anlegen."""
env_vals: dict[str, str] = {}
@@ -227,10 +243,17 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
return False
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
preserved_vendors = None
if cfg.has_option("filament_profiles", "visible_vendors"):
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
if cfg.has_section("filament_profiles"):
cfg.remove_section("filament_profiles")
if profiles:
if profiles or preserved_vendors:
cfg["filament_profiles"] = {}
if preserved_vendors:
cfg["filament_profiles"]["visible_vendors"] = preserved_vendors
for slot_idx in sorted(profiles.keys()):
entry = profiles[slot_idx] or {}
if entry.get("vendor"):
@@ -244,6 +267,43 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
return True
def list_visible_vendors() -> list[str]:
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
"""
path = _find_config_file()
if not path:
return []
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if not cfg.has_option("filament_profiles", "visible_vendors"):
return []
raw = cfg.get("filament_profiles", "visible_vendors")
return [v.strip() for v in raw.split(",") if v.strip()]
def save_visible_vendors(vendors: list[str]) -> bool:
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder."""
path = _find_config_file()
if not path:
return False
cfg = configparser.ConfigParser()
cfg.read(path, encoding="utf-8")
if not cfg.has_section("filament_profiles"):
cfg.add_section("filament_profiles")
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
if clean:
cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean)
elif cfg.has_option("filament_profiles", "visible_vendors"):
cfg.remove_option("filament_profiles", "visible_vendors")
with open(path, "w", encoding="utf-8") as f:
cfg.write(f)
return True
def get(key: str, default: str = "") -> str:
return os.environ.get(key, default)
@@ -259,3 +319,6 @@ DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
SPOOLMAN_SERVER = get("SPOOLMAN_SERVER", "")
SPOOLMAN_SYNC_RATE = int(get("SPOOLMAN_SYNC_RATE", "0"))

View File

@@ -0,0 +1,3 @@
services:
kx-bridge:
image: gitea.it-drui.de/viewit/kx-bridge:nightly

View File

@@ -0,0 +1,25 @@
# KX-Bridge Nightly — Portainer Stack
#
# Paste this into Portainer → Stacks → Add stack → Web editor
#
# Uses the nightly build — may be unstable, for testing new features early.
# For production use, see docker-compose.portainer.yml (:latest).
services:
kx-bridge:
image: gitea.it-drui.de/viewit/kx-bridge:nightly
volumes:
- /mnt/dockerdata/kx-nightly/config:/app/config
- /mnt/dockerdata/kx-nightly/data:/app/data
ports:
# Port 7125 = first printer. Add 7126, 7127, … for each additional printer.
- "7125:7125"
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# Verzeichnisse müssen auf dem Host existieren:
# mkdir -p /mnt/dockerdata/kx-nightly/config /mnt/dockerdata/kx-nightly/data

View File

@@ -48,3 +48,6 @@ MODE_ID = get("MODE_ID", "")
DEVICE_ID = get("DEVICE_ID", "")
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))

View File

@@ -27,6 +27,7 @@ import hashlib
import json
import logging
import os
import select
import socket
import ssl
import sys
@@ -120,6 +121,10 @@ class KobraXClient:
self._buf = b""
self._pid = 1
self._lock = threading.Lock()
# Generations-Marker: wird bei jedem Socket-Swap/Close erhöht, damit der
# Reader-Thread erkennt wenn _reconnect/_do_connect den Socket unter ihm
# ersetzt hat (Issue #53). Schützt gegen recv auf einem stale fd.
self._sock_gen = 0
self._running = False
# Pending requests by msgid (for response ACK)
@@ -167,21 +172,31 @@ class KobraXClient:
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
ctx.load_cert_chain(CERT_FILE, KEY_FILE)
_ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM)
raw = socket.create_connection(_ai[0][4], timeout=5)
self._sock = ctx.wrap_socket(raw)
log.info("TLS connected cipher=%s", self._sock.cipher()[0])
# Socket als lokale Variable aufbauen — der Handshake (Connect + CONNACK)
# läuft OHNE gehaltenes Lock, damit ein langsamer Connect die Sender nicht
# einfriert. Erst der fertige Socket wird unter Lock eingeschwenkt (#53).
_ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM)
raw = socket.create_connection(_ai[0][4], timeout=5)
new_sock = ctx.wrap_socket(raw)
log.info("TLS connected cipher=%s", new_sock.cipher()[0])
self._sock.sendall(_build_connect(self.client_id, self.username, self.password))
self._sock.settimeout(3)
r = self._sock.recv(64)
new_sock.sendall(_build_connect(self.client_id, self.username, self.password))
new_sock.settimeout(3)
r = new_sock.recv(64)
if len(r) < 4 or r[0] != 0x20 or r[3] != 0:
try:
new_sock.close()
except Exception:
pass
raise RuntimeError(f"CONNACK failed: {r.hex()}")
log.info("CONNACK rc=0")
self._sock.settimeout(0.2)
self._buf = b""
self._subscribe(self._sub_topic())
new_sock.settimeout(0.2)
with self._lock:
self._sock = new_sock
self._sock_gen += 1
self._buf = b""
self._subscribe(self._sub_topic()) # nimmt das Lock selbst — nicht verschachteln
log.debug("MQTT connected to %s:%s", self.host, self.port)
def connect(self):
@@ -207,10 +222,14 @@ class KobraXClient:
def disconnect(self):
self._running = False
try:
self._sock.close()
except Exception:
pass
with self._lock:
try:
if self._sock is not None:
self._sock.close()
except Exception:
pass
self._sock = None
self._sock_gen += 1
def _reconnect(self):
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
@@ -219,10 +238,16 @@ class KobraXClient:
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
ausgeschaltet) zu vermeiden."""
log.warning("Verbindung verloren reconnect…")
try:
self._sock.close()
except Exception:
pass
# Close + Invalidierung unter Lock, damit kein Sender mitten im sendall
# auf den gerade geschlossenen Socket trifft (Issue #53).
with self._lock:
try:
if self._sock is not None:
self._sock.close()
except Exception:
pass
self._sock = None
self._sock_gen += 1
delays = [2, 4, 8, 15, 30, 60]
attempt = 0
while self._running:
@@ -246,7 +271,8 @@ class KobraXClient:
with self._lock:
pid = self._pid
self._pid += 1
self._sock.sendall(_build_subscribe(topic, pid))
if self._sock is not None:
self._sock.sendall(_build_subscribe(topic, pid))
log.info("SUB %s", topic)
# -- Read loop -----------------------------------------------------------
@@ -256,17 +282,52 @@ class KobraXClient:
_empty_count = 0
while self._running:
if time.time() - last_ping > 30:
ping_ok = False
with self._lock:
try:
self._sock.sendall(_build_pingreq())
if self._sock is not None:
self._sock.sendall(_build_pingreq())
ping_ok = True
except Exception:
if self._running and not self._reconnect():
break
last_ping = time.time()
continue
ping_ok = False
# _reconnect() AUSSERHALB des Locks aufrufen — es nimmt das Lock
# selbst, und threading.Lock ist nicht reentrant (sonst Deadlock).
if not ping_ok:
if self._running and not self._reconnect():
break
last_ping = time.time()
# Aktuellen Socket + Generation unter Lock greifen, damit ein
# paralleler _reconnect/_do_connect-Swap uns nicht auf einem stale
# fd pollen lässt (Issue #53).
with self._lock:
sock = self._sock
gen = self._sock_gen
if sock is None:
time.sleep(0.05)
continue
# Idle-Wartezeit OHNE Lock — select probt nur die Bereitschaft, so
# blockiert der Reader während Leerlauf nie das gemeinsame Lock.
try:
data = self._sock.recv(65536)
ready, _, _ = select.select([sock], [], [], 0.2)
except (OSError, ValueError):
# fd geschlossen/ungültig (Reconnect oder Disconnect mitten im select)
if not self._running:
break
time.sleep(0.05)
continue
if not ready:
continue # Leerlauf, kein Lock gehalten
# Daten liegen an: Lock kurz greifen für das eine recv, serialisiert
# gegen alle sendall-Caller. recv blockiert nicht lange (select sagte
# ready, Socket-Timeout ist 0.2s).
try:
with self._lock:
# Socket könnte zwischen select und hier ersetzt worden sein.
if self._sock_gen != gen or self._sock is not sock:
continue
data = sock.recv(65536)
if not data:
# Windows SSL kann kurzzeitig b"" liefern ohne echten EOF
_empty_count += 1
@@ -275,7 +336,7 @@ class KobraXClient:
continue
_empty_count = 0
self._buf += data
self._drain()
self._drain() # außerhalb des Locks — Dispatch/event.set() bleibt prompt
except ssl.SSLWantReadError:
continue
except socket.timeout:

View File

@@ -34,6 +34,7 @@ except ImportError:
import env_loader
import asyncio
import hashlib
import copy
import json
import logging
import os
@@ -732,6 +733,40 @@ class CameraCache:
await asyncio.sleep(2.0)
class SpoolmanClient:
"""Thin synchronous HTTP client for Spoolman filament tracking.
Designed to be called from daemon threads (poll loop, _on_print callbacks).
Uses requests (already in requirements) so no event-loop dependency.
"""
def __init__(self, server_url: str, sync_rate: int = 0):
self.server_url = server_url.rstrip("/")
self.sync_rate = sync_rate
def _req(self, method: str, path: str, **kwargs):
import requests
r = requests.request(method, f"{self.server_url}{path}", timeout=5, **kwargs)
r.raise_for_status()
return r.json()
def health_check(self) -> bool:
try:
self._req("GET", "/api/v1/health")
return True
except Exception:
return False
def list_spools(self) -> list:
return self._req("GET", "/api/v1/spool")
def use_filament(self, spool_id: int, use_length_mm: float) -> None:
"""Report consumed filament length in mm. Spoolman converts to weight
using the spool's filament profile density."""
self._req("PUT", f"/api/v1/spool/{spool_id}/use",
json={"use_length": round(use_length_mm, 2)})
class KobraXBridge:
def __init__(self, client: KobraXClient, args=None, store=None, printer_id: str = "1", all_bridges=None):
self.client = client
@@ -751,6 +786,13 @@ class KobraXBridge:
self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles()
except Exception:
self._filament_profiles = {}
# Vendor-Sichtbarkeitsfilter fürs Slot-Profil-Dropdown (Issue #41 Option A).
# Leere Liste = alle Vendoren sichtbar (rückwärtskompatibel).
try:
import config_loader as _cl
self._visible_vendors: list[str] = _cl.list_visible_vendors()
except Exception:
self._visible_vendors = []
self._last_state: dict = {}
self._state = {
"nozzle_temp": 0.0,
@@ -783,7 +825,9 @@ class KobraXBridge:
"print_speed_mode": 2,
"connection_error": "",
"file_ready": "",
"print_start_dialog": getattr(args, "print_start_dialog", 1),
"filament_mode": "toolhead",
"supplies_usage": 0,
"ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None},
}
self._ams_slots: list[dict] = [] # flat global list; each entry has global_index + box_id
@@ -806,6 +850,21 @@ class KobraXBridge:
# Part-Skip: zuletzt vom Drucker gemeldete Skip-Liste (v0.9.10)
self._skip_state: dict = {"objects": [], "skipped": [], "ts": 0}
# Pre-Print-Skip: pending until printer enters printing state
self._pending_preprint_skip: list[str] = []
self._pending_preprint_skip_deadline: float = 0.0
# Spoolman filament tracking
_sm_url = (getattr(args, "spoolman_server", "") or "").strip()
self._spoolman: SpoolmanClient | None = (
SpoolmanClient(_sm_url, getattr(args, "spoolman_sync_rate", 0))
if _sm_url else None
)
self._spoolman_slot_spools: dict[int, int] = {} # {ams_slot_idx: spoolman_spool_id}
self._spoolman_slot_usage: dict[int, float] = {} # per-slot accumulated mm this print
self._spoolman_slot_reported: dict[int, float] = {} # per-slot mm already sent to Spoolman
self._spoolman_last_usage: float = 0.0 # supplies_usage at last attribution tick
self._spoolman_last_sync: float = 0.0
# Theme-Name prüfen (keine Sonderzeichen oder Umlaute)
raw_theme = (getattr(args, "ui_theme", None) or "default").strip()
@@ -825,6 +884,139 @@ class KobraXBridge:
client.callbacks["light/report"] = self._on_light
client.callbacks["skip/report"] = self._on_skip
if self._spoolman:
threading.Thread(
target=lambda: log.info(
f"Spoolman: {'OK' if self._spoolman.health_check() else 'unreachable'} "
f"at {self._spoolman.server_url}"
),
daemon=True, name="spoolman-health",
).start()
# ── Spoolman helpers ──────────────────────────────────────────────────────
def _spoolman_filament_mm(self) -> float:
"""Total filament_used_mm for the current print file from the GCode DB."""
filename = self._state.get("filename", "")
if not filename:
return 0.0
try:
gf = self._store.get_file_by_name(filename)
return float(gf.get("filament_used_mm") or 0.0) if gf else 0.0
except Exception:
return 0.0
def _spoolman_attribute_tick(self, activity_map: dict) -> None:
"""Attribute the supplies_usage delta since last tick to the active slot.
Skips attribution during loading/unloading transitions (tool changes +
purges) to avoid charging the wrong spool for purge material."""
if not self._spoolman or not self._spoolman_slot_spools:
return
if self._state.get("print_state") != "printing":
return
current = self._state.get("supplies_usage", 0)
delta = current - self._spoolman_last_usage
self._spoolman_last_usage = current
if delta <= 0:
return
loaded = self._ams_loaded_slot
if loaded < 0:
return
if activity_map.get(loaded):
return
self._spoolman_slot_usage[loaded] = self._spoolman_slot_usage.get(loaded, 0.0) + delta
def _spoolman_unreported(self) -> dict[int, float]:
"""Return {slot_idx: mm} of usage not yet reported to Spoolman.
Falls back to equal split of total supplies_usage when per-slot
attribution data is absent (e.g. single-extruder with no AMS)."""
total_used = self._state.get("supplies_usage", 0)
if self._spoolman_slot_usage:
return {
slot: self._spoolman_slot_usage.get(slot, 0.0)
- self._spoolman_slot_reported.get(slot, 0.0)
for slot in self._spoolman_slot_spools
}
n = len(self._spoolman_slot_spools)
already = sum(self._spoolman_slot_reported.values())
per = (total_used - already) / n if n else 0.0
return {slot: per for slot in self._spoolman_slot_spools}
def _spoolman_report(self, unreported: dict[int, float], min_mm: float = 0.1) -> None:
"""Fire-and-forget report of unreported mm to each mapped spool."""
sm = self._spoolman
for slot_idx, mm in unreported.items():
if mm < min_mm:
continue
spool_id = self._spoolman_slot_spools.get(slot_idx)
if not spool_id:
continue
self._spoolman_slot_reported[slot_idx] = (
self._spoolman_slot_reported.get(slot_idx, 0.0) + mm
)
def _send(sid=spool_id, length=mm):
try:
sm.use_filament(sid, length)
log.info(f"Spoolman: {length:.1f} mm → spool {sid}")
except Exception as e:
log.warning(f"Spoolman: report failed (spool {sid}): {e}")
threading.Thread(target=_send, daemon=True, name="spoolman-report").start()
def _spoolman_notify_end(self):
"""Report remaining filament on print end."""
if not self._spoolman or not self._spoolman_slot_spools:
return
self._spoolman_report(self._spoolman_unreported())
def _spoolman_sync_midprint(self):
"""Report incremental filament usage during a print (sync_rate interval)."""
if not self._spoolman or not self._spoolman_slot_spools:
return
self._spoolman_report(self._spoolman_unreported(), min_mm=10.0)
# ── Spoolman API handlers ─────────────────────────────────────────────────
async def handle_kx_spoolman_status(self, request):
"""GET /kx/spoolman/status"""
return self._json_cors({
"configured": bool(self._spoolman),
"server": self._spoolman.server_url if self._spoolman else "",
"sync_rate": self._spoolman.sync_rate if self._spoolman else 0,
"slot_spools": {str(k): v for k, v in self._spoolman_slot_spools.items()},
})
async def handle_kx_spoolman_spools(self, request):
"""GET /kx/spoolman/spools — proxied from Spoolman."""
if not self._spoolman:
return self._json_cors({"error": "Spoolman not configured"}, status=503)
try:
spools = await asyncio.get_event_loop().run_in_executor(
None, self._spoolman.list_spools
)
return self._json_cors({"spools": spools})
except Exception as e:
log.warning(f"Spoolman: list_spools failed: {e}")
return self._json_cors({"error": str(e)}, status=502)
async def handle_kx_spoolman_set_active(self, request):
"""POST /kx/spoolman/active-spool
Body: {"slot_map": {"0": 42, "2": 17}} — AMS slot index → Spoolman spool ID."""
try:
data = await request.json()
except Exception:
return self._json_cors({"error": "invalid JSON"}, status=400)
slot_map = data.get("slot_map") or {}
self._spoolman_slot_spools = {
int(k): int(v) for k, v in slot_map.items()
if str(v).isdigit() and int(v) > 0
}
self._spoolman_slot_usage = {}
self._spoolman_slot_reported = {}
self._spoolman_last_usage = 0.0
return self._json_cors({"slot_spools": {str(k): v for k, v in self._spoolman_slot_spools.items()}})
def _default_ace_dry_presets(self) -> dict[str, dict]:
return {
"pla": {"temp": 45, "duration_sec": 4 * 3600},
@@ -939,15 +1131,21 @@ class KobraXBridge:
printer_id=self._printer_id,
)
log.info(f"Job started: {self._current_job_id} for {filename}")
self._spoolman_slot_usage = {}
self._spoolman_slot_reported = {}
self._spoolman_last_usage = 0.0
self._spoolman_last_sync = 0.0
# Job-History: Druckende erkennen
if kobra_state in ("finished",) and self._current_job_id:
self._store.finish_job(self._current_job_id, status="completed")
log.info(f"Job abgeschlossen: {self._current_job_id}")
self._spoolman_notify_end()
self._current_job_id = ""
elif kobra_state in ("stoped", "canceled") and self._current_job_id:
self._store.finish_job(self._current_job_id, status="cancelled")
log.info(f"Job abgebrochen: {self._current_job_id}")
self._spoolman_notify_end()
self._current_job_id = ""
# Nach Druckende das Upload-Banner verschwinden lassen (Issue #29): der
@@ -964,6 +1162,7 @@ class KobraXBridge:
self._state["slicer_time"] = 0
self._state["layer_height"] = 0.0
self._state["first_layer_height"] = 0.0
self._state["supplies_usage"] = 0
self._thumbnail_b64 = ""
self._state["filename"] = d.get("filename", self._state["filename"])
if "progress" in d:
@@ -978,6 +1177,8 @@ class KobraXBridge:
self._state["total_layers"] = d["total_layers"]
if "taskid" in d:
self._state["taskid"] = str(d["taskid"])
if "supplies_usage" in d:
self._state["supplies_usage"] = int(d["supplies_usage"])
settings = d.get("settings") or {}
if "print_speed_mode" in settings:
self._state["print_speed_mode"] = int(settings["print_speed_mode"])
@@ -1059,7 +1260,35 @@ class KobraXBridge:
"""
d = payload.get("data") or {}
skipped = d.get("objects_skip_parts") or d.get("skipped") or d.get("skipped_parts") or []
# Liste immer (auch leer) übernehmen sonst bleibt sie auf alten Stand
# Während ein Pre-Print-Skip noch pending ist, leere Früh-Reports ignorieren
# damit die UI nicht sofort zurückspringt bevor der Drucker den Skip bestätigt.
now = time.time()
if (not skipped and self._pending_preprint_skip
and now <= self._pending_preprint_skip_deadline):
return
# Während eines aktiven Drucks sind Skip-Zustände effektiv monoton.
# Manche Firmware-Reports kommen zwischenzeitlich leer/teilweise zurück;
# diese dürfen bereits bestätigte Skip-Objekte nicht aus der UI löschen.
existing_skipped = [str(n) for n in (self._skip_state.get("skipped") or []) if n]
existing_set = set(existing_skipped)
incoming_skipped = [str(n) for n in (skipped or []) if n]
incoming_set = set(incoming_skipped)
active_print = self._state.get("print_state") in ("printing", "paused")
if active_print and existing_set:
if not incoming_set:
skipped = list(existing_skipped)
elif not incoming_set.issuperset(existing_set):
merged = list(existing_skipped)
for n in incoming_skipped:
if n not in existing_set:
merged.append(n)
skipped = merged
# Pending-Lock aufheben sobald Drucker die gewünschten Objekte bestätigt
if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip):
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
self._skip_state = {
"skipped": list(skipped),
"ts": int(time.time()),
@@ -1071,14 +1300,19 @@ class KobraXBridge:
d = payload.get("data") or {}
details = d.get("file_details") or {}
thumb = details.get("thumbnail") or details.get("png_image") or ""
if thumb:
file_name = d.get("filename") or details.get("filename") or self._last_uploaded_file
active_print = self._state.get("print_state") in ("printing", "paused")
current_print_file = self._state.get("filename") or ""
# Uploads während eines laufenden Drucks dürfen die aktive
# Fortschritts-Vorschau nicht überschreiben.
if thumb and (not active_print or (file_name and file_name == current_print_file)):
self._thumbnail_b64 = thumb
log.info(f"Vorschaubild empfangen: {len(thumb)} Zeichen base64")
# Part-Skip: Objekt-Liste + optionales SVG (v0.9.10)
objs = details.get("objects_skip_parts") or []
svg = details.get("svg_image") or ""
if objs:
filename = d.get("filename") or details.get("filename") or self._last_uploaded_file
filename = file_name
if filename:
try:
self._store.update_file_objects(filename, objs, svg)
@@ -1087,6 +1321,33 @@ class KobraXBridge:
log.warning(f"update_file_objects fehlgeschlagen: {e}")
self._push_status_update()
def _apply_preprint_skip_after_start(self, names: list[str], retries: int = 20, delay_s: float = 0.75):
"""Sendet Skip-Befehl erst nachdem Drucker in printing-State gewechselt hat.
Vorher sendet der Drucker den Befehl ins Leere (kein aktiver Druck).
"""
wanted = [str(n) for n in (names or []) if isinstance(n, str) and n]
if not wanted:
return False
for i in range(max(1, int(retries))):
try:
if self._state.get("print_state") not in ("printing", "paused"):
time.sleep(max(0.1, float(delay_s)))
continue
resp = self.client.skip_objects(wanted)
if resp is not None:
log.info(f"Pre-Print skip applied ({len(wanted)} objects) on attempt {i+1}/{retries}")
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
return True
except Exception as e:
log.debug(f"Pre-Print skip attempt {i+1}/{retries} failed: {e}")
time.sleep(max(0.1, float(delay_s)))
log.warning(f"Pre-Print skip could not be confirmed after {retries} attempts")
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
return False
@staticmethod
def _detect_filament_mode(boxes: list, head_tools_model: int = -1) -> str:
"""Detect active filament topology mode.
@@ -1694,13 +1955,22 @@ class KobraXBridge:
# WebSocket push
# -------------------------------------------------------------------------
# Statische Objekte, die sich zur Laufzeit nie ändern. Sie werden einmalig
# per objects.query/subscribe ausgeliefert, aber NICHT in jedem
# notify_status_update mitgeschickt — sonst läuft Mobilerakers
# ConfigFile.parse (teuer + strikt) bei jedem Status-Tick erneut und die App
# hängt/crasht beim Refresh (Issue #48).
_STATIC_STATUS_OBJECTS = ("configfile", "webhooks", "heaters", "history")
def _push_status_update(self):
if not self.ws_clients:
return
objs = self._build_printer_objects()
live = {k: v for k, v in objs.items() if k not in self._STATIC_STATUS_OBJECTS}
msg = {
"jsonrpc": "2.0",
"method": "notify_status_update",
"params": [self._build_printer_objects(), time.time()],
"params": [live, time.time()],
}
text = json.dumps(msg)
dead = set()
@@ -1858,6 +2128,17 @@ class KobraXBridge:
"homing_origin": [0, 0, 0, 0],
"position": [0, 0, self._estimate_current_z(), 0],
},
# motion_report: Mobileraker liest die Live-Geschwindigkeit hier
# (live_velocity). Der Kobra-X-MQTT liefert KEINE echte mm/s, nur
# einen print_speed_mode (1-4). live_velocity bleibt daher 0 — das
# Objekt muss aber existieren, sonst zeigt Mobileraker nichts an
# (motion_report war zuvor null). live_position spiegelt die
# geschätzte Z-Höhe (wie gcode_move).
"motion_report": {
"live_position": [0, 0, self._estimate_current_z(), 0],
"live_velocity": 0.0,
"live_extruder_velocity": 0.0,
},
"fan": {
"speed": (int(s.get("fan_speed") or 0)) / 100.0,
"rpm": None,
@@ -1894,40 +2175,78 @@ class KobraXBridge:
# configfile stub — Mobileraker und andere Clients crashen ohne
# dieses Objekt (Missing field: configFile). Werte aus der
# entschlüsselten avata_main.conf (ACCFG1.0 — Kobra X Firmware).
"configfile": {
"config": {},
"settings": {
"printer": {
"kinematics": "cartesian",
"max_velocity": 450,
"max_accel": 10000,
"max_z_velocity": 12,
"max_z_accel": 100,
"square_corner_velocity": 20.0,
},
"extruder": {
"nozzle_diameter": 0.4,
"filament_diameter": 1.75,
"sensor_type": "ATC Semitec 104GT-2",
"min_temp": 0,
"max_temp": 320,
"min_extrude_temp": 10,
},
"heater_bed": {
"min_temp": 0,
"max_temp": 120,
},
"stepper_x": {"position_min": -18.5, "position_max": 280},
"stepper_y": {"position_min": -6.5, "position_max": 272.5},
"stepper_z": {"position_min": -4, "position_max": 262},
"virtual_sdcard": {"path": "/data/gcodes"},
"pause_resume": {},
"display_status": {},
},
"warnings": [],
"save_config_pending": False,
"save_config_pending_items": {},
# Mobileraker (Issue #48) parst BEIDE Zweige config + settings durch
# denselben ConfigFile.parse → ConfigExtruder.fromJson; ein leeres
# config:{} ließ den nicht-nullbaren Dart-Parser crashen. Daher wird
# config identisch zu settings gespiegelt.
"configfile": self._klipper_configfile_stub(),
}
def _klipper_configfile_stub(self) -> dict:
"""Minimaler Klipper-configfile-Stub für Mobileraker/OctoApp (Issue #48).
Mobileraker parst BEIDE Zweige `config` und `settings` durch denselben
ConfigFile.parse → ConfigExtruder.fromJson. Ein leeres `config: {}`
ließ den nicht-nullbaren Dart-Parser crashen, daher wird `config`
identisch zu `settings` gespiegelt. Werte aus der entschlüsselten
avata_main.conf (ACCFG1.0 — Kobra X Firmware).
"""
settings = {
"printer": {
"kinematics": "cartesian",
"max_velocity": 450,
"max_accel": 10000,
"max_z_velocity": 12,
"max_z_accel": 100,
"square_corner_velocity": 20.0,
},
"extruder": {
"nozzle_diameter": 0.4,
"filament_diameter": 1.75,
"sensor_type": "ATC Semitec 104GT-2",
"min_temp": 0,
"max_temp": 320,
"min_extrude_temp": 10,
# Mobileraker ConfigExtruder erwartet diese Felder non-nullable
# (max_extrude_only_distance, max_power) bzw. als Key präsent
# (max_extrude_only_velocity/accel dürfen null sein). Fehlen =
# Crash in ConfigExtruder.fromJson (Issue #48).
"max_extrude_only_distance": 100.0,
"max_power": 1.0,
"max_extrude_only_velocity": None,
"max_extrude_only_accel": None,
},
"heater_bed": {
# Mobileraker ConfigHeaterBed: heater_pin, sensor_type, control
# sind non-nullable. Werte sind Platzhalter (Bridge kennt die
# echten Pins nicht — Anycubic-Firmware, kein Klipper-printer.cfg).
"heater_pin": "PA0",
"sensor_type": "ATC Semitec 104GT-2",
"control": "pid",
"min_temp": 0,
"max_temp": 120,
},
# stepper_* mit non-nullable Pflichtfeldern (step_pin, dir_pin,
# rotation_distance) füllen, sonst crasht ConfigStepper.fromJson.
"stepper_x": {"step_pin": "PA1", "dir_pin": "PA2", "rotation_distance": 40,
"position_min": -18.5, "position_max": 280},
"stepper_y": {"step_pin": "PA3", "dir_pin": "PA4", "rotation_distance": 40,
"position_min": -6.5, "position_max": 272.5},
"stepper_z": {"step_pin": "PA5", "dir_pin": "PA6", "rotation_distance": 8,
"position_min": -4, "position_max": 262},
"virtual_sdcard": {"path": "/data/gcodes"},
"pause_resume": {},
"display_status": {},
}
# config + settings müssen dieselben Felder enthalten — Mobileraker
# parst beide. deepcopy, damit kein Client durch geteilte Referenz
# versehentlich beide Zweige mutiert.
return {
"config": copy.deepcopy(settings),
"settings": settings,
"warnings": [],
"save_config_pending": False,
"save_config_pending_items": {},
}
# -------------------------------------------------------------------------
@@ -2247,6 +2566,31 @@ class KobraXBridge:
"name": entry.get("name", ""),
"id": entry.get("id", "")})
async def handle_kx_visible_vendors(self, request):
"""GET/POST /kx/filament/visible_vendors — Vendor-Sichtbarkeitsfilter
fürs Slot-Profil-Dropdown (Issue #41 Option A).
GET → {"result": ["Polymaker", "eSUN", ...]}
POST {"vendors": [...]} → speichert in config.ini [filament_profiles]
visible_vendors. Leere Liste = alle sichtbar. KEIN Bridge-Neustart
nötig (nur Anzeigefilter)."""
if request.method == "POST":
try:
data = await request.json()
except Exception:
data = {}
vendors = data.get("vendors") or []
if not isinstance(vendors, list):
return self._json_cors({"error": "vendors must be a list"}, status=400)
self._visible_vendors = [str(v).strip() for v in vendors if str(v).strip()]
try:
import config_loader as _cl
_cl.save_visible_vendors(self._visible_vendors)
except Exception as e:
log.warning(f"save_visible_vendors failed: {e}")
return self._json_cors({"error": str(e)}, status=500)
return self._json_cors({"result": self._visible_vendors})
def _load_orca_filaments(self) -> list[dict]:
"""Lädt System- + User-Profile aus dem Cache. System-Profile kommen
aus bridge/data/orca_filaments.json (Image-embedded), User-Profile
@@ -2317,6 +2661,18 @@ class KobraXBridge:
names = json.loads(f.get("objects_skip_parts") or "[]")
except Exception:
names = []
# Noch keine Objekte im Store (frischer Orca-/Web-Upload): einmal aktiv
# file/fileDetails beim Drucker anfragen. _on_file() füllt den Store nach,
# das Frontend pollt diesen Endpoint und bekommt die Liste beim nächsten
# Versuch (Issue #57 — Skip-Parität auch außerhalb des File-Browsers).
if not names:
fn = f.get("filename") or ""
if fn:
try:
self.client.publish("file", "fileDetails",
{"root": "local", "filename": fn}, timeout=0)
except Exception as e:
log.debug(f"fileDetails-Nachfrage fehlgeschlagen: {e}")
return self._json_cors({
"result": {
"names": names,
@@ -2343,29 +2699,8 @@ class KobraXBridge:
return self._json_cors({"error": str(e)}, status=502)
return self._json_cors({"result": "ok", "names": names})
async def handle_kx_skip_query(self, request):
"""Druck-Objektliste vom Drucker neu abfragen.
POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten
Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state).
"""
try:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, lambda: self.client.query_skip_objects())
except Exception as e:
return self._json_cors({"error": str(e)}, status=502)
return self._json_cors({"result": self._skip_state})
async def handle_kx_skip_state(self, request):
"""Aktueller Skip-State.
Kombiniert:
- Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell
laufenden filename (file/report beim Druckstart hat die Liste gefüllt).
skip/query_obj liefert nämlich NUR die bereits geskippten zurück,
nicht die Gesamtliste.
- Geskippt: aus self._skip_state (von skip/report aktualisiert).
"""
def _build_skip_state_result(self) -> dict:
"""Baut den kombinierten Skip-State für UI-Endpunkte."""
filename = self._state.get("filename", "")
all_objects: list[str] = []
svg = ""
@@ -2377,14 +2712,46 @@ class KobraXBridge:
svg = f.get("svg_image") or ""
except Exception as e:
log.warning(f"skip_state lookup failed: {e}")
result = {
return {
"objects": all_objects,
"skipped": list(self._skip_state.get("skipped", [])),
"svg_b64": svg,
"ts": self._skip_state.get("ts", 0),
"filename": filename,
}
return self._json_cors({"result": result})
async def handle_kx_skip_query(self, request):
"""Druck-Objektliste vom Drucker neu abfragen.
POST /kx/skip/query → triggert skip/query_obj, wartet kurz auf den
async skip/report und gibt den zusammengeführten Skip-State zurück.
"""
prev_ts = int(self._skip_state.get("ts", 0) or 0)
try:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, lambda: self.client.query_skip_objects())
except Exception as e:
return self._json_cors({"error": str(e)}, status=502)
deadline = time.time() + 1.5
while time.time() < deadline:
if int(self._skip_state.get("ts", 0) or 0) > prev_ts:
break
await asyncio.sleep(0.1)
return self._json_cors({"result": self._build_skip_state_result()})
async def handle_kx_skip_state(self, request):
"""Aktueller Skip-State.
Kombiniert:
- Gesamt-Objektliste: aus dem GCode-Store, gematcht über den aktuell
laufenden filename (file/report beim Druckstart hat die Liste gefüllt).
skip/query_obj liefert nämlich NUR die bereits geskippten zurück,
nicht die Gesamtliste.
- Geskippt: aus self._skip_state (von skip/report aktualisiert).
"""
return self._json_cors({"result": self._build_skip_state_result()})
async def handle_kx_printers(self, request):
# Aktive Drucker (mit IP) sammeln
@@ -2447,7 +2814,7 @@ class KobraXBridge:
ams_box_mapping = self._build_auto_ams_box_mapping()
use_ams = len(ams_box_mapping) > 0
auto_leveling = getattr(self._args, "auto_leveling", 1)
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
filename = gcode_file["filename"]
file_path = gcode_file["path"]
@@ -2479,6 +2846,15 @@ class KobraXBridge:
},
}
# UI erst nach echter Drucker-Bestätigung als "geskippt" markieren.
self._skip_state = {"skipped": [], "ts": int(time.time())}
if excluded_objects:
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
self._pending_preprint_skip_deadline = time.time() + 12.0
else:
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
log.info(f"KX-Store Druckstart: {filename} ams={len(ams_box_mapping)} slots assignments={bool(assignments)} excluded={len(excluded_objects)}")
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
@@ -2487,6 +2863,9 @@ class KobraXBridge:
if result is None:
return self._json_cors({"error": "Keine Antwort vom Drucker"}, status=504)
if excluded_objects:
loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects))
# Job in History starten
self._current_job_id = self._store.start_job(
gcode_file_id=gcode_file["id"],
@@ -2591,18 +2970,16 @@ class KobraXBridge:
})
return web.json_response({"result": files})
async def handle_files_metadata(self, request):
"""Moonraker /server/files/metadata — moonraker-obico-Plugin holt das
einmal pro Druck und liest daraus `object_height` (für `currentZ`-
Anzeige im Obico-UI: `mmProgress` braucht maxZ), `layer_count`,
`layer_height` und `first_layer_height` (für die Layer-Berechnung).
def _build_file_metadata(self, filename: str) -> dict:
"""Baut die Moonraker-file-metadata für eine Datei. Gemeinsame Quelle
für HTTP /server/files/metadata UND den WS-RPC server.files.metadata
(vorher hatte der WS-Pfad eigene, kaputte Logik mit einer nicht
existierenden Store-Methode → leere Antwort → Mobileraker fragte in
Endlosschleife, App hing beim Refresh, Issue #48).
Quelle: aktueller `_state` + GCode-Store-Eintrag wenn vorhanden.
Wenn Layer-Heights weder im State noch im Store sind, Fallback auf die
OrcaSlicer-Default-Filename-Heuristik (`_layer_height_from_filename`)."""
filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "")
if not filename:
return web.json_response({"result": {}})
Liefert Mobileraker-kompatible Pflichtfelder: `filename`, `size`,
`modified` sind in GCodeFile non-nullable; `print_start_time` und die
Slicer-Felder optional."""
s = self._state
layer_h = float(s.get("layer_height") or 0.0)
first_h = float(s.get("first_layer_height") or 0.0)
@@ -2626,16 +3003,27 @@ class KobraXBridge:
if layer_h and not first_h:
first_h = layer_h
object_height = round(first_h + max(0, total_layers - 1) * layer_h, 3) if (layer_h and total_layers) else 0.0
return web.json_response({"result": {
return {
"filename": filename,
"size": size_bytes,
# GCodeFile (Mobileraker) verlangt size als non-nullable int.
"size": size_bytes or 1,
"modified": time.time(),
"estimated_time": est_time or None,
"layer_height": layer_h or None,
"first_layer_height": first_h or None,
"layer_count": total_layers or None,
"object_height": object_height or None,
}})
"thumbnails": [],
}
async def handle_files_metadata(self, request):
"""Moonraker /server/files/metadata — moonraker-obico + Mobileraker
holen Datei-Metadaten (Slicer-Zeit, Layer, object_height).
Logik in _build_file_metadata (gemeinsam mit WS-RPC)."""
filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "")
if not filename:
return web.json_response({"result": {}})
return web.json_response({"result": self._build_file_metadata(filename)})
# ── Moonraker-Stubs für moonraker-obico ──────────────────────────────────
async def handle_access_api_key(self, request):
@@ -2768,6 +3156,18 @@ class KobraXBridge:
if not file_data:
return web.json_response({"error": "no file received"}, status=400)
# Nur druckbare Dateien zulassen (Issue #59) — der Kobra X akzeptiert
# ausschließlich .gcode und .bgcode; .3mf-Uploads werden vom Drucker
# nicht verarbeitet und daher abgelehnt (Issue #59, @gangoke).
_allowed_ext = (".gcode", ".bgcode")
_fn_lower = (remote_filename or "").lower()
if not _fn_lower.endswith(_allowed_ext):
log.warning(f"Upload abgelehnt (kein GCode): {remote_filename}")
return web.json_response(
{"error": f"only GCode files allowed ({', '.join(_allowed_ext)})"},
status=400,
)
file_md5 = hashlib.md5(file_data).hexdigest()
file_size = len(file_data)
@@ -2970,7 +3370,8 @@ class KobraXBridge:
ams_box_mapping = self._build_auto_ams_box_mapping()
use_ams = len(ams_box_mapping) > 0
auto_leveling = getattr(self._args, "auto_leveling", 1)
# Dialog-Checkbox (body) hat Vorrang, sonst Setting-Default (wie handle_kx_print).
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
url = self._state.get("last_upload_url", "")
filesize = self._state.get("last_upload_size", 0)
md5 = self._state.get("last_upload_md5", "")
@@ -3000,6 +3401,15 @@ class KobraXBridge:
},
}
# UI erst nach echter Drucker-Bestätigung als "geskippt" markieren.
self._skip_state = {"skipped": [], "ts": int(time.time())}
if excluded_objects:
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
self._pending_preprint_skip_deadline = time.time() + 12.0
else:
self._pending_preprint_skip = []
self._pending_preprint_skip_deadline = 0.0
log.info(
f"print/start api=1 mode={self._filament_mode} "
f"ams={len(ams_box_mapping)} slots assignments={filament_assignments is not None}"
@@ -3012,6 +3422,9 @@ class KobraXBridge:
if result is None:
return web.json_response({"error": "Keine Antwort vom Drucker"}, status=504)
if excluded_objects:
loop.run_in_executor(None, lambda: self._apply_preprint_skip_after_start(excluded_objects))
return web.json_response({"result": "ok"})
async def handle_print_pause(self, request):
@@ -3646,6 +4059,8 @@ class KobraXBridge:
"camera_url": s["camera_url"],
"fan_speed": s["fan_speed"],
"print_speed_mode": s["print_speed_mode"],
"auto_leveling": getattr(self._args, "auto_leveling", 1),
"camera_on_print": getattr(self._args, "camera_on_print", 0),
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
"light_on": s["light_on"],
"light_brightness": s["light_brightness"],
@@ -3659,6 +4074,7 @@ class KobraXBridge:
"thumbnail": thumbnail,
"connection_error": s["connection_error"],
"file_ready": s["file_ready"],
"print_start_dialog": s.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1)),
"version": self._read_version(),
})
@@ -3797,6 +4213,10 @@ class KobraXBridge:
"auto_leveling": getattr(self._args, "auto_leveling", 1),
"camera_on_print": getattr(self._args, "camera_on_print", 0),
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
"poll_interval": getattr(self._args, "poll_interval", 3),
"filament_profiles": {str(k): v for k, v in self._filament_profiles.items()},
"visible_vendors": self._visible_vendors,
"ace_dry_presets": self._ace_dry_presets,
})
@@ -3827,7 +4247,14 @@ class KobraXBridge:
cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1))))
cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0))))))
cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1))))))
if not cfg.has_option("bridge", "poll_interval"):
cfg.set("print", "print_start_dialog", str(int(bool(data.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1))))))
if "poll_interval" in data:
try:
pi = max(1, min(60, int(data["poll_interval"])))
except (TypeError, ValueError):
pi = 3
cfg.set("bridge", "poll_interval", str(pi))
elif not cfg.has_option("bridge", "poll_interval"):
cfg.set("bridge", "poll_interval", "3")
printer_name = str(data.get("printer_name", "")).strip()
if printer_name:
@@ -3993,7 +4420,8 @@ class KobraXBridge:
# die alten Werte statt der geänderten config.ini.
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "BRIDGE_PRINTER_NAME"):
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "PRINT_START_DIALOG",
"FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME"):
os.environ.pop(_k, None)
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
@@ -4491,23 +4919,13 @@ class KobraXBridge:
elif method == "machine.update.status":
result = {"busy": False, "version_info": {}}
elif method == "server.files.metadata":
# Obico fragt nach Metadaten zu einer Datei (filename in params)
# Obico + Mobileraker fragen Metadaten zu einer Datei. Dieselbe
# Logik wie der HTTP-Endpoint (vorher eigener kaputter Pfad mit
# nicht existierender Store-Methode → leere Antwort →
# Mobileraker-Endlosschleife, Issue #48).
fname = (params or {}).get("filename") if isinstance(params, dict) else None
meta = {}
if fname:
try:
rec = self._store.get_file_by_filename(fname) if hasattr(self._store, "get_file_by_filename") else None
except Exception:
rec = None
if rec:
meta = {
"filename": rec.get("filename"),
"size": rec.get("size_bytes") or 0,
"modified": time.time(),
"estimated_time": rec.get("est_print_time_sec") or 0,
"thumbnails": [],
}
result = meta
fname = fname or self._state.get("filename", "")
result = self._build_file_metadata(fname) if fname else {}
else:
log.debug(f"Unbekannte RPC-Methode: {method}")
result = {}
@@ -4575,6 +4993,14 @@ class KobraXBridge:
print_r = self.client.publish("print", "query", timeout=3.0)
if print_r:
self._on_print(print_r)
# Spoolman mid-print sync
if (self._spoolman and self._spoolman.sync_rate > 0
and self._spoolman_slot_spools
and self._state.get("print_state") == "printing"):
now = time.time()
if now - self._spoolman_last_sync >= self._spoolman.sync_rate:
self._spoolman_sync_midprint()
self._spoolman_last_sync = now
box = self.client.query_multicolor_box()
if box:
data = box.get("data") or {}
@@ -4591,6 +5017,10 @@ class KobraXBridge:
if global_slots:
self._ams_slots = global_slots
self._ams_loaded_slot = global_loaded
self._spoolman_attribute_tick(activity_map)
else:
# No multiColorBox data — still attribute (no transitions to skip)
self._spoolman_attribute_tick({})
except Exception as e:
log.warning(f"Poll-Fehler: {e}")
# Prüfen ob Drucker wirklich weg ist
@@ -4711,6 +5141,8 @@ def build_app(bridge: KobraXBridge) -> web.Application:
r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots)
r.add_get("/kx/filament/profiles", bridge.handle_kx_filament_profiles)
r.add_post("/kx/filament/slots/{idx}/profile", bridge.handle_kx_filament_slot_profile)
r.add_get("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors)
r.add_post("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors)
# Custom-Profile-Import (Issue #41) — User lädt eigene Orca-Filament-
# Profile als ZIP/JSON hoch (z.B. aus ~/.config/OrcaSlicer/user/<id>/filament/),
# weil die Bridge typischerweise nicht auf demselben Host wie OrcaSlicer läuft.
@@ -4723,6 +5155,9 @@ def build_app(bridge: KobraXBridge) -> web.Application:
r.add_post("/kx/skip", bridge.handle_kx_skip)
r.add_post("/kx/skip/query", bridge.handle_kx_skip_query)
r.add_get("/kx/skip/state", bridge.handle_kx_skip_state)
r.add_get("/kx/spoolman/status", bridge.handle_kx_spoolman_status)
r.add_get("/kx/spoolman/spools", bridge.handle_kx_spoolman_spools)
r.add_post("/kx/spoolman/active-spool", bridge.handle_kx_spoolman_set_active)
r.add_route("OPTIONS", "/kx/{path:.*}", bridge.handle_kx_options)
# Root + Printer-Routen (Single-Page, JS liest Pathname)
@@ -4886,6 +5321,12 @@ def main():
parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING)
parser.add_argument("--camera-on-print", type=int, default=env_loader.CAMERA_ON_PRINT)
parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG)
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
parser.add_argument("--spoolman-server", default=env_loader.SPOOLMAN_SERVER,
help="Spoolman URL (e.g. http://192.168.x.x:7912); leave empty to disable")
parser.add_argument("--spoolman-sync-rate", type=int, default=env_loader.SPOOLMAN_SYNC_RATE,
help="Mid-print filament sync interval in seconds (0 = only on print end)")
parser.add_argument("--host", default="0.0.0.0",
help="Bind-Adresse für den Bridge-Server")

31
package-lock.json generated Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "kx-bridge",
"version": "0.9.27",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kx-bridge",
"version": "0.9.27",
"devDependencies": {
"prettier": "^3.0.0"
}
},
"node_modules/prettier": {
"version": "3.8.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz",
"integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
}
}
}

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "kx-bridge",
"version": "0.9.27",
"description": "Moonraker-compatible bridge for Anycubic Kobra X",
"type": "module",
"devDependencies": {
"prettier": "^3.0.0"
},
"scripts": {
"format:js": "prettier --write 'web/**/*.js' '*.js'",
"format:json": "prettier --write '*.json' 'web/**/*.json'",
"format:web": "prettier --write 'web/**/*.{js,json,html,css,yml,yaml,md}'",
"format": "npm run format:web"
},
"prettier": {
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false
}
}

2
requirements-dev.txt Normal file
View File

@@ -0,0 +1,2 @@
# Code formatting
black>=24.1.0

File diff suppressed because it is too large Load Diff

View File

@@ -39,118 +39,16 @@
<option value="en">English</option>
<option value="es">Espanol</option>
<option value="fr">Français</option>
<option value="it">Italiano</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="showPanel('settings')" id="settings-btn" title="Einstellungen"></button>
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
</header>
<!-- ═══ SETTINGS MODAL ═══ -->
<div class="modal-overlay" id="settings-modal" onclick="if(event.target===this)closeSettings()">
<div class="modal-box">
<div class="modal-header">
<span class="modal-title" id="modal-title-settings">Einstellungen</span>
<button class="modal-close" onclick="closeSettings()"></button>
</div>
<div>
<div class="modal-field" style="margin-bottom:12px">
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
</div>
<div class="modal-section" id="modal-sec-connection">Verbindung</div>
<div class="modal-field">
<label id="lbl-printer-ip">Drucker-IP</label>
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
</div>
<div class="modal-field">
<label id="lbl-mqtt-port">MQTT-Port</label>
<input type="number" id="s-mqtt-port" placeholder="9883">
</div>
<div class="modal-field">
<label id="lbl-username">MQTT-Benutzername</label>
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-password">MQTT-Passwort</label>
<input type="password" id="s-password" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-device-id">Device-ID</label>
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
</div>
<div class="modal-field">
<label id="lbl-mode-id">Mode-ID</label>
<input type="text" id="s-mode-id" placeholder="20030">
</div>
</div>
<div>
<div class="modal-section" id="modal-sec-print">Druckeinstellungen</div>
<div class="modal-field">
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
<select id="s-default-slot">
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
<option value="0" id="opt-slot-0">Slot 1</option>
<option value="1" id="opt-slot-1">Slot 2</option>
<option value="2" id="opt-slot-2">Slot 3</option>
<option value="3" id="opt-slot-3">Slot 4</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
</div>
</div>
<div>
<div class="modal-section" id="modal-sec-poll">Poll-Intervall</div>
<div class="poll-btns">
<button class="poll-btn" onclick="setPoll(1000)" id="poll-1">1s</button>
<button class="poll-btn active" onclick="setPoll(2000)" id="poll-2">2s</button>
<button class="poll-btn" onclick="setPoll(5000)" id="poll-5">5s</button>
</div>
</div>
<!-- OrcaSlicer-Profile (Custom-Profile-Import, Issue #41) -->
<div>
<div class="modal-section" id="modal-sec-orca-profiles">OrcaSlicer-Profile</div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
</div>
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
style="background:var(--raised);color:var(--txt)">
<span id="lbl-orca-profiles-import">Profile importieren</span>
</button>
</div>
<div>
<div class="modal-section" id="modal-sec-version">Version</div>
<div class="update-row">
<span id="s-version-label" style="font-size:13px;color:var(--txt)"></span>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
</div>
<div class="update-status" id="update-status" style="margin-top:6px"></div>
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
<span id="lbl-update-apply">Jetzt installieren</span>
</button>
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
</div>
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings">Speichern &amp; Neustart</button>
</div>
</div>
<!-- Settings-Modal entfernt — jetzt #panel-settings (Master-Detail im Main-Bereich) -->
<!-- ═══ AMS SLOT EDIT DIALOG ═══ -->
<div class="modal-overlay" id="slot-edit-modal" onclick="if(event.target===this)closeSlotEdit()">
@@ -230,6 +128,8 @@
<span class="nav-icon">🗂</span><span class="nav-text">Browser</span></button>
<button class="nav-btn" onclick="showPanel('console');clearLogBadge()" id="nb-console">
<span class="nav-icon"></span><span class="nav-text">Konsole</span><span id="log-badge" style="display:none;margin-left:4px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 5px;font-weight:700"></span></button>
<button class="nav-btn" onclick="showPanel('settings')" id="nb-settings">
<span class="nav-icon"></span><span class="nav-text" id="nav-settings">Einstellungen</span></button>
</nav>
<main>
@@ -298,6 +198,12 @@
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none"><span id="d-btn-skip-label">Objekte</span></button>
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
</div>
<!-- Aktionen für eine geladene, aber nicht laufende Datei (Issue #55) -->
<div class="ctrl-btns" id="d-idle-btns" style="margin-top:12px;display:none">
<button class="btn btn-accent btn-sm" id="d-idle-print" onclick="startIdleFile()"><span id="d-idle-print-lbl">Drucken</span></button>
<button class="btn btn-sm" id="d-idle-slots" onclick="startIdleFileWithSlots()" style="background:var(--raised);color:var(--txt)"><span id="d-idle-slots-lbl">Slots zuordnen</span></button>
<button class="btn btn-cancel btn-sm" id="d-idle-clear" onclick="clearIdleFile()"><span id="d-idle-clear-lbl">Leeren</span></button>
</div>
</div>
<!-- Temperatursteuerung + Verlauf -->
@@ -520,6 +426,170 @@
<div class="console" id="console-log" style="height:calc(100vh - 260px);min-height:160px" onscroll="onLogScroll()"></div>
</div>
</div>
<!-- ═══ EINSTELLUNGEN ═══ -->
<div class="panel" id="panel-settings">
<div class="settings-wrap">
<div class="settings-cats">
<button class="set-cat active" id="setcat-connection" onclick="showSettingsCat('connection')"><span>🔌</span> <span id="setcat-lbl-connection">Verbindung</span></button>
<button class="set-cat" id="setcat-printer" onclick="showSettingsCat('printer')"><span>🖨</span> <span id="setcat-lbl-printer">Drucker</span></button>
<button class="set-cat" id="setcat-display" onclick="showSettingsCat('display')"><span>🎨</span> <span id="setcat-lbl-display">Darstellung</span></button>
<button class="set-cat" id="setcat-filament" onclick="showSettingsCat('filament')"><span>🧵</span> <span id="setcat-lbl-filament">Filament</span></button>
<button class="set-cat" id="setcat-system" onclick="showSettingsCat('system')"><span></span> <span id="setcat-lbl-system">System</span></button>
</div>
<div class="settings-content">
<!-- Verbindung -->
<div class="set-group active" id="setgrp-connection">
<div class="card">
<div class="card-title"><span>🔌</span> <span id="modal-sec-connection">Verbindung</span></div>
<div class="modal-field" style="margin-bottom:12px">
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
</div>
<div class="modal-field">
<label id="lbl-printer-ip">Drucker-IP</label>
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
</div>
<div class="modal-field">
<label id="lbl-mqtt-port">MQTT-Port</label>
<input type="number" id="s-mqtt-port" placeholder="9883">
</div>
<div class="modal-field">
<label id="lbl-username">MQTT-Benutzername</label>
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-password">MQTT-Passwort</label>
<input type="password" id="s-password" autocomplete="new-password">
</div>
<div class="modal-field">
<label id="lbl-device-id">Device-ID</label>
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
</div>
<div class="modal-field">
<label id="lbl-mode-id">Mode-ID</label>
<input type="text" id="s-mode-id" placeholder="20030">
</div>
</div>
</div>
<!-- Drucker -->
<div class="set-group" id="setgrp-printer">
<div class="card">
<div class="card-title"><span>🖨</span> <span id="modal-sec-print">Druckeinstellungen</span></div>
<div class="modal-field">
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
<select id="s-default-slot">
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
<option value="0" id="opt-slot-0">Slot 1</option>
<option value="1" id="opt-slot-1">Slot 2</option>
<option value="2" id="opt-slot-2">Slot 3</option>
<option value="3" id="opt-slot-3">Slot 4</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
</div>
<div class="modal-field">
<label id="lbl-file-ready-mode">Nach Upload: Druckstart-Verhalten</label>
<select id="s-file-ready-mode">
<option value="1" id="opt-file-ready-dialog">Print-Dialog</option>
<option value="0" id="opt-file-ready-banner">Print-Leiste</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
</div>
</div>
</div>
<!-- Darstellung -->
<div class="set-group" id="setgrp-display">
<div class="card">
<div class="card-title"><span>🎨</span> <span id="setcat-lbl-display2">Darstellung</span></div>
<div class="modal-field">
<label id="lbl-set-lang">Sprache</label>
<select id="s-lang-select" onchange="setLanguage(this.value)">
<option value="de">Deutsch</option>
<option value="en">English</option>
<option value="es">Espanol</option>
<option value="fr">Français</option>
<option value="it">Italiano</option>
<option value="zh-cn">中文(简体)</option>
</select>
</div>
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="toggleTheme()"><span id="lbl-set-theme">Hell / Dunkel umschalten</span></button>
</div>
<div class="modal-field">
<label id="lbl-poll-interval">Poll-Intervall (Sekunden)</label>
<input type="number" id="s-poll-interval" min="1" max="60" step="1" placeholder="3" oninput="onPollIntervalInput()">
<small style="color:var(--txt2)" id="lbl-poll-hint">Wie oft die Bridge den Drucker-Status abfragt</small>
</div>
</div>
</div>
<!-- Filament -->
<div class="set-group" id="setgrp-filament">
<div class="card">
<div class="card-title"><span>🧵</span> <span id="modal-sec-orca-profiles">OrcaSlicer-Profile</span></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
</div>
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
style="background:var(--raised);color:var(--txt)">
<span id="lbl-orca-profiles-import">Profile importieren</span>
</button>
</div>
<div class="card">
<div class="card-title"><span>🎯</span> <span id="lbl-filament-mapping">Filament-Profil-Mapping (pro Slot)</span></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="filament-mapping-hint">
Festes Orca-Profil pro AMS-Slot. Beim Slicer-Sync sendet die Bridge dieses Profil statt „Generic".
</div>
<div id="filament-mapping-list"></div>
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveFilamentMapping()"><span id="lbl-filament-mapping-save">Mapping speichern</span></button>
</div>
<div class="card">
<div class="card-title"><span>👁</span> <span id="lbl-visible-vendors">Sichtbare Hersteller (Profil-Dropdown)</span></div>
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="visible-vendors-hint">
Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.
</div>
<input type="text" id="vendor-filter-search" placeholder="Hersteller suchen…" oninput="renderVendorChecklist()"
style="width:100%;padding:6px 10px;margin-bottom:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px">
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
</div>
</div>
<!-- System -->
<div class="set-group" id="setgrp-system">
<div class="card">
<div class="card-title"><span></span> <span id="modal-sec-version">Version</span></div>
<div class="update-row">
<span id="s-version-label" style="font-size:13px;color:var(--txt)"></span>
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
</div>
<div class="update-status" id="update-status" style="margin-top:6px"></div>
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
<span id="lbl-update-apply">Jetzt installieren</span>
</button>
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
</div>
</div>
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings" style="margin-top:14px">Speichern &amp; Neustart</button>
</div>
</div>
</div>
</main>
</div>
@@ -528,6 +598,7 @@
<button class="bnav-btn" onclick="showPanel('printers');loadPrinterTab()" id="bnb-printers"><span class="bnav-icon">🖨</span>Drucker</button>
<button class="bnav-btn" onclick="showPanel('store');loadStore()" id="bnb-store"><span class="bnav-icon">🗂</span>Browser</button>
<button class="bnav-btn" onclick="showPanel('console');clearLogBadge()" id="bnb-console"><span class="bnav-icon"></span>Log<span id="log-badge-bot" style="display:none;margin-left:3px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 4px;font-weight:700"></span></button>
<button class="bnav-btn" onclick="showPanel('settings')" id="bnb-settings"><span class="bnav-icon"></span>Setup</button>
</nav>
@@ -557,9 +628,30 @@
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin-bottom:8px">Objekte überspringen (optional):</p>
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
<button type="button" id="fd-objects-toggle" onclick="toggleFdObjects()"
style="display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer;font-size:12px;text-align:left">
<span id="fd-objects-arrow" style="font-size:10px;transition:transform .15s"></span>
<span><span id="fd-objects-toggle-lbl">Objekte überspringen</span></span>
<span id="fd-objects-count" style="margin-left:auto;color:var(--txt2);font-weight:600"></span>
</button>
<div id="fd-objects-body" style="display:none;margin-top:8px">
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
</div>
</div>
<div style="margin-bottom:14px;padding:10px 12px;background:var(--raised);border-radius:8px;border:1px solid var(--border)">
<div style="font-size:11px;font-weight:600;color:var(--txt2);margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em" id="fd-options-title">Druckoptionen</div>
<div style="display:flex;align-items:center;gap:8px">
<input type="checkbox" id="fd-auto-leveling" style="width:auto;margin:0">
<label for="fd-auto-leveling" style="margin:0;cursor:pointer;font-size:13px" id="fd-lbl-auto-leveling">Auto-Leveling</label>
</div>
</div>
<div id="fd-spoolman-section" style="display:none;margin-bottom:16px;border-top:1px solid var(--border);padding-top:12px">
<p style="font-size:12px;color:var(--txt2);margin-bottom:8px;display:flex;align-items:center;gap:6px">
<span id="fd-spoolman-lbl">🧵 Spoolman</span>
<span id="fd-spoolman-loading" style="display:none;font-size:10px"></span>
</p>
<div id="fd-spoolman-rows" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button id="fd-cancel" onclick="closeFilamentDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>

View File

@@ -212,6 +212,24 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
.panel{display:none}
.panel.active{display:block}
/* ── SETTINGS (Master-Detail) ── */
.settings-wrap{display:grid;grid-template-columns:200px 1fr;gap:16px;align-items:start}
.settings-cats{display:flex;flex-direction:column;gap:4px;position:sticky;top:12px}
.set-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;
border:1px solid transparent;background:var(--raised);color:var(--txt2);cursor:pointer;
font-size:13px;text-align:left;transition:background .15s,color .15s}
.set-cat:hover{color:var(--txt)}
.set-cat.active{background:var(--accent);color:#fff;border-color:var(--accent)}
.set-group{display:none}
.set-group.active{display:block}
.set-group .card{margin-bottom:14px}
@media(max-width:768px){
.settings-wrap{grid-template-columns:1fr}
.settings-cats{flex-direction:row;flex-wrap:wrap;position:static;overflow-x:auto}
.set-cat{flex:1;min-width:auto;justify-content:center;padding:8px 6px;font-size:12px}
.set-cat .nav-text{display:inline}
}
/* ── FILE BROWSER UPLOAD ZONE ── */
#store-upload-zone{
display:flex;flex-direction:column;align-items:center;justify-content:center;

View File

@@ -1,248 +1,322 @@
{
"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_auto_refill": "Auto-Nachschub",
"ace_dry_chart": "Verlauf (Temp/Feuchte)",
"ace_dry_current_temp": "Temperatur",
"ace_dry_dialog_cancel": "Abbrechen",
"ace_dry_dialog_confirm": "Bestätigen",
"ace_dry_dialog_custom_name": "Eigener Name",
"ace_dry_dialog_reset_default": "Auf Standard zurücksetzen",
"ace_dry_dialog_save_restart": "Speichern & Neustart",
"ace_dry_dialog_temp": "Temperatur (30-80°C)",
"ace_dry_dialog_time": "Restzeit (h:m:s)",
"ace_dry_dialog_title": "Trockner Temp/Zeit-Einstellungen",
"ace_dry_dryer": "Trockner",
"ace_dry_duration": "Dauer (Min)",
"ace_dry_enable": "Trocknung aktivieren",
"ace_dry_humidity": "Luftfeuchte",
"ace_dry_preset_abs_asa": "ABS / ASA",
"ace_dry_preset_custom": "Custom",
"ace_dry_preset_pa_pc": "PA / PC",
"ace_dry_preset_petg": "PETG",
"ace_dry_preset_pla": "PLA",
"ace_dry_preset_pla_plus": "PLA+",
"ace_dry_preset_tpu": "TPU",
"ace_dry_start": "▶ Start",
"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": "Temperatur (°C)",
"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",
"add_printer": "Drucker hinzufügen",
"ams_empty": "Leer",
"ams_no_data": "Keine AMS-Daten empfangen",
"apd_cancel": "Abbrechen",
"apd_confirm": "Hinzufügen",
"apd_err_ip": "Bitte IP-Adresse eingeben",
"apd_fetching": "Hole Daten vom Drucker…",
"apd_lbl_ip": "Drucker-IP",
"apd_lbl_name": "Name (optional)",
"apd_placeholder_name": "z.B. Kobra X Wohnzimmer",
"apd_success": "Drucker hinzugefügt, Bridge startet neu…",
"apd_title": "Drucker hinzufügen",
"btn_cam_start": "▶ Kamera",
"btn_cam_start2": "▶ Start",
"btn_cam_stop": "◼ Kamera",
"btn_cam_stop2": "◼ Stop",
"btn_cancel": "✕ Stopp",
"btn_cancel_generic": "Abbrechen",
"btn_confirm_generic": "Bestätigen",
"btn_connect": "⚡ Verbinden",
"btn_delete": "Löschen",
"btn_disable_motors": "Motoren aus",
"btn_disconnect": "✕ Trennen",
"btn_home_all": "Home All",
"btn_home_xy": "Home XY",
"btn_home_z": "Home Z",
"btn_pause": "⏸ Pause",
"btn_resume": "▶ Weiter",
"btn_cancel": "✕ Stopp",
"label_nozzle": "Düse",
"cam_placeholder": "📷 Kamera nicht gestartet",
"cam_stream_unavailable": "Stream nicht verfügbar",
"card_ace_dry": "ACE Trocknung",
"card_ams": "Filament",
"card_cam": "Kamera",
"card_light_fan": "Lüfter",
"card_progress": "Fortschritt",
"card_speed": "Druckgeschwindigkeit",
"card_temps": "Temperaturen",
"confirm_cancel": "Druck wirklich abbrechen?",
"fd_cancel": "Abbrechen",
"fd_no_matching_material": "Kein passendes Material",
"fd_no_slots_msg": "Keine belegten AMS-Slots.{br}Druck trotzdem starten?",
"fd_objects_hint": "Objekte überspringen (optional):",
"fd_objects_toggle": "Objekte überspringen",
"fd_options_title": "Optionen",
"fd_print": "▶ Drucken",
"fd_slot": "Slot",
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
"fd_title": "Slot-Zuweisung",
"fd_used": "BELEGT",
"file_cancel_btn": "✕ Abbrechen",
"file_ready_btn": "▶ Druck starten",
"file_slots_btn": "🎨 Slots wählen",
"header_status_complete": "Fertig",
"header_status_error": "Fehler",
"header_status_printing": "Druckt",
"header_status_standby": "Bereit",
"hint_ip_no_port": "Nur IP-Adresse, kein Port (z.B. 192.168.1.102)",
"kobra_auto_leveling": "Nivellierung",
"kobra_busy": "Beschäftigt",
"kobra_canceled": "Abgebrochen",
"kobra_checking": "Prüfung",
"kobra_failed": "Fehler",
"kobra_finished": "Abgeschlossen",
"kobra_free": "Bereit",
"kobra_init": "Initialisierung",
"kobra_offline": "Offline",
"kobra_paused": "Pausiert",
"kobra_pausing": "Pausiert...",
"kobra_preheating": "Aufheizen",
"kobra_printing": "Druckt",
"kobra_resumed": "Fortgesetzt",
"kobra_resuming": "Fortsetzen...",
"kobra_stoped": "Gestoppt",
"kobra_stopping": "Stoppt...",
"kobra_updated": "Aktualisierung",
"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_nozzle": "Düse",
"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_on_off": "Ein / Aus",
"label_set": "Setzen",
"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 →",
"label_speed": "Geschwindigkeit",
"label_step": "Schrittweite:",
"label_target_c": "Ziel:",
"lbl_conn_error": "Verbindungsfehler:",
"lbl_elapsed": "Verstrichen:",
"lbl_feed": "Einziehen",
"lbl_layers": "Layer",
"lbl_light": "💡 Licht",
"lbl_remaining": "Restzeit:",
"lbl_slicer_time": "Slicer-Schätzung:",
"lbl_unload": "Ausziehen",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
"log_axis": "Achse",
"log_home": "Home",
"log_home_all": "Home All",
"log_bed": "Bett →",
"log_cam_start": "Kamera gestartet:",
"log_cam_stop": "Kamera gestoppt",
"log_poll_error": "Poll-Fehler:",
"log_clear": "✕ Leeren",
"log_delete_failed": "Löschung fehlgeschlagen",
"log_dir_all": "Alle",
"log_dir_label": "Richtung:",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_download": "⬇ Download",
"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",
"log_fan": "Lüfter →",
"log_filter_placeholder": "Filtern…",
"log_home": "Home",
"log_home_all": "Home All",
"log_light_off": "Licht aus",
"log_light_on": "Licht an",
"log_lvl_err": "⛔ Fehler",
"log_lvl_label": "Level:",
"log_lvl_warn": "⚠ Warnung",
"log_nozzle": "Düse →",
"log_poll_error": "Poll-Fehler:",
"log_print_action": "Druck:",
"log_print_start": "Druckstart:",
"log_topic_ams": "AMS",
"log_topic_info": "Info",
"log_topic_label": "Thema:",
"log_topic_print": "Druck",
"log_topic_status": "Status",
"nav_ams": "AMS",
"nav_browser": "Browser",
"nav_console": "Konsole",
"nav_dashboard": "Dashboard",
"nav_extras": "Licht / Lüfter",
"nav_motion": "Achsen",
"nav_print": "Druck",
"nav_printers": "Drucker",
"nav_settings": "Einstellungen",
"nav_temps": "Temperaturen",
"orca_profile_done": "Importiert",
"orca_profile_dropmsg": "Hierher ziehen oder klicken",
"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/&lt;id&gt;/filament/</i>",
"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/&lt;id&gt;/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_section": "OrcaSlicer-Profile",
"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.",
"orca_profile_uploading": "Lade hoch…",
"orca_profile_user_empty": " keine ",
"orca_profile_user_label": "Eigene Profile",
"panel_ams_title": "Filament",
"panel_browser_title": "Datei-Browser",
"panel_console_title": "Ereignis-Log",
"panel_extras_camera": "Kamera",
"panel_extras_fan": "Lüfter",
"panel_extras_light": "Licht",
"panel_motion_xy": "XY-Achsen",
"panel_motion_z": "Z-Achse",
"panel_print_btn_cancel": "✕ Abbrechen",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Fortsetzen",
"panel_print_temps_live": "Temperaturen (Live)",
"panel_print_title": "Drucksteuerung",
"panel_temps_bed": "Heizbett",
"panel_temps_chart": "Verlauf (letzte 60 Messungen)",
"panel_temps_nozzle": "Düse",
"print_auto_leveling": "Auto-Leveling für diesen Druck",
"printers_active": "● aktiv",
"printers_current": "Aktueller Drucker",
"printers_empty_hint": "Noch kein Drucker eingerichtet.",
"printers_loading": "Lade…",
"printers_none": "Keine Drucker konfiguriert.",
"printers_remove": "Drucker entfernen",
"printers_remove_confirm": "Drucker \"{name}\" entfernen? Die Bridge startet neu.",
"printers_switch": "Wechseln →",
"progress_action_clear": "Leeren",
"progress_action_print": "Drucken",
"progress_action_slots": "Slots zuordnen",
"settings_auto_leveling": "Auto-Leveling vor Druck",
"settings_auto_leveling_label": "Auto-Leveling vor dem Druck",
"settings_btn_tooltip": "Einstellungen",
"settings_camera_on_print": "Kamera bei Druckstart einschalten",
"settings_cat_connection": "Verbindung",
"settings_cat_display": "Darstellung",
"settings_cat_filament": "Filament",
"settings_cat_language": "Sprache",
"settings_cat_printer": "Drucker",
"settings_cat_system": "System",
"settings_cat_theme": "Hell / Dunkel umschalten",
"settings_connection": "Verbindung",
"settings_default_slot": "Standard-Slot (Einfarbdruck)",
"settings_device_id": "Device-ID",
"settings_device_id_hint": "32 Hexzeichen",
"settings_device_id_placeholder": "32 Hexzeichen",
"settings_filament_mapping": "Filament-Profil-Mapping (pro Slot)",
"settings_filament_mapping_hint": "Festes Orca-Profil pro AMS-Slot. Bei der Slicer-Synchronisierung sendet die Bridge dieses Profil statt \"Generic\".",
"settings_filament_mapping_label": "Filament-Profil-Mapping (pro Slot)",
"settings_filament_mapping_save": "Mapping speichern",
"settings_filament_mapping_save_label": "Mapping speichern",
"settings_file_ready_banner": "Druckleiste",
"settings_file_ready_dialog": "Druckdialog",
"settings_file_ready_mode": "Nach Upload: Druckstart-Verhalten",
"settings_language": "Sprache",
"settings_mode_id": "Mode-ID",
"settings_mode_id_placeholder": "20030",
"settings_mqtt_port": "MQTT-Port",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_orca_profiles_import": "Profile importieren",
"settings_orca_profiles_label": "OrcaSlicer-Profile",
"settings_password": "MQTT-Passwort",
"settings_poll": "Poll-Intervall (Sekunden)",
"settings_poll_interval_hint": "Wie oft die Bridge den Druckerstatus abfragt",
"settings_poll_interval_label": "Poll-Intervall (Sekunden)",
"settings_print": "Druckeinstellungen",
"settings_printer_ip": "Drucker-IP",
"settings_printer_name": "Drucker-Name",
"settings_printer_name_placeholder": "z.B. Kobra X Links",
"settings_save": "Speichern & Neustart",
"settings_slot_auto": "Auto (alle belegten Slots)",
"settings_theme_toggle": "Wechsel Hell / Dunkel",
"settings_title": "Einstellungen",
"settings_username": "MQTT-Benutzername",
"settings_vendor_filter_placeholder": "Hersteller suchen…",
"settings_version": "Version",
"settings_visible_vendors": "Sichtbare Hersteller (Profil-Dropdown)",
"settings_visible_vendors_hint": "Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic\" und eigene Profile sind immer sichtbar.",
"settings_visible_vendors_label": "Sichtbare Hersteller (Profil-Dropdown)",
"settings_visible_vendors_save": "Auswahl speichern",
"settings_visible_vendors_save_label": "Auswahl speichern",
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
"sf_all": "Alle",
"sf_err": "✗ Fehler",
"sf_new": "Neu",
"sf_ok": "✓ Erfolgreich",
"skip_already": "übersprungen",
"skip_btn_label": "Objekte",
"skip_cancel": "Abbrechen",
"skip_confirm": "Überspringen",
"skip_confirm_btn": "Überspringen",
"skip_hint": "Objekte abwählen, die nicht weiter gedruckt werden sollen:",
"skip_no_objects": "Keine Objekte in diesem Druck.",
"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",
"skip_title": "Objekte überspringen",
"slot_edit_color": "Farbe",
"slot_edit_custom": "z.B. PLA, PETG, ABS…",
"slot_edit_load": "⬇ Einziehen",
"slot_edit_material": "Material",
"slot_edit_ok": "AMS Slot",
"slot_edit_profile": "OrcaSlicer-Profil",
"slot_edit_profile_default": "— Generic (Default) —",
"slot_edit_profile_hint": "Sendet beim OrcaSlicer-Sync die konkrete Marke statt nur „Generic\"",
"slot_edit_save": "💾 Speichern",
"slot_edit_title": "Slot bearbeiten",
"slot_edit_unload": "⬆ Ausziehen",
"speed_normal": "⚡ Normal",
"speed_silent": "🐢 Leise",
"speed_sport": "🚀 Sport",
"ss_date": "↓ Datum",
"ss_dur": "⏱ Druckzeit",
"ss_name": "AZ Name",
"ss_dur": "⏱ Druckzeit"
"store_delete_confirm": "Datei löschen?",
"store_download": "⬇ Download",
"store_empty": "Noch keine Dateien hochgeladen.",
"store_estimate": "Schätzung",
"store_never": "noch nicht gedruckt",
"store_no_results": "Keine Dateien gefunden.",
"store_print": "▶ Drucken",
"store_print_confirm": "Datei drucken?",
"store_refresh": "↻ Aktualisieren",
"store_search_placeholder": "🔍 Suche…",
"store_upload_busy": "⏳ Hochladen…",
"store_upload_error": "✗ {error}",
"store_upload_label_browse": "durchsuchen",
"store_upload_label_prefix": "GCode hierher ziehen oder ",
"store_upload_only_gcode": "✗ Nur GCode-Dateien erlaubt (.gcode, .3mf, .bgcode)",
"store_upload_success": "✓ {file}",
"store_web_verify_abort": "Abbrechen",
"store_web_verify_confirm": "Bestätigen",
"store_web_verify_msg": "Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde.",
"store_web_verify_title": "Datei verifizieren",
"update_apply": "Jetzt installieren",
"update_applying": "Lade herunter...",
"update_available": "verfügbar",
"update_check": "Auf Updates prüfen",
"update_checking": "Prüfe...",
"update_error": "Fehler",
"update_none": "Bereits aktuell",
"update_restarting": "Starte neu..."
}

View File

@@ -1,248 +1,322 @@
{
"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_auto_refill": "Auto Refill",
"ace_dry_chart": "History (Temp/Humidity)",
"ace_dry_current_temp": "Temperature",
"ace_dry_dialog_cancel": "Cancel",
"ace_dry_dialog_confirm": "Confirm",
"ace_dry_dialog_custom_name": "Custom Name",
"ace_dry_dialog_reset_default": "Reset to Default",
"ace_dry_dialog_save_restart": "Save & Restart",
"ace_dry_dialog_temp": "Temperature (30-80°C)",
"ace_dry_dialog_time": "Rem. Time (h:m:s)",
"ace_dry_dialog_title": "Dryer Temp/Time Settings",
"ace_dry_dryer": "Dryer",
"ace_dry_duration": "Duration (min)",
"ace_dry_enable": "Enable Drying",
"ace_dry_humidity": "Humidity",
"ace_dry_preset_abs_asa": "ABS / ASA",
"ace_dry_preset_custom": "Custom",
"ace_dry_preset_pa_pc": "PA / PC",
"ace_dry_preset_petg": "PETG",
"ace_dry_preset_pla": "PLA",
"ace_dry_preset_pla_plus": "PLA+",
"ace_dry_preset_tpu": "TPU",
"ace_dry_start": "▶ Start",
"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": "Temperature (°C)",
"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",
"add_printer": "Add printer",
"ams_empty": "Empty",
"ams_no_data": "No AMS data received",
"apd_cancel": "Cancel",
"apd_confirm": "Add",
"apd_err_ip": "Please enter an IP address",
"apd_fetching": "Fetching data from printer…",
"apd_lbl_ip": "Printer IP",
"apd_lbl_name": "Name (optional)",
"apd_placeholder_name": "e.g. Kobra X Living Room",
"apd_success": "Printer added, bridge restarting…",
"apd_title": "Add printer",
"btn_cam_start": "▶ Camera",
"btn_cam_start2": "▶ Start",
"btn_cam_stop": "◼ Camera",
"btn_cam_stop2": "◼ Stop",
"btn_cancel": "✕ Stop",
"btn_cancel_generic": "Cancel",
"btn_confirm_generic": "Confirm",
"btn_connect": "⚡ Connect",
"btn_delete": "Delete",
"btn_disable_motors": "Motors Off",
"btn_disconnect": "✕ Disconnect",
"btn_home_all": "Home All",
"btn_home_xy": "Home XY",
"btn_home_z": "Home Z",
"btn_pause": "⏸ Pause",
"btn_resume": "▶ Resume",
"btn_cancel": "✕ Stop",
"label_nozzle": "Nozzle",
"cam_placeholder": "📷 Camera not started",
"cam_stream_unavailable": "Stream unavailable",
"card_ace_dry": "ACE Drying",
"card_ams": "Filament",
"card_cam": "Camera",
"card_light_fan": "Fan",
"card_progress": "Progress",
"card_speed": "Print Speed",
"card_temps": "Temperatures",
"confirm_cancel": "Really cancel the print?",
"fd_cancel": "Cancel",
"fd_no_matching_material": "No matching material",
"fd_no_slots_msg": "No loaded AMS slots.{br}Start print anyway?",
"fd_objects_hint": "Skip objects (optional):",
"fd_objects_toggle": "Skip objects",
"fd_options_title": "Print Options",
"fd_print": "▶ Print",
"fd_slot": "Slot",
"fd_slots_hint": "Assign GCode channel to AMS slot:",
"fd_title": "Slot Assignment",
"fd_used": "USED",
"file_cancel_btn": "✕ Cancel",
"file_ready_btn": "▶ Start Print",
"file_slots_btn": "🎨 Select Slots",
"header_status_complete": "Complete",
"header_status_error": "Error",
"header_status_printing": "Printing",
"header_status_standby": "Ready",
"hint_ip_no_port": "IP address only, no port (e.g. 192.168.1.102)",
"kobra_auto_leveling": "Auto Leveling",
"kobra_busy": "Busy",
"kobra_canceled": "Cancelled",
"kobra_checking": "Checking",
"kobra_failed": "Error",
"kobra_finished": "Finished",
"kobra_free": "Ready",
"kobra_init": "Initializing",
"kobra_offline": "Offline",
"kobra_paused": "Paused",
"kobra_pausing": "Pausing...",
"kobra_preheating": "Preheating",
"kobra_printing": "Printing",
"kobra_resumed": "Resumed",
"kobra_resuming": "Resuming...",
"kobra_stoped": "Stopped",
"kobra_stopping": "Stopping...",
"kobra_updated": "Updating",
"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_nozzle": "Nozzle",
"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_on_off": "On / Off",
"label_set": "Set",
"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 →",
"label_speed": "Speed",
"label_step": "Step size:",
"label_target_c": "Target:",
"lbl_conn_error": "Connection error:",
"lbl_elapsed": "Elapsed:",
"lbl_feed": "Load",
"lbl_layers": "Layer",
"lbl_light": "💡 Light",
"lbl_remaining": "Remaining:",
"lbl_slicer_time": "Slicer estimate:",
"lbl_unload": "Unload",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
"log_axis": "Axis",
"log_home": "Home",
"log_home_all": "Home All",
"log_bed": "Bed →",
"log_cam_start": "Camera started:",
"log_cam_stop": "Camera stopped",
"log_poll_error": "Poll error:",
"log_clear": "✕ Clear",
"log_delete_failed": "Delete failed",
"log_dir_all": "All",
"log_dir_label": "Dir:",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_download": "⬇ Download",
"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",
"log_fan": "Fan →",
"log_filter_placeholder": "Filter…",
"log_home": "Home",
"log_home_all": "Home All",
"log_light_off": "Light off",
"log_light_on": "Light on",
"log_lvl_err": "⛔ Errors",
"log_lvl_label": "Level:",
"log_lvl_warn": "⚠ Warn",
"log_nozzle": "Nozzle →",
"log_poll_error": "Poll error:",
"log_print_action": "Print:",
"log_print_start": "Print start:",
"log_topic_ams": "AMS",
"log_topic_info": "Info",
"log_topic_label": "Topic:",
"log_topic_print": "Print",
"log_topic_status": "Status",
"nav_ams": "AMS",
"nav_browser": "Browser",
"nav_console": "Console",
"nav_dashboard": "Dashboard",
"nav_extras": "Light / Fan",
"nav_motion": "Motion",
"nav_print": "Print",
"nav_printers": "Printers",
"nav_settings": "Settings",
"nav_temps": "Temperatures",
"orca_profile_done": "Imported",
"orca_profile_dropmsg": "Drop here or click",
"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/&lt;id&gt;/filament/</i>",
"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/&lt;id&gt;/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_section": "OrcaSlicer Profiles",
"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.",
"orca_profile_uploading": "Uploading…",
"orca_profile_user_empty": " none ",
"orca_profile_user_label": "Own profiles",
"panel_ams_title": "Filament",
"panel_browser_title": "File Browser",
"panel_console_title": "Event Log",
"panel_extras_camera": "Camera",
"panel_extras_fan": "Fan",
"panel_extras_light": "Light",
"panel_motion_xy": "XY Axes",
"panel_motion_z": "Z Axis",
"panel_print_btn_cancel": "✕ Cancel",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Resume",
"panel_print_temps_live": "Temperatures (Live)",
"panel_print_title": "Print Control",
"panel_temps_bed": "Heated Bed",
"panel_temps_chart": "History (last 60 readings)",
"panel_temps_nozzle": "Nozzle",
"print_auto_leveling": "Auto-Leveling",
"printers_active": "● active",
"printers_current": "Current printer",
"printers_empty_hint": "No printer set up yet.",
"printers_loading": "Loading…",
"printers_none": "No printers configured.",
"printers_remove": "Remove printer",
"printers_remove_confirm": "Remove printer \"{name}\"? The bridge will restart.",
"printers_switch": "Switch →",
"progress_action_clear": "Clear",
"progress_action_print": "Print",
"progress_action_slots": "Map slots",
"settings_auto_leveling": "Auto-Leveling Default",
"settings_auto_leveling_label": "Auto-Leveling before print",
"settings_btn_tooltip": "Settings",
"settings_camera_on_print": "Turn camera on at print start",
"settings_cat_connection": "Connection",
"settings_cat_display": "Appearance",
"settings_cat_filament": "Filament",
"settings_cat_language": "Language",
"settings_cat_printer": "Printer",
"settings_cat_system": "System",
"settings_cat_theme": "Toggle light / dark",
"settings_connection": "Connection",
"settings_default_slot": "Default Slot (single color)",
"settings_device_id": "Device ID",
"settings_device_id_hint": "32 hex characters",
"settings_device_id_placeholder": "32 hex characters",
"settings_filament_mapping": "Filament profile mapping (per slot)",
"settings_filament_mapping_hint": "Fixed Orca profile per AMS slot. On slicer sync, the bridge sends this profile instead of \"Generic\".",
"settings_filament_mapping_label": "Filament profile mapping (per slot)",
"settings_filament_mapping_save": "Save mapping",
"settings_filament_mapping_save_label": "Save mapping",
"settings_file_ready_banner": "Print bar",
"settings_file_ready_dialog": "Print dialog",
"settings_file_ready_mode": "After upload: Start print behavior",
"settings_language": "Language",
"settings_mode_id": "Mode ID",
"settings_mode_id_placeholder": "20030",
"settings_mqtt_port": "MQTT Port",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_orca_profiles_import": "Import profiles",
"settings_orca_profiles_label": "OrcaSlicer Profiles",
"settings_password": "MQTT Password",
"settings_poll": "Poll Interval (seconds)",
"settings_poll_interval_hint": "How often the bridge queries printer status",
"settings_poll_interval_label": "Poll Interval (seconds)",
"settings_print": "Print Settings",
"settings_printer_ip": "Printer IP",
"settings_printer_name": "Printer Name",
"settings_printer_name_placeholder": "e.g. Kobra X Left",
"settings_save": "Save & Restart",
"settings_slot_auto": "Auto (all loaded slots)",
"settings_theme_toggle": "Toggle light / dark",
"settings_title": "Settings",
"settings_username": "MQTT Username",
"settings_vendor_filter_placeholder": "Search vendors…",
"settings_version": "Version",
"settings_visible_vendors": "Visible vendors (profile dropdown)",
"settings_visible_vendors_hint": "Only these vendors appear in the slot profile dropdown. Nothing selected = show all. \"Generic\" and your own profiles are always visible.",
"settings_visible_vendors_label": "Visible vendors (profile dropdown)",
"settings_visible_vendors_save": "Save selection",
"settings_visible_vendors_save_label": "Save selection",
"settings_web_upload_warning": "Show warning when printing web uploads",
"sf_all": "All",
"sf_err": "✗ Failed",
"sf_new": "New",
"sf_ok": "✓ Completed",
"skip_already": "skipped",
"skip_btn_label": "Objects",
"skip_cancel": "Cancel",
"skip_confirm": "Skip",
"skip_confirm_btn": "Skip",
"skip_hint": "Uncheck objects you no longer want to print:",
"skip_no_objects": "No objects in this print.",
"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",
"skip_title": "Skip objects",
"slot_edit_color": "Color",
"slot_edit_custom": "e.g. PLA, PETG, ABS…",
"slot_edit_load": "⬇ Load",
"slot_edit_material": "Material",
"slot_edit_ok": "AMS Slot",
"slot_edit_profile": "OrcaSlicer profile",
"slot_edit_profile_default": "— Generic (default) —",
"slot_edit_profile_hint": "Sent on OrcaSlicer sync as the specific brand instead of just \"Generic\"",
"slot_edit_save": "💾 Save",
"slot_edit_title": "Edit Slot",
"slot_edit_unload": "⬆ Unload",
"speed_normal": "⚡ Normal",
"speed_silent": "🐢 Silent",
"speed_sport": "🚀 Sport",
"ss_date": "↓ Date",
"ss_dur": "⏱ Print time",
"ss_name": "AZ Name",
"ss_dur": "⏱ Print time"
"store_delete_confirm": "Delete file?",
"store_download": "⬇ Download",
"store_empty": "No files uploaded yet.",
"store_estimate": "Estimate",
"store_never": "never printed",
"store_no_results": "No files found.",
"store_print": "▶ Print",
"store_print_confirm": "Print file?",
"store_refresh": "↻ Refresh",
"store_search_placeholder": "🔍 Search…",
"store_upload_busy": "⏳ Uploading…",
"store_upload_error": "✗ {error}",
"store_upload_label_browse": "browse",
"store_upload_label_prefix": "Drag GCode here or ",
"store_upload_only_gcode": "✗ Only GCode files allowed (.gcode, .3mf, .bgcode)",
"store_upload_success": "✓ {file}",
"store_web_verify_abort": "Cancel",
"store_web_verify_confirm": "Confirm",
"store_web_verify_msg": "Please verify this file was made for Anycubic Kobra X.",
"store_web_verify_title": "Verify file",
"update_apply": "Install Now",
"update_applying": "Downloading...",
"update_available": "available",
"update_check": "Check for Updates",
"update_checking": "Checking...",
"update_error": "Error",
"update_none": "Already up to date",
"update_restarting": "Restarting..."
}

View File

@@ -1,248 +1,322 @@
{
"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_auto_refill": "Relleno automático",
"ace_dry_chart": "Historial (Temp/Humedad)",
"ace_dry_current_temp": "Temperatura",
"ace_dry_dialog_cancel": "Cancelar",
"ace_dry_dialog_confirm": "Confirmar",
"ace_dry_dialog_custom_name": "Nombre personalizado",
"ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
"ace_dry_dialog_save_restart": "Guardar y reiniciar",
"ace_dry_dialog_temp": "Temperatura (30-80°C)",
"ace_dry_dialog_time": "Tiempo restante (h:m:s)",
"ace_dry_dialog_title": "Ajustes de temp/tiempo del secador",
"ace_dry_dryer": "Secador",
"ace_dry_duration": "Duración (min)",
"ace_dry_enable": "Activar secado",
"ace_dry_humidity": "Humedad",
"ace_dry_preset_abs_asa": "ABS / ASA",
"ace_dry_preset_custom": "Personalizado",
"ace_dry_preset_pa_pc": "PA / PC",
"ace_dry_preset_petg": "PETG",
"ace_dry_preset_pla": "PLA",
"ace_dry_preset_pla_plus": "PLA+",
"ace_dry_preset_tpu": "TPU",
"ace_dry_start": "▶ Iniciar",
"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": "Temperatura (°C)",
"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",
"add_printer": "Añadir impresora",
"ams_empty": "Vacío",
"ams_no_data": "No se recibieron datos de AMS",
"apd_cancel": "Cancelar",
"apd_confirm": "Agregar",
"apd_err_ip": "Introduce una dirección IP",
"apd_fetching": "Obteniendo datos de la impresora…",
"apd_lbl_ip": "IP de impresora",
"apd_lbl_name": "Nombre (opcional)",
"apd_placeholder_name": "p. ej. Kobra X Sala",
"apd_success": "Impresora añadida, reiniciando bridge…",
"apd_title": "Agregar impresora",
"btn_cam_start": "▶ Cámara",
"btn_cam_start2": "▶ Iniciar",
"btn_cam_stop": "◼ Cámara",
"btn_cam_stop2": "◼ Detener",
"btn_cancel": "✕ Detener",
"btn_cancel_generic": "Cancelar",
"btn_confirm_generic": "Confirmar",
"btn_connect": "⚡ Conectar",
"btn_delete": "Eliminar",
"btn_disable_motors": "Motores apagados",
"btn_disconnect": "✕ Desconectar",
"btn_home_all": "Home All",
"btn_home_xy": "Home XY",
"btn_home_z": "Home Z",
"btn_pause": "⏸ Pausa",
"btn_resume": "▶ Reanudar",
"btn_cancel": "✕ Detener",
"label_nozzle": "Boquilla",
"cam_placeholder": "📷 Cámara no iniciada",
"cam_stream_unavailable": "Stream no disponible",
"card_ace_dry": "Secado ACE",
"card_ams": "Filamento",
"card_cam": "Cámara",
"card_light_fan": "Ventilador",
"card_progress": "Progreso",
"card_speed": "Velocidad de impresión",
"card_temps": "Temperaturas",
"confirm_cancel": "¿Realmente cancelar la impresión?",
"fd_cancel": "Cancelar",
"fd_no_matching_material": "No hay material compatible",
"fd_no_slots_msg": "No hay slots AMS cargados.{br}¿Iniciar impresión de todos modos?",
"fd_objects_hint": "Omitir objetos (opcional):",
"fd_objects_toggle": "Omitir objetos",
"fd_options_title": "Opciones",
"fd_print": "▶ Imprimir",
"fd_slot": "Ranura",
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
"fd_title": "Asignación de ranura",
"fd_used": "USADO",
"file_cancel_btn": "✕ Cancelar",
"file_ready_btn": "▶ Iniciar impresión",
"file_slots_btn": "🎨 Seleccionar ranuras",
"header_status_complete": "Completado",
"header_status_error": "Error",
"header_status_printing": "Imprimiendo",
"header_status_standby": "Listo",
"hint_ip_no_port": "Solo dirección IP, sin puerto (p. ej. 192.168.1.102)",
"kobra_auto_leveling": "Autonivelado",
"kobra_busy": "Ocupado",
"kobra_canceled": "Cancelado",
"kobra_checking": "Comprobando",
"kobra_failed": "Error",
"kobra_finished": "Finalizado",
"kobra_free": "Listo",
"kobra_init": "Inicializando",
"kobra_offline": "Desconectada",
"kobra_paused": "Pausado",
"kobra_pausing": "Pausando...",
"kobra_preheating": "Precalentando",
"kobra_printing": "Imprimiendo",
"kobra_resumed": "Reanudado",
"kobra_resuming": "Reanudando...",
"kobra_stoped": "Detenido",
"kobra_stopping": "Deteniendo...",
"kobra_updated": "Actualizando",
"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_nozzle": "Boquilla",
"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_on_off": "Encendido / Apagado",
"label_set": "Set",
"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 →",
"label_speed": "Velocidad",
"label_step": "Tamaño del paso:",
"label_target_c": "Objetivo:",
"lbl_conn_error": "Error de conexión:",
"lbl_elapsed": "Transcurrido:",
"lbl_feed": "Cargar",
"lbl_layers": "Capa",
"lbl_light": "💡 Luz",
"lbl_remaining": "Restante:",
"lbl_slicer_time": "Estimación del slicer:",
"lbl_unload": "Descargar",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
"log_axis": "Eje",
"log_home": "Home",
"log_home_all": "Home All",
"log_bed": "Cama →",
"log_cam_start": "Cámara iniciada:",
"log_cam_stop": "Cámara detenida",
"log_poll_error": "Error de sondeo:",
"log_clear": "✕ Limpiar",
"log_delete_failed": "Error al eliminar",
"log_dir_all": "Todos",
"log_dir_label": "Dirección:",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_download": "⬇ Descargar",
"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",
"log_fan": "Ventilador →",
"log_filter_placeholder": "Filtrar…",
"log_home": "Home",
"log_home_all": "Home All",
"log_light_off": "Luz apagada",
"log_light_on": "Luz encendida",
"log_lvl_err": "⛔ Errores",
"log_lvl_label": "Nivel:",
"log_lvl_warn": "⚠ Avisos",
"log_nozzle": "Boquilla →",
"log_poll_error": "Error de sondeo:",
"log_print_action": "Impresión:",
"log_print_start": "Inicio de impresión:",
"log_topic_ams": "AMS",
"log_topic_info": "Info",
"log_topic_label": "Tema:",
"log_topic_print": "Impresión",
"log_topic_status": "Estado",
"nav_ams": "AMS",
"nav_browser": "Explorador",
"nav_console": "Consola",
"nav_dashboard": "Panel",
"nav_extras": "Luz / Ventilador",
"nav_motion": "Movimiento",
"nav_print": "Impresión",
"nav_printers": "Impresoras",
"nav_settings": "Ajustes",
"nav_temps": "Temperaturas",
"orca_profile_done": "Importado",
"orca_profile_dropmsg": "Suelta aquí o haz clic",
"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/&lt;id&gt;/filament/</i>",
"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/&lt;id&gt;/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_section": "Perfiles de OrcaSlicer",
"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.",
"orca_profile_uploading": "Subiendo",
"orca_profile_user_empty": " ninguno ",
"orca_profile_user_label": "Perfiles propios",
"panel_ams_title": "Filamento",
"panel_browser_title": "Explorador de archivos",
"panel_console_title": "Registro de eventos",
"panel_extras_camera": "Cámara",
"panel_extras_fan": "Ventilador",
"panel_extras_light": "Luz",
"panel_motion_xy": "Ejes XY",
"panel_motion_z": "Eje Z",
"panel_print_btn_cancel": "✕ Cancelar",
"panel_print_btn_pause": "⏸ Pausa",
"panel_print_btn_resume": "▶ Reanudar",
"panel_print_temps_live": "Temperaturas (en vivo)",
"panel_print_title": "Control de impresión",
"panel_temps_bed": "Cama caliente",
"panel_temps_chart": "Historial (últimas 60 lecturas)",
"panel_temps_nozzle": "Boquilla",
"print_auto_leveling": "Autonivelado para esta impresión",
"printers_active": "● activa",
"printers_current": "Impresora actual",
"printers_empty_hint": "Aún no hay impresora configurada.",
"printers_loading": "Cargando…",
"printers_none": "No hay impresoras configuradas.",
"printers_remove": "Eliminar impresora",
"printers_remove_confirm": "¿Eliminar impresora \"{name}\"? El bridge se reiniciará.",
"printers_switch": "Cambiar →",
"progress_action_clear": "Vaciar",
"progress_action_print": "Imprimir",
"progress_action_slots": "Asignar ranuras",
"settings_auto_leveling": "Autonivelado antes de imprimir",
"settings_auto_leveling_label": "Autonivelado antes de imprimir",
"settings_btn_tooltip": "Ajustes",
"settings_camera_on_print": "Encender cámara al iniciar impresión",
"settings_cat_connection": "Conexión",
"settings_cat_display": "Apariencia",
"settings_cat_filament": "Filamento",
"settings_cat_language": "Idioma",
"settings_cat_printer": "Impresora",
"settings_cat_system": "Sistema",
"settings_cat_theme": "Alternar claro / oscuro",
"settings_connection": "Conexión",
"settings_default_slot": "Ranura predeterminada (un color)",
"settings_device_id": "ID del dispositivo",
"settings_device_id_hint": "32 caracteres hexadecimales",
"settings_device_id_placeholder": "32 caracteres hexadecimales",
"settings_filament_mapping": "Asignación de perfil de filamento (por ranura)",
"settings_filament_mapping_hint": "Perfil Orca fijo por ranura AMS. Al sincronizar con el slicer, el bridge envía este perfil en lugar de \"Generic\".",
"settings_filament_mapping_label": "Asignación de perfil de filamento (por ranura)",
"settings_filament_mapping_save": "Guardar asignación",
"settings_filament_mapping_save_label": "Guardar asignación",
"settings_file_ready_banner": "Barra de impresión",
"settings_file_ready_dialog": "Diálogo de impresión",
"settings_file_ready_mode": "Después de carga: Comportamiento de inicio de impresión",
"settings_language": "Idioma",
"settings_mode_id": "ID de modo",
"settings_mode_id_placeholder": "20030",
"settings_mqtt_port": "MQTT Port",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_orca_profiles_import": "Importar perfiles",
"settings_orca_profiles_label": "Perfiles de OrcaSlicer",
"settings_password": "Contraseña MQTT",
"settings_poll": "Intervalo de sondeo (segundos)",
"settings_poll_interval_hint": "Con qué frecuencia el bridge consulta el estado de la impresora",
"settings_poll_interval_label": "Intervalo de sondeo (segundos)",
"settings_print": "Ajustes de impresión",
"settings_printer_ip": "IP de impresora",
"settings_printer_name": "Nombre de impresora",
"settings_printer_name_placeholder": "p. ej. Kobra X Sala",
"settings_save": "Guardar y reiniciar",
"settings_slot_auto": "Auto (todos los slots cargados)",
"settings_theme_toggle": "Alternar claro / oscuro",
"settings_title": "Configuración",
"settings_username": "Usuario MQTT",
"settings_vendor_filter_placeholder": "Buscar fabricantes…",
"settings_version": "Versión",
"settings_visible_vendors": "Fabricantes visibles (lista de perfiles)",
"settings_visible_vendors_hint": "Solo estos fabricantes aparecen en la lista de perfiles de ranura. Nada seleccionado = mostrar todos. «Generic» y tus propios perfiles siempre son visibles.",
"settings_visible_vendors_label": "Fabricantes visibles (lista de perfiles)",
"settings_visible_vendors_save": "Guardar selección",
"settings_visible_vendors_save_label": "Guardar selección",
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
"sf_all": "Todos",
"sf_err": "✗ Fallido",
"sf_new": "Nuevo",
"sf_ok": "✓ Completado",
"skip_already": "omitido",
"skip_btn_label": "Objetos",
"skip_cancel": "Cancelar",
"skip_confirm": "Omitir",
"skip_confirm_btn": "Omitir",
"skip_hint": "Deselecciona los objetos que ya no quieras imprimir:",
"skip_no_objects": "No hay objetos en esta impresión.",
"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",
"skip_title": "Omitir objetos",
"slot_edit_color": "Color",
"slot_edit_custom": "p. ej. PLA, PETG, ABS…",
"slot_edit_load": "⬇ Cargar",
"slot_edit_material": "Material",
"slot_edit_ok": "Ranura AMS",
"slot_edit_profile": "Perfil de OrcaSlicer",
"slot_edit_profile_default": "— Genérico (Predeterminado) —",
"slot_edit_profile_hint": "Envía al sincronizar con OrcaSlicer la marca concreta en lugar de solo \"Generic\"",
"slot_edit_save": "💾 Guardar",
"slot_edit_title": "Editar slot",
"slot_edit_unload": "⬆ Descargar",
"speed_normal": "⚡ Normal",
"speed_silent": "🐢 Silencioso",
"speed_sport": "🚀 Sport",
"ss_date": "↓ Fecha",
"ss_dur": "⏱ Tiempo de impresión",
"ss_name": "AZ Nombre",
"ss_dur": "⏱ Tiempo de impresión"
"store_delete_confirm": "¿Eliminar archivo?",
"store_download": "⬇ Descargar",
"store_empty": "Aún no hay archivos subidos.",
"store_estimate": "Estimación",
"store_never": "nunca impreso",
"store_no_results": "No se encontraron archivos.",
"store_print": "▶ Imprimir",
"store_print_confirm": "¿Imprimir archivo?",
"store_refresh": "↻ Actualizar",
"store_search_placeholder": "🔍 Buscar…",
"store_upload_busy": "⏳ Subiendo…",
"store_upload_error": "✗ {error}",
"store_upload_label_browse": "buscar",
"store_upload_label_prefix": "Arrastra el GCode aquí o ",
"store_upload_only_gcode": "✗ Solo se permiten archivos GCode (.gcode, .3mf, .bgcode)",
"store_upload_success": "✓ {file}",
"store_web_verify_abort": "Cancelar",
"store_web_verify_confirm": "Confirmar",
"store_web_verify_msg": "Verifica que este archivo fue hecho para Anycubic Kobra X.",
"store_web_verify_title": "Verificar archivo",
"update_apply": "Instalar ahora",
"update_applying": "Descargando...",
"update_available": "disponible",
"update_check": "Buscar actualizaciones",
"update_checking": "Comprobando...",
"update_error": "Error",
"update_none": "Ya actualizado",
"update_restarting": "Reiniciando..."
}

View File

@@ -1,249 +1,322 @@
{
"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_auto_refill": "Remplissage auto",
"ace_dry_chart": "Historique (Temp/Humidité)",
"ace_dry_current_temp": "Température",
"ace_dry_dialog_cancel": "Annuler",
"ace_dry_dialog_confirm": "Confirmer",
"ace_dry_dialog_custom_name": "Nom personnalisé",
"ace_dry_dialog_reset_default": "Réinitialiser",
"ace_dry_dialog_save_restart": "Enregistrer et redémarrer",
"ace_dry_dialog_temp": "Température (30-80°C)",
"ace_dry_dialog_time": "Temps restant (h:m:s)",
"ace_dry_dialog_title": "Réglages Temp/Durée du séchoir",
"ace_dry_dryer": "Séchoir",
"ace_dry_duration": "Durée (min)",
"ace_dry_enable": "Activer le séchage",
"ace_dry_humidity": "Humidité",
"ace_dry_preset_abs_asa": "ABS / ASA",
"ace_dry_preset_custom": "Personnalisé",
"ace_dry_preset_pa_pc": "PA / PC",
"ace_dry_preset_petg": "PETG",
"ace_dry_preset_pla": "PLA",
"ace_dry_preset_pla_plus": "PLA+",
"ace_dry_preset_tpu": "TPU",
"ace_dry_start": "▶ Démarrer",
"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": "Température (°C)",
"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",
"add_printer": "Ajouter une imprimante",
"ams_empty": "Vide",
"ams_no_data": "Aucune donnée AMS reçue",
"apd_cancel": "Annuler",
"apd_confirm": "Ajouter",
"apd_err_ip": "Veuillez saisir une adresse IP",
"apd_fetching": "Récupération des données de l'imprimante…",
"apd_lbl_ip": "IP de l'imprimante",
"apd_lbl_name": "Nom (optionnel)",
"apd_placeholder_name": "ex. Kobra X Salon",
"apd_success": "Imprimante ajoutée, redémarrage du bridge…",
"apd_title": "Ajouter une imprimante",
"btn_cam_start": "▶ Caméra",
"btn_cam_start2": "▶ Démarrer",
"btn_cam_stop": "◼ Caméra",
"btn_cam_stop2": "◼ Arrêter",
"btn_cancel": "✕ Arrêter",
"btn_cancel_generic": "Annuler",
"btn_confirm_generic": "Confirmer",
"btn_connect": "⚡ Connecter",
"btn_delete": "Supprimer",
"btn_disable_motors": "Moteurs Off",
"btn_disconnect": "✕ Déconnecter",
"btn_home_all": "Origine Tout",
"btn_home_xy": "Origine XY",
"btn_home_z": "Origine Z",
"btn_pause": "⏸ Pause",
"btn_resume": "▶ Reprendre",
"btn_cancel": "✕ Arrêter",
"label_nozzle": "Buse",
"cam_placeholder": "📷 Caméra non démarrée",
"cam_stream_unavailable": "Flux indisponible",
"card_ace_dry": "Séchage ACE",
"card_ams": "Filament",
"card_cam": "Caméra",
"card_light_fan": "Ventilateur",
"card_progress": "Progression",
"card_speed": "Vitesse d'impression",
"card_temps": "Températures",
"confirm_cancel": "Vraiment annuler l'impression ?",
"fd_cancel": "Annuler",
"fd_no_matching_material": "Aucun matériau correspondant",
"fd_no_slots_msg": "Aucun slot AMS chargé.{br}Lancer l'impression quand même ?",
"fd_objects_hint": "Ignorer des objets (optionnel) :",
"fd_objects_toggle": "Ignorer des objets",
"fd_options_title": "Options",
"fd_print": "▶ Imprimer",
"fd_slot": "Slot",
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
"fd_title": "Attribution de fente",
"fd_used": "UTILISÉ",
"file_cancel_btn": "✕ Annuler",
"file_ready_btn": "▶ Lancer l'impression",
"file_slots_btn": "🎨 Choisir les slots",
"header_status_complete": "Terminé",
"header_status_error": "Erreur",
"header_status_printing": "Impression",
"header_status_standby": "Prêt",
"hint_ip_no_port": "Adresse IP uniquement, sans port (ex. 192.168.1.102)",
"kobra_auto_leveling": "Mise à niveau auto",
"kobra_busy": "Occupé",
"kobra_canceled": "Annulé",
"kobra_checking": "Vérification",
"kobra_failed": "Erreur",
"kobra_finished": "Terminé",
"kobra_free": "Disponible",
"kobra_init": "Initialisation",
"kobra_offline": "Hors ligne",
"kobra_paused": "En pause",
"kobra_pausing": "Pause en cours…",
"kobra_preheating": "Préchauffage",
"kobra_printing": "Impression",
"kobra_resumed": "Repris",
"kobra_resuming": "Reprise en cours…",
"kobra_stoped": "Arrêté",
"kobra_stopping": "Arrêt en cours…",
"kobra_updated": "Mise à jour",
"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_nozzle": "Buse",
"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_on_off": "On / Off",
"label_set": "Définir",
"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 →",
"label_speed": "Vitesse",
"label_step": "Pas :",
"label_target_c": "Cible :",
"lbl_conn_error": "Erreur de connexion :",
"lbl_elapsed": "Écoulé :",
"lbl_feed": "Charger",
"lbl_layers": "Couche",
"lbl_light": "💡 Lumière",
"lbl_remaining": "Restant :",
"lbl_slicer_time": "Estimation slicer :",
"lbl_unload": "Décharger",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
"log_axis": "Axe",
"log_home": "Origine",
"log_home_all": "Origine Tout",
"log_bed": "Plateau →",
"log_cam_start": "Caméra démarrée :",
"log_cam_stop": "Caméra arrêtée",
"log_poll_error": "Erreur de sondage :",
"log_clear": "✕ Effacer",
"log_delete_failed": "Échec de la suppression",
"log_dir_all": "Tout",
"log_dir_label": "Sens :",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_download": "⬇ Télécharger",
"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",
"log_fan": "Ventilateur →",
"log_filter_placeholder": "Filtrer…",
"log_home": "Origine",
"log_home_all": "Origine Tout",
"log_light_off": "Lumière éteinte",
"log_light_on": "Lumière allumée",
"log_lvl_err": "⛔ Erreurs",
"log_lvl_label": "Niveau :",
"log_lvl_warn": "⚠ Avert.",
"log_nozzle": "Buse →",
"log_poll_error": "Erreur de sondage :",
"log_print_action": "Impression :",
"log_print_start": "Début de l'impression :",
"log_topic_ams": "AMS",
"log_topic_info": "Info",
"log_topic_label": "Sujet :",
"log_topic_print": "Impression",
"log_topic_status": "Statut",
"nav_ams": "AMS",
"nav_browser": "Navigateur",
"nav_console": "Console",
"nav_dashboard": "Tableau de bord",
"nav_extras": "Lumière / Ventilateur",
"nav_motion": "Mouvement",
"nav_print": "Impression",
"nav_printers": "Imprimantes",
"nav_settings": "Paramètres",
"nav_temps": "Températures",
"orca_profile_done": "Importé",
"orca_profile_dropmsg": "Déposez ici ou cliquez",
"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/&lt;id&gt;/filament/</i>",
"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/&lt;id&gt;/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_section": "Profils OrcaSlicer",
"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.",
"orca_profile_uploading": "Envoi en cours…",
"orca_profile_user_empty": " aucun ",
"orca_profile_user_label": "Mes profils",
"panel_ams_title": "Filament",
"panel_browser_title": "Explorateur de fichiers",
"panel_console_title": "Journal d'événements",
"panel_extras_camera": "Caméra",
"panel_extras_fan": "Ventilateur",
"panel_extras_light": "Lumière",
"panel_motion_xy": "Axes XY",
"panel_motion_z": "Axe Z",
"panel_print_btn_cancel": "✕ Annuler",
"panel_print_btn_pause": "⏸ Pause",
"panel_print_btn_resume": "▶ Reprendre",
"panel_print_temps_live": "Températures (en direct)",
"panel_print_title": "Contrôle impression",
"panel_temps_bed": "Plateau chauffant",
"panel_temps_chart": "Historique (60 dernières valeurs)",
"panel_temps_nozzle": "Buse",
"print_auto_leveling": "Mise à niveau auto pour cette impression",
"printers_active": "● actif",
"printers_current": "Imprimante actuelle",
"printers_empty_hint": "Aucune imprimante configurée.",
"printers_loading": "Chargement…",
"printers_none": "Aucune imprimante configurée.",
"printers_remove": "Supprimer l'imprimante",
"printers_remove_confirm": "Supprimer l'imprimante \"{name}\" ? Le bridge va redémarrer.",
"printers_switch": "Changer →",
"progress_action_clear": "Vider",
"progress_action_print": "Imprimer",
"progress_action_slots": "Affecter les emplacements",
"settings_auto_leveling": "Mise à niveau auto avant impression",
"settings_auto_leveling_label": "Mise à niveau auto avant impression",
"settings_btn_tooltip": "Paramètres",
"settings_camera_on_print": "Activer la caméra au démarrage de l'impression",
"settings_cat_connection": "Connexion",
"settings_cat_display": "Apparence",
"settings_cat_filament": "Filament",
"settings_cat_language": "Langue",
"settings_cat_printer": "Imprimante",
"settings_cat_system": "Système",
"settings_cat_theme": "Basculer clair / sombre",
"settings_connection": "Connexion",
"settings_default_slot": "Slot par défaut (couleur unique)",
"settings_device_id": "ID de l'appareil",
"settings_device_id_hint": "32 caractères hexadécimaux",
"settings_device_id_placeholder": "32 caractères hexadécimaux",
"settings_filament_mapping": "Mappage du profil de filament (par emplacement)",
"settings_filament_mapping_hint": "Profil Orca fixe par emplacement AMS. Lors de la synchronisation du slicer, le bridge envoie ce profil au lieu de « Generic ».",
"settings_filament_mapping_label": "Mappage du profil de filament (par emplacement)",
"settings_filament_mapping_save": "Enregistrer le mappage",
"settings_filament_mapping_save_label": "Enregistrer le mappage",
"settings_file_ready_banner": "Barre d'impression",
"settings_file_ready_dialog": "Dialogue d'impression",
"settings_file_ready_mode": "Après téléchargement : Comportement de démarrage d'impression",
"settings_language": "Langue",
"settings_mode_id": "ID du mode",
"settings_mode_id_placeholder": "20030",
"settings_mqtt_port": "Port MQTT",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_orca_profiles_import": "Importer des profils",
"settings_orca_profiles_label": "Profils OrcaSlicer",
"settings_password": "Mot de passe MQTT",
"settings_poll": "Intervalle de sondage (secondes)",
"settings_poll_interval_hint": "Fréquence à laquelle le bridge interroge l'état de l'imprimante",
"settings_poll_interval_label": "Intervalle de sondage (secondes)",
"settings_print": "Paramètres d'impression",
"settings_printer_ip": "IP de l'imprimante",
"settings_printer_name": "Nom de l'imprimante",
"settings_printer_name_placeholder": "p. ex. Kobra X Salon",
"settings_save": "Enregistrer et redémarrer",
"settings_slot_auto": "Auto (tous les slots chargés)",
"settings_theme_toggle": "Basculer clair / sombre",
"settings_title": "Paramètres",
"settings_username": "Nom d'utilisateur MQTT",
"settings_vendor_filter_placeholder": "Rechercher des fabricants…",
"settings_version": "Version",
"settings_visible_vendors": "Fabricants visibles (liste des profils)",
"settings_visible_vendors_hint": "Seuls ces fabricants apparaissent dans la liste des profils d'emplacement. Rien de sélectionné = tout afficher. « Generic » et vos propres profils sont toujours visibles.",
"settings_visible_vendors_label": "Fabricants visibles (liste des profils)",
"settings_visible_vendors_save": "Enregistrer la sélection",
"settings_visible_vendors_save_label": "Enregistrer la sélection",
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
"sf_all": "Tout",
"sf_err": "✗ Échoués",
"sf_new": "Nouveau",
"sf_ok": "✓ Terminés",
"skip_already": "ignoré",
"skip_btn_label": "Objets",
"skip_cancel": "Annuler",
"skip_confirm": "Ignorer",
"skip_confirm_btn": "Ignorer",
"skip_hint": "Décochez les objets que vous ne souhaitez plus imprimer :",
"skip_no_objects": "Aucun objet dans cette impression.",
"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",
"skip_title": "Ignorer des objets",
"slot_edit_color": "Couleur",
"slot_edit_custom": "ex. PLA, PETG, ABS…",
"slot_edit_load": "⬇ Charger",
"slot_edit_material": "Matériau",
"slot_edit_ok": "Slot AMS",
"slot_edit_profile": "Profil OrcaSlicer",
"slot_edit_profile_default": "— Générique (défaut) —",
"slot_edit_profile_hint": "Envoyé lors de la synchronisation OrcaSlicer comme marque spécifique au lieu de \"Générique\"",
"slot_edit_save": "💾 Enregistrer",
"slot_edit_title": "Modifier le slot",
"slot_edit_unload": "⬆ Décharger",
"speed_normal": "⚡ Normal",
"speed_silent": "🐢 Silencieux",
"speed_sport": "🚀 Sport",
"ss_date": "↓ Date",
"ss_dur": "⏱ Durée d'impression",
"ss_name": "AZ Nom",
"ss_dur": "⏱ Durée d'impression"
"store_delete_confirm": "Supprimer le fichier ?",
"store_download": "⬇ Télécharger",
"store_empty": "Aucun fichier uploadé.",
"store_estimate": "Estimation",
"store_never": "jamais imprimé",
"store_no_results": "Aucun fichier trouvé.",
"store_print": "▶ Imprimer",
"store_print_confirm": "Imprimer le fichier ?",
"store_refresh": "↻ Actualiser",
"store_search_placeholder": "🔍 Rechercher…",
"store_upload_busy": "⏳ Envoi en cours…",
"store_upload_error": "✗ {error}",
"store_upload_label_browse": "parcourir",
"store_upload_label_prefix": "Déposez un GCode ici ou ",
"store_upload_only_gcode": "✗ Seuls les fichiers GCode sont autorisés (.gcode, .3mf, .bgcode)",
"store_upload_success": "✓ {file}",
"store_web_verify_abort": "Annuler",
"store_web_verify_confirm": "Confirmer",
"store_web_verify_msg": "Veuillez vérifier que ce fichier a été créé pour Anycubic Kobra X.",
"store_web_verify_title": "Vérifier le fichier",
"update_apply": "Installer maintenant",
"update_applying": "Téléchargement…",
"update_available": "disponible",
"update_check": "Vérifier les mises à jour",
"update_checking": "Vérification…",
"update_error": "Erreur",
"update_none": "Déjà à jour",
"update_restarting": "Redémarrage…"
}

322
web/translations/it.json Normal file
View File

@@ -0,0 +1,322 @@
{
"ace_dry_auto_refill": "Ricarica automatica",
"ace_dry_chart": "Cronologia (Temp/Umidità)",
"ace_dry_current_temp": "Temperatura",
"ace_dry_dialog_cancel": "Annulla",
"ace_dry_dialog_confirm": "Conferma",
"ace_dry_dialog_custom_name": "Nome personalizzato",
"ace_dry_dialog_reset_default": "Ripristina predefiniti",
"ace_dry_dialog_save_restart": "Salva e riavvia",
"ace_dry_dialog_temp": "Temperatura (30-80°C)",
"ace_dry_dialog_time": "Tempo rim. (h:m:s)",
"ace_dry_dialog_title": "Impostazioni Temp/Tempo essiccatore",
"ace_dry_dryer": "Essiccatore",
"ace_dry_duration": "Durata (min)",
"ace_dry_enable": "Abilita essiccazione",
"ace_dry_humidity": "Umidità",
"ace_dry_preset_abs_asa": "ABS / ASA",
"ace_dry_preset_custom": "Personalizzato",
"ace_dry_preset_pa_pc": "PA / PC",
"ace_dry_preset_petg": "PETG",
"ace_dry_preset_pla": "PLA",
"ace_dry_preset_pla_plus": "PLA+",
"ace_dry_preset_tpu": "TPU",
"ace_dry_start": "▶ Avvia",
"ace_dry_status_off": "Stato: Spento",
"ace_dry_status_on": "Stato: Attivo",
"ace_dry_status_remaining": "Rimanente",
"ace_dry_stop": "■ Ferma",
"ace_dry_temp": "Temperatura (°C)",
"ace_dry_temp_line": "Temperatura di essiccazione",
"ace_dry_time_line": "Tempo di essiccazione",
"ace_dry_ui_pending": "(Solo interfaccia, backend a seguire)",
"add_printer": "Aggiungi stampante",
"ams_empty": "Vuoto",
"ams_no_data": "Nessun dato ricevuto dall' AMS",
"apd_cancel": "Annulla",
"apd_confirm": "Aggiungi",
"apd_err_ip": "Inserisci un indirizzo IP",
"apd_fetching": "Recupero dati dalla stampante…",
"apd_lbl_ip": "IP stampante",
"apd_lbl_name": "Nome (opzionale)",
"apd_placeholder_name": "es. Kobra X Soggiorno",
"apd_success": "Stampante aggiunta, riavvio del bridge in corso…",
"apd_title": "Aggiungi stampante",
"btn_cam_start": "▶ Camera",
"btn_cam_start2": "▶ Avvia",
"btn_cam_stop": "◼ Camera",
"btn_cam_stop2": "◼ Ferma",
"btn_cancel": "✕ Stop",
"btn_cancel_generic": "Annulla",
"btn_confirm_generic": "Conferma",
"btn_connect": "⚡ Connetti",
"btn_delete": "Elimina",
"btn_disable_motors": "Spegni motori",
"btn_disconnect": "✕ Disconnetti",
"btn_home_all": "Home generale",
"btn_home_xy": "Home XY",
"btn_home_z": "Home Z",
"btn_pause": "⏸ Pausa",
"btn_resume": "▶ Riprendi",
"cam_placeholder": "📷 Camera non avviata",
"cam_stream_unavailable": "Flusso video non disponibile",
"card_ace_dry": "Essiccazione ACE",
"card_ams": "Filamento",
"card_cam": "Camera",
"card_light_fan": "Ventola",
"card_progress": "Avanzamento",
"card_speed": "Velocità di stampa",
"card_temps": "Temperature",
"confirm_cancel": "Annullare davvero la stampa?",
"fd_cancel": "Annulla",
"fd_no_matching_material": "Nessun materiale corrispondente",
"fd_no_slots_msg": "Nessuno slot AMS caricato.{br}Avviare comunque la stampa?",
"fd_objects_hint": "Salta oggetti (opzionale):",
"fd_objects_toggle": "Salta oggetti",
"fd_options_title": "Opzioni di stampa",
"fd_print": "▶ Stampa",
"fd_slot": "Slot",
"fd_slots_hint": "Assegna il canale GCode allo slot AMS:",
"fd_title": "Assegnazione slot",
"fd_used": "USATO",
"file_cancel_btn": "✕ Annulla",
"file_ready_btn": "▶ Avvia stampa",
"file_slots_btn": "🎨 Seleziona slot",
"header_status_complete": "Completato",
"header_status_error": "Errore",
"header_status_printing": "In stampa",
"header_status_standby": "Pronto",
"hint_ip_no_port": "Solo indirizzo IP, senza porta (es. 192.168.1.102)",
"kobra_auto_leveling": "Livellamento automatico",
"kobra_busy": "Occupato",
"kobra_canceled": "Annullato",
"kobra_checking": "Verifica",
"kobra_failed": "Errore",
"kobra_finished": "Finito",
"kobra_free": "Pronto",
"kobra_init": "Inizializzazione",
"kobra_offline": "Offline",
"kobra_paused": "In pausa",
"kobra_pausing": "Pausa in corso...",
"kobra_preheating": "Preriscaldamento",
"kobra_printing": "In stampa",
"kobra_resumed": "Ripreso",
"kobra_resuming": "Ripresa...",
"kobra_stoped": "Arrestato",
"kobra_stopping": "Arresto...",
"kobra_updated": "Aggiornamento",
"label_bed": "Piatto",
"label_fan": "🌀 Ventola",
"label_light": "💡 Luce",
"label_nozzle": "Ugello",
"label_off": "Off",
"label_on_off": "On / Off",
"label_set": "Imposta",
"label_slot": "Slot",
"label_speed": "Velocità",
"label_step": "Ampiezza passo:",
"label_target_c": "Target:",
"lbl_conn_error": "Errore di connessione:",
"lbl_elapsed": "Trascorso:",
"lbl_feed": "Carica",
"lbl_layers": "Layer",
"lbl_light": "💡 Luce",
"lbl_remaining": "Rimanente:",
"lbl_slicer_time": "Stima slicer:",
"lbl_unload": "Rimuovi",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ Auto",
"log_axis": "Asse",
"log_bed": "Piatto →",
"log_cam_start": "Camera avviata:",
"log_cam_stop": "Camera arrestata",
"log_clear": "✕ Cancella",
"log_delete_failed": "Eliminazione non riuscita",
"log_dir_all": "Tutti",
"log_dir_label": "Dir:",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_download": "⬇ Scarica",
"log_error": "Errore:",
"log_fan": "Ventola →",
"log_filter_placeholder": "Filtra…",
"log_home": "Home",
"log_home_all": "Home generale",
"log_light_off": "Luce spenta",
"log_light_on": "Luce accesa",
"log_lvl_err": "⛔ Errori",
"log_lvl_label": "Livello:",
"log_lvl_warn": "⚠ Avvisi",
"log_nozzle": "Ugello →",
"log_poll_error": "Errore di sincronizzazione:",
"log_print_action": "Stampa:",
"log_print_start": "Inizio stampa:",
"log_topic_ams": "AMS",
"log_topic_info": "Info",
"log_topic_label": "Argomento:",
"log_topic_print": "Stampa",
"log_topic_status": "Stato",
"nav_ams": "AMS",
"nav_browser": "Browser",
"nav_console": "Console",
"nav_dashboard": "Dashboard",
"nav_extras": "Luce / Ventola",
"nav_motion": "Movimento",
"nav_print": "Stampa",
"nav_printers": "Stampanti",
"nav_settings": "Impostazioni",
"nav_temps": "Temperature",
"orca_profile_done": "Importato",
"orca_profile_dropmsg": "Trascina qui o fai clic",
"orca_profile_help_html": "Carica un file <b>ZIP</b> della tua cartella filamenti di OrcaSlicer o file singoli <b>.json</b>.<br>In OrcaSlicer: <i>Aiuto → Mostra cartella di configurazione → user/&lt;id&gt;/filament/</i>",
"orca_profile_hint": "Importa i tuoi profili di filamento OrcaSlicer (apri la cartella utente tramite Aiuto → Mostra cartella di configurazione)",
"orca_profile_import_btn": "Importa profili",
"orca_profile_import_link": "★ Importa i tuoi profili…",
"orca_profile_import_title": "Importa i tuoi profili OrcaSlicer",
"orca_profile_list_label": "Attualmente importati",
"orca_profile_section": "Profili OrcaSlicer",
"orca_profile_skipped": "saltato",
"orca_profile_uploading": "Caricamento in corso…",
"orca_profile_user_empty": " nessuno ",
"orca_profile_user_label": "Profili personali",
"panel_ams_title": "Filamento",
"panel_browser_title": "Browser dei file",
"panel_console_title": "Registro eventi",
"panel_extras_camera": "Camera",
"panel_extras_fan": "Ventola",
"panel_extras_light": "Luce",
"panel_motion_xy": "Assi XY",
"panel_motion_z": "Asse Z",
"panel_print_btn_cancel": "✕ Annulla",
"panel_print_btn_pause": "⏸ Pausa",
"panel_print_btn_resume": "▶ Riprendi",
"panel_print_temps_live": "Temperature (In tempo reale)",
"panel_print_title": "Controllo stampa",
"panel_temps_bed": "Piatto riscaldato",
"panel_temps_chart": "Cronologia (ultime 60 letture)",
"panel_temps_nozzle": "Ugello",
"print_auto_leveling": "Livellamento automatico",
"printers_active": "● attiva",
"printers_current": "Stampante corrente",
"printers_empty_hint": "Nessuna stampante ancora configurata.",
"printers_loading": "Caricamento in corso…",
"printers_none": "Nessuna stampante configurata.",
"printers_remove": "Rimuovi stampante",
"printers_remove_confirm": "Rimuovere la stampante \"{name}\"? Il bridge si riavvierà.",
"printers_switch": "Cambia →",
"progress_action_clear": "Cancella",
"progress_action_print": "Stampa",
"progress_action_slots": "Mappa slot",
"settings_auto_leveling": "Livellamento automatico predefinito",
"settings_auto_leveling_label": "Livellamento automatico prima della stampa",
"settings_btn_tooltip": "Impostazioni",
"settings_camera_on_print": "Attiva la camera all'avvio della stampa",
"settings_cat_connection": "Connessione",
"settings_cat_display": "Aspetto",
"settings_cat_filament": "Filamento",
"settings_cat_language": "Lingua",
"settings_cat_printer": "Stampante",
"settings_cat_system": "Sistema",
"settings_cat_theme": "Alterna chiaro / scuro",
"settings_connection": "Connessione",
"settings_default_slot": "Slot predefinito (colore singolo)",
"settings_device_id": "ID dispositivo",
"settings_device_id_hint": "32 caratteri esadecimali",
"settings_device_id_placeholder": "32 caratteri esadecimali",
"settings_filament_mapping": "Mappatura profilo filamento (per slot)",
"settings_filament_mapping_hint": "Profilo Orca fisso per slot AMS. Durante la sincronizzazione dello slicer, il bridge invia questo profilo al posto di \"Generic\".",
"settings_filament_mapping_label": "Mappatura profilo filamento (per slot)",
"settings_filament_mapping_save": "Salva mappatura",
"settings_filament_mapping_save_label": "Salva mappatura",
"settings_file_ready_banner": "Barra di stampa",
"settings_file_ready_dialog": "Finestra di dialogo stampa",
"settings_file_ready_mode": "Dopo il caricamento: Comportamento di avvio stampa",
"settings_language": "Lingua",
"settings_mode_id": "ID modalità",
"settings_mode_id_placeholder": "20030",
"settings_mqtt_port": "Porta MQTT",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_orca_profiles_import": "Importa profili",
"settings_orca_profiles_label": "Profili OrcaSlicer",
"settings_password": "Password MQTT",
"settings_poll": "Intervallo di sincronizzazione (secondi)",
"settings_poll_interval_hint": "Con che frequenza il bridge interroga lo stato della stampante",
"settings_poll_interval_label": "Intervallo di sincronizzazione (secondi)",
"settings_print": "Impostazioni di stampa",
"settings_printer_ip": "IP stampante",
"settings_printer_name": "Nome stampante",
"settings_printer_name_placeholder": "p. es. Kobra X Sala",
"settings_save": "Salva e riavvia",
"settings_slot_auto": "Auto (tutti gli slot caricati)",
"settings_theme_toggle": "Attiva/disattiva chiaro / scuro",
"settings_title": "Impostazioni",
"settings_username": "Nome utente MQTT",
"settings_vendor_filter_placeholder": "Cerca produttori…",
"settings_version": "Versione",
"settings_visible_vendors": "Produttori visibili (menu del profilo)",
"settings_visible_vendors_hint": "Solo questi produttori appariranno nel menu del profilo dello slot. Se non selezioni nulla = mostra tutti. I profili \"Generici\" e i tuoi personali sono sempre visibili.",
"settings_visible_vendors_label": "Produttori visibili (menu del profilo)",
"settings_visible_vendors_save": "Salva selezione",
"settings_visible_vendors_save_label": "Salva selezione",
"settings_web_upload_warning": "Mostra un avviso quando si stampano caricamenti web",
"sf_all": "Tutti",
"sf_err": "✗ Fallito",
"sf_new": "Nuovo",
"sf_ok": "✓ Completato",
"skip_already": "saltato",
"skip_btn_label": "Oggetti",
"skip_cancel": "Annulla",
"skip_confirm": "Salta",
"skip_confirm_btn": "Salta",
"skip_hint": "Deseleziona gli oggetti che non vuoi più stampare:",
"skip_no_objects": "Nessun oggetto in questa stampa.",
"skip_select_at_least_one": "Seleziona almeno un oggetto.",
"skip_sending": "Invio in corso …",
"skip_success": "Gli oggetti verranno saltati.",
"skip_title": "✂ Salta oggetti",
"slot_edit_color": "Colore",
"slot_edit_custom": "es. PLA, PETG, ABS…",
"slot_edit_load": "⬇ Carica",
"slot_edit_material": "Materiale",
"slot_edit_ok": "Slot AMS",
"slot_edit_profile": "Profilo OrcaSlicer",
"slot_edit_profile_default": "— Generico (predefinito) —",
"slot_edit_profile_hint": "Inviato durante la sincronizzazione con OrcaSlicer come marchio specifico invece di un semplice \"Generico\"",
"slot_edit_save": "💾 Salva",
"slot_edit_title": "Modifica slot",
"slot_edit_unload": "⬆ Rimuovi",
"speed_normal": "⚡ Normale",
"speed_silent": "🐢 Silenzioso",
"speed_sport": "🚀 Sport",
"ss_date": "↓ Data",
"ss_dur": "⏱ Tempo di stampa",
"ss_name": "Nome AZ",
"store_delete_confirm": "Eliminare il file?",
"store_download": "⬇ Scarica",
"store_empty": "Nessun file caricato.",
"store_estimate": "Stima",
"store_never": "mai stampato",
"store_no_results": "Nessun file trovato.",
"store_print": "▶ Stampa",
"store_print_confirm": "Stampare il file?",
"store_refresh": "↻ Aggiorna",
"store_search_placeholder": "🔍 Cerca…",
"store_upload_busy": "⏳ Caricamento in corso…",
"store_upload_error": "✗ {error}",
"store_upload_label_browse": "sfoglia",
"store_upload_label_prefix": "Trascina il GCode qui o ",
"store_upload_only_gcode": "✗ Sono consentiti solo file GCode (.gcode, .3mf, .bgcode)",
"store_upload_success": "✓ {file}",
"store_web_verify_abort": "Annulla",
"store_web_verify_confirm": "Conferma",
"store_web_verify_msg": "Verifica che questo file sia stato creato per Anycubic Kobra X.",
"store_web_verify_title": "Verifica file",
"update_apply": "Installa ora",
"update_applying": "Download in corso...",
"update_available": "disponibile",
"update_check": "Controlla aggiornamenti",
"update_checking": "Verifica in corso...",
"update_error": "Errore",
"update_none": "Già aggiornato",
"update_restarting": "Riavvio in corso..."
}

View File

@@ -1,248 +1,322 @@
{
"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_auto_refill": "自动补料",
"ace_dry_chart": "历史 (温度/湿度)",
"ace_dry_current_temp": "温度",
"ace_dry_dialog_cancel": "取消",
"ace_dry_dialog_confirm": "确认",
"ace_dry_dialog_custom_name": "自定义名称",
"ace_dry_dialog_reset_default": "恢复默认",
"ace_dry_dialog_save_restart": "保存并重启",
"ace_dry_dialog_temp": "温度 (30-80°C)",
"ace_dry_dialog_time": "剩余时间 (h:m:s)",
"ace_dry_dialog_title": "烘干温度/时间设置",
"ace_dry_dryer": "烘干机",
"ace_dry_duration": "时长 (分钟)",
"ace_dry_enable": "启用烘干",
"ace_dry_humidity": "湿度",
"ace_dry_preset_abs_asa": "ABS / ASA",
"ace_dry_preset_custom": "自定义",
"ace_dry_preset_pa_pc": "PA / PC",
"ace_dry_preset_petg": "PETG",
"ace_dry_preset_pla": "PLA",
"ace_dry_preset_pla_plus": "PLA+",
"ace_dry_preset_tpu": "TPU",
"ace_dry_start": "▶ 启动",
"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": "温度 (°C)",
"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": "视频流不可用",
"add_printer": "添加打印机",
"ams_empty": "",
"ams_no_data": "未收到 AMS 数据",
"apd_cancel": "取消",
"apd_confirm": "添加",
"apd_err_ip": "请输入 IP 地址",
"apd_fetching": "正在从打印机获取数据…",
"apd_lbl_ip": "打印机 IP",
"apd_lbl_name": "名称 (可选)",
"apd_placeholder_name": "例如 Kobra X 客厅",
"apd_success": "打印机已添加Bridge 正在重启…",
"apd_title": "添加打印机",
"btn_cam_start": "▶ 相机",
"btn_cam_start2": "▶ 启动",
"btn_cam_stop": "◼ 相机",
"btn_cam_stop2": "◼ 停止",
"btn_cancel": "✕ 停止",
"btn_cancel_generic": "取消",
"btn_confirm_generic": "确认",
"btn_connect": "⚡ 连接",
"btn_delete": "删除",
"btn_disable_motors": "关闭电机",
"btn_disconnect": "✕ 断开",
"btn_home_all": "全部回零",
"btn_home_xy": "回零 XY",
"btn_home_z": "回零 Z",
"btn_pause": "⏸ 暂停",
"btn_resume": "▶ 继续",
"btn_cancel": "✕ 停止",
"label_nozzle": "喷嘴",
"cam_placeholder": "📷 相机未启动",
"cam_stream_unavailable": "视频流不可用",
"card_ace_dry": "ACE 烘干",
"card_ams": "耗材",
"card_cam": "相机",
"card_light_fan": "风扇",
"card_progress": "进度",
"card_speed": "打印速度",
"card_temps": "温度",
"confirm_cancel": "确定要取消打印吗?",
"fd_cancel": "取消",
"fd_no_matching_material": "无匹配材料",
"fd_no_slots_msg": "没有已装载的 AMS 槽位。{br}仍要开始打印吗?",
"fd_objects_hint": "跳过对象 (可选):",
"fd_objects_toggle": "跳过对象",
"fd_options_title": "选项",
"fd_print": "▶ 打印",
"fd_slot": "槽位",
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
"fd_title": "槽位分配",
"fd_used": "已用",
"file_cancel_btn": "✕ 取消",
"file_ready_btn": "▶ 开始打印",
"file_slots_btn": "🎨 选择槽位",
"header_status_complete": "完成",
"header_status_error": "错误",
"header_status_printing": "打印中",
"header_status_standby": "就绪",
"hint_ip_no_port": "仅填写 IP不要端口 (例如 192.168.1.102)",
"kobra_auto_leveling": "自动调平",
"kobra_busy": "忙碌",
"kobra_canceled": "已取消",
"kobra_checking": "检查中",
"kobra_failed": "错误",
"kobra_finished": "已完成",
"kobra_free": "就绪",
"kobra_init": "初始化中",
"kobra_offline": "离线",
"kobra_paused": "已暂停",
"kobra_pausing": "暂停中...",
"kobra_preheating": "预热中",
"kobra_printing": "打印中",
"kobra_resumed": "已恢复",
"kobra_resuming": "恢复中...",
"kobra_stoped": "已停止",
"kobra_stopping": "停止中...",
"kobra_updated": "更新中",
"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_nozzle": "喷嘴",
"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_on_off": "开 / 关",
"label_set": "设置",
"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": "热床 →",
"label_speed": "速度",
"label_step": "步进:",
"label_target_c": "目标:",
"lbl_conn_error": "连接错误:",
"lbl_elapsed": "已用时间:",
"lbl_feed": "进料",
"lbl_layers": "",
"lbl_light": "💡 灯光",
"lbl_remaining": "剩余时间:",
"lbl_slicer_time": "切片预估:",
"lbl_unload": "退料",
"lbl_zpos": "Z (mm)",
"log_auto": "⬇ 自动",
"log_axis": "轴",
"log_home": "回零",
"log_home_all": "全部回零",
"log_bed": "热床 →",
"log_cam_start": "相机已启动:",
"log_cam_stop": "相机已停止",
"log_poll_error": "轮询错误:",
"log_clear": "✕ 清空",
"log_delete_failed": "删除失败",
"log_dir_all": "全部",
"log_dir_label": "方向:",
"log_dir_rx": "RX",
"log_dir_tx": "TX",
"log_download": "⬇ 下载",
"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 配置",
"log_fan": "风扇 →",
"log_filter_placeholder": "筛选…",
"log_home": "回零",
"log_home_all": "全部回零",
"log_light_off": "灯光已关",
"log_light_on": "灯光已开",
"log_lvl_err": "⛔ 错误",
"log_lvl_label": "级别:",
"log_lvl_warn": "⚠ 警告",
"log_nozzle": "喷嘴 →",
"log_poll_error": "轮询错误:",
"log_print_action": "打印:",
"log_print_start": "打印开始:",
"log_topic_ams": "AMS",
"log_topic_info": "信息",
"log_topic_label": "主题:",
"log_topic_print": "打印",
"log_topic_status": "状态",
"nav_ams": "AMS",
"nav_browser": "浏览器",
"nav_console": "控制台",
"nav_dashboard": "仪表盘",
"nav_extras": "灯光 / 风扇",
"nav_motion": "运动",
"nav_print": "打印",
"nav_printers": "打印机",
"nav_settings": "设置",
"nav_temps": "温度",
"orca_profile_done": "已导入",
"orca_profile_dropmsg": "拖到此处或点击",
"orca_profile_help_html": "上传 OrcaSlicer 耗材文件夹的 <b>ZIP</b> 或单个 <b>.json</b> 文件。<br>在 OrcaSlicer 中: <i>Help → Show Configuration Folder → user/&lt;id&gt;/filament/</i>",
"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/&lt;id&gt;/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_section": "OrcaSlicer 配置",
"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": "此打印任务没有对象。",
"orca_profile_uploading": "上传中…",
"orca_profile_user_empty": "",
"orca_profile_user_label": "自己的配置",
"panel_ams_title": "耗材",
"panel_browser_title": "文件浏览器",
"panel_console_title": "事件日志",
"panel_extras_camera": "相机",
"panel_extras_fan": "风扇",
"panel_extras_light": "灯光",
"panel_motion_xy": "XY 轴",
"panel_motion_z": "Z 轴",
"panel_print_btn_cancel": "✕ 取消",
"panel_print_btn_pause": "⏸ 暂停",
"panel_print_btn_resume": "▶ 继续",
"panel_print_temps_live": "温度 (实时)",
"panel_print_title": "打印控制",
"panel_temps_bed": "热床",
"panel_temps_chart": "历史 (最近 60 次读数)",
"panel_temps_nozzle": "喷嘴",
"print_auto_leveling": "本次打印自动调平",
"printers_active": "● 活动",
"printers_current": "当前打印机",
"printers_empty_hint": "尚未设置打印机。",
"printers_loading": "加载中…",
"printers_none": "未配置打印机。",
"printers_remove": "移除打印机",
"printers_remove_confirm": "移除打印机 \"{name}\"? Bridge 将重启。",
"printers_switch": "切换 →",
"progress_action_clear": "清除",
"progress_action_print": "打印",
"progress_action_slots": "分配槽位",
"settings_auto_leveling": "打印前自动调平",
"settings_auto_leveling_label": "打印前自动调平",
"settings_btn_tooltip": "设置",
"settings_camera_on_print": "打印开始时开启相机",
"settings_cat_connection": "连接",
"settings_cat_display": "外观",
"settings_cat_filament": "耗材",
"settings_cat_language": "语言",
"settings_cat_printer": "打印机",
"settings_cat_system": "系统",
"settings_cat_theme": "切换浅色 / 深色",
"settings_connection": "连接",
"settings_default_slot": "默认槽位 (单色)",
"settings_device_id": "设备 ID",
"settings_device_id_hint": "32 个十六进制字符",
"settings_device_id_placeholder": "32 个十六进制字符",
"settings_filament_mapping": "耗材配置映射(每槽位)",
"settings_filament_mapping_hint": "每个 AMS 槽位的固定 Orca 配置。在切片器同步时Bridge 会发送此配置而不是“Generic”。",
"settings_filament_mapping_label": "耗材配置映射(每槽位)",
"settings_filament_mapping_save": "保存映射",
"settings_filament_mapping_save_label": "保存映射",
"settings_file_ready_banner": "打印栏",
"settings_file_ready_dialog": "打印对话框",
"settings_file_ready_mode": "上传后:开始打印行为",
"settings_language": "语言",
"settings_mode_id": "模式 ID",
"settings_mode_id_placeholder": "20030",
"settings_mqtt_port": "MQTT 端口",
"settings_mqtt_username_placeholder": "userXXXXXXXX",
"settings_orca_profiles_import": "导入配置文件",
"settings_orca_profiles_label": "OrcaSlicer 配置文件",
"settings_password": "MQTT 密码",
"settings_poll": "轮询间隔(秒)",
"settings_poll_interval_hint": "Bridge 查询打印机状态的频率",
"settings_poll_interval_label": "轮询间隔(秒)",
"settings_print": "打印设置",
"settings_printer_ip": "打印机 IP",
"settings_printer_name": "打印机名称",
"settings_printer_name_placeholder": "例如 Kobra X 左",
"settings_save": "保存并重启",
"settings_slot_auto": "自动 (所有已装载槽位)",
"settings_theme_toggle": "切换浅色 / 深色",
"settings_title": "设置",
"settings_username": "MQTT 用户名",
"settings_vendor_filter_placeholder": "搜索厂商…",
"settings_version": "版本",
"settings_visible_vendors": "可见厂商(配置下拉框)",
"settings_visible_vendors_hint": "仅这些厂商会出现在槽位配置下拉框中。未选择 = 显示全部。“Generic”和您自己的配置始终可见。",
"settings_visible_vendors_label": "可见厂商(配置下拉框)",
"settings_visible_vendors_save": "保存选择",
"settings_visible_vendors_save_label": "保存选择",
"settings_web_upload_warning": "打印网页上传文件时显示警告",
"sf_all": "全部",
"sf_err": "✗ 失败",
"sf_new": "新",
"sf_ok": "✓ 已完成",
"skip_already": "已跳过",
"skip_btn_label": "对象",
"skip_cancel": "取消",
"skip_confirm": "跳过",
"skip_confirm_btn": "跳过",
"skip_hint": "取消勾选不想继续打印的对象:",
"skip_no_objects": "此打印任务没有对象。",
"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": "新",
"skip_title": "跳过对象",
"slot_edit_color": "颜色",
"slot_edit_custom": "例如 PLA, PETG, ABS…",
"slot_edit_load": "⬇ 进料",
"slot_edit_material": "材料",
"slot_edit_ok": "AMS 槽位",
"slot_edit_profile": "OrcaSlicer 配置",
"slot_edit_profile_default": "— 通用 (默认) —",
"slot_edit_profile_hint": "在 OrcaSlicer 同步时发送具体品牌而不仅仅是“Generic”",
"slot_edit_save": "💾 保存",
"slot_edit_title": "编辑槽位",
"slot_edit_unload": "⬆ 退料",
"speed_normal": "⚡ 标准",
"speed_silent": "🐢 静音",
"speed_sport": "🚀 运动",
"ss_date": "↓ 日期",
"ss_dur": "⏱ 打印时间",
"ss_name": "AZ 名称",
"ss_dur": "⏱ 打印时间"
"store_delete_confirm": "删除文件?",
"store_download": "⬇ 下载",
"store_empty": "尚未上传文件。",
"store_estimate": "估算",
"store_never": "从未打印",
"store_no_results": "未找到文件。",
"store_print": "▶ 打印",
"store_print_confirm": "打印文件?",
"store_refresh": "↻ 刷新",
"store_search_placeholder": "🔍 搜索…",
"store_upload_busy": "⏳ 上传中…",
"store_upload_error": "✗ {error}",
"store_upload_label_browse": "浏览",
"store_upload_label_prefix": "将 GCode 拖到这里或 ",
"store_upload_only_gcode": "✗ 仅允许 GCode 文件 (.gcode, .3mf, .bgcode)",
"store_upload_success": "✓ {file}",
"store_web_verify_abort": "取消",
"store_web_verify_confirm": "确认",
"store_web_verify_msg": "请验证此文件是为Anycubic Kobra X创建的。",
"store_web_verify_title": "验证文件",
"update_apply": "立即安装",
"update_applying": "下载中...",
"update_available": "可用",
"update_check": "检查更新",
"update_checking": "检查中...",
"update_error": "错误",
"update_none": "已是最新版本",
"update_restarting": "重启中..."
}