forked from viewit/KX-Bridge-Release
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eea570052f | |||
| 303297bfbf |
@@ -1,5 +1,37 @@
|
|||||||
# Changelog
|
# 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
|
## [0.9.23] – 2026-06-16
|
||||||
|
|
||||||
### Neu
|
### Neu
|
||||||
|
|||||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,5 +1,36 @@
|
|||||||
# Changelog
|
# 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
|
## [0.9.23] – 2026-06-16
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
|||||||
113
kobrax_client.py
113
kobrax_client.py
@@ -27,6 +27,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import select
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
@@ -120,6 +121,10 @@ class KobraXClient:
|
|||||||
self._buf = b""
|
self._buf = b""
|
||||||
self._pid = 1
|
self._pid = 1
|
||||||
self._lock = threading.Lock()
|
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
|
self._running = False
|
||||||
|
|
||||||
# Pending requests by msgid (for response ACK)
|
# Pending requests by msgid (for response ACK)
|
||||||
@@ -167,21 +172,31 @@ class KobraXClient:
|
|||||||
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
|
ctx.set_ciphers("DEFAULT:@SECLEVEL=0")
|
||||||
ctx.load_cert_chain(CERT_FILE, KEY_FILE)
|
ctx.load_cert_chain(CERT_FILE, KEY_FILE)
|
||||||
|
|
||||||
_ai = socket.getaddrinfo(self.host, self.port, socket.AF_INET, socket.SOCK_STREAM)
|
# Socket als lokale Variable aufbauen — der Handshake (Connect + CONNACK)
|
||||||
raw = socket.create_connection(_ai[0][4], timeout=5)
|
# läuft OHNE gehaltenes Lock, damit ein langsamer Connect die Sender nicht
|
||||||
self._sock = ctx.wrap_socket(raw)
|
# einfriert. Erst der fertige Socket wird unter Lock eingeschwenkt (#53).
|
||||||
log.info("TLS connected cipher=%s", self._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)
|
||||||
|
new_sock = ctx.wrap_socket(raw)
|
||||||
|
log.info("TLS connected cipher=%s", new_sock.cipher()[0])
|
||||||
|
|
||||||
self._sock.sendall(_build_connect(self.client_id, self.username, self.password))
|
new_sock.sendall(_build_connect(self.client_id, self.username, self.password))
|
||||||
self._sock.settimeout(3)
|
new_sock.settimeout(3)
|
||||||
r = self._sock.recv(64)
|
r = new_sock.recv(64)
|
||||||
if len(r) < 4 or r[0] != 0x20 or r[3] != 0:
|
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()}")
|
raise RuntimeError(f"CONNACK failed: {r.hex()}")
|
||||||
log.info("CONNACK rc=0")
|
log.info("CONNACK rc=0")
|
||||||
|
|
||||||
self._sock.settimeout(0.2)
|
new_sock.settimeout(0.2)
|
||||||
self._buf = b""
|
with self._lock:
|
||||||
self._subscribe(self._sub_topic())
|
self._sock = new_sock
|
||||||
|
self._sock_gen += 1
|
||||||
|
self._buf = b""
|
||||||
|
self._subscribe(self._sub_topic()) # nimmt das Lock selbst — nicht verschachteln
|
||||||
log.debug("MQTT connected to %s:%s", self.host, self.port)
|
log.debug("MQTT connected to %s:%s", self.host, self.port)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
@@ -207,10 +222,14 @@ class KobraXClient:
|
|||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
try:
|
with self._lock:
|
||||||
self._sock.close()
|
try:
|
||||||
except Exception:
|
if self._sock is not None:
|
||||||
pass
|
self._sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._sock = None
|
||||||
|
self._sock_gen += 1
|
||||||
|
|
||||||
def _reconnect(self):
|
def _reconnect(self):
|
||||||
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
|
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
|
||||||
@@ -219,10 +238,16 @@ class KobraXClient:
|
|||||||
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
|
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
|
||||||
ausgeschaltet) zu vermeiden."""
|
ausgeschaltet) zu vermeiden."""
|
||||||
log.warning("Verbindung verloren – reconnect…")
|
log.warning("Verbindung verloren – reconnect…")
|
||||||
try:
|
# Close + Invalidierung unter Lock, damit kein Sender mitten im sendall
|
||||||
self._sock.close()
|
# auf den gerade geschlossenen Socket trifft (Issue #53).
|
||||||
except Exception:
|
with self._lock:
|
||||||
pass
|
try:
|
||||||
|
if self._sock is not None:
|
||||||
|
self._sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._sock = None
|
||||||
|
self._sock_gen += 1
|
||||||
delays = [2, 4, 8, 15, 30, 60]
|
delays = [2, 4, 8, 15, 30, 60]
|
||||||
attempt = 0
|
attempt = 0
|
||||||
while self._running:
|
while self._running:
|
||||||
@@ -246,7 +271,8 @@ class KobraXClient:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
pid = self._pid
|
pid = self._pid
|
||||||
self._pid += 1
|
self._pid += 1
|
||||||
self._sock.sendall(_build_subscribe(topic, pid))
|
if self._sock is not None:
|
||||||
|
self._sock.sendall(_build_subscribe(topic, pid))
|
||||||
log.info("SUB %s", topic)
|
log.info("SUB %s", topic)
|
||||||
|
|
||||||
# -- Read loop -----------------------------------------------------------
|
# -- Read loop -----------------------------------------------------------
|
||||||
@@ -256,17 +282,52 @@ class KobraXClient:
|
|||||||
_empty_count = 0
|
_empty_count = 0
|
||||||
while self._running:
|
while self._running:
|
||||||
if time.time() - last_ping > 30:
|
if time.time() - last_ping > 30:
|
||||||
|
ping_ok = False
|
||||||
with self._lock:
|
with self._lock:
|
||||||
try:
|
try:
|
||||||
self._sock.sendall(_build_pingreq())
|
if self._sock is not None:
|
||||||
|
self._sock.sendall(_build_pingreq())
|
||||||
|
ping_ok = True
|
||||||
except Exception:
|
except Exception:
|
||||||
if self._running and not self._reconnect():
|
ping_ok = False
|
||||||
break
|
# _reconnect() AUSSERHALB des Locks aufrufen — es nimmt das Lock
|
||||||
last_ping = time.time()
|
# selbst, und threading.Lock ist nicht reentrant (sonst Deadlock).
|
||||||
continue
|
if not ping_ok:
|
||||||
|
if self._running and not self._reconnect():
|
||||||
|
break
|
||||||
last_ping = time.time()
|
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:
|
try:
|
||||||
data = self._sock.recv(65536)
|
ready, _, _ = select.select([sock], [], [], 0.2)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
# fd geschlossen/ungültig (Reconnect oder Disconnect mitten im select)
|
||||||
|
if not self._running:
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
continue
|
||||||
|
if not ready:
|
||||||
|
continue # Leerlauf, kein Lock gehalten
|
||||||
|
|
||||||
|
# Daten liegen an: Lock kurz greifen für das eine recv, serialisiert
|
||||||
|
# gegen alle sendall-Caller. recv blockiert nicht lange (select sagte
|
||||||
|
# ready, Socket-Timeout ist 0.2s).
|
||||||
|
try:
|
||||||
|
with self._lock:
|
||||||
|
# Socket könnte zwischen select und hier ersetzt worden sein.
|
||||||
|
if self._sock_gen != gen or self._sock is not sock:
|
||||||
|
continue
|
||||||
|
data = sock.recv(65536)
|
||||||
if not data:
|
if not data:
|
||||||
# Windows SSL kann kurzzeitig b"" liefern ohne echten EOF
|
# Windows SSL kann kurzzeitig b"" liefern ohne echten EOF
|
||||||
_empty_count += 1
|
_empty_count += 1
|
||||||
@@ -275,7 +336,7 @@ class KobraXClient:
|
|||||||
continue
|
continue
|
||||||
_empty_count = 0
|
_empty_count = 0
|
||||||
self._buf += data
|
self._buf += data
|
||||||
self._drain()
|
self._drain() # außerhalb des Locks — Dispatch/event.set() bleibt prompt
|
||||||
except ssl.SSLWantReadError:
|
except ssl.SSLWantReadError:
|
||||||
continue
|
continue
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
|
|||||||
@@ -2925,6 +2925,18 @@ class KobraXBridge:
|
|||||||
if not file_data:
|
if not file_data:
|
||||||
return web.json_response({"error": "no file received"}, status=400)
|
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_md5 = hashlib.md5(file_data).hexdigest()
|
||||||
file_size = len(file_data)
|
file_size = len(file_data)
|
||||||
|
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ function applyLang(){
|
|||||||
setText('skip-title',T.skip_title);
|
setText('skip-title',T.skip_title);
|
||||||
setText('skip-hint',T.skip_hint);
|
setText('skip-hint',T.skip_hint);
|
||||||
setText('d-btn-skip-label',T.skip_btn_label);
|
setText('d-btn-skip-label',T.skip_btn_label);
|
||||||
setText('fd-objects-hint',T.fd_objects_hint);
|
setText('fd-objects-toggle-lbl',T.fd_objects_toggle);
|
||||||
setText('apd-lbl-ip',T.apd_lbl_ip);
|
setText('apd-lbl-ip',T.apd_lbl_ip);
|
||||||
setText('apd-lbl-name',T.apd_lbl_name);
|
setText('apd-lbl-name',T.apd_lbl_name);
|
||||||
var apn=document.getElementById('apd-name');if(apn)apn.setAttribute('placeholder',T.apd_placeholder_name);
|
var apn=document.getElementById('apd-name');if(apn)apn.setAttribute('placeholder',T.apd_placeholder_name);
|
||||||
@@ -2006,6 +2006,14 @@ function uploadGcode(file){
|
|||||||
var zone=document.getElementById('store-upload-zone');
|
var zone=document.getElementById('store-upload-zone');
|
||||||
var status=document.getElementById('store-upload-status');
|
var status=document.getElementById('store-upload-status');
|
||||||
var label=document.getElementById('store-upload-label');
|
var label=document.getElementById('store-upload-label');
|
||||||
|
// Nur druckbare Dateien zulassen (Issue #59) — Drag&Drop umgeht das
|
||||||
|
// accept-Attribut, daher hier explizit prüfen.
|
||||||
|
var _fn=(file.name||'').toLowerCase();
|
||||||
|
if(!/\.(gcode|gcode\.3mf|3mf|bgcode)$/.test(_fn)){
|
||||||
|
if(status){ status.textContent=T.store_upload_only_gcode||'Only GCode files allowed'; status.style.display=''; status.className='upload-status-err'; }
|
||||||
|
clog('Upload abgelehnt (kein GCode): '+file.name,'msg-err');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(status) { status.textContent=T.store_upload_busy; status.style.display=''; status.className='upload-status-busy'; }
|
if(status) { status.textContent=T.store_upload_busy; status.style.display=''; status.className='upload-status-busy'; }
|
||||||
if(label) label.style.display='none';
|
if(label) label.style.display='none';
|
||||||
if(zone) zone.style.pointerEvents='none';
|
if(zone) zone.style.pointerEvents='none';
|
||||||
@@ -2329,6 +2337,18 @@ function _normalizeMaterialKey(material){
|
|||||||
function _materialsCompatible(a,b){
|
function _materialsCompatible(a,b){
|
||||||
return _normalizeMaterialKey(a)===_normalizeMaterialKey(b);
|
return _normalizeMaterialKey(a)===_normalizeMaterialKey(b);
|
||||||
}
|
}
|
||||||
|
// Issue #57 Punkt 4: konkreter Profilname (User-Override) statt generischem Typ.
|
||||||
|
// Fällt auf den Material-Typ zurück wenn kein Profil gemappt ist.
|
||||||
|
function _slotProfileLabel(slot){
|
||||||
|
if(!slot)return '';
|
||||||
|
if(slot.filament_name){
|
||||||
|
return slot.filament_name+(slot.filament_vendor?' — '+slot.filament_vendor:'');
|
||||||
|
}
|
||||||
|
return slot.material||'';
|
||||||
|
}
|
||||||
|
function _escAttr(s){
|
||||||
|
return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
function _updateSlotMarker(sel){
|
function _updateSlotMarker(sel){
|
||||||
var opt=sel.options[sel.selectedIndex];
|
var opt=sel.options[sel.selectedIndex];
|
||||||
var color=opt&&opt.dataset.color?opt.dataset.color:'#888';
|
var color=opt&&opt.dataset.color?opt.dataset.color:'#888';
|
||||||
@@ -2339,6 +2359,7 @@ function _updateSlotMarker(sel){
|
|||||||
marker.style.background=color;
|
marker.style.background=color;
|
||||||
marker.style.color=_contrastText(color);
|
marker.style.color=_contrastText(color);
|
||||||
marker.textContent=(slotIdx+1);
|
marker.textContent=(slotIdx+1);
|
||||||
|
marker.title=(opt&&opt.dataset.profile)?opt.dataset.profile:'';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2353,12 +2374,19 @@ function openFilamentDialog(slots){
|
|||||||
// Auto-Leveling-Checkbox mit globalem Default vorbelegen
|
// Auto-Leveling-Checkbox mit globalem Default vorbelegen
|
||||||
var fdAl=document.getElementById('fd-auto-leveling');
|
var fdAl=document.getElementById('fd-auto-leveling');
|
||||||
if(fdAl) fdAl.checked=(S.auto_leveling===undefined?true:!!S.auto_leveling);
|
if(fdAl) fdAl.checked=(S.auto_leveling===undefined?true:!!S.auto_leveling);
|
||||||
// Objekt-Liste laden (nur Store-Modus: per File-ID; Banner-Modus hat keine ID)
|
// Objekt-Liste laden — sobald eine File-ID auflösbar ist (Issue #57 Punkt 3:
|
||||||
|
// Skip-Parität auch im Banner-/Upload-Modus, nicht nur im Store-Modus).
|
||||||
|
// startReadyFileWithSlots() setzt _storeFileId auch im Banner-Modus per
|
||||||
|
// filename→fileObj-Lookup, daher reicht hier die _storeFileId-Prüfung.
|
||||||
_printObjects=[];
|
_printObjects=[];
|
||||||
_printObjectsSvg='';
|
_printObjectsSvg='';
|
||||||
var objSection=document.getElementById('fd-objects-section');
|
var objSection=document.getElementById('fd-objects-section');
|
||||||
|
var objBody=document.getElementById('fd-objects-body');
|
||||||
|
var objArrow=document.getElementById('fd-objects-arrow');
|
||||||
if(objSection)objSection.style.display='none';
|
if(objSection)objSection.style.display='none';
|
||||||
if(_filamentDialogMode==='store'&&_storeFileId){
|
if(objBody)objBody.style.display='none'; // immer eingeklappt starten
|
||||||
|
if(objArrow)objArrow.style.transform='';
|
||||||
|
if(_storeFileId){
|
||||||
fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects'))
|
fetch(_apiUrl('/kx/files/'+encodeURIComponent(_storeFileId)+'/objects'))
|
||||||
.then(function(r){return r.json()})
|
.then(function(r){return r.json()})
|
||||||
.then(function(d){
|
.then(function(d){
|
||||||
@@ -2367,6 +2395,8 @@ function openFilamentDialog(slots){
|
|||||||
if(names.length>=2){
|
if(names.length>=2){
|
||||||
_printObjects=names.map(function(n){return {name:n,skip:false};});
|
_printObjects=names.map(function(n){return {name:n,skip:false};});
|
||||||
renderObjectChecklist(); renderObjectSvg();
|
renderObjectChecklist(); renderObjectSvg();
|
||||||
|
var cnt=document.getElementById('fd-objects-count');
|
||||||
|
if(cnt)cnt.textContent='('+names.length+')';
|
||||||
if(objSection)objSection.style.display='block';
|
if(objSection)objSection.style.display='block';
|
||||||
}
|
}
|
||||||
}).catch(function(){});
|
}).catch(function(){});
|
||||||
@@ -2430,8 +2460,8 @@ function openFilamentDialog(slots){
|
|||||||
var defaultSlot=compatible.find(function(s){return s.slot_index===defaultSlotIndex;})||null;
|
var defaultSlot=compatible.find(function(s){return s.slot_index===defaultSlotIndex;})||null;
|
||||||
var opts=compatible.map(function(s){
|
var opts=compatible.map(function(s){
|
||||||
var sel=(defaultSlot&&s.slot_index===defaultSlot.slot_index)?'selected':'';
|
var sel=(defaultSlot&&s.slot_index===defaultSlot.slot_index)?'selected':'';
|
||||||
return '<option value="'+s.slot_index+'" data-color="'+s.color_hex+'" data-material="'+s.material+'" '+sel+'>'+
|
return '<option value="'+s.slot_index+'" data-color="'+s.color_hex+'" data-material="'+s.material+'" data-profile="'+_escAttr(_slotProfileLabel(s))+'" '+sel+'>'+
|
||||||
'● '+T.fd_slot+' '+(s.slot_index+1)+' · '+s.material+'</option>';
|
'● '+T.fd_slot+' '+(s.slot_index+1)+' · '+_slotProfileLabel(s)+'</option>';
|
||||||
}).join('');
|
}).join('');
|
||||||
if(!compatible.length){
|
if(!compatible.length){
|
||||||
opts='<option value="-1" data-color="#888888" data-material="" selected>⚠ '+T.fd_no_matching_material+'</option>';
|
opts='<option value="-1" data-color="#888888" data-material="" selected>⚠ '+T.fd_no_matching_material+'</option>';
|
||||||
@@ -2448,7 +2478,7 @@ function openFilamentDialog(slots){
|
|||||||
'<span style="font-size:11px;color:var(--txt2);min-width:36px">'+gc.material+'</span>'+
|
'<span style="font-size:11px;color:var(--txt2);min-width:36px">'+gc.material+'</span>'+
|
||||||
usedBadge+
|
usedBadge+
|
||||||
'<span style="font-size:16px;color:var(--txt2)">→</span>'+
|
'<span style="font-size:16px;color:var(--txt2)">→</span>'+
|
||||||
'<span class="fd-slot-marker" data-for-paint="'+i+'" style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:5px;background:'+slotColor+';color:'+slotTxt+';font-weight:700;font-size:12px;border:1px solid var(--border);flex-shrink:0">'+(defaultSlot?defaultSlot.slot_index+1:'?')+'</span>'+
|
'<span class="fd-slot-marker" data-for-paint="'+i+'" title="'+_escAttr(defaultSlot?_slotProfileLabel(defaultSlot):'')+'" style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:5px;background:'+slotColor+';color:'+slotTxt+';font-weight:700;font-size:12px;border:1px solid var(--border);flex-shrink:0">'+(defaultSlot?defaultSlot.slot_index+1:'?')+'</span>'+
|
||||||
'<select data-paint="'+i+'" data-paint-color="'+gc.color_hex+'" data-is-used="'+(isUsed?'1':'0')+'" data-has-compatible="'+(compatible.length?'1':'0')+'" '+(compatible.length?'':'disabled')+' onchange="_updateSlotMarker(this)" style="flex:1;min-width:0;padding:4px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+
|
'<select data-paint="'+i+'" data-paint-color="'+gc.color_hex+'" data-is-used="'+(isUsed?'1':'0')+'" data-has-compatible="'+(compatible.length?'1':'0')+'" '+(compatible.length?'':'disabled')+' onchange="_updateSlotMarker(this)" style="flex:1;min-width:0;padding:4px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+
|
||||||
opts+'</select>'+
|
opts+'</select>'+
|
||||||
'</div>';
|
'</div>';
|
||||||
@@ -2549,6 +2579,15 @@ function _toggleObjectSkip(idx,val){
|
|||||||
if(_printObjects[idx])_printObjects[idx].skip=!!val;
|
if(_printObjects[idx])_printObjects[idx].skip=!!val;
|
||||||
renderObjectSvg();
|
renderObjectSvg();
|
||||||
}
|
}
|
||||||
|
// Issue #57 Punkt 3: Skip-Objekte-Bereich ein-/ausklappen
|
||||||
|
function toggleFdObjects(){
|
||||||
|
var body=document.getElementById('fd-objects-body');
|
||||||
|
var arrow=document.getElementById('fd-objects-arrow');
|
||||||
|
if(!body)return;
|
||||||
|
var open=body.style.display!=='none';
|
||||||
|
body.style.display=open?'none':'block';
|
||||||
|
if(arrow)arrow.style.transform=open?'':'rotate(90deg)';
|
||||||
|
}
|
||||||
function renderObjectSvg(){
|
function renderObjectSvg(){
|
||||||
var box=document.getElementById('fd-objects-svg');
|
var box=document.getElementById('fd-objects-svg');
|
||||||
if(!box)return;
|
if(!box)return;
|
||||||
|
|||||||
@@ -626,9 +626,16 @@
|
|||||||
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
|
<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-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
|
||||||
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
|
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
|
||||||
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin-bottom:8px">Objekte überspringen (optional):</p>
|
<button type="button" id="fd-objects-toggle" onclick="toggleFdObjects()"
|
||||||
<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>
|
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">
|
||||||
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
|
<span id="fd-objects-arrow" style="font-size:10px;transition:transform .15s">▶</span>
|
||||||
|
<span>✂ <span id="fd-objects-toggle-lbl">Objekte überspringen</span></span>
|
||||||
|
<span id="fd-objects-count" style="margin-left:auto;color:var(--txt2);font-weight:600"></span>
|
||||||
|
</button>
|
||||||
|
<div id="fd-objects-body" style="display:none;margin-top:8px">
|
||||||
|
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
|
||||||
|
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-bottom:14px;padding:10px 12px;background:var(--raised);border-radius:8px;border:1px solid var(--border)">
|
<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="font-size:11px;font-weight:600;color:var(--txt2);margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em" id="fd-options-title">Druckoptionen</div>
|
||||||
|
|||||||
@@ -206,6 +206,7 @@
|
|||||||
"skip_sending": "Sende …",
|
"skip_sending": "Sende …",
|
||||||
"skip_success": "Objekte werden übersprungen.",
|
"skip_success": "Objekte werden übersprungen.",
|
||||||
"fd_objects_hint": "Objekte überspringen (optional):",
|
"fd_objects_hint": "Objekte überspringen (optional):",
|
||||||
|
"fd_objects_toggle": "Objekte überspringen",
|
||||||
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
|
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
|
||||||
"fd_cancel": "Abbrechen",
|
"fd_cancel": "Abbrechen",
|
||||||
"fd_print": "▶ Drucken",
|
"fd_print": "▶ Drucken",
|
||||||
@@ -251,6 +252,7 @@
|
|||||||
"store_upload_busy": "⏳ Hochladen…",
|
"store_upload_busy": "⏳ Hochladen…",
|
||||||
"store_upload_success": "✓ {file}",
|
"store_upload_success": "✓ {file}",
|
||||||
"store_upload_error": "✗ {error}",
|
"store_upload_error": "✗ {error}",
|
||||||
|
"store_upload_only_gcode": "✗ Nur GCode-Dateien erlaubt (.gcode, .3mf, .bgcode)",
|
||||||
"sf_all": "Alle",
|
"sf_all": "Alle",
|
||||||
"sf_ok": "✓ Erfolgreich",
|
"sf_ok": "✓ Erfolgreich",
|
||||||
"sf_err": "✗ Fehler",
|
"sf_err": "✗ Fehler",
|
||||||
|
|||||||
@@ -234,6 +234,7 @@
|
|||||||
"skip_sending": "Sending …",
|
"skip_sending": "Sending …",
|
||||||
"skip_success": "Objects will be skipped.",
|
"skip_success": "Objects will be skipped.",
|
||||||
"fd_objects_hint": "Skip objects (optional):",
|
"fd_objects_hint": "Skip objects (optional):",
|
||||||
|
"fd_objects_toggle": "Skip objects",
|
||||||
"fd_slots_hint": "Assign GCode channel to AMS slot:",
|
"fd_slots_hint": "Assign GCode channel to AMS slot:",
|
||||||
"fd_cancel": "Cancel",
|
"fd_cancel": "Cancel",
|
||||||
"fd_print": "▶ Print",
|
"fd_print": "▶ Print",
|
||||||
@@ -279,6 +280,7 @@
|
|||||||
"store_upload_busy": "⏳ Uploading…",
|
"store_upload_busy": "⏳ Uploading…",
|
||||||
"store_upload_success": "✓ {file}",
|
"store_upload_success": "✓ {file}",
|
||||||
"store_upload_error": "✗ {error}",
|
"store_upload_error": "✗ {error}",
|
||||||
|
"store_upload_only_gcode": "✗ Only GCode files allowed (.gcode, .3mf, .bgcode)",
|
||||||
"sf_all": "All",
|
"sf_all": "All",
|
||||||
"sf_ok": "✓ Completed",
|
"sf_ok": "✓ Completed",
|
||||||
"sf_err": "✗ Failed",
|
"sf_err": "✗ Failed",
|
||||||
|
|||||||
@@ -206,6 +206,7 @@
|
|||||||
"skip_sending": "Enviando …",
|
"skip_sending": "Enviando …",
|
||||||
"skip_success": "Se omitirán los objetos.",
|
"skip_success": "Se omitirán los objetos.",
|
||||||
"fd_objects_hint": "Omitir objetos (opcional):",
|
"fd_objects_hint": "Omitir objetos (opcional):",
|
||||||
|
"fd_objects_toggle": "Omitir objetos",
|
||||||
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
|
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
|
||||||
"fd_cancel": "Cancelar",
|
"fd_cancel": "Cancelar",
|
||||||
"fd_print": "▶ Imprimir",
|
"fd_print": "▶ Imprimir",
|
||||||
@@ -251,6 +252,7 @@
|
|||||||
"store_upload_busy": "⏳ Subiendo…",
|
"store_upload_busy": "⏳ Subiendo…",
|
||||||
"store_upload_success": "✓ {file}",
|
"store_upload_success": "✓ {file}",
|
||||||
"store_upload_error": "✗ {error}",
|
"store_upload_error": "✗ {error}",
|
||||||
|
"store_upload_only_gcode": "✗ Solo se permiten archivos GCode (.gcode, .3mf, .bgcode)",
|
||||||
"sf_all": "Todos",
|
"sf_all": "Todos",
|
||||||
"sf_ok": "✓ Completado",
|
"sf_ok": "✓ Completado",
|
||||||
"sf_err": "✗ Fallido",
|
"sf_err": "✗ Fallido",
|
||||||
|
|||||||
@@ -206,6 +206,7 @@
|
|||||||
"skip_sending": "Envoi …",
|
"skip_sending": "Envoi …",
|
||||||
"skip_success": "Les objets seront ignorés.",
|
"skip_success": "Les objets seront ignorés.",
|
||||||
"fd_objects_hint": "Ignorer des objets (optionnel) :",
|
"fd_objects_hint": "Ignorer des objets (optionnel) :",
|
||||||
|
"fd_objects_toggle": "Ignorer des objets",
|
||||||
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
|
"fd_slots_hint": "Associer le canal GCode au slot AMS :",
|
||||||
"fd_cancel": "Annuler",
|
"fd_cancel": "Annuler",
|
||||||
"fd_print": "▶ Imprimer",
|
"fd_print": "▶ Imprimer",
|
||||||
@@ -251,6 +252,7 @@
|
|||||||
"store_upload_busy": "⏳ Envoi en cours…",
|
"store_upload_busy": "⏳ Envoi en cours…",
|
||||||
"store_upload_success": "✓ {file}",
|
"store_upload_success": "✓ {file}",
|
||||||
"store_upload_error": "✗ {error}",
|
"store_upload_error": "✗ {error}",
|
||||||
|
"store_upload_only_gcode": "✗ Seuls les fichiers GCode sont autorisés (.gcode, .3mf, .bgcode)",
|
||||||
"sf_all": "Tout",
|
"sf_all": "Tout",
|
||||||
"sf_ok": "✓ Terminés",
|
"sf_ok": "✓ Terminés",
|
||||||
"sf_err": "✗ Échoués",
|
"sf_err": "✗ Échoués",
|
||||||
|
|||||||
@@ -206,6 +206,7 @@
|
|||||||
"skip_sending": "发送中 …",
|
"skip_sending": "发送中 …",
|
||||||
"skip_success": "对象将被跳过。",
|
"skip_success": "对象将被跳过。",
|
||||||
"fd_objects_hint": "跳过对象 (可选):",
|
"fd_objects_hint": "跳过对象 (可选):",
|
||||||
|
"fd_objects_toggle": "跳过对象",
|
||||||
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
|
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
|
||||||
"fd_cancel": "取消",
|
"fd_cancel": "取消",
|
||||||
"fd_print": "▶ 打印",
|
"fd_print": "▶ 打印",
|
||||||
@@ -251,6 +252,7 @@
|
|||||||
"store_upload_busy": "⏳ 上传中…",
|
"store_upload_busy": "⏳ 上传中…",
|
||||||
"store_upload_success": "✓ {file}",
|
"store_upload_success": "✓ {file}",
|
||||||
"store_upload_error": "✗ {error}",
|
"store_upload_error": "✗ {error}",
|
||||||
|
"store_upload_only_gcode": "✗ 仅允许 GCode 文件 (.gcode, .3mf, .bgcode)",
|
||||||
"sf_all": "全部",
|
"sf_all": "全部",
|
||||||
"sf_ok": "✓ 已完成",
|
"sf_ok": "✓ 已完成",
|
||||||
"sf_err": "✗ 失败",
|
"sf_err": "✗ 失败",
|
||||||
|
|||||||
Reference in New Issue
Block a user