forked from viewit/KX-Bridge-Release
Compare commits
6 Commits
print-work
...
start-prin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c91ba66801 | ||
|
|
9517493c39 | ||
|
|
5d691c6153 | ||
|
|
2a02c598b5 | ||
|
|
e64aebd145 | ||
|
|
778c4ce85a |
@@ -1,78 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
90
CHANGELOG.md
90
CHANGELOG.md
@@ -1,95 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
@@ -34,6 +34,9 @@ auto_leveling = 1
|
||||
# Kamera-Stream bei Druckstart automatisch einschalten (1 = an, 0 = aus)
|
||||
camera_on_print = 0
|
||||
|
||||
# Statt grünem Ready-Banner den Filament/Color-Selector automatisch öffnen (1 = an, 0 = aus)
|
||||
print_start_dialog = 1
|
||||
|
||||
# Warnung vor Druck von Web-Uploads (1 = an, 0 = aus)
|
||||
web_upload_warning = 1
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ def _load_config_file(path: pathlib.Path):
|
||||
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
|
||||
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||
}
|
||||
for env_key, (section, option) in mapping.items():
|
||||
@@ -73,8 +73,7 @@ def _load_config_file(path: pathlib.Path):
|
||||
except (configparser.NoSectionError, configparser.NoOptionError):
|
||||
pass
|
||||
|
||||
|
||||
# Backward compatibility: old key FILE_READY_DIALOG → PRINT_START_DIALOG
|
||||
# Backward compatibility: legacy option and env var
|
||||
if "PRINT_START_DIALOG" not in os.environ:
|
||||
try:
|
||||
legacy = cfg.get(CONFIG_SECTION_PRINT, "file_ready_dialog")
|
||||
@@ -111,6 +110,7 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
||||
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||
"print_start_dialog": env_vals.get("PRINT_START_DIALOG", env_vals.get("FILE_READY_DIALOG", "1")),
|
||||
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
|
||||
}
|
||||
cfg[CONFIG_SECTION_BRIDGE] = {
|
||||
@@ -240,17 +240,10 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
# visible_vendors (Issue #41) ist kein Slot-Mapping — beim Ersetzen der
|
||||
# Sektion erhalten, sonst geht der Vendor-Filter beim Slot-Save verloren.
|
||||
preserved_vendors = None
|
||||
if cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
preserved_vendors = cfg.get("filament_profiles", "visible_vendors")
|
||||
if cfg.has_section("filament_profiles"):
|
||||
cfg.remove_section("filament_profiles")
|
||||
if profiles or preserved_vendors:
|
||||
if profiles:
|
||||
cfg["filament_profiles"] = {}
|
||||
if preserved_vendors:
|
||||
cfg["filament_profiles"]["visible_vendors"] = preserved_vendors
|
||||
for slot_idx in sorted(profiles.keys()):
|
||||
entry = profiles[slot_idx] or {}
|
||||
if entry.get("vendor"):
|
||||
@@ -264,43 +257,6 @@ def save_filament_profiles(profiles: dict[int, dict]) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def list_visible_vendors() -> list[str]:
|
||||
"""Liest [filament_profiles] visible_vendors (komma-separiert) aus config.ini.
|
||||
|
||||
Vendor-Sichtbarkeitsfilter für das Slot-Profil-Dropdown (Issue #41 Option A).
|
||||
Leere Liste = keine Einschränkung (rückwärtskompatibel: alle Vendoren).
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return []
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
return []
|
||||
raw = cfg.get("filament_profiles", "visible_vendors")
|
||||
return [v.strip() for v in raw.split(",") if v.strip()]
|
||||
|
||||
|
||||
def save_visible_vendors(vendors: list[str]) -> bool:
|
||||
"""Schreibt visible_vendors in [filament_profiles], ohne die Slot-Mappings
|
||||
(slot_N_*) zu verlieren. Leere Liste entfernt den Key wieder."""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_section("filament_profiles"):
|
||||
cfg.add_section("filament_profiles")
|
||||
clean = [v.strip() for v in (vendors or []) if v and v.strip()]
|
||||
if clean:
|
||||
cfg["filament_profiles"]["visible_vendors"] = ", ".join(clean)
|
||||
elif cfg.has_option("filament_profiles", "visible_vendors"):
|
||||
cfg.remove_option("filament_profiles", "visible_vendors")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
cfg.write(f)
|
||||
return True
|
||||
|
||||
|
||||
def get(key: str, default: str = "") -> str:
|
||||
return os.environ.get(key, default)
|
||||
|
||||
@@ -315,5 +271,5 @@ DEVICE_ID = get("DEVICE_ID", "")
|
||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG","1")))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
|
||||
115
kobrax_client.py
115
kobrax_client.py
@@ -27,7 +27,6 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
@@ -121,10 +120,6 @@ 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)
|
||||
@@ -172,31 +167,21 @@ class KobraXClient:
|
||||
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
|
||||
ctx.load_cert_chain(CERT_FILE, KEY_FILE)
|
||||
|
||||
# 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])
|
||||
_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])
|
||||
|
||||
new_sock.sendall(_build_connect(self.client_id, self.username, self.password))
|
||||
new_sock.settimeout(3)
|
||||
r = new_sock.recv(64)
|
||||
self._sock.sendall(_build_connect(self.client_id, self.username, self.password))
|
||||
self._sock.settimeout(3)
|
||||
r = self._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")
|
||||
|
||||
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
|
||||
self._sock.settimeout(0.2)
|
||||
self._buf = b""
|
||||
self._subscribe(self._sub_topic())
|
||||
log.debug("MQTT connected to %s:%s", self.host, self.port)
|
||||
|
||||
def connect(self):
|
||||
@@ -222,14 +207,10 @@ class KobraXClient:
|
||||
|
||||
def disconnect(self):
|
||||
self._running = False
|
||||
with self._lock:
|
||||
try:
|
||||
if self._sock is not None:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._sock = None
|
||||
self._sock_gen += 1
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _reconnect(self):
|
||||
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
|
||||
@@ -238,16 +219,10 @@ class KobraXClient:
|
||||
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
|
||||
ausgeschaltet) zu vermeiden."""
|
||||
log.warning("Verbindung verloren – reconnect…")
|
||||
# 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
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
delays = [2, 4, 8, 15, 30, 60]
|
||||
attempt = 0
|
||||
while self._running:
|
||||
@@ -271,8 +246,7 @@ class KobraXClient:
|
||||
with self._lock:
|
||||
pid = self._pid
|
||||
self._pid += 1
|
||||
if self._sock is not None:
|
||||
self._sock.sendall(_build_subscribe(topic, pid))
|
||||
self._sock.sendall(_build_subscribe(topic, pid))
|
||||
log.info("SUB %s", topic)
|
||||
|
||||
# -- Read loop -----------------------------------------------------------
|
||||
@@ -282,52 +256,17 @@ class KobraXClient:
|
||||
_empty_count = 0
|
||||
while self._running:
|
||||
if time.time() - last_ping > 30:
|
||||
ping_ok = False
|
||||
with self._lock:
|
||||
try:
|
||||
if self._sock is not None:
|
||||
self._sock.sendall(_build_pingreq())
|
||||
ping_ok = True
|
||||
self._sock.sendall(_build_pingreq())
|
||||
except Exception:
|
||||
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:
|
||||
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:
|
||||
if self._running and not self._reconnect():
|
||||
break
|
||||
last_ping = time.time()
|
||||
continue
|
||||
data = sock.recv(65536)
|
||||
last_ping = time.time()
|
||||
try:
|
||||
data = self._sock.recv(65536)
|
||||
if not data:
|
||||
# Windows SSL kann kurzzeitig b"" liefern ohne echten EOF
|
||||
_empty_count += 1
|
||||
@@ -336,7 +275,7 @@ class KobraXClient:
|
||||
continue
|
||||
_empty_count = 0
|
||||
self._buf += data
|
||||
self._drain() # außerhalb des Locks — Dispatch/event.set() bleibt prompt
|
||||
self._drain()
|
||||
except ssl.SSLWantReadError:
|
||||
continue
|
||||
except socket.timeout:
|
||||
|
||||
@@ -34,7 +34,6 @@ except ImportError:
|
||||
import env_loader
|
||||
import asyncio
|
||||
import hashlib
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -752,13 +751,6 @@ class KobraXBridge:
|
||||
self._filament_profiles: dict[int, dict] = _cl.list_filament_profiles()
|
||||
except Exception:
|
||||
self._filament_profiles = {}
|
||||
# Vendor-Sichtbarkeitsfilter fürs Slot-Profil-Dropdown (Issue #41 Option A).
|
||||
# Leere Liste = alle Vendoren sichtbar (rückwärtskompatibel).
|
||||
try:
|
||||
import config_loader as _cl
|
||||
self._visible_vendors: list[str] = _cl.list_visible_vendors()
|
||||
except Exception:
|
||||
self._visible_vendors = []
|
||||
self._last_state: dict = {}
|
||||
self._state = {
|
||||
"nozzle_temp": 0.0,
|
||||
@@ -791,7 +783,7 @@ class KobraXBridge:
|
||||
"print_speed_mode": 2,
|
||||
"connection_error": "",
|
||||
"file_ready": "",
|
||||
"print_start_dialog": getattr(args, "print_start_dialog", 1),
|
||||
"print_start_dialog": getattr(args, "print_start_dialog", 0),
|
||||
"filament_mode": "toolhead",
|
||||
"ace_drying": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0, "humidity": None, "current_temp": None},
|
||||
}
|
||||
@@ -815,7 +807,7 @@ 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
|
||||
# Pre-Print-Skip: pending until confirmed/applied via skip/start
|
||||
self._pending_preprint_skip: list[str] = []
|
||||
self._pending_preprint_skip_deadline: float = 0.0
|
||||
|
||||
@@ -1071,49 +1063,31 @@ class KobraXBridge:
|
||||
"""
|
||||
d = payload.get("data") or {}
|
||||
skipped = d.get("objects_skip_parts") or d.get("skipped") or d.get("skipped_parts") or []
|
||||
# Während ein Pre-Print-Skip noch pending ist, leere Früh-Reports ignorieren
|
||||
# damit die UI nicht sofort zurückspringt bevor der Drucker den Skip bestätigt.
|
||||
# Während ein Pre-Print-Skip noch als pending markiert ist, ignorieren wir
|
||||
# leere Früh-Reports kurzzeitig, damit die UI nicht sofort zurückspringt.
|
||||
now = time.time()
|
||||
if (not skipped and self._pending_preprint_skip
|
||||
and now <= self._pending_preprint_skip_deadline):
|
||||
return
|
||||
|
||||
# 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
|
||||
# Liste übernehmen (auch leer), sobald kein Pending-Lock aktiv ist.
|
||||
self._skip_state = {
|
||||
"skipped": list(skipped),
|
||||
"ts": int(time.time()),
|
||||
}
|
||||
if self._pending_preprint_skip and set(skipped) >= set(self._pending_preprint_skip):
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
if payload.get("state") == "done" or payload.get("code") == 200:
|
||||
log.info(f"Skip-Antwort: state={payload.get('state')} code={payload.get('code')} skipped={skipped}")
|
||||
|
||||
def _on_file(self, payload: dict):
|
||||
d = payload.get("data") or {}
|
||||
details = d.get("file_details") or {}
|
||||
thumb = details.get("thumbnail") or details.get("png_image") or ""
|
||||
file_name = d.get("filename") or details.get("filename") or self._last_uploaded_file
|
||||
active_print = self._state.get("print_state") in ("printing", "paused")
|
||||
current_print_file = self._state.get("filename") or ""
|
||||
thumb = details.get("thumbnail") or details.get("png_image") 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)):
|
||||
@@ -1132,36 +1106,6 @@ 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:
|
||||
# kobra_state ist waehrend Start oft nicht exakt "printing"
|
||||
# (z.B. busy/preheating). Auf print_state pruefen, sonst wird
|
||||
# skip/start nie abgeschickt.
|
||||
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.
|
||||
@@ -1769,22 +1713,13 @@ class KobraXBridge:
|
||||
# WebSocket push
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Statische Objekte, die sich zur Laufzeit nie ändern. Sie werden einmalig
|
||||
# per objects.query/subscribe ausgeliefert, aber NICHT in jedem
|
||||
# notify_status_update mitgeschickt — sonst läuft Mobilerakers
|
||||
# ConfigFile.parse (teuer + strikt) bei jedem Status-Tick erneut und die App
|
||||
# hängt/crasht beim Refresh (Issue #48).
|
||||
_STATIC_STATUS_OBJECTS = ("configfile", "webhooks", "heaters", "history")
|
||||
|
||||
def _push_status_update(self):
|
||||
if not self.ws_clients:
|
||||
return
|
||||
objs = self._build_printer_objects()
|
||||
live = {k: v for k, v in objs.items() if k not in self._STATIC_STATUS_OBJECTS}
|
||||
msg = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notify_status_update",
|
||||
"params": [live, time.time()],
|
||||
"params": [self._build_printer_objects(), time.time()],
|
||||
}
|
||||
text = json.dumps(msg)
|
||||
dead = set()
|
||||
@@ -1942,17 +1877,6 @@ class KobraXBridge:
|
||||
"homing_origin": [0, 0, 0, 0],
|
||||
"position": [0, 0, self._estimate_current_z(), 0],
|
||||
},
|
||||
# motion_report: Mobileraker liest die Live-Geschwindigkeit hier
|
||||
# (live_velocity). Der Kobra-X-MQTT liefert KEINE echte mm/s, nur
|
||||
# einen print_speed_mode (1-4). live_velocity bleibt daher 0 — das
|
||||
# Objekt muss aber existieren, sonst zeigt Mobileraker nichts an
|
||||
# (motion_report war zuvor null). live_position spiegelt die
|
||||
# geschätzte Z-Höhe (wie gcode_move).
|
||||
"motion_report": {
|
||||
"live_position": [0, 0, self._estimate_current_z(), 0],
|
||||
"live_velocity": 0.0,
|
||||
"live_extruder_velocity": 0.0,
|
||||
},
|
||||
"fan": {
|
||||
"speed": (int(s.get("fan_speed") or 0)) / 100.0,
|
||||
"rpm": None,
|
||||
@@ -1989,78 +1913,40 @@ 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).
|
||||
# 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,
|
||||
"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": {},
|
||||
},
|
||||
"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": {},
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2380,31 +2266,6 @@ class KobraXBridge:
|
||||
"name": entry.get("name", ""),
|
||||
"id": entry.get("id", "")})
|
||||
|
||||
async def handle_kx_visible_vendors(self, request):
|
||||
"""GET/POST /kx/filament/visible_vendors — Vendor-Sichtbarkeitsfilter
|
||||
fürs Slot-Profil-Dropdown (Issue #41 Option A).
|
||||
|
||||
GET → {"result": ["Polymaker", "eSUN", ...]}
|
||||
POST {"vendors": [...]} → speichert in config.ini [filament_profiles]
|
||||
visible_vendors. Leere Liste = alle sichtbar. KEIN Bridge-Neustart
|
||||
nötig (nur Anzeigefilter)."""
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
data = {}
|
||||
vendors = data.get("vendors") or []
|
||||
if not isinstance(vendors, list):
|
||||
return self._json_cors({"error": "vendors must be a list"}, status=400)
|
||||
self._visible_vendors = [str(v).strip() for v in vendors if str(v).strip()]
|
||||
try:
|
||||
import config_loader as _cl
|
||||
_cl.save_visible_vendors(self._visible_vendors)
|
||||
except Exception as e:
|
||||
log.warning(f"save_visible_vendors failed: {e}")
|
||||
return self._json_cors({"error": str(e)}, status=500)
|
||||
return self._json_cors({"result": self._visible_vendors})
|
||||
|
||||
def _load_orca_filaments(self) -> list[dict]:
|
||||
"""Lädt System- + User-Profile aus dem Cache. System-Profile kommen
|
||||
aus bridge/data/orca_filaments.json (Image-embedded), User-Profile
|
||||
@@ -2504,47 +2365,15 @@ class KobraXBridge:
|
||||
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.
|
||||
POST /kx/skip/query → triggert skip/query_obj, gibt zuletzt bekannten
|
||||
Stand zurück (skip/report kommt async, Frontend pollt /kx/skip/state).
|
||||
"""
|
||||
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)
|
||||
|
||||
# skip/report kommt asynchron; kurz warten, damit der Rückgabewert
|
||||
# möglichst den frisch bestätigten Druckerzustand enthält.
|
||||
deadline = time.time() + 1.5
|
||||
while time.time() < deadline:
|
||||
cur_ts = int(self._skip_state.get("ts", 0) or 0)
|
||||
if cur_ts > prev_ts:
|
||||
break
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
return self._json_cors({"result": self._build_skip_state_result()})
|
||||
|
||||
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 = ""
|
||||
if filename:
|
||||
try:
|
||||
f = self._store.get_file_by_name(filename)
|
||||
if f:
|
||||
all_objects = json.loads(f.get("objects_skip_parts") or "[]")
|
||||
svg = f.get("svg_image") or ""
|
||||
except Exception as e:
|
||||
log.warning(f"skip_state lookup failed: {e}")
|
||||
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": self._skip_state})
|
||||
|
||||
async def handle_kx_skip_state(self, request):
|
||||
"""Aktueller Skip-State.
|
||||
@@ -2556,7 +2385,24 @@ class KobraXBridge:
|
||||
nicht die Gesamtliste.
|
||||
- Geskippt: aus self._skip_state (von skip/report aktualisiert).
|
||||
"""
|
||||
result = self._build_skip_state_result()
|
||||
filename = self._state.get("filename", "")
|
||||
all_objects: list[str] = []
|
||||
svg = ""
|
||||
if filename:
|
||||
try:
|
||||
f = self._store.get_file_by_name(filename)
|
||||
if f:
|
||||
all_objects = json.loads(f.get("objects_skip_parts") or "[]")
|
||||
svg = f.get("svg_image") or ""
|
||||
except Exception as e:
|
||||
log.warning(f"skip_state lookup failed: {e}")
|
||||
result = {
|
||||
"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_printers(self, request):
|
||||
@@ -2606,6 +2452,15 @@ class KobraXBridge:
|
||||
excluded_objects = body.get("excluded_objects") or []
|
||||
if not isinstance(excluded_objects, list):
|
||||
excluded_objects = []
|
||||
# Pre-Print-Auswahl sofort im UI-Status spiegeln (wird später durch
|
||||
# skip/report vom Drucker überschrieben/aktualisiert).
|
||||
self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())}
|
||||
if excluded_objects:
|
||||
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
|
||||
self._pending_preprint_skip_deadline = time.time() + 12.0
|
||||
else:
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
|
||||
if assignments:
|
||||
ams_box_mapping, unused_count, invalid_count = self._build_assigned_ams_box_mapping(assignments)
|
||||
@@ -2648,19 +2503,13 @@ class KobraXBridge:
|
||||
"ai_settings": {"status": 0, "count": 0, "type": 1},
|
||||
"timelapse": {"status": 0, "count": 0, "type": 64},
|
||||
"drying_settings": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0},
|
||||
# Firmware-Variante A (funktioniert bei mehreren Builds)
|
||||
"objects_skip_parts": excluded_objects,
|
||||
# Firmware-Variante B (beibehalten für Kompatibilität)
|
||||
"model_objects_skip_parts": excluded_objects,
|
||||
},
|
||||
}
|
||||
|
||||
# UI erst nach echter Drucker-Bestaetigung 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(
|
||||
@@ -2776,16 +2625,18 @@ class KobraXBridge:
|
||||
})
|
||||
return web.json_response({"result": files})
|
||||
|
||||
def _build_file_metadata(self, filename: str) -> dict:
|
||||
"""Baut die Moonraker-file-metadata für eine Datei. Gemeinsame Quelle
|
||||
für HTTP /server/files/metadata UND den WS-RPC server.files.metadata
|
||||
(vorher hatte der WS-Pfad eigene, kaputte Logik mit einer nicht
|
||||
existierenden Store-Methode → leere Antwort → Mobileraker fragte in
|
||||
Endlosschleife, App hing beim Refresh, Issue #48).
|
||||
async def handle_files_metadata(self, request):
|
||||
"""Moonraker /server/files/metadata — moonraker-obico-Plugin holt das
|
||||
einmal pro Druck und liest daraus `object_height` (für `currentZ`-
|
||||
Anzeige im Obico-UI: `mmProgress` braucht maxZ), `layer_count`,
|
||||
`layer_height` und `first_layer_height` (für die Layer-Berechnung).
|
||||
|
||||
Liefert Mobileraker-kompatible Pflichtfelder: `filename`, `size`,
|
||||
`modified` sind in GCodeFile non-nullable; `print_start_time` und die
|
||||
Slicer-Felder optional."""
|
||||
Quelle: aktueller `_state` + GCode-Store-Eintrag wenn vorhanden.
|
||||
Wenn Layer-Heights weder im State noch im Store sind, Fallback auf die
|
||||
OrcaSlicer-Default-Filename-Heuristik (`_layer_height_from_filename`)."""
|
||||
filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "")
|
||||
if not filename:
|
||||
return web.json_response({"result": {}})
|
||||
s = self._state
|
||||
layer_h = float(s.get("layer_height") or 0.0)
|
||||
first_h = float(s.get("first_layer_height") or 0.0)
|
||||
@@ -2809,27 +2660,16 @@ class KobraXBridge:
|
||||
if layer_h and not first_h:
|
||||
first_h = layer_h
|
||||
object_height = round(first_h + max(0, total_layers - 1) * layer_h, 3) if (layer_h and total_layers) else 0.0
|
||||
return {
|
||||
return web.json_response({"result": {
|
||||
"filename": filename,
|
||||
# GCodeFile (Mobileraker) verlangt size als non-nullable int.
|
||||
"size": size_bytes or 1,
|
||||
"size": size_bytes,
|
||||
"modified": time.time(),
|
||||
"estimated_time": est_time or None,
|
||||
"layer_height": layer_h or None,
|
||||
"first_layer_height": first_h or None,
|
||||
"layer_count": total_layers or None,
|
||||
"object_height": object_height or None,
|
||||
"thumbnails": [],
|
||||
}
|
||||
|
||||
async def handle_files_metadata(self, request):
|
||||
"""Moonraker /server/files/metadata — moonraker-obico + Mobileraker
|
||||
holen Datei-Metadaten (Slicer-Zeit, Layer, object_height).
|
||||
Logik in _build_file_metadata (gemeinsam mit WS-RPC)."""
|
||||
filename = request.rel_url.query.get("filename", "") or self._state.get("filename", "")
|
||||
if not filename:
|
||||
return web.json_response({"result": {}})
|
||||
return web.json_response({"result": self._build_file_metadata(filename)})
|
||||
}})
|
||||
|
||||
# ── Moonraker-Stubs für moonraker-obico ──────────────────────────────────
|
||||
async def handle_access_api_key(self, request):
|
||||
@@ -2935,6 +2775,7 @@ class KobraXBridge:
|
||||
ct = request.headers.get("Content-Type", "")
|
||||
if "multipart" not in ct:
|
||||
return web.json_response({"error": "expected multipart"}, status=400)
|
||||
active_print = self._state.get("print_state") in ("printing", "paused")
|
||||
auto_print = False
|
||||
web_upload = False
|
||||
reader = await request.multipart()
|
||||
@@ -2962,29 +2803,20 @@ class KobraXBridge:
|
||||
if not file_data:
|
||||
return web.json_response({"error": "no file received"}, status=400)
|
||||
|
||||
# Nur druckbare Dateien zulassen (Issue #59) — verhindert dass z.B. ein
|
||||
# JPG im File-Browser landet. OrcaSlicer-Uploads sind .gcode/.gcode.3mf,
|
||||
# der Kobra X akzeptiert .gcode/.3mf/.bgcode.
|
||||
_allowed_ext = (".gcode", ".gcode.3mf", ".3mf", ".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)
|
||||
|
||||
# Slicer-Zeitschätzung + Thumbnail aus GCode auslesen
|
||||
est_time = _parse_gcode_estimated_time(file_data)
|
||||
self._state["slicer_time"] = est_time
|
||||
thumbnail_b64 = _extract_thumbnail(file_data)
|
||||
gcode_filaments = _extract_filament_info(file_data)
|
||||
layer_h, first_h = _parse_gcode_layer_heights(file_data)
|
||||
self._state["layer_height"] = layer_h
|
||||
self._state["first_layer_height"] = first_h
|
||||
# Aktiven Druck nicht mit Metadaten eines neu hochgeladenen Files
|
||||
# überschreiben (Fortschrittsansicht muss beim laufenden Job bleiben).
|
||||
if not active_print:
|
||||
self._state["slicer_time"] = est_time
|
||||
self._state["layer_height"] = layer_h
|
||||
self._state["first_layer_height"] = first_h
|
||||
|
||||
# Datei persistent im GCode-Store ablegen
|
||||
self._store.save_file(
|
||||
@@ -3025,9 +2857,12 @@ class KobraXBridge:
|
||||
if not auto_print:
|
||||
auto_print = request.rel_url.query.get("print", "false").lower() == "true"
|
||||
|
||||
# Thumbnail immer anfordern (Drucker antwortet async mit file/report)
|
||||
self._thumbnail_b64 = ""
|
||||
self.client.publish("file", "fileDetails", {"root": "local", "filename": remote_filename}, timeout=0)
|
||||
# Thumbnail nur dann im Runtime-State aktualisieren, wenn kein aktiver
|
||||
# Druck läuft. Sonst könnte das Upload-Thumbnail die laufende
|
||||
# Fortschrittsvorschau verdrängen.
|
||||
if not active_print:
|
||||
self._thumbnail_b64 = ""
|
||||
self.client.publish("file", "fileDetails", {"root": "local", "filename": remote_filename}, timeout=0)
|
||||
|
||||
self._state["last_upload_url"] = serve_url
|
||||
self._state["last_upload_md5"] = file_md5
|
||||
@@ -3065,6 +2900,10 @@ class KobraXBridge:
|
||||
def _start_print(self, filename: str, url: str = "", md5: str = "", filesize: int = 0,
|
||||
gcode_filaments: list | None = None):
|
||||
self._state["file_ready"] = ""
|
||||
# Neuer Druck ohne Pre-Print-Excludes: Skip-State zurücksetzen.
|
||||
self._skip_state = {"skipped": [], "ts": int(time.time())}
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
loaded = self._select_loaded_slots_for_print(warn_on_empty_default=True)
|
||||
|
||||
# Nur die im GCode TATSÄCHLICH genutzten Paints auf Slots mappen. OrcaSlicer
|
||||
@@ -3088,7 +2927,7 @@ class KobraXBridge:
|
||||
use_ams = len(loaded) > 0
|
||||
ams_box_mapping = self._build_auto_ams_box_mapping(loaded_slots=loaded)
|
||||
log.debug(f"AMS-Slots: {len(loaded)} gemappt (genutzte Paints: {used_paint_indices}) → {[i for i, _ in loaded]}")
|
||||
auto_leveling = getattr(self._args, "auto_leveling", 1)
|
||||
auto_leveling = int(body.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
|
||||
payload = {
|
||||
"taskid": "-1",
|
||||
"url": url,
|
||||
@@ -3120,6 +2959,37 @@ class KobraXBridge:
|
||||
else:
|
||||
log.warning("Druckstart: keine Antwort vom Drucker")
|
||||
|
||||
def _apply_preprint_skip_after_start(self, names: list[str], retries: int = 20, delay_s: float = 0.75):
|
||||
wanted = [str(n) for n in (names or []) if isinstance(n, str) and n]
|
||||
if not wanted:
|
||||
return False
|
||||
|
||||
self._pending_preprint_skip = list(wanted)
|
||||
self._pending_preprint_skip_deadline = time.time() + max(3.0, float(retries) * float(delay_s) + 2.0)
|
||||
|
||||
# UI should already reflect intended skips immediately.
|
||||
self._skip_state = {"skipped": list(wanted), "ts": int(time.time())}
|
||||
|
||||
for i in range(max(1, int(retries))):
|
||||
try:
|
||||
if self._state.get("kobra_state") != "printing":
|
||||
time.sleep(max(0.1, float(delay_s)))
|
||||
continue
|
||||
resp = self.client.skip_objects(wanted)
|
||||
if resp is not None:
|
||||
log.info(f"Pre-Print skip applied ({len(wanted)} objects) on attempt {i+1}/{retries}")
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
return True
|
||||
except Exception as e:
|
||||
log.debug(f"Pre-Print skip attempt {i+1}/{retries} failed: {e}")
|
||||
time.sleep(max(0.1, float(delay_s)))
|
||||
|
||||
log.warning(f"Pre-Print skip could not be confirmed after {retries} attempts")
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
return False
|
||||
|
||||
def _theme_index_path(self) -> str:
|
||||
return os.path.join(_WEB_BASE, "web", "themes", self._ui_theme, "index.html")
|
||||
|
||||
@@ -3163,6 +3033,15 @@ class KobraXBridge:
|
||||
excluded_objects = body.get("excluded_objects") or []
|
||||
if not isinstance(excluded_objects, list):
|
||||
excluded_objects = []
|
||||
# Pre-Print-Auswahl sofort im UI-Status spiegeln (wird später durch
|
||||
# skip/report vom Drucker überschrieben/aktualisiert).
|
||||
self._skip_state = {"skipped": list(excluded_objects), "ts": int(time.time())}
|
||||
if excluded_objects:
|
||||
self._pending_preprint_skip = [str(n) for n in excluded_objects if isinstance(n, str) and n]
|
||||
self._pending_preprint_skip_deadline = time.time() + 12.0
|
||||
else:
|
||||
self._pending_preprint_skip = []
|
||||
self._pending_preprint_skip_deadline = 0.0
|
||||
if filament_assignments is not None:
|
||||
ams_box_mapping, unused_count, invalid_count = self._build_assigned_ams_box_mapping(filament_assignments)
|
||||
if unused_count:
|
||||
@@ -3202,19 +3081,13 @@ class KobraXBridge:
|
||||
"ai_settings": {"status": 0, "count": 0, "type": 0},
|
||||
"timelapse": {"status": 0, "count": 0, "type": 0},
|
||||
"drying_settings": {"status": 0, "target_temp": 0, "duration": 0, "remain_time": 0},
|
||||
# Firmware-Variante A (funktioniert bei mehreren Builds)
|
||||
"objects_skip_parts": excluded_objects,
|
||||
# Firmware-Variante B (beibehalten für Kompatibilität)
|
||||
"model_objects_skip_parts": excluded_objects,
|
||||
},
|
||||
}
|
||||
|
||||
# UI erst nach echter Drucker-Bestaetigung 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}"
|
||||
@@ -3252,6 +3125,15 @@ class KobraXBridge:
|
||||
|
||||
async def handle_api_file_ready_clear(self, request):
|
||||
self._state["file_ready"] = ""
|
||||
self._state["filename"] = ""
|
||||
self._state["progress"] = 0.0
|
||||
self._state["print_duration"] = 0
|
||||
self._state["remain_time"] = 0
|
||||
self._state["curr_layer"] = 0
|
||||
self._state["total_layers"] = 0
|
||||
self._state["slicer_time"] = 0
|
||||
self._state["layer_height"] = 0.0
|
||||
self._state["first_layer_height"] = 0.0
|
||||
self._thumbnail_b64 = ""
|
||||
self._push_status_update()
|
||||
return web.json_response({"result": "ok"})
|
||||
@@ -3878,7 +3760,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)),
|
||||
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
|
||||
"version": self._read_version(),
|
||||
})
|
||||
|
||||
@@ -4016,11 +3898,8 @@ class KobraXBridge:
|
||||
"default_ams_slot": getattr(self._args, "default_ams_slot", "auto"),
|
||||
"auto_leveling": getattr(self._args, "auto_leveling", 1),
|
||||
"camera_on_print": getattr(self._args, "camera_on_print", 0),
|
||||
"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,
|
||||
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
|
||||
"ace_dry_presets": self._ace_dry_presets,
|
||||
})
|
||||
|
||||
@@ -4050,15 +3929,10 @@ class KobraXBridge:
|
||||
cfg.set("print", "default_ams_slot", str(data.get("default_ams_slot", getattr(self._args, "default_ams_slot", "auto"))))
|
||||
cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1))))
|
||||
cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0))))))
|
||||
print_start_dialog = int(bool(data.get("print_start_dialog", data.get("file_ready_dialog", getattr(self._args, "print_start_dialog", 0)))))
|
||||
cfg.set("print", "print_start_dialog", str(print_start_dialog))
|
||||
cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1))))))
|
||||
cfg.set("print", "print_start_dialog", str(int(bool(data.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1))))))
|
||||
if "poll_interval" in data:
|
||||
try:
|
||||
pi = max(1, min(60, int(data["poll_interval"])))
|
||||
except (TypeError, ValueError):
|
||||
pi = 3
|
||||
cfg.set("bridge", "poll_interval", str(pi))
|
||||
elif not cfg.has_option("bridge", "poll_interval"):
|
||||
if not cfg.has_option("bridge", "poll_interval"):
|
||||
cfg.set("bridge", "poll_interval", "3")
|
||||
printer_name = str(data.get("printer_name", "")).strip()
|
||||
if printer_name:
|
||||
@@ -4066,6 +3940,9 @@ class KobraXBridge:
|
||||
elif cfg.has_option("bridge", "printer_name"):
|
||||
cfg.remove_option("bridge", "printer_name")
|
||||
|
||||
self._args.print_start_dialog = print_start_dialog
|
||||
self._args.auto_leveling = int(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1)))
|
||||
|
||||
incoming_presets = data.get("ace_dry_presets") if isinstance(data, dict) else None
|
||||
presets = self._sanitize_ace_dry_presets(incoming_presets if isinstance(incoming_presets, dict) else self._ace_dry_presets)
|
||||
for key, val in presets.items():
|
||||
@@ -4223,9 +4100,8 @@ class KobraXBridge:
|
||||
# Bei einem Restart muss environ bereinigt werden, sonst liest der neue Prozess
|
||||
# die alten Werte statt der geänderten config.ini.
|
||||
for _k in ("PRINTER_IP", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD",
|
||||
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
|
||||
"CAMERA_ON_PRINT", "WEB_UPLOAD_WARNING", "PRINT_START_DIALOG",
|
||||
"FILE_READY_DIALOG", "BRIDGE_PRINTER_NAME"):
|
||||
"MODE_ID", "DEVICE_ID", "DEFAULT_AMS_SLOT", "AUTO_LEVELING",
|
||||
"CAMERA_ON_PRINT", "PRINT_START_DIALOG", "FILE_READY_DIALOG", "WEB_UPLOAD_WARNING", "BRIDGE_PRINTER_NAME"):
|
||||
os.environ.pop(_k, None)
|
||||
|
||||
in_docker = os.path.exists("/.dockerenv") or os.environ.get("KX_IN_DOCKER")
|
||||
@@ -4723,13 +4599,23 @@ class KobraXBridge:
|
||||
elif method == "machine.update.status":
|
||||
result = {"busy": False, "version_info": {}}
|
||||
elif method == "server.files.metadata":
|
||||
# Obico + Mobileraker fragen Metadaten zu einer Datei. Dieselbe
|
||||
# Logik wie der HTTP-Endpoint (vorher eigener kaputter Pfad mit
|
||||
# nicht existierender Store-Methode → leere Antwort →
|
||||
# Mobileraker-Endlosschleife, Issue #48).
|
||||
# Obico fragt nach Metadaten zu einer Datei (filename in params)
|
||||
fname = (params or {}).get("filename") if isinstance(params, dict) else None
|
||||
fname = fname or self._state.get("filename", "")
|
||||
result = self._build_file_metadata(fname) if fname else {}
|
||||
meta = {}
|
||||
if fname:
|
||||
try:
|
||||
rec = self._store.get_file_by_filename(fname) if hasattr(self._store, "get_file_by_filename") else None
|
||||
except Exception:
|
||||
rec = None
|
||||
if rec:
|
||||
meta = {
|
||||
"filename": rec.get("filename"),
|
||||
"size": rec.get("size_bytes") or 0,
|
||||
"modified": time.time(),
|
||||
"estimated_time": rec.get("est_print_time_sec") or 0,
|
||||
"thumbnails": [],
|
||||
}
|
||||
result = meta
|
||||
else:
|
||||
log.debug(f"Unbekannte RPC-Methode: {method}")
|
||||
result = {}
|
||||
@@ -4933,8 +4819,6 @@ def build_app(bridge: KobraXBridge) -> web.Application:
|
||||
r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots)
|
||||
r.add_get("/kx/filament/profiles", bridge.handle_kx_filament_profiles)
|
||||
r.add_post("/kx/filament/slots/{idx}/profile", bridge.handle_kx_filament_slot_profile)
|
||||
r.add_get("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors)
|
||||
r.add_post("/kx/filament/visible_vendors", bridge.handle_kx_visible_vendors)
|
||||
# Custom-Profile-Import (Issue #41) — User lädt eigene Orca-Filament-
|
||||
# Profile als ZIP/JSON hoch (z.B. aus ~/.config/OrcaSlicer/user/<id>/filament/),
|
||||
# weil die Bridge typischerweise nicht auf demselben Host wie OrcaSlicer läuft.
|
||||
@@ -5109,9 +4993,9 @@ def main():
|
||||
parser.add_argument("--default-ams-slot",default=env_loader.DEFAULT_AMS_SLOT)
|
||||
parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING)
|
||||
parser.add_argument("--camera-on-print", type=int, default=env_loader.CAMERA_ON_PRINT)
|
||||
parser.add_argument("--print-start-dialog", type=int, default=env_loader.PRINT_START_DIALOG)
|
||||
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
|
||||
parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
|
||||
parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG)
|
||||
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
|
||||
|
||||
parser.add_argument("--host", default="0.0.0.0",
|
||||
help="Bind-Adresse für den Bridge-Server")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -42,12 +42,122 @@
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="theme-btn" onclick="showPanel('settings')" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title="">⚙</button>
|
||||
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
||||
</header>
|
||||
|
||||
<!-- ═══ SETTINGS MODAL ═══ -->
|
||||
<!-- Settings-Modal entfernt — jetzt #panel-settings (Master-Detail im Main-Bereich) -->
|
||||
<div class="modal-overlay" id="settings-modal" onclick="if(event.target===this)closeSettings()">
|
||||
<div class="modal-box">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="modal-title-settings">Einstellungen</span>
|
||||
<button class="modal-close" onclick="closeSettings()">✕</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-field" style="margin-bottom:12px">
|
||||
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
|
||||
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
|
||||
</div>
|
||||
<div class="modal-section" id="modal-sec-connection">Verbindung</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-printer-ip">Drucker-IP</label>
|
||||
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
|
||||
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mqtt-port">MQTT-Port</label>
|
||||
<input type="number" id="s-mqtt-port" placeholder="9883">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-username">MQTT-Benutzername</label>
|
||||
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-password">MQTT-Passwort</label>
|
||||
<input type="password" id="s-password" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-device-id">Device-ID</label>
|
||||
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mode-id">Mode-ID</label>
|
||||
<input type="text" id="s-mode-id" placeholder="20030">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-print">Druckeinstellungen</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-file-ready-mode"></label>
|
||||
<select id="s-file-ready-mode">
|
||||
<option value="dialog" id="opt-file-ready-dialog"></option>
|
||||
<option value="banner" id="opt-file-ready-banner"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
|
||||
<select id="s-default-slot">
|
||||
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
|
||||
<option value="0" id="opt-slot-0">Slot 1</option>
|
||||
<option value="1" id="opt-slot-1">Slot 2</option>
|
||||
<option value="2" id="opt-slot-2">Slot 3</option>
|
||||
<option value="3" id="opt-slot-3">Slot 4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
||||
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
|
||||
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-poll">Poll-Intervall</div>
|
||||
<div class="poll-btns">
|
||||
<button class="poll-btn" onclick="setPoll(1000)" id="poll-1">1s</button>
|
||||
<button class="poll-btn active" onclick="setPoll(2000)" id="poll-2">2s</button>
|
||||
<button class="poll-btn" onclick="setPoll(5000)" id="poll-5">5s</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OrcaSlicer-Profile (Custom-Profile-Import, Issue #41) -->
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-orca-profiles">OrcaSlicer-Profile</div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
|
||||
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
|
||||
</div>
|
||||
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
|
||||
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
|
||||
style="background:var(--raised);color:var(--txt)">
|
||||
⬆ <span id="lbl-orca-profiles-import">Profile importieren</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-version">Version</div>
|
||||
<div class="update-row">
|
||||
<span id="s-version-label" style="font-size:13px;color:var(--txt)">–</span>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
|
||||
</div>
|
||||
<div class="update-status" id="update-status" style="margin-top:6px"></div>
|
||||
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
|
||||
<span id="lbl-update-apply">Jetzt installieren</span>
|
||||
</button>
|
||||
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
|
||||
</div>
|
||||
|
||||
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings">Speichern & Neustart</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ AMS SLOT EDIT DIALOG ═══ -->
|
||||
<div class="modal-overlay" id="slot-edit-modal" onclick="if(event.target===this)closeSlotEdit()">
|
||||
@@ -127,8 +237,6 @@
|
||||
<span class="nav-icon">🗂</span><span class="nav-text">Browser</span></button>
|
||||
<button class="nav-btn" onclick="showPanel('console');clearLogBadge()" id="nb-console">
|
||||
<span class="nav-icon">≡</span><span class="nav-text">Konsole</span><span id="log-badge" style="display:none;margin-left:4px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 5px;font-weight:700"></span></button>
|
||||
<button class="nav-btn" onclick="showPanel('settings')" id="nb-settings">
|
||||
<span class="nav-icon">⚙</span><span class="nav-text" id="nav-settings">Einstellungen</span></button>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
@@ -192,17 +300,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="fname" id="d-fname" title="" style="margin-top:6px">–</div>
|
||||
<div class="ctrl-btns" id="d-ready-btns" style="margin-top:12px;display:none">
|
||||
<button class="btn btn-accent btn-sm" id="d-btn-ready-print" onclick="startProgressReadyFile()">▶ Print</button>
|
||||
<button class="btn btn-cancel btn-sm" id="d-btn-ready-clear" onclick="clearProgressReadyFile()">✕ Clear</button>
|
||||
</div>
|
||||
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
|
||||
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
|
||||
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none">✂ <span id="d-btn-skip-label">Objekte</span></button>
|
||||
<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 -->
|
||||
@@ -395,199 +501,36 @@
|
||||
<div class="card">
|
||||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span><span>≡</span> <span id="ptitle-console">Ereignis-Log</span></span>
|
||||
<a id="btn-log-dl" href="/api/log/download" download
|
||||
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none">⬇ Download</a>
|
||||
<a id="btn-log-dl" href="/api/log/download" download
|
||||
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none"></a>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;align-items:center">
|
||||
<input id="log-filter" type="text" placeholder="Filter…"
|
||||
<input id="log-filter" type="text" placeholder=""
|
||||
oninput="renderLog()"
|
||||
style="flex:1;min-width:120px;padding:5px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;font-family:var(--mono)">
|
||||
<button id="btn-autoscroll" onclick="toggleAutoScroll()"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap">⬇ Auto</button>
|
||||
<button onclick="consoleLogs=[];renderLog()"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">✕ Clear</button>
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap"></button>
|
||||
<button id="btn-log-clear" onclick="consoleLogs=[];renderLog()"
|
||||
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
</div>
|
||||
<div style="display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap">
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px">Dir:</span>
|
||||
<span id="log-lbl-dir" style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px"></span>
|
||||
<button class="log-dir-btn active" id="logdir-all" onclick="setLogDir('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">RX</button>
|
||||
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">TX</button>
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level">Level:</span>
|
||||
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level"></span>
|
||||
<button class="log-lvl-btn active" id="loglvl-all" onclick="setLogLevel('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⛔ Errors</button>
|
||||
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⚠ Warn</button>
|
||||
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px">Topic:</span>
|
||||
<button class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">AMS</button>
|
||||
<button class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">print</button>
|
||||
<button class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">info</button>
|
||||
<button class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">status</button>
|
||||
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<span id="log-lbl-topic" style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px"></span>
|
||||
<button id="log-topic-ams" class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button id="log-topic-print" class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button id="log-topic-info" class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
<button id="log-topic-status" class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||
</div>
|
||||
<div class="console" id="console-log" style="height:calc(100vh - 260px);min-height:160px" onscroll="onLogScroll()"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ EINSTELLUNGEN ═══ -->
|
||||
<div class="panel" id="panel-settings">
|
||||
<div class="settings-wrap">
|
||||
<div class="settings-cats">
|
||||
<button class="set-cat active" id="setcat-connection" onclick="showSettingsCat('connection')"><span>🔌</span> <span id="setcat-lbl-connection">Verbindung</span></button>
|
||||
<button class="set-cat" id="setcat-printer" onclick="showSettingsCat('printer')"><span>🖨</span> <span id="setcat-lbl-printer">Drucker</span></button>
|
||||
<button class="set-cat" id="setcat-display" onclick="showSettingsCat('display')"><span>🎨</span> <span id="setcat-lbl-display">Darstellung</span></button>
|
||||
<button class="set-cat" id="setcat-filament" onclick="showSettingsCat('filament')"><span>🧵</span> <span id="setcat-lbl-filament">Filament</span></button>
|
||||
<button class="set-cat" id="setcat-system" onclick="showSettingsCat('system')"><span>⚙</span> <span id="setcat-lbl-system">System</span></button>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<!-- Verbindung -->
|
||||
<div class="set-group active" id="setgrp-connection">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🔌</span> <span id="modal-sec-connection">Verbindung</span></div>
|
||||
<div class="modal-field" style="margin-bottom:12px">
|
||||
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
|
||||
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-printer-ip">Drucker-IP</label>
|
||||
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
|
||||
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mqtt-port">MQTT-Port</label>
|
||||
<input type="number" id="s-mqtt-port" placeholder="9883">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-username">MQTT-Benutzername</label>
|
||||
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-password">MQTT-Passwort</label>
|
||||
<input type="password" id="s-password" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-device-id">Device-ID</label>
|
||||
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-mode-id">Mode-ID</label>
|
||||
<input type="text" id="s-mode-id" placeholder="20030">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drucker -->
|
||||
<div class="set-group" id="setgrp-printer">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🖨</span> <span id="modal-sec-print">Druckeinstellungen</span></div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
|
||||
<select id="s-default-slot">
|
||||
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
|
||||
<option value="0" id="opt-slot-0">Slot 1</option>
|
||||
<option value="1" id="opt-slot-1">Slot 2</option>
|
||||
<option value="2" id="opt-slot-2">Slot 3</option>
|
||||
<option value="3" id="opt-slot-3">Slot 4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
||||
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-file-ready-mode">Nach Upload: Druckstart-Verhalten</label>
|
||||
<select id="s-file-ready-mode">
|
||||
<option value="1" id="opt-file-ready-dialog">Print-Dialog</option>
|
||||
<option value="0" id="opt-file-ready-banner">Print-Leiste</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
|
||||
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Darstellung -->
|
||||
<div class="set-group" id="setgrp-display">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🎨</span> <span id="setcat-lbl-display2">Darstellung</span></div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-set-lang">Sprache</label>
|
||||
<select id="s-lang-select" onchange="setLanguage(this.value)">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Espanol</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="toggleTheme()"><span id="lbl-set-theme">Hell / Dunkel umschalten</span></button>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<label id="lbl-poll-interval">Poll-Intervall (Sekunden)</label>
|
||||
<input type="number" id="s-poll-interval" min="1" max="60" step="1" placeholder="3" oninput="onPollIntervalInput()">
|
||||
<small style="color:var(--txt2)" id="lbl-poll-hint">Wie oft die Bridge den Drucker-Status abfragt</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filament -->
|
||||
<div class="set-group" id="setgrp-filament">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🧵</span> <span id="modal-sec-orca-profiles">OrcaSlicer-Profile</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="orca-profiles-hint">
|
||||
Eigene Profile aus OrcaSlicer importieren (User-Dir öffnen via Help → Show Configuration Folder)
|
||||
</div>
|
||||
<div id="orca-profiles-list" style="margin-bottom:8px;font-size:12px;color:var(--txt2)"></div>
|
||||
<button class="btn btn-sm" id="btn-orca-profiles-import" onclick="openProfileImport()"
|
||||
style="background:var(--raised);color:var(--txt)">
|
||||
⬆ <span id="lbl-orca-profiles-import">Profile importieren</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span>🎯</span> <span id="lbl-filament-mapping">Filament-Profil-Mapping (pro Slot)</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="filament-mapping-hint">
|
||||
Festes Orca-Profil pro AMS-Slot. Beim Slicer-Sync sendet die Bridge dieses Profil statt „Generic".
|
||||
</div>
|
||||
<div id="filament-mapping-list"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveFilamentMapping()"><span id="lbl-filament-mapping-save">Mapping speichern</span></button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title"><span>👁</span> <span id="lbl-visible-vendors">Sichtbare Hersteller (Profil-Dropdown)</span></div>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:8px" id="visible-vendors-hint">
|
||||
Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.
|
||||
</div>
|
||||
<input type="text" id="vendor-filter-search" placeholder="Hersteller suchen…" oninput="renderVendorChecklist()"
|
||||
style="width:100%;padding:6px 10px;margin-bottom:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px">
|
||||
<div id="visible-vendors-list" style="max-height:260px;overflow-y:auto;border:1px solid var(--border);border-radius:6px;padding:8px"></div>
|
||||
<button class="btn btn-sm" style="background:var(--accent);color:#fff;margin-top:8px" onclick="saveVisibleVendors()"><span id="lbl-visible-vendors-save">Auswahl speichern</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System -->
|
||||
<div class="set-group" id="setgrp-system">
|
||||
<div class="card">
|
||||
<div class="card-title"><span>⚙</span> <span id="modal-sec-version">Version</span></div>
|
||||
<div class="update-row">
|
||||
<span id="s-version-label" style="font-size:13px;color:var(--txt)">–</span>
|
||||
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
|
||||
</div>
|
||||
<div class="update-status" id="update-status" style="margin-top:6px"></div>
|
||||
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
|
||||
<span id="lbl-update-apply">Jetzt installieren</span>
|
||||
</button>
|
||||
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings" style="margin-top:14px">Speichern & Neustart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -596,14 +539,13 @@
|
||||
<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>
|
||||
|
||||
|
||||
<!-- Web-Upload-Verify-Dialog -->
|
||||
<div class="modal-overlay" id="store-web-verify-dialog" onclick="if(event.target===this)closeStoreWebVerifyDialog()">
|
||||
<div class="modal-box" style="max-width:420px;width:100%">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<div class="modal-header" style="margin-bottom:12px">
|
||||
<span class="modal-title" id="store-web-verify-title">Datei verifizieren</span>
|
||||
<button onclick="closeStoreWebVerifyDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
@@ -618,31 +560,24 @@
|
||||
|
||||
<!-- Filament-Slot-Dialog -->
|
||||
<div class="modal-overlay" id="filament-dialog" onclick="if(event.target===this)closeFilamentDialog()">
|
||||
<div class="modal-box" style="max-width:380px;width:100%">
|
||||
<div class="modal-box" style="max-width:380px;width:100%;gap:0">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<span class="modal-title" id="fd-title" style="font-size:14px;word-break:break-all"></span>
|
||||
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
|
||||
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
|
||||
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
|
||||
<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 id="fd-options-section" style="margin-bottom:6px">
|
||||
<p id="fd-options-title" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0"></p>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px;padding:8px 10px;background:var(--raised);border:1px solid var(--border);border-radius:8px">
|
||||
<input type="checkbox" id="fd-auto-leveling" style="width:auto;margin:0" onchange="_printOptionsAutoLeveling=this.checked?1:0">
|
||||
<label id="lbl-fd-auto-leveling" style="margin:0;cursor:pointer;line-height:1.2" for="fd-auto-leveling"></label>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0">GCode-Kanal → AMS-Slot zuweisen:</p>
|
||||
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
|
||||
<div id="fd-objects-section" style="display:none;margin-bottom:14px">
|
||||
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin:0 0 6px 0">Objekte überspringen (optional):</p>
|
||||
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
|
||||
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -680,7 +615,7 @@
|
||||
<div id="skip-list" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;margin-bottom:12px"></div>
|
||||
<div id="skip-status" style="font-size:12px;color:var(--txt2);min-height:16px;margin-bottom:8px"></div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="skip-cancel" onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer"></button>
|
||||
<button id="skip-confirm" onclick="confirmSkip()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Überspringen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -716,15 +651,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:8px">
|
||||
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA+</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PETG</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">TPU</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">ABS / ASA</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PA / PC</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 1</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 2</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 3</button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer"></button>
|
||||
</div>
|
||||
<div id="ace-dry-dialog-custom-name-row" style="display:none;align-items:center;gap:12px;margin-bottom:14px">
|
||||
<label id="ace-dry-dialog-custom-name-label" style="min-width:190px;font-size:12px;color:var(--txt)">Custom Name</label>
|
||||
|
||||
@@ -212,24 +212,6 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
|
||||
.panel{display:none}
|
||||
.panel.active{display:block}
|
||||
|
||||
/* ── SETTINGS (Master-Detail) ── */
|
||||
.settings-wrap{display:grid;grid-template-columns:200px 1fr;gap:16px;align-items:start}
|
||||
.settings-cats{display:flex;flex-direction:column;gap:4px;position:sticky;top:12px}
|
||||
.set-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;
|
||||
border:1px solid transparent;background:var(--raised);color:var(--txt2);cursor:pointer;
|
||||
font-size:13px;text-align:left;transition:background .15s,color .15s}
|
||||
.set-cat:hover{color:var(--txt)}
|
||||
.set-cat.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||
.set-group{display:none}
|
||||
.set-group.active{display:block}
|
||||
.set-group .card{margin-bottom:14px}
|
||||
@media(max-width:768px){
|
||||
.settings-wrap{grid-template-columns:1fr}
|
||||
.settings-cats{flex-direction:row;flex-wrap:wrap;position:static;overflow-x:auto}
|
||||
.set-cat{flex:1;min-width:auto;justify-content:center;padding:8px 6px;font-size:12px}
|
||||
.set-cat .nav-text{display:inline}
|
||||
}
|
||||
|
||||
/* ── FILE BROWSER UPLOAD ZONE ── */
|
||||
#store-upload-zone{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
|
||||
@@ -69,6 +69,13 @@
|
||||
"ace_dry_dialog_save_restart": "Speichern & Neustart",
|
||||
"ace_dry_dialog_custom_name": "Eigener Name",
|
||||
"ace_dry_dialog_reset_default": "Auf Standard zurücksetzen",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Custom",
|
||||
"cam_placeholder": "📷 Kamera nicht gestartet",
|
||||
"cam_stream_unavailable": "Stream nicht verfügbar",
|
||||
"btn_cam_start": "▶ Kamera",
|
||||
@@ -127,21 +134,8 @@
|
||||
"settings_title": "Einstellungen",
|
||||
"settings_connection": "Verbindung",
|
||||
"settings_print": "Druckeinstellungen",
|
||||
"settings_poll": "Poll-Intervall (Sekunden)",
|
||||
"settings_poll": "Poll-Intervall",
|
||||
"settings_version": "Version",
|
||||
"nav_settings": "Einstellungen",
|
||||
"settings_cat_display": "Darstellung",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Sprache",
|
||||
"settings_cat_theme": "Hell / Dunkel umschalten",
|
||||
"settings_filament_mapping": "Filament-Profil-Mapping (pro Slot)",
|
||||
"settings_filament_mapping_save": "Mapping speichern",
|
||||
"settings_visible_vendors": "Sichtbare Hersteller (Profil-Dropdown)",
|
||||
"settings_visible_vendors_hint": "Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic\" und eigene Profile sind immer sichtbar.",
|
||||
"settings_visible_vendors_save": "Auswahl speichern",
|
||||
"progress_action_print": "Drucken",
|
||||
"progress_action_slots": "Slots zuordnen",
|
||||
"progress_action_clear": "Leeren",
|
||||
"settings_save": "Speichern & Neustart",
|
||||
"settings_printer_name": "Drucker-Name",
|
||||
"settings_printer_ip": "Drucker-IP",
|
||||
@@ -154,7 +148,12 @@
|
||||
"settings_default_slot": "Standard-Slot (Einfarbdruck)",
|
||||
"settings_slot_auto": "Auto (alle belegten Slots)",
|
||||
"settings_auto_leveling": "Auto-Leveling vor Druck",
|
||||
"fd_options_title": "Optionen",
|
||||
"print_auto_leveling": "Auto-Leveling für diesen Druck",
|
||||
"settings_camera_on_print": "Kamera bei Druckstart einschalten",
|
||||
"settings_file_ready_mode": "Druckdialog starten",
|
||||
"settings_file_ready_banner": "Druckleiste",
|
||||
"settings_file_ready_dialog": "Druckdialog",
|
||||
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
|
||||
"update_check": "Auf Updates prüfen",
|
||||
"update_checking": "Prüfe...",
|
||||
@@ -192,7 +191,21 @@
|
||||
"orca_profile_done": "Importiert",
|
||||
"orca_profile_skipped": "übersprungen",
|
||||
"log_dir_all": "Alle",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Richtung:",
|
||||
"log_lvl_label": "Level:",
|
||||
"log_lvl_err": "⛔ Fehler",
|
||||
"log_lvl_warn": "⚠ Warnung",
|
||||
"log_topic_label": "Thema:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Druck",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Status",
|
||||
"log_download": "⬇ Download",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Leeren",
|
||||
"log_filter_placeholder": "Filtern…",
|
||||
"file_ready_btn": "▶ Druck starten",
|
||||
"file_slots_btn": "🎨 Slots wählen",
|
||||
"file_cancel_btn": "✕ Abbrechen",
|
||||
@@ -205,8 +218,9 @@
|
||||
"skip_select_at_least_one": "Bitte mindestens ein Objekt wählen.",
|
||||
"skip_sending": "Sende …",
|
||||
"skip_success": "Objekte werden übersprungen.",
|
||||
"skip_cancel": "Abbrechen",
|
||||
"skip_confirm": "Überspringen",
|
||||
"fd_objects_hint": "Objekte überspringen (optional):",
|
||||
"fd_objects_toggle": "Objekte überspringen",
|
||||
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
|
||||
"fd_cancel": "Abbrechen",
|
||||
"fd_print": "▶ Drucken",
|
||||
@@ -252,40 +266,11 @@
|
||||
"store_upload_busy": "⏳ Hochladen…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Nur GCode-Dateien erlaubt (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "Alle",
|
||||
"sf_ok": "✓ Erfolgreich",
|
||||
"sf_err": "✗ Fehler",
|
||||
"sf_new": "Neu",
|
||||
"ss_date": "↓ Datum",
|
||||
"ss_name": "A–Z Name",
|
||||
"ss_dur": "⏱ Druckzeit",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Custom",
|
||||
"fd_options_title": "Optionen",
|
||||
"print_auto_leveling": "Auto-Leveling für diesen Druck",
|
||||
"settings_file_ready_mode": "Druckdialog starten",
|
||||
"settings_file_ready_banner": "Druckleiste",
|
||||
"settings_file_ready_dialog": "Druckdialog",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Richtung:",
|
||||
"log_lvl_err": "⛔ Fehler",
|
||||
"log_lvl_warn": "⚠ Warnung",
|
||||
"log_topic_label": "Thema:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Druck",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Status",
|
||||
"log_download": "⬇ Download",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Leeren",
|
||||
"log_filter_placeholder": "Filtern…",
|
||||
"skip_cancel": "Abbrechen",
|
||||
"skip_confirm": "Überspringen"
|
||||
"ss_dur": "⏱ Druckzeit"
|
||||
}
|
||||
|
||||
@@ -134,20 +134,7 @@
|
||||
"settings_title": "Settings",
|
||||
"settings_connection": "Connection",
|
||||
"settings_print": "Print Settings",
|
||||
"settings_poll": "Poll Interval (seconds)",
|
||||
"nav_settings": "Settings",
|
||||
"settings_cat_display": "Appearance",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Language",
|
||||
"settings_cat_theme": "Toggle light / dark",
|
||||
"settings_filament_mapping": "Filament profile mapping (per slot)",
|
||||
"settings_filament_mapping_save": "Save mapping",
|
||||
"settings_visible_vendors": "Visible vendors (profile dropdown)",
|
||||
"settings_visible_vendors_hint": "Only these vendors appear in the slot profile dropdown. Nothing selected = show all. \"Generic\" and your own profiles are always visible.",
|
||||
"settings_visible_vendors_save": "Save selection",
|
||||
"progress_action_print": "Print",
|
||||
"progress_action_slots": "Map slots",
|
||||
"progress_action_clear": "Clear",
|
||||
"settings_poll": "Poll Interval",
|
||||
"settings_version": "Version",
|
||||
"settings_save": "Save & Restart",
|
||||
"settings_printer_name": "Printer Name",
|
||||
@@ -163,10 +150,10 @@
|
||||
"settings_auto_leveling": "Auto-Leveling Default",
|
||||
"fd_options_title": "Print Options",
|
||||
"print_auto_leveling": "Auto-Leveling",
|
||||
"settings_camera_on_print": "Turn camera on at print start",
|
||||
"settings_file_ready_mode": "Start Print Behavior",
|
||||
"settings_file_ready_banner": "Print Bar",
|
||||
"settings_file_ready_dialog": "Print Dialog",
|
||||
"settings_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...",
|
||||
@@ -228,13 +215,12 @@
|
||||
"skip_btn_label": "Objects",
|
||||
"skip_no_objects": "No objects in this print.",
|
||||
"skip_already": "skipped",
|
||||
"skip_cancel": "Cancel",
|
||||
"skip_confirm": "Skip",
|
||||
"skip_select_at_least_one": "Please pick at least one object.",
|
||||
"skip_sending": "Sending …",
|
||||
"skip_success": "Objects will be skipped.",
|
||||
"skip_cancel": "Cancel",
|
||||
"skip_confirm": "Skip",
|
||||
"fd_objects_hint": "Skip objects (optional):",
|
||||
"fd_objects_toggle": "Skip objects",
|
||||
"fd_slots_hint": "Assign GCode channel to AMS slot:",
|
||||
"fd_cancel": "Cancel",
|
||||
"fd_print": "▶ Print",
|
||||
@@ -280,7 +266,6 @@
|
||||
"store_upload_busy": "⏳ Uploading…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Only GCode files allowed (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "All",
|
||||
"sf_ok": "✓ Completed",
|
||||
"sf_err": "✗ Failed",
|
||||
|
||||
@@ -69,6 +69,13 @@
|
||||
"ace_dry_dialog_save_restart": "Guardar y reiniciar",
|
||||
"ace_dry_dialog_custom_name": "Nombre personalizado",
|
||||
"ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Personalizado",
|
||||
"cam_placeholder": "📷 Cámara no iniciada",
|
||||
"cam_stream_unavailable": "Stream no disponible",
|
||||
"btn_cam_start": "▶ Cámara",
|
||||
@@ -127,20 +134,7 @@
|
||||
"settings_title": "Configuración",
|
||||
"settings_connection": "Conexión",
|
||||
"settings_print": "Ajustes de impresión",
|
||||
"settings_poll": "Intervalo de sondeo (segundos)",
|
||||
"nav_settings": "Ajustes",
|
||||
"settings_cat_display": "Apariencia",
|
||||
"settings_cat_filament": "Filamento",
|
||||
"settings_cat_language": "Idioma",
|
||||
"settings_cat_theme": "Alternar claro / oscuro",
|
||||
"settings_filament_mapping": "Asignación de perfil de filamento (por ranura)",
|
||||
"settings_filament_mapping_save": "Guardar asignación",
|
||||
"settings_visible_vendors": "Fabricantes visibles (lista de perfiles)",
|
||||
"settings_visible_vendors_hint": "Solo estos fabricantes aparecen en la lista de perfiles de ranura. Nada seleccionado = mostrar todos. «Generic» y tus propios perfiles siempre son visibles.",
|
||||
"settings_visible_vendors_save": "Guardar selección",
|
||||
"progress_action_print": "Imprimir",
|
||||
"progress_action_slots": "Asignar ranuras",
|
||||
"progress_action_clear": "Vaciar",
|
||||
"settings_poll": "Intervalo de sondeo",
|
||||
"settings_version": "Versión",
|
||||
"settings_save": "Guardar y reiniciar",
|
||||
"settings_printer_name": "Nombre de impresora",
|
||||
@@ -154,7 +148,12 @@
|
||||
"settings_default_slot": "Ranura predeterminada (un color)",
|
||||
"settings_slot_auto": "Auto (todos los slots cargados)",
|
||||
"settings_auto_leveling": "Autonivelado antes de imprimir",
|
||||
"fd_options_title": "Opciones",
|
||||
"print_auto_leveling": "Autonivelado para esta impresión",
|
||||
"settings_camera_on_print": "Encender cámara al iniciar impresión",
|
||||
"settings_file_ready_mode": "Iniciar diálogo de impresión",
|
||||
"settings_file_ready_banner": "Barra de impresión",
|
||||
"settings_file_ready_dialog": "Diálogo de impresión",
|
||||
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
|
||||
"update_check": "Buscar actualizaciones",
|
||||
"update_checking": "Comprobando...",
|
||||
@@ -192,7 +191,21 @@
|
||||
"orca_profile_done": "Importado",
|
||||
"orca_profile_skipped": "omitido",
|
||||
"log_dir_all": "Todos",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dirección:",
|
||||
"log_lvl_label": "Nivel:",
|
||||
"log_lvl_err": "⛔ Errores",
|
||||
"log_lvl_warn": "⚠ Avisos",
|
||||
"log_topic_label": "Tema:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Impresión",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Estado",
|
||||
"log_download": "⬇ Descargar",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Limpiar",
|
||||
"log_filter_placeholder": "Filtrar…",
|
||||
"file_ready_btn": "▶ Iniciar impresión",
|
||||
"file_slots_btn": "🎨 Seleccionar ranuras",
|
||||
"file_cancel_btn": "✕ Cancelar",
|
||||
@@ -205,8 +218,9 @@
|
||||
"skip_select_at_least_one": "Selecciona al menos un objeto.",
|
||||
"skip_sending": "Enviando …",
|
||||
"skip_success": "Se omitirán los objetos.",
|
||||
"skip_cancel": "Cancelar",
|
||||
"skip_confirm": "Omitir",
|
||||
"fd_objects_hint": "Omitir objetos (opcional):",
|
||||
"fd_objects_toggle": "Omitir objetos",
|
||||
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
|
||||
"fd_cancel": "Cancelar",
|
||||
"fd_print": "▶ Imprimir",
|
||||
@@ -252,40 +266,11 @@
|
||||
"store_upload_busy": "⏳ Subiendo…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Solo se permiten archivos GCode (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "Todos",
|
||||
"sf_ok": "✓ Completado",
|
||||
"sf_err": "✗ Fallido",
|
||||
"sf_new": "Nuevo",
|
||||
"ss_date": "↓ Fecha",
|
||||
"ss_name": "A–Z Nombre",
|
||||
"ss_dur": "⏱ Tiempo de impresión",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Personalizado",
|
||||
"fd_options_title": "Opciones",
|
||||
"print_auto_leveling": "Autonivelado para esta impresión",
|
||||
"settings_file_ready_mode": "Iniciar diálogo de impresión",
|
||||
"settings_file_ready_banner": "Barra de impresión",
|
||||
"settings_file_ready_dialog": "Diálogo de impresión",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Dirección:",
|
||||
"log_lvl_err": "⛔ Errores",
|
||||
"log_lvl_warn": "⚠ Avisos",
|
||||
"log_topic_label": "Tema:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Impresión",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Estado",
|
||||
"log_download": "⬇ Descargar",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Limpiar",
|
||||
"log_filter_placeholder": "Filtrar…",
|
||||
"skip_cancel": "Cancelar",
|
||||
"skip_confirm": "Omitir"
|
||||
"ss_dur": "⏱ Tiempo de impresión"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,13 @@
|
||||
"ace_dry_dialog_save_restart": "Enregistrer et redémarrer",
|
||||
"ace_dry_dialog_custom_name": "Nom personnalisé",
|
||||
"ace_dry_dialog_reset_default": "Réinitialiser",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Personnalisé",
|
||||
"cam_placeholder": "📷 Caméra non démarrée",
|
||||
"cam_stream_unavailable": "Flux indisponible",
|
||||
"btn_cam_start": "▶ Caméra",
|
||||
@@ -127,20 +134,7 @@
|
||||
"settings_title": "Paramètres",
|
||||
"settings_connection": "Connexion",
|
||||
"settings_print": "Paramètres d'impression",
|
||||
"settings_poll": "Intervalle de sondage (secondes)",
|
||||
"nav_settings": "Paramètres",
|
||||
"settings_cat_display": "Apparence",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Langue",
|
||||
"settings_cat_theme": "Basculer clair / sombre",
|
||||
"settings_filament_mapping": "Mappage du profil de filament (par emplacement)",
|
||||
"settings_filament_mapping_save": "Enregistrer le mappage",
|
||||
"settings_visible_vendors": "Fabricants visibles (liste des profils)",
|
||||
"settings_visible_vendors_hint": "Seuls ces fabricants apparaissent dans la liste des profils d'emplacement. Rien de sélectionné = tout afficher. « Generic » et vos propres profils sont toujours visibles.",
|
||||
"settings_visible_vendors_save": "Enregistrer la sélection",
|
||||
"progress_action_print": "Imprimer",
|
||||
"progress_action_slots": "Affecter les emplacements",
|
||||
"progress_action_clear": "Vider",
|
||||
"settings_poll": "Intervalle de sondage",
|
||||
"settings_version": "Version",
|
||||
"settings_save": "Enregistrer et redémarrer",
|
||||
"settings_printer_name": "Nom de l'imprimante",
|
||||
@@ -154,7 +148,12 @@
|
||||
"settings_default_slot": "Slot par défaut (couleur unique)",
|
||||
"settings_slot_auto": "Auto (tous les slots chargés)",
|
||||
"settings_auto_leveling": "Mise à niveau auto avant impression",
|
||||
"fd_options_title": "Options",
|
||||
"print_auto_leveling": "Mise à niveau auto pour cette impression",
|
||||
"settings_camera_on_print": "Activer la caméra au démarrage de l'impression",
|
||||
"settings_file_ready_mode": "Démarrer le dialogue d'impression",
|
||||
"settings_file_ready_banner": "Barre d'impression",
|
||||
"settings_file_ready_dialog": "Dialogue d'impression",
|
||||
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
|
||||
"update_check": "Vérifier les mises à jour",
|
||||
"update_checking": "Vérification…",
|
||||
@@ -192,7 +191,21 @@
|
||||
"orca_profile_done": "Importé",
|
||||
"orca_profile_skipped": "ignoré",
|
||||
"log_dir_all": "Tout",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Sens :",
|
||||
"log_lvl_label": "Niveau :",
|
||||
"log_lvl_err": "⛔ Erreurs",
|
||||
"log_lvl_warn": "⚠ Avert.",
|
||||
"log_topic_label": "Sujet :",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Impression",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Statut",
|
||||
"log_download": "⬇ Télécharger",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Effacer",
|
||||
"log_filter_placeholder": "Filtrer…",
|
||||
"file_ready_btn": "▶ Lancer l'impression",
|
||||
"file_slots_btn": "🎨 Choisir les slots",
|
||||
"file_cancel_btn": "✕ Annuler",
|
||||
@@ -205,8 +218,9 @@
|
||||
"skip_select_at_least_one": "Veuillez sélectionner au moins un objet.",
|
||||
"skip_sending": "Envoi …",
|
||||
"skip_success": "Les objets seront ignorés.",
|
||||
"skip_cancel": "Annuler",
|
||||
"skip_confirm": "Ignorer",
|
||||
"fd_objects_hint": "Ignorer des objets (optionnel) :",
|
||||
"fd_objects_toggle": "Ignorer des objets",
|
||||
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
|
||||
"fd_cancel": "Annuler",
|
||||
"fd_print": "▶ Imprimer",
|
||||
@@ -252,40 +266,12 @@
|
||||
"store_upload_busy": "⏳ Envoi en cours…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ Seuls les fichiers GCode sont autorisés (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "Tout",
|
||||
"sf_ok": "✓ Terminés",
|
||||
"sf_err": "✗ Échoués",
|
||||
"sf_new": "Nouveau",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_name": "A–Z Nom",
|
||||
"ss_dur": "⏱ Durée d'impression",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "Personnalisé",
|
||||
"fd_options_title": "Options",
|
||||
"print_auto_leveling": "Mise à niveau auto pour cette impression",
|
||||
"settings_file_ready_mode": "Démarrer le dialogue d'impression",
|
||||
"settings_file_ready_banner": "Barre d'impression",
|
||||
"settings_file_ready_dialog": "Dialogue d'impression",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "Sens :",
|
||||
"log_lvl_err": "⛔ Erreurs",
|
||||
"log_lvl_warn": "⚠ Avert.",
|
||||
"log_topic_label": "Sujet :",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "Impression",
|
||||
"log_topic_info": "Info",
|
||||
"log_topic_status": "Statut",
|
||||
"log_download": "⬇ Télécharger",
|
||||
"log_auto": "⬇ Auto",
|
||||
"log_clear": "✕ Effacer",
|
||||
"log_filter_placeholder": "Filtrer…",
|
||||
"skip_cancel": "Annuler",
|
||||
"skip_confirm": "Ignorer"
|
||||
"ss_dur": "⏱ Durée d'impression"
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,13 @@
|
||||
"ace_dry_dialog_save_restart": "保存并重启",
|
||||
"ace_dry_dialog_custom_name": "自定义名称",
|
||||
"ace_dry_dialog_reset_default": "恢复默认",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "自定义",
|
||||
"cam_placeholder": "📷 相机未启动",
|
||||
"cam_stream_unavailable": "视频流不可用",
|
||||
"btn_cam_start": "▶ 相机",
|
||||
@@ -127,20 +134,7 @@
|
||||
"settings_title": "设置",
|
||||
"settings_connection": "连接",
|
||||
"settings_print": "打印设置",
|
||||
"settings_poll": "轮询间隔(秒)",
|
||||
"nav_settings": "设置",
|
||||
"settings_cat_display": "外观",
|
||||
"settings_cat_filament": "耗材",
|
||||
"settings_cat_language": "语言",
|
||||
"settings_cat_theme": "切换浅色 / 深色",
|
||||
"settings_filament_mapping": "耗材配置映射(每槽位)",
|
||||
"settings_filament_mapping_save": "保存映射",
|
||||
"settings_visible_vendors": "可见厂商(配置下拉框)",
|
||||
"settings_visible_vendors_hint": "仅这些厂商会出现在槽位配置下拉框中。未选择 = 显示全部。“Generic”和您自己的配置始终可见。",
|
||||
"settings_visible_vendors_save": "保存选择",
|
||||
"progress_action_print": "打印",
|
||||
"progress_action_slots": "分配槽位",
|
||||
"progress_action_clear": "清除",
|
||||
"settings_poll": "轮询间隔",
|
||||
"settings_version": "版本",
|
||||
"settings_save": "保存并重启",
|
||||
"settings_printer_name": "打印机名称",
|
||||
@@ -154,7 +148,12 @@
|
||||
"settings_default_slot": "默认槽位 (单色)",
|
||||
"settings_slot_auto": "自动 (所有已装载槽位)",
|
||||
"settings_auto_leveling": "打印前自动调平",
|
||||
"fd_options_title": "选项",
|
||||
"print_auto_leveling": "本次打印自动调平",
|
||||
"settings_camera_on_print": "打印开始时开启相机",
|
||||
"settings_file_ready_mode": "开始打印对话框",
|
||||
"settings_file_ready_banner": "打印栏",
|
||||
"settings_file_ready_dialog": "打印对话框",
|
||||
"settings_web_upload_warning": "打印网页上传文件时显示警告",
|
||||
"update_check": "检查更新",
|
||||
"update_checking": "检查中...",
|
||||
@@ -192,7 +191,21 @@
|
||||
"orca_profile_done": "已导入",
|
||||
"orca_profile_skipped": "跳过",
|
||||
"log_dir_all": "全部",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "方向:",
|
||||
"log_lvl_label": "级别:",
|
||||
"log_lvl_err": "⛔ 错误",
|
||||
"log_lvl_warn": "⚠ 警告",
|
||||
"log_topic_label": "主题:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "打印",
|
||||
"log_topic_info": "信息",
|
||||
"log_topic_status": "状态",
|
||||
"log_download": "⬇ 下载",
|
||||
"log_auto": "⬇ 自动",
|
||||
"log_clear": "✕ 清空",
|
||||
"log_filter_placeholder": "筛选…",
|
||||
"file_ready_btn": "▶ 开始打印",
|
||||
"file_slots_btn": "🎨 选择槽位",
|
||||
"file_cancel_btn": "✕ 取消",
|
||||
@@ -205,8 +218,9 @@
|
||||
"skip_select_at_least_one": "请至少选择一个对象。",
|
||||
"skip_sending": "发送中 …",
|
||||
"skip_success": "对象将被跳过。",
|
||||
"skip_cancel": "取消",
|
||||
"skip_confirm": "跳过",
|
||||
"fd_objects_hint": "跳过对象 (可选):",
|
||||
"fd_objects_toggle": "跳过对象",
|
||||
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
|
||||
"fd_cancel": "取消",
|
||||
"fd_print": "▶ 打印",
|
||||
@@ -252,40 +266,11 @@
|
||||
"store_upload_busy": "⏳ 上传中…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"store_upload_only_gcode": "✗ 仅允许 GCode 文件 (.gcode, .3mf, .bgcode)",
|
||||
"sf_all": "全部",
|
||||
"sf_ok": "✓ 已完成",
|
||||
"sf_err": "✗ 失败",
|
||||
"sf_new": "新",
|
||||
"ss_date": "↓ 日期",
|
||||
"ss_name": "A–Z 名称",
|
||||
"ss_dur": "⏱ 打印时间",
|
||||
"ace_dry_preset_pla": "PLA",
|
||||
"ace_dry_preset_pla_plus": "PLA+",
|
||||
"ace_dry_preset_petg": "PETG",
|
||||
"ace_dry_preset_tpu": "TPU",
|
||||
"ace_dry_preset_abs_asa": "ABS / ASA",
|
||||
"ace_dry_preset_pa_pc": "PA / PC",
|
||||
"ace_dry_preset_custom": "自定义",
|
||||
"fd_options_title": "选项",
|
||||
"print_auto_leveling": "本次打印自动调平",
|
||||
"settings_file_ready_mode": "开始打印对话框",
|
||||
"settings_file_ready_banner": "打印栏",
|
||||
"settings_file_ready_dialog": "打印对话框",
|
||||
"log_dir_rx": "RX",
|
||||
"log_dir_tx": "TX",
|
||||
"log_dir_label": "方向:",
|
||||
"log_lvl_err": "⛔ 错误",
|
||||
"log_lvl_warn": "⚠ 警告",
|
||||
"log_topic_label": "主题:",
|
||||
"log_topic_ams": "AMS",
|
||||
"log_topic_print": "打印",
|
||||
"log_topic_info": "信息",
|
||||
"log_topic_status": "状态",
|
||||
"log_download": "⬇ 下载",
|
||||
"log_auto": "⬇ 自动",
|
||||
"log_clear": "✕ 清空",
|
||||
"log_filter_placeholder": "筛选…",
|
||||
"skip_cancel": "取消",
|
||||
"skip_confirm": "跳过"
|
||||
"ss_dur": "⏱ 打印时间"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user